Skip to main content

Overview

The VideoPlayer component is StreamVault’s built-in video player, providing a full-featured HTML5 video experience with automatic transcoding fallback, progress tracking, and keyboard controls.

Features

  • HTML5 Video Controls: Custom UI with play/pause, seek, volume, and fullscreen
  • Progress Tracking: Automatic progress updates every 5 seconds
  • Transcoding Fallback: Auto-transcode unsupported formats using FFmpeg
  • Keyboard Shortcuts: Space/K to play/pause, F for fullscreen, M to mute, Arrow keys to seek
  • Large File Support: Chunked loading for files >4GB
  • Blob URL Loading: Local files loaded as blob URLs for browser compatibility

Component Interface

VideoPlayer.tsx

interface VideoPlayerProps {
  src: string              // File path or URL
  title: string            // Display title
  poster?: string          // Poster image URL
  onClose: () => void      // Close handler
  onProgress?: (currentTime: number, duration: number) => void
  initialTime?: number     // Resume position in seconds
  isCloud?: boolean        // Cloud streaming mode
  accessToken?: string     // Auth token for cloud
  mediaId?: number         // Media ID for transcoding
}
Source: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:6-18

Usage

Basic Playback

import { VideoPlayer } from '@/components/VideoPlayer'

function MediaPage() {
  const [showPlayer, setShowPlayer] = useState(false)

  return (
    <>
      <button onClick={() => setShowPlayer(true)}>
        Play Video
      </button>
      
      {showPlayer && (
        <VideoPlayer
          src="/path/to/video.mp4"
          title="Movie Title"
          poster="https://image.tmdb.org/poster.jpg"
          onClose={() => setShowPlayer(false)}
        />
      )}
    </>
  )
}

Resume Playback with Progress

const handlePlayWithProgress = () => {
  <VideoPlayer
    src={media.file_path}
    title={media.title}
    initialTime={media.current_position || 0}
    onProgress={(currentTime, duration) => {
      // Update progress in database
      invoke('update_progress', {
        mediaId: media.id,
        currentTime,
        duration
      })
    }}
    onClose={() => setShowPlayer(false)}
  />
}

Transcoding Support

The player automatically transcodes unsupported formats using FFmpeg:

Auto-Transcode Formats

const needsTranscode = [
  'mkv', 'avi', 'wmv', 'flv', 'mov', 
  'm2ts', 'ts', 'vob', 'divx', 'xvid', 'rmvb', 'rm'
]
Source: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:106

Transcode Flow

const attemptTranscode = async (filePath: string) => {
  try {
    const result = await invoke<{ 
      session_id: number
      stream_url: string 
    }>('start_transcode_stream', {
      filePath: filePath,
      startTime: initialTime > 0 ? initialTime : null
    })
    
    setVideoSrc(result.stream_url)
    return true
  } catch (e) {
    setError(
      `Transcoding failed: ${e}. ` +
      `Please configure FFmpeg in Settings > Player, ` +
      `or use MPV/VLC player.`
    )
    return false
  }
}
Source: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:44-74

Keyboard Shortcuts

KeyAction
Space / KToggle play/pause
FToggle fullscreen
MToggle mute
Seek backward 10 seconds
Seek forward 10 seconds
EscExit fullscreen or close player
Implementation: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:315-351

Progress Tracking

Automatic Updates

Progress is reported every 5 seconds while playing:
useEffect(() => {
  if (onProgress && currentTime > 0) {
    const now = Date.now()
    if (now - progressReportRef.current > 5000) {
      progressReportRef.current = now
      onProgress(currentTime, duration)
    }
  }
}, [currentTime, duration, onProgress])
Source: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:354-362

Video Loading Strategy

Chunked Loading

For large files, video is loaded in 10MB chunks:
const chunkSize = 10 * 1024 * 1024 // 10MB chunks
const chunks: Uint8Array[] = []
let offset = 0

while (offset < fileSize) {
  const chunk = await invoke<number[]>('read_video_chunk', {
    filePath: src,
    offset: offset,
    chunkSize: Math.min(chunkSize, fileSize - offset)
  })
  
  chunks.push(new Uint8Array(chunk))
  offset += chunk.length
}

// Create blob URL
const blob = new Blob(chunks, { type: mimeType })
const url = URL.createObjectURL(blob)
Source: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:133-169

MIME Type Detection

let mimeType = 'video/mp4'
if (ext === 'webm') mimeType = 'video/webm'
else if (ext === 'm4v') mimeType = 'video/mp4'
Source: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:161-163

VideasyPlayer (Streaming)

For cloud streaming, use the VideasyPlayer component:
import { VideasyPlayer } from '@/components/VideasyPlayer'

<VideasyPlayer
  tmdbId="123456"
  mediaType="movie"
  title="Movie Title"
  onClose={() => setShowPlayer(false)}
  onProgress={(timestamp, duration, progress) => {
    console.log(`${progress}% watched`)
  }}
/>

VideasyPlayer Interface

interface VideasyPlayerProps {
  tmdbId: string
  mediaType: 'movie' | 'tv'
  title: string
  season?: number          // For TV shows
  episode?: number         // For TV shows
  initialProgress?: number // Resume percentage
  onClose: () => void
  onProgress?: (timestamp: number, duration: number, progress: number) => void
}
Source: /home/daytona/workspace/source/src/components/VideasyPlayer.tsx:6-15

Progress Events

Videasy sends progress updates via postMessage:
interface VideasyProgressEvent {
  id: string
  type: 'movie' | 'tv' | 'anime'
  progress: number      // Percentage (0-100)
  timestamp: number     // Current time in seconds
  duration: number      // Total duration in seconds
  season?: number
  episode?: number
}
Source: /home/daytona/workspace/source/src/components/VideasyPlayer.tsx:17-25

Error Handling

Video Errors

const handleVideoError = async (e: React.SyntheticEvent<HTMLVideoElement>) => {
  const errorCode = e.currentTarget.error?.code
  
  // Error code 4 = MEDIA_ERR_SRC_NOT_SUPPORTED
  if (errorCode === 4 && !transcodeAttempted && !isCloud) {
    // Try transcoding
    const success = await attemptTranscode(src)
    if (success) return
  }
  
  setError(
    `Failed to play video: ${errorMessage}. ` +
    `Try using MPV or VLC player instead.`
  )
}
Source: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:202-225

Controls UI

Auto-Hide Controls

Controls fade out after 3 seconds of inactivity:
const startHideControlsTimer = useCallback(() => {
  if (hideControlsTimeout.current) {
    clearTimeout(hideControlsTimeout.current)
  }
  hideControlsTimeout.current = setTimeout(() => {
    if (isPlaying) {
      setShowControls(false)
    }
  }, 3000)
}, [isPlaying])
Source: /home/daytona/workspace/source/src/components/VideoPlayer.tsx:239-248

Build docs developers (and LLMs) love