18 changed files with 394 additions and 68 deletions
@ -0,0 +1,123 @@ |
|||||
|
// 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); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
.uk-lightbox-toolbar { |
||||
|
background-color: rgba(0,0,0, 0.85); |
||||
|
border-top: solid 1px #6a6a6a; |
||||
|
} |
Loading…
Reference in new issue