iter 2
This commit is contained in:
@@ -3,14 +3,7 @@
|
||||
import type { ISegmentEditModalProps } from "./SegmentEditModal.d"
|
||||
import type { JSX } from "react"
|
||||
|
||||
import { MediaPlayer, MediaProvider, useMediaState } from "@vidstack/react"
|
||||
import {
|
||||
DefaultVideoLayout,
|
||||
defaultLayoutIcons,
|
||||
} from "@vidstack/react/player/layouts/default"
|
||||
import "@vidstack/react/player/styles/default/theme.css"
|
||||
import "@vidstack/react/player/styles/default/layouts/video.css"
|
||||
import { LoaderCircle, Scissors } from "lucide-react"
|
||||
import { LoaderCircle, Pause, Play, Scissors } from "lucide-react"
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { Button, Modal } from "@shared/ui"
|
||||
@@ -23,50 +16,146 @@ import { SegmentSplitter } from "@features/project/SegmentSplitter"
|
||||
|
||||
import styles from "./SegmentEditModal.module.scss"
|
||||
|
||||
const SegmentPlayer = ({
|
||||
videoUrl,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
const SegmentPlayer: FunctionComponent<{
|
||||
videoUrl: string
|
||||
start: number
|
||||
end: number
|
||||
}) => {
|
||||
const currentTime = useMediaState("currentTime")
|
||||
const playing = useMediaState("playing")
|
||||
const hasPausedRef = useRef(false)
|
||||
const playerRef = useRef<HTMLElement | null>(null)
|
||||
}> = ({ videoUrl, start, end }) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const trackRef = useRef<HTMLDivElement>(null)
|
||||
const rafRef = useRef<number>(0)
|
||||
const [currentTime, setCurrentTime] = useState(start)
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
const duration = end - start
|
||||
const progress =
|
||||
duration > 0
|
||||
? Math.min(Math.max((currentTime - start) / duration, 0), 1)
|
||||
: 0
|
||||
|
||||
/* Time tracking via rAF — only runs while playing or dragging */
|
||||
useEffect(() => {
|
||||
hasPausedRef.current = false
|
||||
if (!playing && !dragging) return
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
const tick = () => {
|
||||
setCurrentTime(video.currentTime)
|
||||
if (video.currentTime >= end && !video.paused) {
|
||||
video.pause()
|
||||
setPlaying(false)
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(tick)
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(tick)
|
||||
return () => cancelAnimationFrame(rafRef.current)
|
||||
}, [playing, dragging, end])
|
||||
|
||||
/* Set initial time once video is ready */
|
||||
useEffect(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
const onLoaded = () => {
|
||||
video.currentTime = start
|
||||
}
|
||||
video.addEventListener("loadedmetadata", onLoaded)
|
||||
if (video.readyState >= 1) onLoaded()
|
||||
return () => video.removeEventListener("loadedmetadata", onLoaded)
|
||||
}, [start])
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
if (video.paused) {
|
||||
if (video.currentTime >= end) video.currentTime = start
|
||||
video.play()
|
||||
setPlaying(true)
|
||||
} else {
|
||||
video.pause()
|
||||
setPlaying(false)
|
||||
}
|
||||
}, [start, end])
|
||||
|
||||
const seekToPosition = useCallback(
|
||||
(clientX: number) => {
|
||||
const track = trackRef.current
|
||||
const video = videoRef.current
|
||||
if (!track || !video || duration <= 0) return
|
||||
const rect = track.getBoundingClientRect()
|
||||
const fraction = Math.min(
|
||||
Math.max((clientX - rect.left) / rect.width, 0),
|
||||
1,
|
||||
)
|
||||
video.currentTime = start + fraction * duration
|
||||
},
|
||||
[start, duration],
|
||||
)
|
||||
|
||||
const handleTrackMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
setDragging(true)
|
||||
seekToPosition(e.clientX)
|
||||
},
|
||||
[seekToPosition],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!playing) return
|
||||
if (currentTime >= end && !hasPausedRef.current) {
|
||||
hasPausedRef.current = true
|
||||
const player = playerRef.current as HTMLElement & {
|
||||
pause?: () => void
|
||||
}
|
||||
player?.pause?.()
|
||||
if (!dragging) return
|
||||
const handleMouseMove = (e: MouseEvent) => seekToPosition(e.clientX)
|
||||
const handleMouseUp = () => setDragging(false)
|
||||
window.addEventListener("mousemove", handleMouseMove)
|
||||
window.addEventListener("mouseup", handleMouseUp)
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove)
|
||||
window.removeEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
}, [currentTime, end, playing])
|
||||
}, [dragging, seekToPosition])
|
||||
|
||||
return (
|
||||
<div className={styles.playerWrapper}>
|
||||
<MediaProvider />
|
||||
<DefaultVideoLayout
|
||||
icons={defaultLayoutIcons}
|
||||
slots={{
|
||||
settingsMenu: null,
|
||||
pipButton: null,
|
||||
fullscreenButton: null,
|
||||
airPlayButton: null,
|
||||
googleCastButton: null,
|
||||
}}
|
||||
/>
|
||||
<div className={styles.timeRange}>
|
||||
{secondsToTimecode(start)} — {secondsToTimecode(end)}
|
||||
<div className={styles.videoArea}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoUrl}
|
||||
crossOrigin="anonymous"
|
||||
playsInline
|
||||
preload="auto"
|
||||
className={styles.video}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.playButton}
|
||||
onClick={togglePlay}
|
||||
>
|
||||
{playing ? <Pause size={24} /> : <Play size={24} />}
|
||||
</button>
|
||||
<div className={styles.timeRange}>
|
||||
{secondsToTimecode(start)} — {secondsToTimecode(end)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.segmentControls}>
|
||||
<span className={styles.segmentTime}>
|
||||
{secondsToTimecode(Math.max(currentTime, start))}
|
||||
</span>
|
||||
<div
|
||||
className={styles.segmentTrack}
|
||||
ref={trackRef}
|
||||
onMouseDown={handleTrackMouseDown}
|
||||
>
|
||||
<div
|
||||
className={styles.segmentTrackFill}
|
||||
style={{ width: `${progress * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className={styles.segmentTrackThumb}
|
||||
style={{ left: `${progress * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={styles.segmentTime}>
|
||||
{secondsToTimecode(end)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -147,18 +236,11 @@ export const SegmentEditModal: FunctionComponent<
|
||||
>
|
||||
<div className={styles.root} data-testid="SegmentEditModal">
|
||||
{videoUrl && (
|
||||
<MediaPlayer
|
||||
src={videoUrl}
|
||||
currentTime={segment.start}
|
||||
className={styles.player}
|
||||
autoPlay
|
||||
>
|
||||
<SegmentPlayer
|
||||
videoUrl={videoUrl}
|
||||
start={segment.start}
|
||||
end={segment.end}
|
||||
/>
|
||||
</MediaPlayer>
|
||||
<SegmentPlayer
|
||||
videoUrl={videoUrl}
|
||||
start={segment.start}
|
||||
end={segment.end}
|
||||
/>
|
||||
)}
|
||||
|
||||
{splitMode ? (
|
||||
|
||||
Reference in New Issue
Block a user