summaryrefslogtreecommitdiff
path: root/apps/web/src/components/ui/audio-player.tsx
blob: df15010511727009a40ce3656cfc3eb4f26f3c79 (plain) (blame)
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
"use client";

import { useRef, useEffect } from "react";
import { usePlaybackStore } from "@/stores/playback-store";

interface AudioPlayerProps {
	src: string;
	className?: string;
	clipStartTime: number;
	trimStart: number;
	trimEnd: number;
	clipDuration: number;
	trackMuted?: boolean;
}

export function AudioPlayer({
	src,
	className = "",
	clipStartTime,
	trimStart,
	trimEnd,
	clipDuration,
	trackMuted = false,
}: AudioPlayerProps) {
	const audioRef = useRef<HTMLAudioElement>(null);
	const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore();

	// Calculate if we're within this clip's timeline range
	const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
	const isInClipRange =
		currentTime >= clipStartTime && currentTime < clipEndTime;

	// Sync playback events
	useEffect(() => {
		const audio = audioRef.current;
		if (!audio || !isInClipRange) return;

		const handleSeekEvent = (e: CustomEvent) => {
			// Always update audio time, even if outside clip range
			const timelineTime = e.detail.time;
			const audioTime = Math.max(
				trimStart,
				Math.min(
					clipDuration - trimEnd,
					timelineTime - clipStartTime + trimStart,
				),
			);
			audio.currentTime = audioTime;
		};

		const handleUpdateEvent = (e: CustomEvent) => {
			// Always update audio time, even if outside clip range
			const timelineTime = e.detail.time;
			const targetTime = Math.max(
				trimStart,
				Math.min(
					clipDuration - trimEnd,
					timelineTime - clipStartTime + trimStart,
				),
			);

			if (Math.abs(audio.currentTime - targetTime) > 0.5) {
				audio.currentTime = targetTime;
			}
		};

		const handleSpeed = (e: CustomEvent) => {
			audio.playbackRate = e.detail.speed;
		};

		window.addEventListener("playback-seek", handleSeekEvent as EventListener);
		window.addEventListener(
			"playback-update",
			handleUpdateEvent as EventListener,
		);
		window.addEventListener("playback-speed", handleSpeed as EventListener);

		return () => {
			window.removeEventListener(
				"playback-seek",
				handleSeekEvent as EventListener,
			);
			window.removeEventListener(
				"playback-update",
				handleUpdateEvent as EventListener,
			);
			window.removeEventListener(
				"playback-speed",
				handleSpeed as EventListener,
			);
		};
	}, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);

	// Sync playback state
	useEffect(() => {
		const audio = audioRef.current;
		if (!audio) return;

		if (isPlaying && isInClipRange && !trackMuted) {
			audio.play().catch(() => {});
		} else {
			audio.pause();
		}
	}, [isPlaying, isInClipRange, trackMuted]);

	// Sync volume and speed
	useEffect(() => {
		const audio = audioRef.current;
		if (!audio) return;

		audio.volume = volume;
		audio.muted = muted || trackMuted;
		audio.playbackRate = speed;
	}, [volume, speed, muted, trackMuted]);

	return (
		<audio
			ref={audioRef}
			src={src}
			className={className}
			preload="auto"
			controls={false}
			style={{ display: "none" }} // Audio elements don't need visual representation
			onContextMenu={(e) => e.preventDefault()}
		/>
	);
}