summaryrefslogtreecommitdiff
path: root/apps/web/src/components/ui/video-player.tsx
blob: 8731e1a80e0f45d37b7ff5ad1f7cf91df898ae1b (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
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
"use client";

import { useRef, useEffect } from "react";
import { Button } from "./button";
import { Play, Pause, Volume2 } from "lucide-react";
import { usePlaybackStore } from "@/stores/playback-store";

interface VideoPlayerProps {
    src: string;
    poster?: string;
    className?: string;
    clipStartTime: number;
    trimStart: number;
    trimEnd: number;
    clipDuration: number;
}

export function VideoPlayer({
    src,
    poster,
    className = "",
    clipStartTime,
    trimStart,
    trimEnd,
    clipDuration
}: VideoPlayerProps) {
    const videoRef = useRef<HTMLVideoElement>(null);
    const { isPlaying, currentTime, volume, speed, play, pause, setVolume } = usePlaybackStore();

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

    // Calculate the video's internal time based on timeline position
    const videoTime = Math.max(trimStart, Math.min(
        clipDuration - trimEnd,
        currentTime - clipStartTime + trimStart
    ));

    useEffect(() => {
        const video = videoRef.current;
        if (!video) return;

        const handleSeekEvent = (e: CustomEvent) => {
            if (!isInClipRange) return;
            const timelineTime = e.detail.time;
            const newVideoTime = Math.max(trimStart, Math.min(
                clipDuration - trimEnd,
                timelineTime - clipStartTime + trimStart
            ));
            video.currentTime = newVideoTime;
        };

        const handleUpdateEvent = (e: CustomEvent) => {
            if (!isInClipRange) return;
            const timelineTime = e.detail.time;
            const targetVideoTime = Math.max(trimStart, Math.min(
                clipDuration - trimEnd,
                timelineTime - clipStartTime + trimStart
            ));

            // Only sync if there's a significant difference to avoid micro-adjustments
            if (Math.abs(video.currentTime - targetVideoTime) > 0.5) {
                video.currentTime = targetVideoTime;
            }
        };

        const handleSpeedEvent = (e: CustomEvent) => {
            if (!isInClipRange) return;
            // Set playbackRate directly without any additional checks
            video.playbackRate = e.detail.speed;
        };

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

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

    // Sync video playback state - only play if in clip range
    useEffect(() => {
        const video = videoRef.current;
        if (!video) return;

        if (isPlaying && isInClipRange) {
            video.play().catch(console.error);
        } else {
            video.pause();
        }
    }, [isPlaying, isInClipRange]);

    // Sync volume
    useEffect(() => {
        const video = videoRef.current;
        if (!video) return;
        video.volume = volume;
    }, [volume]);

    // Sync speed immediately when it changes
    useEffect(() => {
        const video = videoRef.current;
        if (!video) return;
        video.playbackRate = speed;
    }, [speed]);

    return (
        <div className={`relative group ${className}`}>
            <video
                ref={videoRef}
                src={src}
                poster={poster}
                className="w-full h-full object-cover"
                playsInline
                preload="auto"
            />

            <div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />

            <div className="absolute bottom-2 left-2 right-2 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
                <Button
                    variant="ghost"
                    size="icon"
                    className="h-8 w-8 bg-black/50 text-white hover:bg-black/70"
                    onClick={isPlaying ? pause : play}
                >
                    {isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
                </Button>

                <div className="flex-1 h-1 bg-white/30 rounded-full overflow-hidden">
                    <div
                        className="h-full bg-white transition-all duration-100"
                        style={{ width: `${(currentTime / usePlaybackStore.getState().duration) * 100}%` }}
                    />
                </div>

                <Button
                    variant="ghost"
                    size="icon"
                    className="h-8 w-8 bg-black/50 text-white hover:bg-black/70"
                    onClick={() => setVolume(volume > 0 ? 0 : 1)}
                >
                    <Volume2 className="h-4 w-4" />
                </Button>
            </div>
        </div>
    );
}