Arnav Singh commited on
Commit
a6d3871
·
unverified ·
1 Parent(s): a9ce748

Revert "Replace react-player with native HTML5 video element"

Browse files
README.md CHANGED
@@ -10,39 +10,3 @@ pinned: false
10
 
11
  # Streamer
12
  A synced video/music room app based on Web-SyncPlay.
13
-
14
- ## Video Player
15
-
16
- This app uses a native HTML5 `<video>` element for video playback with the following features:
17
-
18
- ### Supported Features
19
- - **Native Browser Controls**: Play/pause, seek, volume, fullscreen
20
- - **Picture-in-Picture (PiP)**: Native browser PiP support where available
21
- - **HLS Streaming**: Automatic HLS support using hls.js for browsers without native support
22
- - **Synchronized Playback**: Real-time sync across multiple clients in the same room
23
- - **Error Handling**: Graceful fallback using yt-dlp for unsupported formats
24
-
25
- ### Supported Video Formats
26
- - Direct video URLs (mp4, webm, ogg, etc.)
27
- - HLS streams (.m3u8)
28
- - Any format playable in modern browsers
29
-
30
- ### Note on YouTube Videos
31
- For YouTube videos, the app uses a fallback API (yt-dlp) to extract direct video URLs since YouTube's iframe player cannot be used with native HTML5 `<video>` elements.
32
-
33
- ## Development
34
-
35
- ```bash
36
- npm install
37
- npm run dev
38
- ```
39
-
40
- Build for production:
41
- ```bash
42
- npm run build
43
- ```
44
-
45
- Lint code:
46
- ```bash
47
- npm run lint
48
- ```
 
10
 
11
  # Streamer
12
  A synced video/music room app based on Web-SyncPlay.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/player/Html5PlayerWrapper.tsx DELETED
@@ -1,119 +0,0 @@
1
- "use client"
2
- import React, { forwardRef, useImperativeHandle, useRef, useState } from "react"
3
- import Html5VideoPlayer from "./Html5VideoPlayer"
4
-
5
- export interface Html5PlayerWrapperHandle {
6
- seekTo: (seconds: number, type?: string) => void
7
- getCurrentTime: () => number
8
- getInternalPlayer: () => HTMLVideoElement | null
9
- }
10
-
11
- interface Html5PlayerWrapperProps {
12
- url: string
13
- playing?: boolean
14
- volume?: number
15
- muted?: boolean
16
- playbackRate?: number
17
- loop?: boolean
18
- pip?: boolean
19
- controls?: boolean
20
- width?: string | number
21
- height?: string | number
22
- style?: React.CSSProperties
23
- config?: any
24
- onReady?: () => void
25
- onPlay?: () => void
26
- onPause?: () => void
27
- onEnded?: () => void
28
- onError?: (error: any) => void
29
- onProgress?: (state: { playedSeconds: number }) => void
30
- onDuration?: (duration: number) => void
31
- onBuffer?: () => void
32
- onBufferEnd?: () => void
33
- }
34
-
35
- const Html5PlayerWrapper = forwardRef<Html5PlayerWrapperHandle, Html5PlayerWrapperProps>(
36
- (props, ref) => {
37
- const {
38
- url,
39
- playing = false,
40
- volume = 1,
41
- muted = false,
42
- playbackRate = 1,
43
- loop = false,
44
- pip = false,
45
- width = "100%",
46
- height = "100%",
47
- style = {},
48
- onReady = () => {},
49
- onPlay = () => {},
50
- onPause = () => {},
51
- onEnded = () => {},
52
- onError = () => {},
53
- onProgress = () => {},
54
- onDuration = () => {},
55
- onBuffer = () => {},
56
- onBufferEnd = () => {},
57
- } = props
58
-
59
- const internalVideoRef = useRef<HTMLVideoElement | null>(null)
60
- const [seekToValue, setSeekToValue] = useState<number | undefined>(undefined)
61
-
62
- useImperativeHandle(ref, () => ({
63
- seekTo: (seconds: number) => {
64
- setSeekToValue(seconds)
65
- // Reset after a frame to allow re-seeking to the same position
66
- setTimeout(() => setSeekToValue(undefined), 0)
67
- },
68
- getCurrentTime: () => {
69
- return internalVideoRef.current?.currentTime || 0
70
- },
71
- getInternalPlayer: () => {
72
- return internalVideoRef.current
73
- },
74
- }))
75
-
76
- const containerStyle: React.CSSProperties = {
77
- width: typeof width === 'number' ? `${width}px` : width,
78
- height: typeof height === 'number' ? `${height}px` : height,
79
- ...style,
80
- }
81
-
82
- const videoStyle: React.CSSProperties = {
83
- width: '100%',
84
- height: '100%',
85
- objectFit: 'contain',
86
- backgroundColor: '#000',
87
- }
88
-
89
- return (
90
- <div style={containerStyle} className="html5-player-wrapper">
91
- <Html5VideoPlayer
92
- url={url}
93
- playing={playing}
94
- volume={volume}
95
- muted={muted}
96
- playbackRate={playbackRate}
97
- loop={loop}
98
- pip={pip}
99
- seekTo={seekToValue}
100
- style={videoStyle}
101
- videoRef={internalVideoRef}
102
- onReady={onReady}
103
- onPlay={onPlay}
104
- onPause={onPause}
105
- onEnded={onEnded}
106
- onError={onError}
107
- onProgress={onProgress}
108
- onDuration={onDuration}
109
- onBuffer={onBuffer}
110
- onBufferEnd={onBufferEnd}
111
- />
112
- </div>
113
- )
114
- }
115
- )
116
-
117
- Html5PlayerWrapper.displayName = "Html5PlayerWrapper"
118
-
119
- export default Html5PlayerWrapper
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/player/Html5VideoPlayer.tsx DELETED
@@ -1,302 +0,0 @@
1
- "use client"
2
- import React, { FC, useEffect, useRef, useState, useCallback } from "react"
3
- import Hls from "hls.js"
4
-
5
- interface Html5VideoPlayerProps {
6
- url: string
7
- playing: boolean
8
- volume: number
9
- muted: boolean
10
- playbackRate: number
11
- loop: boolean
12
- onReady: () => void
13
- onPlay: () => void
14
- onPause: () => void
15
- onEnded: () => void
16
- onError: (error: any) => void
17
- onProgress: (state: { playedSeconds: number }) => void
18
- onDuration: (duration: number) => void
19
- onBuffer: () => void
20
- onBufferEnd: () => void
21
- seekTo?: number
22
- pip?: boolean
23
- style?: React.CSSProperties
24
- className?: string
25
- videoRef?: React.RefObject<HTMLVideoElement>
26
- }
27
-
28
- const Html5VideoPlayer: FC<Html5VideoPlayerProps> = ({
29
- url,
30
- playing,
31
- volume,
32
- muted,
33
- playbackRate,
34
- loop,
35
- onReady,
36
- onPlay,
37
- onPause,
38
- onEnded,
39
- onError,
40
- onProgress,
41
- onDuration,
42
- onBuffer,
43
- onBufferEnd,
44
- seekTo,
45
- pip = false,
46
- style,
47
- className,
48
- videoRef: externalVideoRef,
49
- }) => {
50
- const internalVideoRef = useRef<HTMLVideoElement>(null)
51
- const videoRef = externalVideoRef || internalVideoRef
52
- const [isReady, setIsReady] = useState(false)
53
- const [currentUrl, setCurrentUrl] = useState(url)
54
- const progressIntervalRef = useRef<NodeJS.Timeout | null>(null)
55
- const seekingRef = useRef(false)
56
- const lastSeekToRef = useRef<number | undefined>(undefined)
57
- const hlsRef = useRef<Hls | null>(null)
58
-
59
- // Handle PiP state
60
- useEffect(() => {
61
- const video = videoRef.current
62
- if (!video || !document.pictureInPictureEnabled) return
63
-
64
- const handlePiP = async () => {
65
- try {
66
- if (pip && document.pictureInPictureElement !== video) {
67
- await video.requestPictureInPicture()
68
- } else if (!pip && document.pictureInPictureElement === video) {
69
- await document.exitPictureInPicture()
70
- }
71
- } catch (error) {
72
- console.error("PiP error:", error)
73
- }
74
- }
75
-
76
- handlePiP()
77
- }, [pip])
78
-
79
- // Handle URL changes
80
- useEffect(() => {
81
- if (url !== currentUrl) {
82
- setIsReady(false)
83
- setCurrentUrl(url)
84
-
85
- // Clean up HLS instance if it exists
86
- if (hlsRef.current) {
87
- hlsRef.current.destroy()
88
- hlsRef.current = null
89
- }
90
-
91
- const video = videoRef.current
92
- if (!video) return
93
-
94
- // Check if URL is HLS - look for .m3u8 extension or m3u8 in query params
95
- const isHLS = /\.m3u8($|\?)/i.test(url) || /[?&]format=m3u8/i.test(url)
96
-
97
- if (isHLS) {
98
- // Try native HLS support first (Safari)
99
- if (video.canPlayType('application/vnd.apple.mpegurl')) {
100
- video.src = url
101
- } else if (Hls.isSupported()) {
102
- // Use hls.js for browsers that don't support native HLS
103
- const hls = new Hls()
104
- hls.loadSource(url)
105
- hls.attachMedia(video)
106
- hls.on(Hls.Events.MANIFEST_PARSED, () => {
107
- console.log("HLS manifest parsed")
108
- })
109
- hls.on(Hls.Events.ERROR, (_event: any, data: any) => {
110
- console.error("HLS error:", data)
111
- if (data.fatal) {
112
- onError(new Error(`HLS Error: ${data.type}`))
113
- }
114
- })
115
- hlsRef.current = hls
116
- } else {
117
- // Fallback to direct source
118
- video.src = url
119
- }
120
- } else {
121
- video.src = url
122
- }
123
- }
124
- }, [url, currentUrl, onError])
125
-
126
- // Handle play/pause
127
- useEffect(() => {
128
- const video = videoRef.current
129
- if (!video || !isReady) return
130
-
131
- if (playing) {
132
- const playPromise = video.play()
133
- if (playPromise !== undefined) {
134
- playPromise.catch((error) => {
135
- console.warn("Play interrupted:", error)
136
- })
137
- }
138
- } else {
139
- video.pause()
140
- }
141
- }, [playing, isReady])
142
-
143
- // Handle volume
144
- useEffect(() => {
145
- const video = videoRef.current
146
- if (!video) return
147
- video.volume = volume
148
- }, [volume])
149
-
150
- // Handle muted
151
- useEffect(() => {
152
- const video = videoRef.current
153
- if (!video) return
154
- video.muted = muted
155
- }, [muted])
156
-
157
- // Handle playback rate
158
- useEffect(() => {
159
- const video = videoRef.current
160
- if (!video) return
161
- video.playbackRate = playbackRate
162
- }, [playbackRate])
163
-
164
- // Handle loop
165
- useEffect(() => {
166
- const video = videoRef.current
167
- if (!video) return
168
- video.loop = loop
169
- }, [loop])
170
-
171
- // Handle seeking
172
- useEffect(() => {
173
- if (seekTo !== undefined && seekTo !== lastSeekToRef.current) {
174
- const video = videoRef.current
175
- if (video && !seekingRef.current) {
176
- lastSeekToRef.current = seekTo
177
- video.currentTime = seekTo
178
- }
179
- }
180
- }, [seekTo])
181
-
182
- // Progress reporting
183
- useEffect(() => {
184
- const video = videoRef.current
185
- if (!video || !isReady) return
186
-
187
- const reportProgress = () => {
188
- if (!seekingRef.current) {
189
- onProgress({ playedSeconds: video.currentTime })
190
- }
191
- }
192
-
193
- // Report progress every 250ms when playing
194
- if (playing) {
195
- progressIntervalRef.current = setInterval(reportProgress, 250)
196
- } else {
197
- if (progressIntervalRef.current) {
198
- clearInterval(progressIntervalRef.current)
199
- progressIntervalRef.current = null
200
- }
201
- }
202
-
203
- return () => {
204
- if (progressIntervalRef.current) {
205
- clearInterval(progressIntervalRef.current)
206
- progressIntervalRef.current = null
207
- }
208
- }
209
- }, [isReady, playing, onProgress])
210
-
211
- // Video element event handlers
212
- const handleLoadedMetadata = useCallback(() => {
213
- const video = videoRef.current
214
- if (video) {
215
- onDuration(video.duration)
216
- }
217
- }, [onDuration])
218
-
219
- const handleCanPlay = useCallback(() => {
220
- if (!isReady) {
221
- setIsReady(true)
222
- onReady()
223
- }
224
- onBufferEnd()
225
- }, [isReady, onReady, onBufferEnd])
226
-
227
- const handlePlay = useCallback(() => {
228
- onPlay()
229
- }, [onPlay])
230
-
231
- const handlePause = useCallback(() => {
232
- onPause()
233
- }, [onPause])
234
-
235
- const handleEnded = useCallback(() => {
236
- onEnded()
237
- }, [onEnded])
238
-
239
- const handleError = useCallback((e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
240
- const video = e.currentTarget
241
- const error = video.error
242
- console.error("Video error:", error)
243
- onError(error || new Error("Unknown video error"))
244
- }, [onError])
245
-
246
- const handleWaiting = useCallback(() => {
247
- onBuffer()
248
- }, [onBuffer])
249
-
250
- const handleSeeking = useCallback(() => {
251
- seekingRef.current = true
252
- }, [])
253
-
254
- const handleSeeked = useCallback(() => {
255
- seekingRef.current = false
256
- const video = videoRef.current
257
- if (video) {
258
- onProgress({ playedSeconds: video.currentTime })
259
- }
260
- }, [onProgress])
261
-
262
- const handleStalled = useCallback(() => {
263
- console.warn("Video stalled")
264
- onBuffer()
265
- }, [onBuffer])
266
-
267
- // Cleanup
268
- useEffect(() => {
269
- return () => {
270
- if (hlsRef.current) {
271
- hlsRef.current.destroy()
272
- hlsRef.current = null
273
- }
274
- if (progressIntervalRef.current) {
275
- clearInterval(progressIntervalRef.current)
276
- progressIntervalRef.current = null
277
- }
278
- }
279
- }, [])
280
-
281
- return (
282
- <video
283
- ref={videoRef}
284
- style={style}
285
- className={className}
286
- playsInline
287
- crossOrigin="anonymous"
288
- onLoadedMetadata={handleLoadedMetadata}
289
- onCanPlay={handleCanPlay}
290
- onPlay={handlePlay}
291
- onPause={handlePause}
292
- onEnded={handleEnded}
293
- onError={handleError}
294
- onWaiting={handleWaiting}
295
- onSeeking={handleSeeking}
296
- onSeeked={handleSeeked}
297
- onStalled={handleStalled}
298
- />
299
- )
300
- }
301
-
302
- export default Html5VideoPlayer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/player/Player.tsx CHANGED
@@ -12,7 +12,7 @@ import {
12
  FullScreenProps,
13
  useFullScreenHandle,
14
  } from "react-full-screen"
15
- import Html5PlayerWrapper, { Html5PlayerWrapperHandle } from "./Html5PlayerWrapper"
16
  import {
17
  MediaElement,
18
  MediaOption,
@@ -133,7 +133,7 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
133
  const [pipEnabled, setPipEnabled] = useState(false)
134
  const [musicMode, setMusicMode] = useState(false)
135
  const fullscreenHandle = useFullScreenHandle()
136
- const player = useRef<Html5PlayerWrapperHandle>(null)
137
 
138
  useEffect(() => {
139
  if (!muted && !unmuted) {
@@ -264,7 +264,7 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
264
  </div>
265
  </div>
266
  )}
267
- <Html5PlayerWrapper
268
  style={{
269
  maxHeight: fullscreen || fullHeight ? "100vh" : "calc(100vh - 210px)",
270
  visibility: musicMode ? "hidden" : "visible",
@@ -273,6 +273,20 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
273
  ref={player}
274
  width={"100%"}
275
  height={fullscreen || fullHeight ? "100vh" : "calc((9 / 16) * 100vw)"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  url={currentSrc.src}
277
  pip={pipEnabled}
278
  playing={!paused}
@@ -282,17 +296,35 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
282
  volume={volume}
283
  muted={muted}
284
  onReady={() => {
285
- console.log("HTML5 Video Player is ready")
286
  setReady(true)
287
  setBuffering(false)
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  }}
289
  onPlay={() => {
290
  console.log("player started to play")
291
  if (paused) {
292
  const internalPlayer = player.current?.getInternalPlayer()
293
  console.warn("Started to play despite being paused", internalPlayer)
294
- if (internalPlayer) {
295
- internalPlayer.pause()
 
 
 
 
 
296
  }
297
  }
298
  }}
@@ -304,8 +336,13 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
304
  "Started to pause despite being not paused",
305
  internalPlayer
306
  )
307
- if (internalPlayer) {
308
- internalPlayer.play()
 
 
 
 
 
309
  }
310
  }
311
  }}
@@ -314,72 +351,42 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
314
  onEnded={() => socket?.emit("playEnded")}
315
  onError={(e) => {
316
  console.error("playback error", e)
317
- // For HTML5 video, we get MediaError objects or Error instances
318
- const isMediaError = (error: any): error is MediaError => {
319
- return error && typeof error === 'object' &&
320
- 'code' in error &&
321
- typeof error.code === 'number' &&
322
- error.code >= MediaError.MEDIA_ERR_ABORTED &&
323
- error.code <= MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
324
- }
325
-
326
- if (isMediaError(e)) {
327
- const mediaError = e
328
- let errorMessage = "Video playback error"
329
- switch (mediaError.code) {
330
- case MediaError.MEDIA_ERR_ABORTED:
331
- errorMessage = "Video playback aborted"
332
- break
333
- case MediaError.MEDIA_ERR_NETWORK:
334
- errorMessage = "Network error while loading video"
335
- break
336
- case MediaError.MEDIA_ERR_DECODE:
337
- errorMessage = "Video decoding error"
338
- break
339
- case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
340
- errorMessage = "Video format not supported"
341
- break
342
- }
343
- console.error(errorMessage, mediaError)
344
-
345
- // Try to get video url via yt-dlp for unsupported sources
346
- if (mediaError.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
347
- console.log("Trying to get video url via yt-dlp...")
348
- fetch("/api/source", { method: "POST", body: currentSrc.src })
349
- .then((res) => {
350
- if (res.status === 200) {
351
- return res.json()
352
- }
353
- return res.text()
354
- })
355
- .then((data) => {
356
- console.log("Received data", data)
357
- if (typeof data === "string") {
358
- throw new Error(data)
359
- }
360
- if (data.error) {
361
- throw new Error(data.stderr)
362
- }
363
 
364
- const videoSrc: string[] = data.stdout
365
- .split("\n")
366
- .filter((v: string) => v !== "")
367
- setCurrentSrc({
368
- src: videoSrc[0],
369
- resolution: "",
370
- })
371
  })
372
- .catch((error) => {
373
- console.error("Failed to get video url", error)
374
- })
375
- }
 
376
  }
377
- setError(e)
378
  }}
379
  onProgress={({ playedSeconds }) => {
380
  if (!ready) {
381
  console.warn(
382
- "HTML5 Video Player did not report it being ready, but already playing"
383
  )
384
  // sometimes onReady doesn't fire, but if there's playback...
385
  setReady(true)
 
12
  FullScreenProps,
13
  useFullScreenHandle,
14
  } from "react-full-screen"
15
+ import ReactPlayer from "react-player"
16
  import {
17
  MediaElement,
18
  MediaOption,
 
133
  const [pipEnabled, setPipEnabled] = useState(false)
134
  const [musicMode, setMusicMode] = useState(false)
135
  const fullscreenHandle = useFullScreenHandle()
136
+ const player = useRef<ReactPlayer>(null)
137
 
138
  useEffect(() => {
139
  if (!muted && !unmuted) {
 
264
  </div>
265
  </div>
266
  )}
267
+ <ReactPlayer
268
  style={{
269
  maxHeight: fullscreen || fullHeight ? "100vh" : "calc(100vh - 210px)",
270
  visibility: musicMode ? "hidden" : "visible",
 
273
  ref={player}
274
  width={"100%"}
275
  height={fullscreen || fullHeight ? "100vh" : "calc((9 / 16) * 100vw)"}
276
+ config={{
277
+ youtube: {
278
+ playerVars: {
279
+ disablekb: 1,
280
+ modestbranding: 1,
281
+ origin: window.location.host,
282
+ },
283
+ },
284
+ file: {
285
+ hlsVersion: "1.1.3",
286
+ dashVersion: "4.2.1",
287
+ flvVersion: "1.6.2",
288
+ },
289
+ }}
290
  url={currentSrc.src}
291
  pip={pipEnabled}
292
  playing={!paused}
 
296
  volume={volume}
297
  muted={muted}
298
  onReady={() => {
299
+ console.log("React-Player is ready")
300
  setReady(true)
301
  setBuffering(false)
302
+ // need "long" timeout for yt to be ready
303
+ setTimeout(() => {
304
+ const internalPlayer = player.current?.getInternalPlayer()
305
+ console.log("Internal player:", player)
306
+ if (
307
+ typeof internalPlayer !== "undefined" &&
308
+ internalPlayer.unloadModule
309
+ ) {
310
+ console.log("Unloading cc of youtube player")
311
+ internalPlayer.unloadModule("cc") // Works for AS3 ignored by html5
312
+ internalPlayer.unloadModule("captions") // Works for html5 ignored by AS3
313
+ }
314
+ }, 1000)
315
  }}
316
  onPlay={() => {
317
  console.log("player started to play")
318
  if (paused) {
319
  const internalPlayer = player.current?.getInternalPlayer()
320
  console.warn("Started to play despite being paused", internalPlayer)
321
+ if (typeof internalPlayer !== "undefined") {
322
+ if ("pause" in internalPlayer) {
323
+ internalPlayer.pause()
324
+ }
325
+ if ("pauseVideo" in internalPlayer) {
326
+ internalPlayer.pauseVideo()
327
+ }
328
  }
329
  }
330
  }}
 
336
  "Started to pause despite being not paused",
337
  internalPlayer
338
  )
339
+ if (typeof internalPlayer !== "undefined") {
340
+ if ("play" in internalPlayer) {
341
+ internalPlayer.play()
342
+ }
343
+ if ("playVideo" in internalPlayer) {
344
+ internalPlayer.playVideo()
345
+ }
346
  }
347
  }
348
  }}
 
351
  onEnded={() => socket?.emit("playEnded")}
352
  onError={(e) => {
353
  console.error("playback error", e)
354
+ if ("target" in e && "type" in e && e.type === "error") {
355
+ console.log("Trying to get video url via yt-dlp...")
356
+ fetch("/api/source", { method: "POST", body: currentSrc.src })
357
+ .then((res) => {
358
+ if (res.status === 200) {
359
+ return res.json()
360
+ }
361
+ return res.text()
362
+ })
363
+ .then((data) => {
364
+ console.log("Received data", data)
365
+ if (typeof data === "string") {
366
+ throw new Error(data)
367
+ }
368
+ if (data.error) {
369
+ throw new Error(data.stderr)
370
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
+ const videoSrc: string[] = data.stdout
373
+ .split("\n")
374
+ .filter((v: string) => v !== "")
375
+ setCurrentSrc({
376
+ src: videoSrc[0],
377
+ resolution: "",
 
378
  })
379
+ })
380
+ .catch((error) => {
381
+ console.error("Failed to get video url", error)
382
+ })
383
+ setError(e)
384
  }
 
385
  }}
386
  onProgress={({ playedSeconds }) => {
387
  if (!ready) {
388
  console.warn(
389
+ "React-Player did not report it being ready, but already playing"
390
  )
391
  // sometimes onReady doesn't fire, but if there's playback...
392
  setReady(true)
package-lock.json CHANGED
@@ -5,7 +5,6 @@
5
  "packages": {
6
  "": {
7
  "dependencies": {
8
- "hls.js": "^1.6.15",
9
  "next": "14.2.10",
10
  "react": "^18.2.0",
11
  "react-beautiful-dnd": "^13.1.1",
@@ -3403,12 +3402,6 @@
3403
  "node": ">= 0.4"
3404
  }
3405
  },
3406
- "node_modules/hls.js": {
3407
- "version": "1.6.15",
3408
- "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
3409
- "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==",
3410
- "license": "Apache-2.0"
3411
- },
3412
  "node_modules/hoist-non-react-statics": {
3413
  "version": "3.3.2",
3414
  "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
 
5
  "packages": {
6
  "": {
7
  "dependencies": {
 
8
  "next": "14.2.10",
9
  "react": "^18.2.0",
10
  "react-beautiful-dnd": "^13.1.1",
 
3402
  "node": ">= 0.4"
3403
  }
3404
  },
 
 
 
 
 
 
3405
  "node_modules/hoist-non-react-statics": {
3406
  "version": "3.3.2",
3407
  "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
package.json CHANGED
@@ -7,7 +7,6 @@
7
  "lint": "next lint"
8
  },
9
  "dependencies": {
10
- "hls.js": "^1.6.15",
11
  "next": "14.2.10",
12
  "react": "^18.2.0",
13
  "react-beautiful-dnd": "^13.1.1",
@@ -38,4 +37,4 @@
38
  "ts-node": "^10.9.2",
39
  "typescript": "5.3.3"
40
  }
41
- }
 
7
  "lint": "next lint"
8
  },
9
  "dependencies": {
 
10
  "next": "14.2.10",
11
  "react": "^18.2.0",
12
  "react-beautiful-dnd": "^13.1.1",
 
37
  "ts-node": "^10.9.2",
38
  "typescript": "5.3.3"
39
  }
40
+ }
yarn.lock CHANGED
@@ -1620,11 +1620,6 @@ hasown@^2.0.0:
1620
  dependencies:
1621
  function-bind "^1.1.2"
1622
 
1623
- hls.js@^1.6.15:
1624
- version "1.6.15"
1625
- resolved "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz"
1626
- integrity sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==
1627
-
1628
  hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
1629
  version "3.3.2"
1630
  resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
 
1620
  dependencies:
1621
  function-bind "^1.1.2"
1622
 
 
 
 
 
 
1623
  hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
1624
  version "3.3.2"
1625
  resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"