refactor(player): delete the webview video path; mpv selects its own tracks
DR-235 phase 3. Every video renderer is native now: mpv on Linux and Windows, ExoPlayer on Android, all drawing behind the transparent webview. The HTML5 <video> path is gone, not bypassed: - Frontend: hls.js, Html5PlayerAdapter and its compatibility shim, the createAdapter factory, streamTransport, hlsRecovery, timeTracking, videoFit, the <video>/<track> markup and every element handler in VideoPlayer (3277 -> 2144 lines), the experimentalNativeVideo store and its Settings toggle, webviewVideoFallback/supportsNativeVideo, and the setHtml5VideoState PiP bridge call. NativePlayerAdapter is the one video adapter; webview audio gets its own adapter kind. - Rust: use_html5 dropped from player_seek_video, player_switch_audio_track and player_set_stream_quality with the Html5* strategies and ReloadStream responses; use_html5_element and VideoBackend dropped from PlayerStatus; player_play_item always loads the backend (set_current_item removed); Capabilities::webview removed; the WebKitGTK GStreamer/VAAPI setup (and its gst-inspect spawn) removed. - Android: the HTML5 video state in PictureInPictureManager and ScreenWakeManager, and the bridge method feeding it. - CSP: connect-src loses http:/https: and worker-src loses blob: - both existed for hls.js; with it gone they were only an exfiltration channel and a blob worker for injected script. A test now keeps them out. mpv takes over what the <video> element did (mpv_tracks, UT-275): subtitles are the WebVTT list the play request carries, queued on sub-files and selected by position in that list, starting off; audio tracks are selected by position in the file; sid/aid are reset before each load. Without this, Linux video had no subtitle selection and a direct-play audio switch failed since mpv became its renderer. Verified: Rust 948 passing, and the same 948 cross-compiled for Windows under wine against the shipped DLL (track tests included); frontend 1111 passing; aarch64 debug APK builds. Lint warnings 158 -> 146, CI ratchet tightened to match. Not yet seen on Windows hardware.
This commit is contained in:
@@ -5,30 +5,19 @@ import {
|
||||
subtitleStreamsOf,
|
||||
subtitleTrackLabel,
|
||||
resolveSubtitleTracks,
|
||||
reconcileSelectedSubtitle,
|
||||
videoCrossOriginMode,
|
||||
nativeSubtitleTracks,
|
||||
nativeSubtitleArrayIndex,
|
||||
type SubtitleStreamLike,
|
||||
} from "./subtitleTracks";
|
||||
|
||||
/**
|
||||
* Subtitles on the Linux / WebKitGTK HTML5 `<video>` path.
|
||||
* Subtitle resolution — the list the play request carries.
|
||||
*
|
||||
* TRACES: UR-020 | DR-023 | UT-143, UT-144
|
||||
*
|
||||
* The bug this guards: VideoPlayer rendered no `<track>` children at all (the
|
||||
* block was commented out "to debug playback issues"), so
|
||||
* `Html5PlayerAdapter.selectSubtitle()` walked an empty `textTracks` list and
|
||||
* the subtitle menu was inert on Linux. The reason it had to be disabled is
|
||||
* visible in the original markup — `src={getSubtitleUrl(track.index)}` bound the
|
||||
* *Promise* returned by an async function to the attribute, so every track's src
|
||||
* stringified to "[object Promise]", an unloadable resource hanging off the
|
||||
* media element.
|
||||
*
|
||||
* So the fix has two halves and both are tested here: URLs must be resolved into
|
||||
* plain strings *before* they reach the markup, and the markup must actually
|
||||
* render the tracks (with the `data-stream-index` the adapter matches on).
|
||||
* URLs are resolved into plain strings before they reach the player: the
|
||||
* original markup bound the *Promise* returned by an async function to a
|
||||
* `<track src>`, so every track's src stringified to "[object Promise]".
|
||||
*/
|
||||
|
||||
const SUBS: SubtitleStreamLike[] = [
|
||||
@@ -177,56 +166,6 @@ describe("resolveSubtitleTracks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reconcileSelectedSubtitle", () => {
|
||||
it("starts off (null) and keeps 'off' selectable", async () => {
|
||||
const tracks = await resolveSubtitleTracks(SUBS, async (i) => url(i));
|
||||
expect(reconcileSelectedSubtitle(tracks, null)).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps a selection that is still renderable", async () => {
|
||||
const tracks = await resolveSubtitleTracks(SUBS, async (i) => url(i));
|
||||
expect(reconcileSelectedSubtitle(tracks, 3)).toBe(3);
|
||||
});
|
||||
|
||||
it("falls back to off when the selected track is gone (new item / failed URL)", async () => {
|
||||
const tracks = await resolveSubtitleTracks(SUBS, async (i) => url(i));
|
||||
expect(reconcileSelectedSubtitle(tracks, 9)).toBeNull();
|
||||
expect(reconcileSelectedSubtitle([], 3)).toBeNull();
|
||||
});
|
||||
|
||||
it("never auto-selects the server's default track", async () => {
|
||||
// The menu opens on "Off" and a <track default> would auto-show, so the UI
|
||||
// would claim subtitles are off while they are burned over the picture.
|
||||
const tracks = await resolveSubtitleTracks(SUBS, async (i) => url(i));
|
||||
expect(tracks[0].isDefault).toBe(true);
|
||||
expect(reconcileSelectedSubtitle(tracks, null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("videoCrossOriginMode", () => {
|
||||
it("opts into CORS for a server stream that has subtitles", () => {
|
||||
expect(videoCrossOriginMode("http://jelly.example/Videos/x/master.m3u8", 2)).toBe("anonymous");
|
||||
expect(videoCrossOriginMode("https://jelly.example/Videos/x/stream.mp4", 1)).toBe("anonymous");
|
||||
});
|
||||
|
||||
it("leaves a local/offline source alone so playback cannot regress", () => {
|
||||
expect(videoCrossOriginMode("asset://localhost/movie.mkv", 2)).toBeUndefined();
|
||||
expect(videoCrossOriginMode("file:///home/u/movie.mkv", 2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stays out of the way when there is nothing to load", () => {
|
||||
expect(videoCrossOriginMode("http://jelly.example/x.m3u8", 0)).toBeUndefined();
|
||||
expect(videoCrossOriginMode("", 0)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("is decided by inputs known at first render, so it cannot flip mid-load", () => {
|
||||
// Same answer before and after the async URL resolution completes.
|
||||
const before = videoCrossOriginMode("http://jelly.example/x.m3u8", SUBS.length);
|
||||
const after = videoCrossOriginMode("http://jelly.example/x.m3u8", SUBS.length);
|
||||
expect(before).toBe(after);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Subtitles on the Android / ExoPlayer native path.
|
||||
*
|
||||
@@ -320,31 +259,6 @@ describe("nativeSubtitleArrayIndex", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("VideoPlayer markup (the regression that made the menu inert)", () => {
|
||||
const source = readFileSync(resolve(__dirname, "VideoPlayer.svelte"), "utf-8");
|
||||
|
||||
it("renders <track> elements instead of leaving them commented out", () => {
|
||||
expect(source).not.toContain("Temporarily disabled to debug playback issues");
|
||||
expect(source).toMatch(/<track\b/);
|
||||
expect(source).toContain('kind="subtitles"');
|
||||
});
|
||||
|
||||
/** The rendered element, not a `<track>` mentioned in prose. */
|
||||
const trackElement = source.slice(source.search(/<track\s/), source.search(/<track\s/) + 400);
|
||||
|
||||
it("keeps data-stream-index — Html5PlayerAdapter.selectSubtitle matches on it", () => {
|
||||
expect(trackElement).toContain("data-stream-index");
|
||||
});
|
||||
|
||||
it("never binds the async getSubtitleUrl() Promise to src", () => {
|
||||
expect(source).not.toMatch(/src=\{\s*getSubtitleUrl\(/);
|
||||
});
|
||||
|
||||
it("does not mark any track default (a default track auto-shows)", () => {
|
||||
expect(trackElement).not.toMatch(/\bdefault=/);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The half of the Android fix that lives in the component: the resolved list has
|
||||
* to actually be handed to `playerPlayItem`, and the index sent to the backend
|
||||
|
||||
Reference in New Issue
Block a user