Playback fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m28s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m37s
Build & Release / Run Tests (push) Successful in 4m12s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 18m57s
Build & Release / Create Release (push) Successful in 13s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m28s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m37s
Build & Release / Run Tests (push) Successful in 4m12s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 18m57s
Build & Release / Create Release (push) Successful in 13s
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
import CachedImage from "../common/CachedImage.svelte";
|
||||
import { sleepTimerActive, sleepTimerExpiredSignal } from "$lib/stores/sleepTimer";
|
||||
import { playbackPosition } from "$lib/stores/player";
|
||||
import * as html5Adapter from "$lib/player/html5Adapter";
|
||||
|
||||
interface Props {
|
||||
media: MediaItem | null;
|
||||
@@ -216,6 +217,7 @@
|
||||
hasPerformedInitialSeek = false; // Reset so new video can seek to initial position
|
||||
lastAppliedInitialPosition = undefined; // New stream - forget the previously-applied resume point
|
||||
endedFired = false; // New stream loaded - allow onEnded to fire again
|
||||
html5Adapter.resetReporting(); // New stream - clear position-report throttle
|
||||
}
|
||||
});
|
||||
|
||||
@@ -319,6 +321,25 @@
|
||||
console.log('[VideoPlayer] HLS manifest parsed, ready to play');
|
||||
});
|
||||
|
||||
// On the Android WebView the element's own `canplay` may not fire for
|
||||
// MSE-fed HLS, so treat the first buffered fragment as "ready" too.
|
||||
// This reveals the <video> element (otherwise it stays invisible behind
|
||||
// the black poster card while audio plays).
|
||||
hls.on(Hls.Events.FRAG_BUFFERED, () => {
|
||||
markMediaReady();
|
||||
});
|
||||
|
||||
// The canplay-fallback timeout is normally armed from the element's
|
||||
// `loadstart` event, but with hls.js the element's `src` is "" and
|
||||
// `loadstart` may not fire, so arm a backstop here directly.
|
||||
if (canplayFallbackTimeout) clearTimeout(canplayFallbackTimeout);
|
||||
canplayFallbackTimeout = setTimeout(() => {
|
||||
if (!isMediaReady && videoElement && videoElement.readyState >= 2) {
|
||||
console.warn('[VideoPlayer] HLS canplay fallback - revealing video (readyState:', videoElement.readyState, ')');
|
||||
markMediaReady();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Reset recovery attempts for new HLS instance
|
||||
hlsFatalRecoveryAttempts = 0;
|
||||
|
||||
@@ -490,9 +511,27 @@
|
||||
backendChosen = true;
|
||||
console.log(`[VideoPlayer] Backend: ${response.backend}, useHtml5Element: ${useHtml5Element}`);
|
||||
|
||||
// INTERIM (until the video-player API refactor lands): always render
|
||||
// through the webview HTML5 element, including Android. The native
|
||||
// ExoPlayer SurfaceView sits behind an opaque webview and has never
|
||||
// actually been visible (an init bug kept the app on the HTML5 path
|
||||
// since the POC), so true native mode plays audio behind a frozen
|
||||
// picture. Stop the native backend and let the webview own playback,
|
||||
// matching Linux behavior and avoiding dual audio.
|
||||
if (!useHtml5Element) {
|
||||
console.warn("[VideoPlayer] Native video backend reported - overriding to HTML5 rendering (native surface not visible through webview)");
|
||||
useHtml5Element = true;
|
||||
try {
|
||||
await commands.playerStop();
|
||||
didStopBackendEarly = true;
|
||||
} catch (err) {
|
||||
console.warn("[VideoPlayer] Failed to stop native backend:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// If using HTML5 element for non-transcoded content, stop the backend player
|
||||
// For transcoded content, we need to keep the backend running to handle seeking/audio track switching
|
||||
if (useHtml5Element && !needsTranscoding) {
|
||||
if (useHtml5Element && !needsTranscoding && !didStopBackendEarly) {
|
||||
try {
|
||||
console.log("[VideoPlayer] Using HTML5 for direct stream - stopping backend player to prevent dual audio");
|
||||
await commands.playerStop();
|
||||
@@ -646,6 +685,9 @@
|
||||
const newCurrentTime = seekOffset + videoElement.currentTime;
|
||||
if (videoElement.readyState >= 2) {
|
||||
currentTime = newCurrentTime;
|
||||
// Feed the Rust controller a throttled position tick (~250ms) so it
|
||||
// stays the source of truth for HTML5 video without flooding IPC.
|
||||
html5Adapter.reportPosition(currentTime, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,6 +737,10 @@
|
||||
console.log("[VideoPlayer] videoDuration state is now:", videoDuration);
|
||||
}
|
||||
|
||||
// Tell the Rust controller the media is loaded and its duration (mirrors the
|
||||
// native MediaLoaded event so the backend has a duration for HTML5 video).
|
||||
html5Adapter.reportMediaLoaded(duration);
|
||||
|
||||
// Use setTimeout to log the derived value after reactive updates
|
||||
setTimeout(() => {
|
||||
console.log("[VideoPlayer] Derived duration value:", duration);
|
||||
@@ -702,6 +748,20 @@
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Flip out of the Loading state and reveal the <video> element (which is
|
||||
// `invisible` and covered by the black poster card until then). Multiple
|
||||
// signals can legitimately mean "ready": the native `canplay` event, hls.js
|
||||
// buffering its first fragment, or the element actually reaching `playing`.
|
||||
// On the Android system WebView the HLS path feeds the element through MSE
|
||||
// with `src=""`, so `loadstart`/`canplay` don't fire reliably and the
|
||||
// canplay-fallback timeout was never armed — audio played while the video
|
||||
// stayed invisible. Any of these callers now reveals it.
|
||||
function markMediaReady() {
|
||||
if (isMediaReady) return;
|
||||
console.log("[VideoPlayer] Marking media ready");
|
||||
isMediaReady = true;
|
||||
}
|
||||
|
||||
async function handleCanPlay() {
|
||||
// Media is ready to play - transition from Loading to Playing state (DR-001)
|
||||
console.log("[VideoPlayer] canplay event fired - media is ready");
|
||||
@@ -799,6 +859,9 @@
|
||||
function handlePlaying() {
|
||||
console.log("[VideoPlayer] playing event - playback resumed");
|
||||
isBuffering = false;
|
||||
// Safety net: if we reached `playing` we are definitely renderable, even if
|
||||
// `canplay`/hls FRAG_BUFFERED were missed on this WebView.
|
||||
markMediaReady();
|
||||
}
|
||||
|
||||
function handleLoadStart() {
|
||||
@@ -881,6 +944,10 @@
|
||||
function handlePlay() {
|
||||
isPlaying = true;
|
||||
startTimeUpdates(); // Start RAF loop for smooth time updates
|
||||
// Mirror the DOM state into the Rust PlayerController so it is the single
|
||||
// source of truth for HTML5 video (the <video> lives in the webview, which
|
||||
// Rust cannot observe directly). See html5Adapter.ts.
|
||||
html5Adapter.reportState("playing", reportMediaId ?? null);
|
||||
// Report playback start on first play (skip for live - no resume tracking)
|
||||
if (!isLive && !hasReportedStart && onReportStart) {
|
||||
onReportStart(currentTime, reportMediaId);
|
||||
@@ -891,6 +958,8 @@
|
||||
function handlePause() {
|
||||
isPlaying = false;
|
||||
stopTimeUpdates(); // Stop RAF loop when paused
|
||||
html5Adapter.reportState("paused", reportMediaId ?? null);
|
||||
html5Adapter.reportPosition(currentTime, duration, { force: true });
|
||||
// Report progress when paused
|
||||
if (onReportProgress) {
|
||||
onReportProgress(currentTime, true, reportMediaId);
|
||||
@@ -900,6 +969,7 @@
|
||||
function handleEnded() {
|
||||
isPlaying = false;
|
||||
stopTimeUpdates(); // Stop RAF loop when ended
|
||||
html5Adapter.reportState("stopped", reportMediaId ?? null);
|
||||
// Report stop when video ends (skip for live - no resume tracking)
|
||||
if (!isLive && onReportStop) {
|
||||
onReportStop(currentTime, reportMediaId);
|
||||
|
||||
Reference in New Issue
Block a user