All files / src/hooks useMpv.ts

91.09% Statements 133/146
91.66% Branches 33/36
100% Functions 1/1
91.09% Lines 133/146

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 1791x 1x   1x                   1x 1x 1x 1x 1x 1x 1x 1x   1x 42x 42x 42x 42x 42x 42x     42x 16x 1x 1x 16x 16x     42x     42x 16x 1x 16x 16x     42x           42x 16x 16x 16x 3x 3x 16x 16x 42x   42x 5x 4x 4x 4x 4x   4x 4x 4x     3x 5x 1x 1x 1x 5x 4x 4x 42x   42x 2x 2x 2x 1x 1x 1x 1x 42x   42x 2x 2x 2x 1x 1x 1x 1x 42x   42x 1x 1x 1x 1x 1x     42x   42x 1x 1x 1x 1x 1x     42x   42x 1x 1x 1x 1x 1x     42x   42x 16x 16x 15x 15x 15x 15x 15x 15x 15x 15x 15x 16x 1x 1x 42x   42x 16x 16x 16x 16x       42x   42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x  
import { useState, useCallback, useRef, useEffect } from "react";
import { listen } from "@tauri-apps/api/event";
import type { PlayerState } from "@/lib/types";
import {
	mpvLoad,
	mpvPlay,
	mpvPause,
	mpvStop,
	mpvSeek,
	mpvSetVolume,
	mpvGetState,
} from "@/lib/tauri";
 
const DEFAULT_STATE: PlayerState = {
	isPlaying: false,
	isPaused: false,
	currentUrl: null,
	volume: 100,
	position: 0,
	duration: 0,
};
 
export const useMpv = () => {
	const [state, setState] = useState<PlayerState>(DEFAULT_STATE);
	const [error, setError] = useState<string | null>(null);
	const [fallbackActive, setFallbackActive] = useState(false);
	const [firstFrameReady, setFirstFrameReady] = useState(false);
	const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
	const loadingRef = useRef(false);
 
	// Listen for fallback event emitted when embedded renderer fails.
	useEffect(() => {
		const unlistenPromise = listen<{ reason: string }>("mpv://render-fallback", (event) => {
			console.warn("[useMpv] render fallback:", event.payload.reason);
			setFallbackActive(true);
		});
		return () => {
			unlistenPromise.then((fn) => fn());
		};
	}, []);
 
	// Listen for first-frame event so the frontend knows when the video is actually visible.
	useEffect(() => {
		const unlistenPromise = listen("mpv://first-frame", () => {
			setFirstFrameReady(true);
		});
		return () => {
			unlistenPromise.then((fn) => fn());
		};
	}, []);
 
	// On mount, check if mpv is already playing (e.g. user navigated away and back).
	// If so, restore firstFrameReady immediately so the background turns transparent.
	// Skip if a load is already in progress (loadingRef set before this IPC resolves)
	// to avoid a transparent flash that load() would immediately cancel.
	useEffect(() => {
		mpvGetState()
			.then((s) => {
				if (!loadingRef.current && (s.isPlaying || s.isPaused)) {
					setFirstFrameReady(true);
				}
			})
			.catch(() => {});
	}, []);
 
	const load = useCallback(async (url: string) => {
		if (loadingRef.current) return;
		loadingRef.current = true;
		setError(null);
		setFallbackActive(false);
		setFirstFrameReady(false);
		// Reset playing state and position immediately so the bar doesn't show stale values.
		setState((s) => ({ ...s, isPlaying: false, isPaused: false, position: 0, duration: 0 }));
		try {
			await mpvLoad(url);
			// Don't set isPlaying optimistically — let the next poll confirm it from Rust
			// so transparency only kicks in once MPV is actually rendering frames.
			setState((s) => ({ ...s, currentUrl: url }));
		} catch (e) {
			const msg = String(e);
			setError(msg);
			throw e;
		} finally {
			loadingRef.current = false;
		}
	}, []);
 
	const play = useCallback(async () => {
		console.log("[useMpv] play called");
		try {
			await mpvPlay();
			setState((s) => ({ ...s, isPlaying: true, isPaused: false }));
		} catch (e) {
			console.error("[useMpv] mpvPlay failed:", e);
		}
	}, []);
 
	const pause = useCallback(async () => {
		console.log("[useMpv] pause called");
		try {
			await mpvPause();
			setState((s) => ({ ...s, isPaused: true }));
		} catch (e) {
			console.error("[useMpv] mpvPause failed:", e);
		}
	}, []);
 
	const stop = useCallback(async () => {
		console.log("[useMpv] stop called");
		try {
			await mpvStop();
			setState(DEFAULT_STATE);
		} catch (e) {
			console.error("[useMpv] mpvStop failed:", e);
		}
	}, []);
 
	const seek = useCallback(async (position: number) => {
		console.log("[useMpv] seek position=", position);
		try {
			await mpvSeek(position);
			setState((s) => ({ ...s, position }));
		} catch (e) {
			console.error("[useMpv] mpvSeek failed:", e);
		}
	}, []);
 
	const setVolume = useCallback(async (volume: number) => {
		console.log("[useMpv] setVolume volume=", volume);
		try {
			await mpvSetVolume(volume);
			setState((s) => ({ ...s, volume }));
		} catch (e) {
			console.error("[useMpv] mpvSetVolume failed:", e);
		}
	}, []);
 
	const refresh = useCallback(async () => {
		try {
			const s = await mpvGetState();
			console.debug("[useMpv] poll state:", JSON.stringify(s));
			setState({
				isPlaying: s.isPlaying,
				isPaused: s.isPaused,
				currentUrl: s.currentUrl,
				volume: s.volume,
				position: s.position,
				duration: s.duration,
			});
		} catch (e) {
			console.warn("[useMpv] poll failed:", e);
		}
	}, []);
 
	useEffect(() => {
		console.log("[useMpv] starting poll interval");
		refresh();
		pollRef.current = setInterval(refresh, 1000);
		return () => {
			console.log("[useMpv] clearing poll interval");
			if (pollRef.current) clearInterval(pollRef.current);
		};
	}, [refresh]);
 
	return {
		state,
		error,
		fallbackActive,
		firstFrameReady,
		load,
		play,
		pause,
		stop,
		seek,
		setVolume,
		refresh,
	};
};