11import type React from 'react' ;
2- import { useState , useRef , useEffect } from 'react' ;
2+ import { useState , useRef , useEffect , useMemo } from 'react' ;
33import {
44 Play ,
55 Pause ,
@@ -11,6 +11,7 @@ import {
1111 VolumeX ,
1212} from 'lucide-react' ;
1313import { Slider } from '../../components/ui/Slider' ;
14+ import { convertFileSrc } from '@tauri-apps/api/core' ;
1415
1516interface NetflixStylePlayerProps {
1617 videoSrc : string ;
@@ -27,9 +28,13 @@ export default function NetflixStylePlayer({
2728 const [ isMuted , setIsMuted ] = useState ( false ) ;
2829 const [ showControls , setShowControls ] = useState ( false ) ;
2930 const [ isFullscreen , setIsFullscreen ] = useState ( false ) ;
31+ const [ currentTime , setCurrentTime ] = useState ( 0 ) ;
32+ const [ duration , setDuration ] = useState ( 0 ) ;
3033 const videoRef = useRef < HTMLVideoElement > ( null ) ;
3134 const containerRef = useRef < HTMLDivElement > ( null ) ;
3235
36+ const resolvedSrc = useMemo ( ( ) => convertFileSrc ( videoSrc ) , [ videoSrc ] ) ;
37+
3338 useEffect ( ( ) => {
3439 let timeout : NodeJS . Timeout ;
3540 const showControlsTemporarily = ( ) => {
@@ -44,16 +49,77 @@ export default function NetflixStylePlayer({
4449 container . addEventListener ( 'mouseenter' , showControlsTemporarily ) ;
4550 }
4651
52+ const handleKeyDown = ( e : KeyboardEvent ) => {
53+ const activeEl = document . activeElement ;
54+ if (
55+ activeEl &&
56+ ( activeEl . tagName === 'INPUT' ||
57+ activeEl . tagName === 'TEXTAREA' ||
58+ activeEl . getAttribute ( 'contenteditable' ) === 'true' )
59+ ) {
60+ return ;
61+ }
62+
63+ switch ( e . key ) {
64+ case ' ' :
65+ e . preventDefault ( ) ;
66+ togglePlay ( ) ;
67+ showControlsTemporarily ( ) ;
68+ break ;
69+ case 'ArrowLeft' :
70+ e . preventDefault ( ) ;
71+ skipTime ( - 10 ) ;
72+ showControlsTemporarily ( ) ;
73+ break ;
74+ case 'ArrowRight' :
75+ e . preventDefault ( ) ;
76+ skipTime ( 10 ) ;
77+ showControlsTemporarily ( ) ;
78+ break ;
79+ case 'm' :
80+ case 'M' :
81+ e . preventDefault ( ) ;
82+ toggleMute ( ) ;
83+ showControlsTemporarily ( ) ;
84+ break ;
85+ case 'f' :
86+ case 'F' :
87+ e . preventDefault ( ) ;
88+ toggleFullScreen ( ) ;
89+ showControlsTemporarily ( ) ;
90+ break ;
91+ default :
92+ break ;
93+ }
94+ } ;
95+
96+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
97+
4798 return ( ) => {
4899 if ( container ) {
49100 container . removeEventListener ( 'mousemove' , showControlsTemporarily ) ;
50101 container . removeEventListener ( 'mouseenter' , showControlsTemporarily ) ;
51102 }
103+ document . removeEventListener ( 'keydown' , handleKeyDown ) ;
52104 clearTimeout ( timeout ) ;
53105 } ;
54106 } , [ ] ) ;
55107
108+ useEffect ( ( ) => {
109+ const handleFullscreenChange = ( ) => {
110+ setIsFullscreen ( document . fullscreenElement === containerRef . current ) ;
111+ } ;
112+
113+ document . addEventListener ( 'fullscreenchange' , handleFullscreenChange ) ;
114+ return ( ) => {
115+ document . removeEventListener ( 'fullscreenchange' , handleFullscreenChange ) ;
116+ } ;
117+ } , [ ] ) ;
118+
56119 const formatTime = ( timeInSeconds : number ) => {
120+ if ( ! Number . isFinite ( timeInSeconds ) ) {
121+ return '0:00' ;
122+ }
57123 const hours = Math . floor ( timeInSeconds / 3600 ) ;
58124 const minutes = Math . floor ( ( timeInSeconds % 3600 ) / 60 ) ;
59125 const seconds = Math . floor ( timeInSeconds % 60 ) ;
@@ -65,27 +131,51 @@ export default function NetflixStylePlayer({
65131 } ;
66132
67133 const togglePlay = ( ) => {
68- if ( videoRef . current ) {
69- isPlaying ? videoRef . current . pause ( ) : videoRef . current . play ( ) ;
70- setIsPlaying ( ! isPlaying ) ;
134+ const video = videoRef . current ;
135+ if ( ! video ) return ;
136+ if ( video . paused ) {
137+ video . play ( ) . catch ( ( ) => { } ) ;
138+ } else {
139+ video . pause ( ) ;
71140 }
72141 } ;
73142
74143 const handleProgress = ( ) => {
75144 if ( videoRef . current ) {
145+ setCurrentTime ( videoRef . current . currentTime ) ;
76146 setProgress (
77- ( videoRef . current . currentTime / videoRef . current . duration ) * 100 ,
147+ videoRef . current . duration
148+ ? ( videoRef . current . currentTime / videoRef . current . duration ) * 100
149+ : 0 ,
78150 ) ;
79151 }
80152 } ;
81153
154+ const handleLoadedMetadata = ( ) => {
155+ if ( videoRef . current ) {
156+ setDuration ( videoRef . current . duration ) ;
157+ }
158+ } ;
159+
160+ const handleDurationChange = ( ) => {
161+ if ( videoRef . current ) {
162+ setDuration ( videoRef . current . duration ) ;
163+ }
164+ } ;
165+
82166 const handleProgressBarClick = ( e : React . MouseEvent < HTMLDivElement > ) => {
83167 if ( videoRef . current ) {
84168 const progressBar = e . currentTarget ;
85169 const clickPosition =
86170 ( e . clientX - progressBar . getBoundingClientRect ( ) . left ) /
87171 progressBar . offsetWidth ;
88- videoRef . current . currentTime = clickPosition * videoRef . current . duration ;
172+ const rawTime = clickPosition * videoRef . current . duration ;
173+ const maxTime = Number . isFinite ( videoRef . current . duration )
174+ ? videoRef . current . duration
175+ : 0 ;
176+ const newTime = Math . min ( Math . max ( rawTime , 0 ) , maxTime ) ;
177+ videoRef . current . currentTime = newTime ;
178+ setCurrentTime ( newTime ) ;
89179 }
90180 } ;
91181
@@ -95,27 +185,33 @@ export default function NetflixStylePlayer({
95185 } else {
96186 document . exitFullscreen ( ) ;
97187 }
98- setIsFullscreen ( ! isFullscreen ) ;
99188 } ;
100189
101190 const skipTime = ( seconds : number ) => {
102191 if ( videoRef . current ) {
103- videoRef . current . currentTime += seconds ;
192+ const rawTime = videoRef . current . currentTime + seconds ;
193+ const maxTime = Number . isFinite ( videoRef . current . duration )
194+ ? videoRef . current . duration
195+ : 0 ;
196+ const newTime = Math . min ( Math . max ( rawTime , 0 ) , maxTime ) ;
197+ videoRef . current . currentTime = newTime ;
198+ setCurrentTime ( newTime ) ;
104199 }
105200 } ;
106201
107202 const handleVolumeChange = ( newVolume : number [ ] ) => {
108203 if ( videoRef . current ) {
109204 const volumeValue = newVolume [ 0 ] ;
110205 videoRef . current . volume = volumeValue ;
206+ videoRef . current . muted = volumeValue === 0 ;
111207 setVolume ( volumeValue ) ;
112208 setIsMuted ( volumeValue === 0 ) ;
113209 }
114210 } ;
115211
116212 const toggleMute = ( ) => {
117213 if ( videoRef . current ) {
118- const newMuteState = ! isMuted ;
214+ const newMuteState = ! videoRef . current . muted ;
119215 videoRef . current . muted = newMuteState ;
120216 setIsMuted ( newMuteState ) ;
121217 }
@@ -130,9 +226,14 @@ export default function NetflixStylePlayer({
130226 >
131227 < video
132228 ref = { videoRef }
133- src = { videoSrc }
229+ src = { resolvedSrc }
134230 className = "h-full w-full"
135231 onTimeUpdate = { handleProgress }
232+ onLoadedMetadata = { handleLoadedMetadata }
233+ onDurationChange = { handleDurationChange }
234+ onPlay = { ( ) => setIsPlaying ( true ) }
235+ onPause = { ( ) => setIsPlaying ( false ) }
236+ onEnded = { ( ) => setIsPlaying ( false ) }
136237 preload = "auto"
137238 />
138239 </ div >
@@ -167,9 +268,7 @@ export default function NetflixStylePlayer({
167268 < FastForward size = { 24 } />
168269 </ button >
169270 < div className = "text-white" >
170- { formatTime ( videoRef . current ?. currentTime ?? 0 ) +
171- ' / ' +
172- formatTime ( videoRef . current ?. duration ?? 0 ) }
271+ { formatTime ( currentTime ) + ' / ' + formatTime ( duration ) }
173272 </ div >
174273 </ div >
175274
0 commit comments