Skip to content

Commit

Permalink
feat: upload video (#29)
Browse files Browse the repository at this point in the history
* feat(web): accept videos

* feat(functions): generate thumbnail from movie using ffmpeg

* feat(web): show movie thumbnail
  • Loading branch information
KazuyaHara authored Nov 12, 2022
1 parent 1b55d5e commit 09d3244
Show file tree
Hide file tree
Showing 12 changed files with 358 additions and 191 deletions.
2 changes: 2 additions & 0 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
"exif-reader": "^1.0.3",
"firebase-admin": "^10.0.2",
"firebase-functions": "^3.18.0",
"fluent-ffmpeg": "^2.1.2",
"mkdirp": "^1.0.4",
"sharp": "^0.31.2"
},
"devDependencies": {
"@types/exif-reader": "^1.0.0",
"@types/fluent-ffmpeg": "^2.1.20",
"@types/mkdirp": "^1.0.2",
"@types/sharp": "^0.31.0",
"@typescript-eslint/eslint-plugin": "^5.42.0",
Expand Down
146 changes: 0 additions & 146 deletions functions/src/storage/media/onFinalize.ts

This file was deleted.

136 changes: 136 additions & 0 deletions functions/src/storage/media/onFinalize/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

import { firestore, storage } from 'firebase-admin';
import { logger } from 'firebase-functions';

import { zonedTimeToUtc } from 'date-fns-tz';
import exifReader from 'exif-reader';
import mkdirp from 'mkdirp';
import sharp from 'sharp';

const { increment, serverTimestamp } = firestore.FieldValue;
const getZonedTime = (date?: any) => {
if (!date) return new Date(0);
return zonedTimeToUtc(date, 'Asia/Tokyo');
};

export default async function processUploadedImage(name: string) {
// Download original file from bucket.
const localFile = path.join(os.tmpdir(), name);
await mkdirp(path.dirname(localFile));
const bucket = storage().bucket();
await bucket.file(name).download({ destination: localFile });

// Read exif from original file
const exif = await sharp(localFile)
.metadata()
.then((metadata) => metadata.exif && exifReader(metadata.exif));

// Generate a thumbnail using sharp.
const destinationFileName = 'thumbnail.jpg';
const destination = path.normalize(path.join(path.dirname(name), destinationFileName));
const localThumbnail = path.join(os.tmpdir(), destination);
await sharp(localFile)
.resize(1000)
.toFormat('jpeg')
.toFile(localThumbnail)
.catch((error) => {
logger.error('Error occurred while processing thumbnail');
throw new Error(error);
});

// Upload the thumbnail.
await bucket
.upload(localThumbnail, { destination })
.then(() => logger.info('Thumbnail uploaded.'))
.catch((error) => {
logger.error('Error occurred while uploading thumbnail');
throw new Error(error);
});

// Once the image has been uploaded delete the local files to free up disk space.
fs.unlinkSync(localFile);
fs.unlinkSync(localThumbnail);

// Register gear if needed
const gearName = exif?.image?.Model;
if (!gearName) return logger.log('No model name.');
const gearsRef = firestore().collection('gears');
const gear = await gearsRef
.where('model', '==', gearName)
.limit(1)
.get()
.then((querySnapshot) => {
if (querySnapshot.empty) return null;
return querySnapshot.docs[0];
});
let gearId = gear?.id;
if (gear) {
await gear.ref
.set({ items: increment(1), updatedAt: serverTimestamp() }, { merge: true })
.then(() => logger.info('Gear items has been incremented.'))
.catch((error) => {
logger.error('Error occurred while updating gear');
throw new Error(error);
});
} else {
await gearsRef
.add({
items: increment(1),
maker: exif.image?.Make || null,
model: gearName,
name: gearName,
type: 'photo',
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
})
.then((ref) => {
logger.info('New gear has been added.');
gearId = ref.id;
})
.catch((error) => {
logger.error('Error occurred while creating gear');
throw new Error(error);
});
}

// Update item
const mediaPath = path.dirname(name);
const itemPath = mediaPath.replace('media/', 'items/');
const itemRef = firestore().doc(itemPath);
return itemRef
.set(
{
date: getZonedTime(exif?.exif?.DateTimeOriginal),
gearId,
medium: {
metadata: {
...exif,
exif: {
...exif?.exif,
DateTimeDigitized: getZonedTime(exif?.exif?.DateTimeDigitized),
DateTimeOriginal: getZonedTime(exif?.exif?.DateTimeOriginal),
},
image: {
...exif?.image,
ModifyDate: getZonedTime(exif?.image?.ModifyDate),
},
thumbnail: {
...exif?.thumbnail,
ModifyDate: getZonedTime(exif?.thumbnail?.ModifyDate),
},
},
thumbnail: `${mediaPath}/${destinationFileName}`,
},
updatedAt: serverTimestamp(),
},
{ merge: true }
)
.then(() => logger.info('Item information updated.'))
.catch((error) => {
logger.error('Error occurred while updating thumbnail path');
throw new Error(error);
});
}
19 changes: 19 additions & 0 deletions functions/src/storage/media/onFinalize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as path from 'path';

import { logger, region } from 'firebase-functions';

import processUploadedImage from './image';
import processUploadedVideo from './video';

export const processUploadedMedia = region('asia-northeast1')
.storage.object()
.onFinalize(async ({ contentType, name }) => {
if (!contentType) return logger.log('File has no Content-Type.');
if (!name) return logger.log('File has no file name.');
const fileName = path.basename(name).split('.')[0];
if (fileName === 'thumbnail') return logger.log('Already a Thumbnail.');

if (contentType.startsWith('image/')) return processUploadedImage(name);
if (contentType.startsWith('video/')) return processUploadedVideo(name);
return logger.log('This content is not supported.');
});
Loading

0 comments on commit 09d3244

Please sign in to comment.