feat: add on-demand live transcoding for browser-incompatible video formats#1145
Open
cat101 wants to merge 4 commits into
Open
feat: add on-demand live transcoding for browser-incompatible video formats#1145cat101 wants to merge 4 commits into
cat101 wants to merge 4 commits into
Conversation
| } | ||
|
|
||
| async function getOrStartJob(fullMediaPath: string): Promise<HLSJob> { | ||
| const stat = await fsp.stat(fullMediaPath); |
| const deadline = Date.now() + SEGMENT_WAIT_TIMEOUT_MS; | ||
| while (Date.now() < deadline) { | ||
| try { | ||
| await fsp.access(filePath); |
|
|
||
| const isInit = filename === 'init.mp4'; | ||
| res.setHeader('Content-Type', isInit ? 'video/mp4' : 'video/iso.segment'); | ||
| res.sendFile(filePath); |
| HLSMWs.checkEnabled, | ||
| AuthenticationMWs.authenticate, | ||
| AuthenticationMWs.normalizePathParam('mediaPath'), | ||
| AuthenticationMWs.authoriseMedia('mediaPath'), |
| AuthenticationMWs.authenticate, | ||
| AuthenticationMWs.normalizePathParam('mediaPath'), | ||
| AuthenticationMWs.authoriseMedia('mediaPath'), | ||
| HLSMWs.servePlaylist |
| HLSMWs.checkEnabled, | ||
| AuthenticationMWs.authenticate, | ||
| AuthenticationMWs.normalizePathParam('mediaPath'), | ||
| AuthenticationMWs.authoriseMedia('mediaPath'), |
| AuthenticationMWs.authenticate, | ||
| AuthenticationMWs.normalizePathParam('mediaPath'), | ||
| AuthenticationMWs.authoriseMedia('mediaPath'), | ||
| HLSMWs.serveSegmentFile |
2ee60bd to
5d01fe8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).