Skip to content
Source
Sign inSign up

videoAudioSession.ts

65 lines2.0 KB

1import { requireOptionalNativeModule } from "expo";
2import { useEffect, useSyncExternalStore } from "react";
3import { Platform } from "react-native";
4​
5const nativeControls = requireOptionalNativeModule<{
6 releaseVideoAudioSession(): Promise<void>;
7}>("T3NativeControls");
8​
9// Keep the session through seeks, buffering, and brief gaps between players.
10const RELEASE_DELAY_MS = 1_000;
11​
12let playingCount = 0;
13let releaseTimer: ReturnType<typeof setTimeout> | undefined;
14​
15function mediaPlaybackStarted(): void {
16 playingCount += 1;
18​
19/** expo-video never deactivates iOS playback, preventing interrupted apps from resuming. Include audio previews to avoid stopping them. */
20function mediaPlaybackStopped(): void {
21 playingCount -= 1;
22 if (playingCount > 0 || Platform.OS !== "ios") return;
23 clearTimeout(releaseTimer);
24 releaseTimer = setTimeout(() => {
25 if (playingCount > 0) return;
26 void nativeControls?.releaseVideoAudioSession().catch(() => undefined);
27 }, RELEASE_DELAY_MS);
29​
30let fullscreenCount = 0;
31const fullscreenListeners = new Set<() => void>();
32​
33export function fullscreenVideoOpened(): void {
34 fullscreenCount += 1;
35 for (const listener of fullscreenListeners) listener();
37​
38export function fullscreenVideoClosed(): void {
39 fullscreenCount -= 1;
40 for (const listener of fullscreenListeners) listener();
42​
43function subscribeFullscreenVideo(listener: () => void): () => void {
44 fullscreenListeners.add(listener);
45 return () => {
46 fullscreenListeners.delete(listener);
47 };
49​
50function isFullscreenVideoOpen(): boolean {
51 return fullscreenCount > 0;
53​
54export function useFullscreenVideoOpen(): boolean {
55 return useSyncExternalStore(subscribeFullscreenVideo, isFullscreenVideoOpen);
57​
58export function useMediaAudioSessionHold(active: boolean): void {
59 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- holds the shared native audio session while media plays and releases it on cleanup
60 useEffect(() => {
61 if (!active) return;
62 mediaPlaybackStarted();
63 return () => mediaPlaybackStopped();
64 }, [active]);