Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Video processing initial commit #538

Draft
wants to merge 1 commit into
base: stable
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 39 additions & 37 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ on: [push]

jobs:
build:

runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
node-version: [12.x, 14.x, 15.x]
postgres-version: ["10.15", "11.10", "12.5", "13.1"]
redis-version: ["6"]
postgres-version: ['10.15', '11.10', '12.5', '13.1']
redis-version: ['6']

services:
db:
Expand All @@ -25,45 +24,48 @@ jobs:
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

steps:
- name: Start Redis v${{ matrix.redis-version }}
uses: supercharge/[email protected]
with:
redis-version: ${{ matrix.redis-version }}
- name: Start Redis v${{ matrix.redis-version }}
uses: supercharge/[email protected]
with:
redis-version: ${{ matrix.redis-version }}

- name: install GraphicsMagick
run: sudo apt-get install graphicsmagick

- name: install ffmpeg
run: sudo apt-get install ffmpeg

- name: install GraphicsMagick
run: sudo apt-get install graphicsmagick
- uses: actions/checkout@v2

- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"

- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: create directories for attachments
run: |
mkdir -p /tmp/pepyatka-media/attachments
mkdir /tmp/pepyatka-media/attachments/thumbnails
mkdir /tmp/pepyatka-media/attachments/thumbnails2
mkdir /tmp/pepyatka-media/attachments/anotherTestSize
- name: create directories for attachments
run: |
mkdir -p /tmp/pepyatka-media/attachments
mkdir /tmp/pepyatka-media/attachments/thumbnails
mkdir /tmp/pepyatka-media/attachments/thumbnails2
mkdir /tmp/pepyatka-media/attachments/anotherTestSize

- name: Install dependencies
run: yarn
- name: Install dependencies
run: yarn

- name: run lint
run: yarn lint
- name: run lint
run: yarn lint

- name: run tests
run: yarn test
- name: run tests
run: yarn test
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FROM node:14-buster
RUN apt-get update && \
apt-get install -y \
graphicsmagick \
ffmpeg \
g++ \
git \
make
Expand All @@ -18,4 +19,4 @@ RUN rm -rf node_modules && \

ENV NODE_ENV production

CMD ["yarn","start"]
CMD ["yarn","start"]
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ FreeFeed is based on [Pepyatka](https://github.com/pepyatka/pepyatka-server/) pr
brew install redis
redis-server /usr/local/etc/redis.conf
brew install graphicsmagick
brew install ffmpeg
brew install postgres
brew services start postgresql
createuser -P -s freefeed (enter freefeed as password)
Expand All @@ -27,6 +28,7 @@ createdb -O freefeed freefeed
### Or setup dependencies via Docker

1. `brew install graphicsmagick`
1. `brew install ffmpeg`
1. [Install and run Docker](https://www.docker.com/get-started)
1. docker-compose up -d

Expand All @@ -40,7 +42,9 @@ yarn install
yarn knex --env production migrate:latest
mkdir ./public/files/attachments/thumbnails/ && mkdir ./public/files/attachments/thumbnails2/
```
Now create config `config/local.json` with some random secret string: `{ "secret": "myverysecretstring" }`

Now create config `config/local.json` with some random secret string: `{ "secret": "myverysecretstring" }`

```
yarn start
```
Expand Down Expand Up @@ -90,4 +94,5 @@ You can drop your question [here](https://freefeed.net/support).
FreeFeed is licensed under the MIT License.

## License

[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FFreeFeed%2Ffreefeed-server.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FFreeFeed%2Ffreefeed-server?ref=badge_large)
113 changes: 110 additions & 3 deletions app/models/attachment.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { promises as fs, createReadStream } from 'fs';
import childProcess from 'child_process';
import { parse as parsePath } from 'path';
import { basename, dirname, parse as parsePath } from 'path';
import util from 'util';

import config from 'config';
import createDebug from 'debug';
import gm from 'gm';
import { parseFile } from 'music-metadata';
import fluentFfmpeg from 'fluent-ffmpeg';
import { fromFile } from 'file-type';
import mime from 'mime-types';
import mmm from 'mmmagic';
Expand Down Expand Up @@ -266,6 +267,10 @@ export function addModel(dbAdapter) {
'audio/ogg': 'ogg',
'audio/x-wav': 'wav',
};
const supportedVideoTypes = {
'video/mp4': 'mp4',
'video/quicktime': 'mp4',
};

this.mimeType = await mimeTypeDetect(tmpAttachmentFileName, tmpAttachmentFile);
debug(`Mime-type of ${tmpAttachmentFileName} is ${this.mimeType}`);
Expand Down Expand Up @@ -298,14 +303,22 @@ export function addModel(dbAdapter) {
} else {
this.artist = metadata.artist;
}
} else if (supportedVideoTypes[this.mimeType]) {
this.mediaType = 'video';
this.fileExtension = supportedVideoTypes[this.mimeType];
this.mimeType = 'video/mp4';
this.noThumbnail = '1';
this.discardOriginal = true;
await this.handleVideo(tmpAttachmentFile);
} else {
// Set media properties for 'general' type
this.mediaType = 'general';
this.noThumbnail = '1';
}

// Store an original attachment
if (this.s3) {
if (this.discardOriginal) {
await fs.unlink(tmpAttachmentFile);
} else if (this.s3) {
await this.uploadToS3(
tmpAttachmentFile,
config.attachments.path + this.getFilename(),
Expand Down Expand Up @@ -456,6 +469,100 @@ export function addModel(dbAdapter) {
}
}

/**
* @param {string} originalFile
*/
async handleVideo(originalFile) {
const thumbnailTmpPath = `${this.file.path}.thumbnail.png`;
const thumbnailTmpFilename = basename(thumbnailTmpPath);
const thumbnailTmpDirname = dirname(thumbnailTmpPath);
const encodedVideoTmpPath = `${this.file.path}.encoded.mp4`;

this.imageSizes.o = {
url: config.attachments.url + config.attachments.path + this.getFilename(),
};

this.fileName = this.getFilename();

// encode video and generate a thumbnail
await (() => {
return new Promise((resolve, reject) => {
const ffmpeg = new fluentFfmpeg();
ffmpeg
.input(originalFile)
.on('codecData', (data) => {
const [h, w] = data.video_details[3].split('x');
this.imageSizes.o.w = parseInt(w, 10);
this.imageSizes.o.h = parseInt(h, 10);
})
.audioCodec('aac')
.videoCodec('libx264')
.format('mp4')
.output(encodedVideoTmpPath)
.thumbnail(
{
count: 1,
timemarks: ['00.1'],
size: `?x${config.attachments.imageSizes.t.bounds.height}`,
filename: thumbnailTmpFilename,
},
thumbnailTmpDirname,
)
.on('end', resolve)
.on('error', reject)
.run();
});
})();

// use encoded file size, not the original file size
const resultFileSize = await fs.stat(encodedVideoTmpPath);
this.fileSize = resultFileSize.size;

this.noThumbnail = '0';
this.imageSizes.t = {
w: Math.round(
(this.imageSizes.o.w * config.attachments.imageSizes.t.bounds.height) /
this.imageSizes.o.h,
),
h: config.attachments.imageSizes.t.bounds.height,
url:
config.attachments.url +
config.attachments.imageSizes['t'].path +
this.getFilename('png'),
};

const files = [
{
tmp: thumbnailTmpPath,
mime: 'image/png',
path: config.attachments.storage.rootDir + config.attachments.imageSizes['t'].path,
filename: this.getFilename('png'),
},
{
tmp: encodedVideoTmpPath,
mime: 'video/mp4',
path: config.attachments.storage.rootDir + config.attachments.path,
filename: this.getFilename(),
},
];

// Save files (permanently)
if (this.s3) {
await Promise.all(
files.map(async (file) => {
await this.uploadToS3(file.tmp, file.path + file.filename, file.mime);
await fs.unlink(file.tmp);
}),
);
} else {
await Promise.all(
files.map(async (file) => {
await mvAsync(file.tmp, file.path + file.filename, {});
}),
);
}
}

// Upload original attachment or its thumbnail to the S3 bucket
async uploadToS3(sourceFile, destPath, mimeType) {
const dispositionName = parsePath(this.fileName).name + parsePath(destPath).ext;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"ejs": "~3.1.6",
"ff-url-finder": "~2.3.7",
"file-type": "~16.5.2",
"fluent-ffmpeg": "^2.1.2",
"gifsicle": "~4.0.1",
"gm": "~1.23.1",
"grapheme-breaker": "0.3.2",
Expand Down
20 changes: 19 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,13 @@ __metadata:
languageName: node
linkType: hard

"async@npm:>=0.2.9":
version: 3.2.1
resolution: "async@npm:3.2.1"
checksum: 2ec69da2266d9418eefbf828a4b238f62cf07a5d5ba2d7fb91bc74aa588e38bbf7aef03df6d267dd38999802e97061768e5f58c8a38c62d71eb5c1a0b2957ef4
languageName: node
linkType: hard

"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
Expand Down Expand Up @@ -4527,6 +4534,16 @@ __metadata:
languageName: node
linkType: hard

"fluent-ffmpeg@npm:^2.1.2":
version: 2.1.2
resolution: "fluent-ffmpeg@npm:2.1.2"
dependencies:
async: ">=0.2.9"
which: ^1.1.1
checksum: c86a0e4f9fbb47a03c73eee9fe90e4ae85f6b728e64ad35091f2ac526d071f94e116684209baaef63e2b6d5ccfe322539c9bbecd1098f230dc289bda367ca250
languageName: node
linkType: hard

"for-in@npm:^1.0.1, for-in@npm:^1.0.2":
version: 1.0.2
resolution: "for-in@npm:1.0.2"
Expand Down Expand Up @@ -4658,6 +4675,7 @@ __metadata:
eslint-plugin-you-dont-need-lodash-underscore: ~6.12.0
ff-url-finder: ~2.3.7
file-type: ~16.5.2
fluent-ffmpeg: ^2.1.2
form-data: ~3.0.1
gifsicle: ~4.0.1
gm: ~1.23.1
Expand Down Expand Up @@ -11201,7 +11219,7 @@ typescript@~4.3.5:
languageName: node
linkType: hard

"which@npm:^1.2.14, which@npm:^1.2.9":
"which@npm:^1.1.1, which@npm:^1.2.14, which@npm:^1.2.9":
version: 1.3.1
resolution: "which@npm:1.3.1"
dependencies:
Expand Down