Add live broadcast detection and UI indicator
Introduces isLiveBroadcast to VideoState and PlayerContext to detect live streams (duration is Infinity or 0). Updates ControlsLayer to show a 'LIVE' badge and hide progress/time for live broadcasts. Adjusts VideoElement to set and update isLiveBroadcast on metadata and duration changes. Adds related CSS for the live indicator and improves ESLint config for unused vars.
This commit is contained in:
@@ -12,7 +12,8 @@
|
||||
"Bash(npm publish)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(npm test:*)",
|
||||
"Bash(npm install:*)"
|
||||
"Bash(npm install:*)",
|
||||
"Bash(pnpm lint:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -35,6 +35,10 @@ export default [
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_'
|
||||
}],
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'react-refresh/only-export-components': 'warn'
|
||||
|
||||
@@ -85,6 +85,45 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Live indicator */
|
||||
.live-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(220, 38, 38, 0.2);
|
||||
border: 1px solid rgb(220, 38, 38);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--player-text-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: rgb(220, 38, 38);
|
||||
border-radius: 50%;
|
||||
animation: live-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes live-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.live-text {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.controls-bar {
|
||||
padding: var(--player-spacing-md) var(--player-spacing-md) var(--player-spacing-sm);
|
||||
@@ -99,4 +138,15 @@
|
||||
.controls-right {
|
||||
gap: var(--player-spacing-xs);
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,17 +210,27 @@ export const ControlsLayer: React.FC<ControlsLayerProps> = ({
|
||||
|
||||
{/* Bottom controls bar */}
|
||||
<div className="controls-bar">
|
||||
{/* Progress bar (full width on top) */}
|
||||
<div className="progress-container">
|
||||
<ProgressBar />
|
||||
</div>
|
||||
{/* Progress bar (full width on top) - hidden for live broadcasts */}
|
||||
{!videoState.isLiveBroadcast && (
|
||||
<div className="progress-container">
|
||||
<ProgressBar />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Control buttons */}
|
||||
<div className="controls-row">
|
||||
<div className="controls-left">
|
||||
<PlayPauseButton />
|
||||
<VolumeControl />
|
||||
<TimeDisplay />
|
||||
{/* Time display - hidden for live broadcasts */}
|
||||
{!videoState.isLiveBroadcast && <TimeDisplay />}
|
||||
{/* Show "LIVE" badge for live broadcasts */}
|
||||
{videoState.isLiveBroadcast && (
|
||||
<div className="live-indicator">
|
||||
<span className="live-dot"></span>
|
||||
<span className="live-text">LIVE</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="controls-right">
|
||||
|
||||
@@ -90,11 +90,16 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
// Check if this is a live broadcast (duration is Infinity for live streams)
|
||||
const isLiveBroadcast = !isFinite(video.duration) || video.duration === 0
|
||||
console.log('[VideoElement] Is live broadcast?', isLiveBroadcast, 'duration:', video.duration)
|
||||
|
||||
setVideoState((prev) => ({
|
||||
...prev,
|
||||
duration: video.duration,
|
||||
volume: video.volume,
|
||||
muted: video.muted,
|
||||
isLiveBroadcast,
|
||||
}))
|
||||
|
||||
// Enable default subtitle if specified
|
||||
@@ -111,6 +116,21 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
onLoadedMetadata?.()
|
||||
}, [videoRef, setVideoState, onLoadedMetadata, processedSubtitles, setSubtitle])
|
||||
|
||||
const handleDurationChange = useCallback(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
// Re-check if this is a live broadcast when duration changes
|
||||
const isLiveBroadcast = !isFinite(video.duration) || video.duration === 0
|
||||
console.log('[VideoElement] Duration changed. Is live broadcast?', isLiveBroadcast, 'duration:', video.duration)
|
||||
|
||||
setVideoState((prev) => ({
|
||||
...prev,
|
||||
duration: video.duration,
|
||||
isLiveBroadcast,
|
||||
}))
|
||||
}, [videoRef, setVideoState])
|
||||
|
||||
const handleVolumeChange = useCallback(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
@@ -721,6 +741,7 @@ export const VideoElement: React.FC<VideoElementProps> = ({
|
||||
onPause={handlePause}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onLoadedMetadata={handleLoadedMetadata}
|
||||
onDurationChange={handleDurationChange}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
onSeeking={handleSeeking}
|
||||
onSeeked={handleSeeked}
|
||||
|
||||
@@ -57,6 +57,7 @@ export const PlayerProvider: React.FC<PlayerProviderProps> = ({
|
||||
loading: false,
|
||||
error: null,
|
||||
seeking: false,
|
||||
isLiveBroadcast: false,
|
||||
})
|
||||
|
||||
const [uiState, setUIState] = useState<UIState>({
|
||||
|
||||
@@ -28,9 +28,19 @@ export default class Hls {
|
||||
}
|
||||
}
|
||||
|
||||
loadSource(_src: string) {}
|
||||
attachMedia(_video: HTMLVideoElement) {}
|
||||
destroy() {}
|
||||
on(_event: string, _handler: Function) {}
|
||||
off(_event: string, _handler: Function) {}
|
||||
loadSource(_src: string) {
|
||||
// Mock implementation
|
||||
}
|
||||
attachMedia(_video: HTMLVideoElement) {
|
||||
// Mock implementation
|
||||
}
|
||||
destroy() {
|
||||
// Mock implementation
|
||||
}
|
||||
on(_event: string, _handler: (...args: unknown[]) => void) {
|
||||
// Mock implementation
|
||||
}
|
||||
off(_event: string, _handler: (...args: unknown[]) => void) {
|
||||
// Mock implementation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface VideoState {
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
seeking: boolean
|
||||
isLiveBroadcast: boolean // True for actual live broadcasts (not VOD)
|
||||
}
|
||||
|
||||
export interface UIState {
|
||||
|
||||
Reference in New Issue
Block a user