You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
5.1 KiB
197 lines
5.1 KiB
// video.js
|
|
// Copyright (C) 2024 DTP Technologies, LLC
|
|
// All Rights Reserved
|
|
|
|
'use strict';
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import mongoose from 'mongoose';
|
|
const Video = mongoose.model('Video');
|
|
|
|
import mime from 'mime';
|
|
import numeral from 'numeral';
|
|
|
|
import { SiteService, SiteError } from '../../lib/site-lib.js';
|
|
import user from '../models/user.js';
|
|
|
|
export default class VideoService extends SiteService {
|
|
|
|
static get name ( ) { return 'VideoService'; }
|
|
static get slug ( ) { return 'video'; }
|
|
|
|
constructor (dtp) {
|
|
super(dtp, VideoService);
|
|
}
|
|
|
|
async start ( ) {
|
|
const { user: userService } = this.dtp.services;
|
|
await super.start();
|
|
|
|
await fs.promises.mkdir(process.env.VIDEO_WORK_PATH, { recursive: true });
|
|
|
|
this.populateVideo = [
|
|
{
|
|
path: 'owner',
|
|
select: userService.USER_SELECT,
|
|
},
|
|
{
|
|
path: 'thumbnail',
|
|
},
|
|
];
|
|
}
|
|
|
|
async createVideo (owner, attachmentDefinition, file) {
|
|
const NOW = new Date();
|
|
const { media: mediaService, minio: minioService } = this.dtp.services;
|
|
|
|
this.log.debug('running ffprobe on uploaded library media', { path: file.path });
|
|
const metadata = await mediaService.ffprobe(file.path);
|
|
this.log.debug('video file probed', { path: file.path, metadata });
|
|
|
|
if (user.membership) {
|
|
if (file.size > (100 * 1024 * 1024)) {
|
|
throw new SiteError(403, 'Video attachments are limited to 100MB file size.');
|
|
}
|
|
} else {
|
|
if (file.size > (25 * 1024 * 1024)) {
|
|
throw new SiteError(403, 'Video attachments are limited to 25MB file size.');
|
|
}
|
|
}
|
|
|
|
const video = new Video();
|
|
video.created = NOW;
|
|
video.owner = owner._id;
|
|
video.duration = metadata.duration;
|
|
video.status = 'new';
|
|
|
|
const ownerId = owner._id.toString();
|
|
const videoId = video._id.toString();
|
|
const fileBucket = process.env.MINIO_VIDEO_BUCKET || 'videos';
|
|
const mediaExt = mime.getExtension(file.mimetype) || 'dat';
|
|
const fileKey = `/video/${ownerId.slice(0, 3)}/${ownerId}/${videoId.slice(0, 3)}/${videoId}/media.${mediaExt}`;
|
|
|
|
this.log.debug('uploading video to storage', {
|
|
_id: video._id,
|
|
bucket: fileBucket,
|
|
key: fileKey,
|
|
path: file.path,
|
|
});
|
|
|
|
await minioService.uploadFile({
|
|
bucket: fileBucket,
|
|
key: fileKey,
|
|
filePath: file.path,
|
|
metadata: {
|
|
'Video-ID': videoId,
|
|
'Content-Type': file.mimetype,
|
|
'Content-Length': file.size,
|
|
},
|
|
});
|
|
|
|
video.media = {
|
|
bucket: fileBucket,
|
|
key: fileKey,
|
|
metadata: {
|
|
type: file.mimetype,
|
|
size: metadata.format.size,
|
|
bitRate: metadata.format.bit_rate,
|
|
duration: metadata.duration,
|
|
video: {
|
|
width: metadata.width,
|
|
height: metadata.height,
|
|
fps: metadata.fps,
|
|
},
|
|
},
|
|
};
|
|
|
|
await video.save();
|
|
|
|
await this.extractVideoAttachmentThumbnail(video, file);
|
|
|
|
return video.toObject();
|
|
}
|
|
|
|
async extractVideoAttachmentThumbnail (video, file) {
|
|
const { media: mediaService } = this.dtp.services;
|
|
|
|
const thumbnailFile = path.join(process.env.VIDEO_WORK_PATH, `${video._id}.png`);
|
|
const ffmpegThumbnailArgs = [
|
|
'-y', '-i', file.path,
|
|
'-ss', numeral(video.media.metadata.duration * 0.05).format('hh:mm:ss'),
|
|
'-frames:v', '1',
|
|
thumbnailFile,
|
|
];
|
|
|
|
this.log.debug('extracting video thumbnail', { ffmpegThumbnailArgs });
|
|
await mediaService.ffmpeg(ffmpegThumbnailArgs);
|
|
|
|
this.log.info('persisting thumbnail to video', { videoId: video._id });
|
|
await this.setVideoThumbnailImage(video, thumbnailFile);
|
|
}
|
|
|
|
async setVideoThumbnailImage (video, thumbnailFile) {
|
|
const { image: imageService } = this.dtp.services;
|
|
|
|
await this.removeVideoThumbnailImage(video);
|
|
|
|
const imageFileDesc = { path: thumbnailFile };
|
|
const outputs = [
|
|
{
|
|
width: 960,
|
|
height: 540,
|
|
format: 'jpeg',
|
|
formatParameters: { quality: 95 },
|
|
}
|
|
];
|
|
await imageService.processImageFile(
|
|
video.owner._id,
|
|
imageFileDesc,
|
|
outputs
|
|
);
|
|
|
|
await Video.updateOne(
|
|
{ _id: video._id },
|
|
{ $set: { thumbnail: outputs[0].image._id } },
|
|
);
|
|
|
|
return outputs[0].image._id;
|
|
}
|
|
|
|
async removeVideoThumbnailImage (video) {
|
|
const { image: imageService } = this.dtp.services;
|
|
|
|
if (!video.thumbnail) {
|
|
return;
|
|
}
|
|
|
|
this.log.info('removing current video thumbnail', { videoId: video._id });
|
|
await imageService.deleteImage(video.thumbnail);
|
|
await Video.updateOne({ _id: video._id }, { $unset: { thumbnail: 1 } });
|
|
|
|
delete video.thumbnail;
|
|
}
|
|
|
|
async setVideoStatus (video, status) {
|
|
await Video.updateOne({ _id: video._id }, { $set: { status } });
|
|
}
|
|
|
|
async getVideoById (videoId) {
|
|
const video = await Video
|
|
.findOne({ _id: videoId })
|
|
.populate(this.populateVideo)
|
|
.lean();
|
|
return video;
|
|
}
|
|
|
|
async removeVideo (video) {
|
|
const { minio: minioService } = this.dtp.services;
|
|
|
|
if (video.thumbnail) {
|
|
await this.removeVideoThumbnailImage(video);
|
|
}
|
|
|
|
await minioService.removeObject(video.media.bucket, video.media.key);
|
|
}
|
|
}
|