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.
 
 
 
 

123 lines
3.5 KiB

// video.js
// Copyright (C) 2024 DTP Technologies, LLC
// All Rights Reserved
'use strict';
import express from 'express';
import { pipeline } from 'node:stream';
import { SiteController, SiteError } from '../../lib/site-lib.js';
export default class VideoController extends SiteController {
static get name ( ) { return 'VideoController'; }
static get slug ( ) { return 'video'; }
constructor (dtp) {
super(dtp, VideoController);
}
async start ( ) {
const {
limiter: limiterService,
session: sessionService,
} = this.dtp.services;
const limiterConfig = limiterService.config.video;
this.templates = {
passwordResetComplete: this.loadViewTemplate('auth/password-reset-complete.pug'),
};
const authRequired = sessionService.authCheckMiddleware({ requireLogin: true });
const router = express.Router();
this.dtp.app.use('/video', authRequired, router);
router.use(async (req, res, next) => {
res.locals.currentView = 'video';
return next();
});
router.param('videoId', this.populateVideoId.bind(this));
router.get(
'/:videoId/media',
limiterService.create(limiterConfig.getVideoMedia),
this.getVideoMedia.bind(this),
);
return router;
}
async populateVideoId (req, res, next, videoId) {
const { video: videoService } = this.dtp.services;
try {
res.locals.video = await videoService.getVideoById(videoId);
if (!res.locals.video) {
throw new SiteError(404, 'Video not found');
}
return next();
} catch (error) {
this.log.error('failed to populate video', { videoId, error });
return next(error);
}
}
async getVideoMedia (req, res, next) {
const { minio: minioService } = this.dtp.services;
try {
let artifact = res.locals.video.media;
const artifactStat = await minioService.statObject(artifact.bucket, artifact.key);
const fileInfo = Object.assign({ }, artifact);
const range = req.get('range');
if (range) {
let [start, end] = range.replace(/bytes=/, '').split('-').map((term) => parseInt(term.trim(), 10));
if (!isNaN(start) && isNaN(end)) {
end = artifactStat.size - 1;
}
if (isNaN(start) && !isNaN(end)) {
start = artifactStat.size - end;
end = artifactStat.size - 1;
}
fileInfo.range = { start, end };
}
this.log.debug('starting video media stream', {
media: artifact,
fileInfo,
});
const stream = await minioService.openDownloadStream(fileInfo);
if (fileInfo.range) {
res.writeHead(206, {
'Content-Type': 'video/mp4',
'Content-Length': fileInfo.range.end - fileInfo.range.start + 1,
'Accept-Ranges': 'bytes',
'Content-Range': `bytes ${fileInfo.range.start}-${fileInfo.range.end}/${artifactStat.size}`,
'Cache-Control': 'public, maxage=86400, s-maxage=86400, immutable',
});
} else {
res.writeHead(200, {
'Accept-Ranges': 'bytes',
'Content-Type': 'video/mp4',
'Content-Length': artifactStat.size,
'Cache-Control': 'public, maxage=86400, s-maxage=86400, immutable',
});
}
pipeline(stream, res, (err) => {
if (err) {
this.log.debug('failed to stream media', { err });
return;
}
this.log.info('media stream sent');
});
} catch (error) {
this.log.error('failed to open media stream', { videoId: res.locals.video._id, error });
return next(error);
}
}
}