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;
17}
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);
28}
29
30let fullscreenCount = 0;
31const fullscreenListeners = new Set<() => void>();
32
33export function fullscreenVideoOpened(): void {
34 fullscreenCount += 1;
35 for (const listener of fullscreenListeners) listener();
36}
37
38export function fullscreenVideoClosed(): void {
39 fullscreenCount -= 1;
40 for (const listener of fullscreenListeners) listener();
41}
42
43function subscribeFullscreenVideo(listener: () => void): () => void {
44 fullscreenListeners.add(listener);
45 return () => {
46 fullscreenListeners.delete(listener);
47 };
48}
49
50function isFullscreenVideoOpen(): boolean {
51 return fullscreenCount > 0;
52}
53
54export function useFullscreenVideoOpen(): boolean {
55 return useSyncExternalStore(subscribeFullscreenVideo, isFullscreenVideoOpen);
56}
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]);
65}