summaryrefslogtreecommitdiff
path: root/apps/web/src/components/ui/video-player.tsx
blob: 32a12916c0135e2aa86d3b950a72ac1d1e29193a (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
"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;
}

export function VideoPlayer({
    src,
    poster,
    className = "",
    clipStartTime,
    trimStart,
    trimEnd,
    clipDuration
}: VideoPlayerProps) {
    const videoRef = useRef<HTMLVideoElement>(null);
    const { isPlaying, currentTime, volume, speed } = 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.playbackRate = speed;
    }, [volume, speed]);

    return (
        <video
            ref={videoRef}
            src={src}
            poster={poster}
            className={`w-full h-full object-cover ${className}`}
            playsInline
            preload="auto"
            controls={false}
            disablePictureInPicture
            disableRemotePlayback
            style={{ pointerEvents: 'none' }}
            onContextMenu={(e) => e.preventDefault()}
        />
    );
}