DTP Base provides a scalable and secure Node.js application development harness ready for production service.
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

// 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);
}
}