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 thumbnail #334

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ jobs:
id-token: 'write'

steps:
- name: Setup ffmpeg
uses: FedericoCarboni/setup-ffmpeg@v2
- name: Setup ffmpeg built with libsvtav1
# https://www.reddit.com/r/ffmpeg/comments/tb5frl/comment/i0glb7d/
run: |
wget https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n6.1-latest-linux64-gpl-6.1.tar.xz &&
tar Jxvf ffmpeg-n6.1-latest-linux64-gpl-6.1.tar.xz &&
sudo cp -r ffmpeg-n6.1-latest-linux64-gpl-6.1/bin/* /usr/local/bin/
- run: ffmpeg -encoders
- uses: actions/checkout@v3
with:
submodules: true
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"rumors-db:install": "cd test/rumors-db && npm i"
},
"dependencies": {
"@cofacts/media-manager": "^0.3.0",
"@cofacts/media-manager": "^0.3.1",
"@elastic/elasticsearch": "^6.8.6",
"@google-cloud/bigquery": "^6.2.0",
"@google-cloud/vision": "^3.1.4",
Expand Down
Binary file not shown.
34 changes: 34 additions & 0 deletions src/graphql/__tests__/media-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import fetch from 'node-fetch';
import gql from 'util/GraphQL';
import client from 'util/client';
import delayForMs from 'util/delayForMs';
import { uploadMedia } from 'graphql/mutations/CreateMediaArticle';
import { getReplyRequestId } from 'graphql/mutations/CreateOrUpdateReplyRequest';
import replaceMedia from 'scripts/replaceMedia';
import { VIDEO_PREVIEW } from 'util/mediaManager';

if (process.env.GCS_CREDENTIALS && process.env.GCS_BUCKET_NAME) {
// File server serving test input file in __fixtures__/media-integration
Expand Down Expand Up @@ -165,6 +167,38 @@ if (process.env.GCS_CREDENTIALS && process.env.GCS_BUCKET_NAME) {
15000
);

it(
'uploads video and can get media entry',
async () => {
const mediaEntry = await uploadMedia({
// Video credit: Hitokage Production https://hitokageproduction.com/article/83
//
// Re-encoded with the following ffmpeg options
// `-c:a aac -c:v libx264 -vf "scale=-1:720,fps=30,format=yuv420p" -crf 30 -movflags +faststart`
mediaUrl: `${serverUrl}/video.mp4`,
articleType: 'VIDEO',
});

expect(mediaEntry.variants).toMatchInlineSnapshot(`
Array [
"original",
"av1-240p5s",
"av1-0.75x30s",
]
`);

let previewCacheControlHeader;
while (!previewCacheControlHeader) {
await delayForMs(1000); // Wait for upload to finish
const file = mediaEntry.getFile(VIDEO_PREVIEW);
if (!(await file.exists())[0]) continue;
previewCacheControlHeader = (await file.getMetadata())[0].cacheControl;
}
console.log('previewCacheControlHeader', previewCacheControlHeader);
},
15000
);

it(
'can replace media for article',
async () => {
Expand Down
23 changes: 13 additions & 10 deletions src/graphql/models/Article.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import ArticleReference from 'graphql/models/ArticleReference';
import User, { userFieldResolver } from 'graphql/models/User';
import mediaManager, {
IMAGE_PREVIEW,
VIDEO_PREVIEW,
IMAGE_THUMBNAIL,
VIDEO_THUMBNAIL,
} from 'util/mediaManager';
import ArticleReplyStatusEnum from './ArticleReplyStatusEnum';
import ArticleReply from './ArticleReply';
Expand Down Expand Up @@ -528,17 +530,18 @@ const Article = new GraphQLObjectType({
if (!attachmentHash) return null;

let variant = 'original';
switch (variantArg) {
case 'PREVIEW':
if (articleType === 'IMAGE') {
variant = IMAGE_PREVIEW;
}
switch (`${articleType}_${variantArg}`) {
case 'IMAGE_PREVIEW':
variant = IMAGE_PREVIEW;
break;

case 'THUMBNAIL':
if (articleType === 'IMAGE') {
variant = IMAGE_THUMBNAIL;
}
case 'VIDEO_PREVIEW':
variant = VIDEO_PREVIEW;
break;
case 'IMAGE_THUMBNAIL':
variant = IMAGE_THUMBNAIL;
break;
case 'VIDEO_THUMBNAIL':
variant = VIDEO_THUMBNAIL;
break;
}

Expand Down
63 changes: 63 additions & 0 deletions src/graphql/mutations/CreateMediaArticle.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { PassThrough, Duplex } from 'stream';
import { GraphQLString, GraphQLNonNull } from 'graphql';
import sharp from 'sharp';
import ffmpeg from 'fluent-ffmpeg';
import { MediaType, variants } from '@cofacts/media-manager';
import mediaManager, {
IMAGE_PREVIEW,
IMAGE_THUMBNAIL,
VIDEO_PREVIEW,
VIDEO_THUMBNAIL,
} from 'util/mediaManager';
import { assertUser, getContentDefaultStatus } from 'util/user';
import client from 'util/client';
Expand Down Expand Up @@ -34,6 +38,23 @@ const VALID_ARTICLE_TYPE_TO_MEDIA_TYPE = {
AUDIO: MediaType.audio,
};

/**
* @param {function} setupFn - A function to setup the ffmpeg command
* @returns A transform stream
*/
function getFFMpegTransform(setupFn = cmd => cmd) {
const input = new PassThrough();
const output = setupFn(
ffmpeg(input)
// .on('stderr', function(stderrLine) {
// console.log('[ffmpeg]', stderrLine);
// })
.inputFormat('mp4')
).pipe();

return Duplex.from({ readable: output, writable: input });
}

/**
* Upload media of specified article type from the given mediaUrl
*
Expand Down Expand Up @@ -77,6 +98,48 @@ export async function uploadMedia({ mediaUrl, articleType }) {
},
];

case MediaType.video:
return [
variants.original(contentType),
{
name: VIDEO_THUMBNAIL,
contentType: 'video/mp4',
transform: getFFMpegTransform(cmd =>
cmd
.size('?x240')
.videoCodec('libsvtav1')
.videoFilters('setpts=0.5*PTS') // 2x speed
.noAudio()
.duration(5)
.outputOptions([
// https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues/346#issuecomment-67299526
'-movflags dash',
// Smaller file size
'-crf 40',
])
.format('mp4')
),
},
{
name: VIDEO_PREVIEW,
contentType: 'video/mp4',
transform: getFFMpegTransform(cmd =>
cmd
.videoCodec('libsvtav1')
.size('75%')
.audioCodec('copy')
.duration(30)
.outputOptions([
// https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues/346#issuecomment-67299526
'-movflags dash',
// Slightly better quality
'-crf 38',
])
.format('mp4')
),
},
];

default:
return variants.defaultGetVariantSettings(options);
}
Expand Down
3 changes: 3 additions & 0 deletions src/util/mediaManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { MediaManager } from '@cofacts/media-manager';
export const IMAGE_PREVIEW = 'webp600w';
export const IMAGE_THUMBNAIL = 'jpg240h';

export const VIDEO_PREVIEW = 'av1-0.75x30s';
export const VIDEO_THUMBNAIL = 'av1-240p5s';

const mediaManager = new MediaManager({
bucketName: process.env.GCS_BUCKET_NAME,
credentialsJSON: process.env.GCS_CREDENTIALS,
Expand Down