Merge: promote dev to prod
This commit is contained in:
@@ -5,57 +5,19 @@ import { Download, Link2, Lock, PlayCircle } from "lucide-react";
|
||||
// tree (no auth, no providers) — like the /privacy and /terms pages. Uses plain fetch and a tiny
|
||||
// self-contained i18n dict (the public viewer has no app language preference).
|
||||
|
||||
// Keep audio playing when the page is backgrounded (iOS Safari/Brave otherwise suspend a <video>
|
||||
// the moment you leave the tab — the "background playback" a native app / YouTube Premium gets). Two
|
||||
// pieces: (1) route the element's audio through a Web Audio graph — once that graph is live, iOS
|
||||
// keeps the audio session running in the background while the video frame freezes; (2) Media Session
|
||||
// metadata + handlers, which give lock-screen controls AND signal the OS that media is active. The
|
||||
// file is same-origin (served by us), so createMediaElementSource isn't CORS-tainted. All of it is
|
||||
// best-effort: any unsupported bit falls back to the plain <video>. Set up lazily on the first play
|
||||
// (a user gesture — required to create/resume an AudioContext) and only once (a second
|
||||
// createMediaElementSource on the same element throws).
|
||||
function useBackgroundAudio(
|
||||
// Advertise the playing video to the OS via the Media Session API: lock-screen / control-center
|
||||
// metadata + play/pause handlers. This is also what lets a browser's own "background playback"
|
||||
// feature keep the audio going when the page is backgrounded — it's exactly how youtube.com stays
|
||||
// playing in the background on Brave iOS. We deliberately do NOT route the audio through the Web
|
||||
// Audio API: `createMediaElementSource` pulls the element out of the normal media pipeline that the
|
||||
// background feature (and iOS) hooks into, and iOS then suspends the AudioContext on background —
|
||||
// which STOPPED playback instead of keeping it alive (measured on iPad Brave). A plain <video> +
|
||||
// MediaSession leaves the browser's native background handling in charge.
|
||||
function useMediaSession(
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>,
|
||||
meta: Meta | null,
|
||||
ready: boolean,
|
||||
) {
|
||||
const ctxRef = useRef<AudioContext | null>(null);
|
||||
const wiredRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video || !ready) return;
|
||||
// Only iOS suspends a backgrounded <video> — and only there does rerouting the audio through
|
||||
// Web Audio buy anything. Every other platform backgrounds media audio natively, so we leave
|
||||
// their <video> untouched rather than risk the reroute leaving foreground audio silent if the
|
||||
// AudioContext can't resume. (iPadOS reports as MacIntel + touch.)
|
||||
const isIOS =
|
||||
/iP(hone|ad|od)/.test(navigator.userAgent) ||
|
||||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
if (!isIOS) return;
|
||||
const wire = () => {
|
||||
if (wiredRef.current) {
|
||||
ctxRef.current?.resume().catch(() => {});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const Ctx =
|
||||
window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
||||
if (!Ctx) return;
|
||||
const ctx = new Ctx();
|
||||
ctx.createMediaElementSource(video).connect(ctx.destination);
|
||||
ctxRef.current = ctx;
|
||||
wiredRef.current = true;
|
||||
ctx.resume().catch(() => {});
|
||||
} catch {
|
||||
/* unsupported / already wired — plain <video> still plays, just not in the background */
|
||||
}
|
||||
};
|
||||
video.addEventListener("play", wire);
|
||||
return () => video.removeEventListener("play", wire);
|
||||
}, [videoRef, ready]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready || !meta || !("mediaSession" in navigator)) return;
|
||||
const video = videoRef.current;
|
||||
@@ -73,15 +35,6 @@ function useBackgroundAudio(
|
||||
/* MediaMetadata / handlers unsupported — ignore */
|
||||
}
|
||||
}, [ready, meta, videoRef]);
|
||||
|
||||
// Recover if the context gets suspended (e.g. iOS suspends it on background) once we're back.
|
||||
useEffect(() => {
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState === "visible") ctxRef.current?.resume().catch(() => {});
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVisible);
|
||||
return () => document.removeEventListener("visibilitychange", onVisible);
|
||||
}, []);
|
||||
}
|
||||
|
||||
type Meta = {
|
||||
@@ -160,7 +113,7 @@ export default function WatchPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [unlocking, setUnlocking] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
useBackgroundAudio(videoRef, meta, status === "ready");
|
||||
useMediaSession(videoRef, meta, status === "ready");
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
|
||||
@@ -14,6 +14,13 @@ export interface ReleaseEntry {
|
||||
}
|
||||
|
||||
export const RELEASE_NOTES: ReleaseEntry[] = [
|
||||
{
|
||||
version: "0.57.2",
|
||||
date: "2026-07-28",
|
||||
chores: [
|
||||
"Sharing: the shared-video page now advertises playback to the OS via the Media Session API only, dropping a Web Audio workaround that interfered with the browser's own background playback on iOS (it left the audio to be suspended when the tab was backgrounded, the opposite of the goal) — so background audio is left to the browser's native handling, the way it works for youtube.com.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "0.57.1",
|
||||
date: "2026-07-28",
|
||||
|
||||
Reference in New Issue
Block a user