Spaces:
Sleeping
Sleeping
Arnav Singh commited on
Revert "Replace react-player with native HTML5 video element"
Browse files- README.md +0 -36
- components/player/Html5PlayerWrapper.tsx +0 -119
- components/player/Html5VideoPlayer.tsx +0 -302
- components/player/Player.tsx +74 -67
- package-lock.json +0 -7
- package.json +1 -2
- yarn.lock +0 -5
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
|
| 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<
|
| 137 |
|
| 138 |
useEffect(() => {
|
| 139 |
if (!muted && !unmuted) {
|
|
@@ -264,7 +264,7 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
|
|
| 264 |
</div>
|
| 265 |
</div>
|
| 266 |
)}
|
| 267 |
-
<
|
| 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("
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 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 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
})
|
| 371 |
})
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
| 376 |
}
|
| 377 |
-
setError(e)
|
| 378 |
}}
|
| 379 |
onProgress={({ playedSeconds }) => {
|
| 380 |
if (!ready) {
|
| 381 |
console.warn(
|
| 382 |
-
"
|
| 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"
|