feat: add on-demand live transcoding for browser-incompatible video formats#1145
feat: add on-demand live transcoding for browser-incompatible video formats#1145cat101 wants to merge 5 commits into
Conversation
| } | ||
|
|
||
| async function getOrStartJob(fullMediaPath: string): Promise<HLSJob> { | ||
| const stat = await fsp.stat(fullMediaPath); |
There was a problem hiding this comment.
Mitigated by the route middleware chain: AuthenticationMWs.normalizePathParam('mediaPath') (path.normalize + strips leading ../) and AuthenticationMWs.authoriseMedia('mediaPath') (403 unless the user has gallery ACL on that media) both run before servePlaylist. Same protection as every existing /api/gallery/photo/:mediaPath/... and /api/gallery/video/:mediaPath/bestFit route in GalleryRouter. Treating as false positive — CodeQL can't reason about Express middleware chains.
| HLSMWs.checkEnabled, | ||
| AuthenticationMWs.authenticate, | ||
| AuthenticationMWs.normalizePathParam('mediaPath'), | ||
| AuthenticationMWs.authoriseMedia('mediaPath'), |
There was a problem hiding this comment.
Consistent with the rest of the codebase — pigallery2 has no rate-limiting on any existing route (no express-rate-limit dep, no usage in src/). /api/gallery/photo, /api/gallery/video/bestFit, /api/gallery/zip, and login all do authorization + FS access without a limiter. Adding one only to HLS routes would be inconsistent; if desired, it belongs in a separate repo-wide PR layered at Router.ts.
| AuthenticationMWs.authenticate, | ||
| AuthenticationMWs.normalizePathParam('mediaPath'), | ||
| AuthenticationMWs.authoriseMedia('mediaPath'), | ||
| HLSMWs.servePlaylist |
There was a problem hiding this comment.
Consistent with the rest of the codebase — pigallery2 has no rate-limiting on any existing route (no express-rate-limit dep, no usage in src/). /api/gallery/photo, /api/gallery/video/bestFit, /api/gallery/zip, and login all do authorization + FS access without a limiter. Adding one only to HLS routes would be inconsistent; if desired, it belongs in a separate repo-wide PR layered at Router.ts.
| HLSMWs.checkEnabled, | ||
| AuthenticationMWs.authenticate, | ||
| AuthenticationMWs.normalizePathParam('mediaPath'), | ||
| AuthenticationMWs.authoriseMedia('mediaPath'), |
There was a problem hiding this comment.
Consistent with the rest of the codebase — pigallery2 has no rate-limiting on any existing route (no express-rate-limit dep, no usage in src/). /api/gallery/photo, /api/gallery/video/bestFit, /api/gallery/zip, and login all do authorization + FS access without a limiter. Adding one only to HLS routes would be inconsistent; if desired, it belongs in a separate repo-wide PR layered at Router.ts.
| AuthenticationMWs.authenticate, | ||
| AuthenticationMWs.normalizePathParam('mediaPath'), | ||
| AuthenticationMWs.authoriseMedia('mediaPath'), | ||
| HLSMWs.serveSegmentFile |
There was a problem hiding this comment.
Consistent with the rest of the codebase — pigallery2 has no rate-limiting on any existing route (no express-rate-limit dep, no usage in src/). /api/gallery/photo, /api/gallery/video/bestFit, /api/gallery/zip, and login all do authorization + FS access without a limiter. Adding one only to HLS routes would be inconsistent; if desired, it belongs in a separate repo-wide PR layered at Router.ts.
5d01fe8 to
e385896
Compare
3b65b1d to
1a793f6
Compare
CodeQL flagged path.join(cacheDir, filename) in serveSegmentFile where filename comes directly from req.params. Although the join is bounded inside tmp/hls/, the player only ever requests init.mp4 or segment_NNN.m4s — reject anything else with 404 before touching the filesystem. Closes CodeQL findings 219, 220 (and the segment-route half of 222, 224) in bpatrik#1145.
CodeQL flagged path.join(cacheDir, filename) in serveSegmentFile where filename comes directly from req.params. Although the join is bounded inside tmp/hls/, the player only ever requests init.mp4 or segment_NNN.m4s — reject anything else with 404 before touching the filesystem. Closes CodeQL findings 219, 220 (and the segment-route half of 222, 224) in bpatrik#1145.
4ac99ae to
f0f302a
Compare
Hi. I love that pigallery2 works over the filesystem and does not need to create a parallel database/index. I found that one exception was the need to pre-transcode all the incompatible videos. In may case, I have over 100k files which most of them will never be accessed. In my case I also run on a beefy server so I can transcode on near realtime.
Since some folks may run on smaller hardware I have made this feature as opt-in. Like you did with transcoding I have built a segment cache so that work is done only once. The feature also plays nice with existing transcoded files
I know this is a more complex feature so let me know if you need any changes to merge it. This feature was executed using AI. It is running in my home server and I'm using it/testing it/enjoying it daily :)
--------------------------- AI Generated summary --------------------
Adds an opt-in HLS live transcoding fallback for video formats that browsers cannot decode natively (RealMedia, AVI, MKV/non-H.264, MOV, WMV, etc.).
How it works
When a video fails to load natively (
onSourceError), andliveVideoTranscodingEnabledistruein Settings → Media → Video, the browser lazy-loadshls.jsv1.x and requests an HLS playlist from the new/api/gallery/hls/:path/playlist.m3u8endpoint.The backend spawns FFmpeg on-demand:
-c copy) for H.264+AAC sources (MKV, etc.) — near-instantlibx264 + aac) for incompatible codecs (RealMedia, AVI, WMV, etc.)fMP4 segments (
.m4s) are served progressively as FFmpeg writes them. The playlist is returned as soon as the first segment is ready (~6 seconds), using-hls_playlist_type eventso hls.js treats it as a live/growing stream. Playback starts immediately while remaining segments are transcoded in the background. When FFmpeg finishes it appends#EXT-X-ENDLIST, converting the playlist to a standard VOD manifest — hls.js detects this automatically and enables full seeking.FFmpeg flags for early-start streaming:
-hls_playlist_type event— playlist grows dynamically;#EXT-X-ENDLISTwritten at end-hls_init_time 0— write first segment at first keyframe boundary (minimal delay)-hls_flags independent_segments+discont_start— decoder-independent segments;discont_starthandles PTS discontinuities common in RealMedia/AVI sources-force_key_frames expr:gte(t,n_forced*6)(transcode mode only) — inserts an IDR keyframe every 6s so FFmpeg can always cut at the target boundary. Without this, RealMedia sources produce 12–15s segments (TARGETDURATION: 12) with 22s gaps between segments. With forced keyframes:TARGETDURATION: 6, exact 6sEXTINF, and playback resumes in ~6s after each segment is encoded.Playlist serving strategy (server-side long-poll): The playlist endpoint holds the HTTP connection open until FFmpeg writes a new segment (or
ENDLIST), then responds immediately. This eliminates hls.js's fixedtargetDuration/2poll interval lag — the player receives the updated manifest the moment a segment is produced (~100ms after FFmpeg flushes it), rather than waiting up to 3s for its scheduled poll.Cache-Control: no-cache, no-storeensures the response is never cached.Interrupted transcode recovery: on startup, if a cached
playlist.m3u8exists but lacks#EXT-X-ENDLIST, the cache dir is deleted and FFmpeg is re-spawned from scratch.What's unchanged
/bestFit(no HLS overhead)<video>element,<source>, all player controls (seek, volume, loop, fullscreen)New & modified files
src/backend/middlewares/HLSMWs.tssrc/backend/routes/HLSRouter.tsplaylist.m3u8,init.mp4,segment_*.m4ssrc/frontend/.../hls.d.tstsccompiles beforenpm installcompletessrc/common/config/public/ClientConfig.tsliveVideoTranscodingEnabled: boolean = falseinClientVideoConfigsrc/backend/routes/Router.tsHLSRoutersrc/backend/model/jobs/jobs/TempFolderCleaningJob.tsrm -rf ./tmp/hls/on cleanupsrc/frontend/.../MediaIcon.tsgetHLSPlaylistPath()methodsrc/frontend/.../media.lightbox.gallery.component.tsonSourceError()fallback +initHLSPlayer()/destroyHLS()package.json"hls.js": "1.5.20"dependencyNew config
Media.Video.liveVideoTranscodingEnabled(boolean, default:false) — appears in the standard Media → Video settings panel, tagged asexperimental.Dependencies
hls.jsv1.x (frontend, lazy-loaded — only bundled when feature is used)fluent-ffmpeg+ffmpeg-static)Segment cache layout
Cache key includes
mtime— invalidates automatically when source file changes. Cleaned by TempFolderCleaningJob (Settings → Jobs → Temp Folder Cleaning).