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