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
|
"use client";
import { useRef, useEffect } from "react";
import { usePlaybackStore } from "@/stores/playback-store";
interface VideoPlayerProps {
src: string;
poster?: string;
className?: string;
clipStartTime: number;
trimStart: number;
trimEnd: number;
clipDuration: number;
trackMuted?: boolean;
}
export function VideoPlayer({
src,
poster,
className = "",
clipStartTime,
trimStart,
trimEnd,
clipDuration,
trackMuted = false,
}: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(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 video = videoRef.current;
if (!video || !isInClipRange) return;
const handleSeekEvent = (e: CustomEvent) => {
// Always update video time, even if outside clip range
const timelineTime = e.detail.time;
const videoTime = Math.max(
trimStart,
Math.min(
clipDuration - trimEnd,
timelineTime - clipStartTime + trimStart
)
);
video.currentTime = videoTime;
};
const handleUpdateEvent = (e: CustomEvent) => {
// Always update video 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(video.currentTime - targetTime) > 0.5) {
video.currentTime = targetTime;
}
};
const handleSpeed = (e: CustomEvent) => {
video.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 video = videoRef.current;
if (!video) return;
if (isPlaying && isInClipRange) {
video.play().catch(() => {});
} else {
video.pause();
}
}, [isPlaying, isInClipRange]);
// Sync volume and speed
useEffect(() => {
const video = videoRef.current;
if (!video) return;
video.volume = volume;
video.muted = muted || trackMuted;
video.playbackRate = speed;
}, [volume, speed, muted, trackMuted]);
return (
<video
ref={videoRef}
src={src}
poster={poster}
className={`max-w-full max-h-full object-contain ${className}`}
playsInline
preload="auto"
controls={false}
disablePictureInPicture
disableRemotePlayback
style={{ pointerEvents: "none" }}
onContextMenu={(e) => e.preventDefault()}
/>
);
}
|