feat: adds gif download option in addition to video (#278)

* feat: adding gif recorder

* feat: adding gif convertion

* lint

* pr follow up

* PR follow up
This commit is contained in:
Erick
2022-12-21 14:28:42 -03:00
committed by GitHub
parent 6591852cf4
commit d306efa5c0
9 changed files with 185 additions and 29 deletions

View File

@ -27,6 +27,7 @@
"firebase-mock": "^2.3.2",
"jest": "^29.3.1",
"jest-mock-extended": "^3.0.1",
"jest-when": "^3.5.2",
"mocha": "^10.2.0",
"prettier": "^2.8.1",
"prettier-eslint": "^12.0.0",
@ -5722,6 +5723,15 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-when": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/jest-when/-/jest-when-3.5.2.tgz",
"integrity": "sha512-4rDvnhaWh08RcPsoEVXgxRnUIE9wVIbZtGqZ5x2Wm9Ziz9aQs89PipQFmOK0ycbEhVAgiV3MUeTXp3Ar4s2FcQ==",
"dev": true,
"peerDependencies": {
"jest": ">= 25"
}
},
"node_modules/jest-worker": {
"version": "29.3.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.3.1.tgz",
@ -13296,6 +13306,13 @@
"string-length": "^4.0.1"
}
},
"jest-when": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/jest-when/-/jest-when-3.5.2.tgz",
"integrity": "sha512-4rDvnhaWh08RcPsoEVXgxRnUIE9wVIbZtGqZ5x2Wm9Ziz9aQs89PipQFmOK0ycbEhVAgiV3MUeTXp3Ar4s2FcQ==",
"dev": true,
"requires": {}
},
"jest-worker": {
"version": "29.3.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.3.1.tgz",

View File

@ -65,8 +65,11 @@ function setUpFileMakePublicFunction(returnValue) {
function setUpFfmpeg(currentEvent: string) {
ffmpeg = mockDeep<ffmpeg>({
addInput: jest.fn().mockReturnThis(),
addOutput: jest.fn().mockReturnThis(),
addOptions: jest.fn().mockReturnThis(),
inputFPS: jest.fn().mockReturnThis(),
videoFilters: jest.fn().mockReturnThis(),
run: jest.fn().mockReturnThis(),
mergeToFile: jest.fn().mockReturnThis(),
on: jest.fn((event, handler) => {
if (currentEvent == event && event === 'error') {
@ -135,8 +138,11 @@ jest.mock('busboy', () => () => {
jest.mock('fluent-ffmpeg', () => () => {
return {
addInput: jest.fn().mockReturnThis(),
addOutput: jest.fn().mockReturnThis(),
run: jest.fn().mockReturnThis(),
addOptions: jest.fn().mockReturnThis(),
inputFPS: jest.fn().mockReturnThis(),
videoFilters: jest.fn().mockReturnThis(),
mergeToFile: jest.fn().mockReturnThis(),
on: jest.fn((event, handler) => {
if (event === 'end') {
@ -201,9 +207,10 @@ describe('convert', () => {
await convert.convert(mockRequest, mockResponse);
expect(mockResponse.status).toHaveBeenCalledWith(200);
expect(mockResponse.send).toHaveBeenCalledWith(
'https://storage.googleapis.com/test-bucket/test-file'
);
expect(mockResponse.send).toHaveBeenCalledWith({
video_url: 'https://storage.googleapis.com/test-bucket/test-file',
gif_url: 'https://storage.googleapis.com/test-bucket/test-file',
});
});
it('returns status 500 on error', async () => {
@ -242,9 +249,9 @@ describe('convertImages', () => {
setUpMockReadStream('finish');
setUpFileMakePublicFunction(true);
const { status, url } = await convert.convertImages(mockRequest);
const { status, videoUrl } = await convert.convertImages(mockRequest);
expect(status).toEqual(200);
expect(url).toEqual('https://storage.googleapis.com/test-bucket/test-file');
expect(videoUrl).toEqual('https://storage.googleapis.com/test-bucket/test-file');
});
});
@ -342,6 +349,24 @@ describe('convertToVideo', () => {
});
});
describe('convertVideoToGif', () => {
it('returns path for the file', async () => {
setUpFfmpeg('end');
await expect(
convert.convertVideoToGif(ffmpeg, `${tempDir}/video.mp4`, tempDir)
).resolves.toBe(`${tempDir}/video.gif`);
});
it('throws error when unable to convert video.', async () => {
setUpFfmpeg('error');
await expect(
convert.convertVideoToGif(ffmpeg, `${tempDir}/video.mp4`, tempDir)
).rejects.toThrow();
});
});
describe('uploadFile', () => {
const videoName = 'test-video-name';
const videoPath = 'test-path';

View File

@ -17,10 +17,13 @@ export const errorMessage = 'Something went wrong';
*/
export const convert = functions.https.onRequest(async (req, res) => {
try {
const { status, url } = await convertImages(req);
const { status, videoUrl, gifUrl } = await convertImages(req);
res.set('Access-Control-Allow-Origin', '*');
res.status(status).send(url);
res.status(status).send({
video_url: videoUrl,
gif_url: gifUrl,
});
} catch (error) {
functions.logger.error(error);
res.status(500).send(errorMessage);
@ -29,7 +32,11 @@ export const convert = functions.https.onRequest(async (req, res) => {
export async function convertImages(
req: functions.https.Request,
): Promise<{ status: number; url: string }> {
): Promise<{
status: number;
videoUrl: string,
gifUrl: string,
}> {
let tempDir: string | null = null;
try {
@ -48,13 +55,14 @@ export async function convertImages(
tempDir = await createTempDirectory(userId);
const busboy = _busboy({ headers: req.headers });
const frames = await readFramesFromRequest(busboy, req, tempDir);
const videoPath = await convertToVideo(ffmpeg(), frames, tempDir);
const url = await uploadFile(userId + '.mp4', videoPath);
return {
status: 200,
url: url,
};
const videoPath = await convertToVideo(ffmpeg(), frames, tempDir);
const gifPath = await convertVideoToGif(ffmpeg(), videoPath, tempDir);
const videoUrl = await uploadFile(userId + '.mp4', videoPath);
const gifUrl = await uploadFile(userId + '.gif', gifPath);
return { status: 200, videoUrl, gifUrl };
} catch (error) {
functions.logger.error(error);
throw error;
@ -149,6 +157,28 @@ export async function convertToVideo(
});
}
export async function convertVideoToGif(
ffmpeg: ffmpeg,
videoPath: string,
folder: string
): Promise<string> {
const gifPath = `${folder}/video.gif`;
return new Promise((resolve, reject) => {
ffmpeg
.addInput(videoPath)
.videoFilters('fps=10,split [o1] [o2];[o1] palettegen [p]; [o2] fifo [o3];[o3] [p] paletteuse')
.addOutput(gifPath)
.on('end', () => {
resolve(gifPath);
})
.on('error', function(error) {
functions.logger.error(error);
return reject( error);
})
.run();
});
}
export function uploadFile(
videoName: string,
path: string