feat(android): play the original file — decode Dolby/DTS audio with FFmpeg
Android ships no AC-3, E-AC-3, DTS or TrueHD decoders; they are licensed codecs, present only where a vendor paid for them. The ROD2-W09 tablet has a vendor DTS decoder and no AC-3/E-AC-3 at all. So every film with Dolby audio was re-encoded by the server, for streaming and for download alike, and a transcoded download has no Content-Length and ignores Range: ~1 MB/s, restarting from byte zero on every network blip. ExoPlayer now carries Jellyfin's media3 FFmpeg audio decoder in extension mode ON (platform decoders first, FFmpeg for what they lack), and CodecDetector reports its codecs so the device profile and the download policy agree with what actually decodes. The download policy judges audio against the renderer that will play the file (renderer_can_decode_audio) instead of the webview's list, so Android downloads are always the direct copy — a 910 MB E-AC-3 5.1 episode downloaded in 94 s and played offline. The webview video path is removed on Android: it decodes none of these codecs, so a stored "native video off" would play every original-file download silent. Rust reports webview_video_fallback (false on Android, true only beside mpv native video on Linux); Settings offers the switch and the player honours it only then. Linux keeps the fallback and, with it, the server transcode for undecodable audio. The decoder is GPL-3.0; the distributed APK carries its terms and the source stays MIT (THIRD_PARTY_NOTICES.md). The on-device remux spec this replaces is folded into 05-platform-backends.md and deleted. DR-293, UT-259, UT-262.
This commit is contained in:
@@ -2886,7 +2886,13 @@ usesWebviewAudio: boolean;
|
||||
* beneath the WebView. Linux cannot do this (WebKitGTK/Wayland
|
||||
* compositing), so it stays on the HTML5 element.
|
||||
*/
|
||||
supportsNativeVideo: boolean }
|
||||
supportsNativeVideo: boolean;
|
||||
/**
|
||||
* True when the user may send video to the webview element instead of the
|
||||
* native renderer — the frontend offers the switch only then, and honours
|
||||
* the stored preference only then. See [`webview_video_fallback`].
|
||||
*/
|
||||
webviewVideoFallback: boolean }
|
||||
/**
|
||||
* Playback information
|
||||
*/
|
||||
|
||||
@@ -44,6 +44,18 @@ vi.mock("$lib/stores/nativeVideo", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
// The webview path only exists where Rust offers a fallback from the native
|
||||
// renderer — beside mpv native video on Linux; never on Android since DR-293,
|
||||
// where a stored "off" is ignored. These tests guard that path's scrubbing, so
|
||||
// they declare a platform that has it.
|
||||
vi.mock("$lib/services/playbackCapabilities", () => ({
|
||||
getPlaybackCapabilities: async () => ({
|
||||
usesWebviewAudio: false,
|
||||
supportsNativeVideo: true,
|
||||
webviewVideoFallback: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/event", () => ({
|
||||
listen: vi.fn(async (channel: string, handler: any) => {
|
||||
channelHandlers[channel] = handler;
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
type Html5ElementBridge,
|
||||
} from "$lib/player/adapters";
|
||||
import { createRustReportHost } from "$lib/player/adapters/rustReportHost";
|
||||
import { experimentalNativeVideo } from "$lib/stores/nativeVideo";
|
||||
import { experimentalNativeVideo, nativeVideoWanted } from "$lib/stores/nativeVideo";
|
||||
import { getPlaybackCapabilities } from "$lib/services/playbackCapabilities";
|
||||
import {
|
||||
enableNativeVideoCompositing,
|
||||
disableNativeVideoCompositing,
|
||||
@@ -1020,7 +1021,12 @@
|
||||
// it is off we must also stop the native backend that player_play_item
|
||||
// just started, or ExoPlayer and the <video> element both decode the
|
||||
// same stream and the audio doubles.
|
||||
if (!useHtml5Element && !$experimentalNativeVideo) {
|
||||
// The stored choice only counts where Rust says a webview fallback
|
||||
// exists — never on Android, where the webview would play an
|
||||
// original-file download silent (DR-293).
|
||||
const { webviewVideoFallback } = await getPlaybackCapabilities();
|
||||
const wantNative = nativeVideoWanted($experimentalNativeVideo, webviewVideoFallback);
|
||||
if (!useHtml5Element && !wantNative) {
|
||||
log.debug("Native backend available but experimentalNativeVideo is off - using HTML5");
|
||||
useHtml5Element = true;
|
||||
try {
|
||||
@@ -1077,7 +1083,7 @@
|
||||
bridge: adapterBridge,
|
||||
// useHtml5Element is already the resolved decision above, so the
|
||||
// flag has had its say; pass it through for the invariant check.
|
||||
experimentalNativeVideo: $experimentalNativeVideo,
|
||||
experimentalNativeVideo: wantNative,
|
||||
});
|
||||
// No-op for the native adapter, which owns no DOM element.
|
||||
playerAdapter.attach(videoElement);
|
||||
|
||||
@@ -47,6 +47,18 @@ vi.mock("$lib/stores/nativeVideo", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
// The webview path only exists where Rust offers a fallback from the native
|
||||
// renderer — beside mpv native video on Linux; never on Android since DR-293,
|
||||
// where a stored "off" is ignored. These tests guard that path's scrubbing, so
|
||||
// they declare a platform that has it.
|
||||
vi.mock("$lib/services/playbackCapabilities", () => ({
|
||||
getPlaybackCapabilities: async () => ({
|
||||
usesWebviewAudio: false,
|
||||
supportsNativeVideo: true,
|
||||
webviewVideoFallback: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/event", () => ({
|
||||
listen: vi.fn(async (channel: string, handler: any) => {
|
||||
channelHandlers[channel] = handler;
|
||||
|
||||
@@ -22,6 +22,12 @@ export interface PlaybackCapabilities {
|
||||
usesWebviewAudio: boolean;
|
||||
/** Video can render on a native surface behind a transparent webview. */
|
||||
supportsNativeVideo: boolean;
|
||||
/**
|
||||
* The user may send video to the webview element instead of the native
|
||||
* renderer. False on Android, where ExoPlayer is the only video renderer
|
||||
* (DR-293). Rust decides; see `webview_video_fallback`.
|
||||
*/
|
||||
webviewVideoFallback: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,6 +39,7 @@ export interface PlaybackCapabilities {
|
||||
const FALLBACK: PlaybackCapabilities = {
|
||||
usesWebviewAudio: false,
|
||||
supportsNativeVideo: false,
|
||||
webviewVideoFallback: false,
|
||||
};
|
||||
|
||||
let cached: PlaybackCapabilities | null = null;
|
||||
@@ -52,6 +59,7 @@ export async function getPlaybackCapabilities(): Promise<PlaybackCapabilities> {
|
||||
cached = {
|
||||
usesWebviewAudio: !!caps?.usesWebviewAudio,
|
||||
supportsNativeVideo: !!caps?.supportsNativeVideo,
|
||||
webviewVideoFallback: !!caps?.webviewVideoFallback,
|
||||
};
|
||||
return cached;
|
||||
} catch (err) {
|
||||
|
||||
@@ -116,6 +116,23 @@ function createExperimentalNativeVideoStore() {
|
||||
*/
|
||||
export const experimentalNativeVideo = createExperimentalNativeVideoStore();
|
||||
|
||||
/**
|
||||
* Whether video should take the native path, given the user's stored choice
|
||||
* and whether this platform lets the user choose at all.
|
||||
*
|
||||
* On Android the answer is always native: ExoPlayer is the only video renderer
|
||||
* there, and the webview element decodes none of the AC-3/E-AC-3/DTS/TrueHD
|
||||
* that ExoPlayer plays through the FFmpeg extension — so a stored "off" would
|
||||
* turn every original-file download into a silent film (DR-293). Rust reports
|
||||
* whether a fallback exists (`webviewVideoFallback`); only then does the
|
||||
* stored choice count.
|
||||
*
|
||||
* TRACES: UR-003, UR-071 | DR-293 | UT-262
|
||||
*/
|
||||
export function nativeVideoWanted(storedChoice: boolean, webviewVideoFallback: boolean): boolean {
|
||||
return webviewVideoFallback ? storedChoice : true;
|
||||
}
|
||||
|
||||
function createNativeVideoActiveStore() {
|
||||
const { subscribe, set } = writable<boolean>(false);
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { nativeVideoWanted } from "./nativeVideo";
|
||||
|
||||
// TRACES: UR-003, UR-071 | DR-293 | UT-262
|
||||
describe("nativeVideoWanted", () => {
|
||||
it("ignores a stored 'off' where there is no webview fallback (Android)", () => {
|
||||
// Someone who once switched native video off on Android must not be
|
||||
// routed to the webview, which plays original-file downloads silent.
|
||||
expect(nativeVideoWanted(false, false)).toBe(true);
|
||||
expect(nativeVideoWanted(true, false)).toBe(true);
|
||||
});
|
||||
|
||||
it("honours the stored choice where a fallback exists (Linux beside mpv)", () => {
|
||||
expect(nativeVideoWanted(false, true)).toBe(false);
|
||||
expect(nativeVideoWanted(true, true)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user