From b1fc23abbb5c9065a8d773a45ad154dd41e6c42f Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:13:19 +0200 Subject: [PATCH] fix(playback): route MKV sources as Matroska (#1210) * docs(playback): design native MKV source routing * fix(playback): route MKV sources as Matroska * chore(playback): address MKV review feedback --- docs/architecture/embedded-inline-playback.md | 7 +++++ .../web-player-view.component.spec.ts | 27 +++++++++++++++++++ .../web-player-view.component.ts | 8 +++--- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index 6dbb15a33..0348653e0 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -264,6 +264,13 @@ The diagnostic surface covers the inline player viewport when playback fails, wi URL extension metadata is filtered before diagnostics and player selection use it. Web script extensions such as `.php` are not shown as stream containers; explicit media query metadata such as `extension=ts` or `format=m3u8` is preferred when present. +MKV sources are attempted through Chromium's native Matroska path. Video.js +receives `video/matroska` for `.mkv` URLs and explicit query metadata such as +`extension=mkv` or `container=mkv`; ArtPlayer and HTML5 continue to use their +native video paths. This is container support rather than a universal codec +guarantee: native source or decode failures still produce the existing +diagnostic and explicit MPV/VLC fallback. + Portal VOD and episode payloads with `contentInfo` are treated as non-live by the inline players unless `isLive` is explicitly set. If Chromium leaves the underlying MediaSource duration at `Infinity` for a finite TS VOD, the Video.js wrapper normalizes its UI duration from the finite `seekable` or `buffered` range. Embedded MPV uses the same live decision rule and shows an unknown duration placeholder for VOD/episode snapshots until MPV reports a finite duration. This removes the misleading `LIVE` control state without changing stream decoding, diagnostics, or external fallback behavior. When a diagnostic is actionable in Electron, the diagnostic surface may offer `Open in MPV`, `Open in VLC`, `Copy URL`, technical details, and `Retry`. Web builds only expose copy/help text and retry. MPV/VLC fallback requests carry the original `ResolvedPortalPlayback` payload so headers, referer, origin, user-agent, content metadata, and resume offset stay intact. Retry clears the current diagnostic and rebuilds the active inline player inputs; it does not change the saved player setting. diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts index 42cc4d5d6..444cdf566 100644 --- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts +++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts @@ -219,6 +219,33 @@ describe('WebPlayerViewComponent', () => { ]); }); + it('uses the Matroska mime type for MKV paths', () => { + const streamUrl = 'https://example.com/archive/movie.mkv'; + + component.setVjsOptions(streamUrl); + + expect(component.vjsOptions.sources).toEqual([ + { + src: streamUrl, + type: 'video/matroska', + }, + ]); + }); + + it('uses the Matroska mime type for query-declared MKV streams', () => { + const streamUrl = + 'https://example.com/play?container=mkv&token=signed'; + + component.setVjsOptions(streamUrl); + + expect(component.vjsOptions.sources).toEqual([ + { + src: streamUrl, + type: 'video/matroska', + }, + ]); + }); + it('treats web script playback URLs without declared media extension as MPEG-TS', () => { const streamUrl = 'https://example.com/live.php?stream=123&token=x'; diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts index fbab90b9e..6d77cf6f7 100644 --- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts +++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts @@ -157,9 +157,7 @@ export class WebPlayerViewComponent { this.settings()?.player ?? VideoPlayer.VideoJs ); - readonly recordingFolder = computed( - () => this.settings()?.recordingFolder ?? '' - ); + readonly recordingFolder = computed(() => this.settings()?.recordingFolder ?? ''); constructor() { effect(() => { @@ -180,7 +178,9 @@ export class WebPlayerViewComponent { ? 'application/x-mpegURL' : extension === 'ts' || !extension ? 'video/mp2t' - : 'video/mp4'; + : extension === 'mkv' + ? 'video/matroska' + : 'video/mp4'; this.vjsOptions = { isLive,