A modern HLS video player SDK for educational platforms with S3 integration
npm install @obipascal/playerA modern, feature-rich HLS video player SDK for educational platforms with CloudFront/S3 integration. Built with TypeScript, inspired by Mux Player with a unique modern design.
- ๐ฌ HLS Streaming: Full HLS.js support with adaptive bitrate streaming
- ๐ CloudFront Integration: Native support for CloudFront signed cookies and S3-hosted videos
- ๐ฏ Skip Controls: 10-second forward/backward skip with circular arrow buttons
- ๐ฏ Click to Play/Pause: Click video to toggle playback
- ๐บ Fullscreen: Native fullscreen API support
- ๐๏ธ Playback Rate: Adjustable speed (0.5x - 2x)
- ๐ Subtitle Support: Full subtitle/caption support with programmatic API
- ๐ Multi-language: Support for multiple subtitle tracks with language selection
- โฟ Accessibility: WCAG compliant with keyboard navigation
- ๐จ Modern UI Design: Beautiful controls with blur effects, gradients, and smooth animations
- ๐ฑ๏ธ Smart Controls: Auto-hide on inactivity, fade on hover
- ๐ Sticky Controls: Optional persistent controls (toggle in settings)
- ๐ Vertical Volume: Modern vertical volume slider with popup interface
- โ๏ธ Settings Menu: Quality selection, playback speed, subtitle management
- ๐จ 7 Pre-made Themes: Netflix, YouTube, Modern, Green, Cyberpunk, Pastel, Education
- ๐จ Custom Theming: Full CSS variable theming with 8 customizable properties
- โ๏ธ React Support: Component, Hook, and Context Provider patterns
- ๐ง TypeScript: Full TypeScript support with comprehensive type definitions
- ๐ Analytics & QoE: Built-in analytics tracking and Quality of Experience metrics
- ๐ Real-time Analytics: Native WebSocket and Socket.IO support for live analytics streaming
- ๐ฏ 25 Events: Complete event system compatible with Mux Player
- ๐ฑ Responsive: Mobile-friendly with touch support
- ๐ฌ Quality Selector: Automatic quality switching with manual override
``bash`
npm install @obipascal/player hls.js
Or with yarn:
`bash`
yarn add @obipascal/player hls.js
Optional: For Socket.IO real-time analytics support:
`bash`
npm install socket.io-client
`html
`
`tsx
import { WontumPlayerReact } from "@obipascal/player"
import { useRef } from "react"
import { WontumPlayer } from "@obipascal/player"
function VideoPlayer() {
const playerRef = useRef
const handleReady = (player: WontumPlayer) => {
playerRef.current = player
console.log("Player is ready!")
}
const changeVideo = (newUrl: string) => {
if (playerRef.current) {
playerRef.current.updateSource(newUrl)
}
}
return (
#### No Source Initialization (Load Later)
You can initialize the player without a source and load it later:
`tsx
import { WontumPlayerReact } from "@obipascal/player"
import { useRef, useState } from "react"
import { WontumPlayer } from "@obipascal/player"function VideoPlayer() {
const playerRef = useRef(null)
const [videoUrl, setVideoUrl] = useState()
const handleReady = (player: WontumPlayer) => {
playerRef.current = player
}
const loadVideo = (url: string) => {
if (playerRef.current) {
playerRef.current.updateSource(url)
setVideoUrl(url)
}
}
return (
{/ Player initializes without source /}
)
}
```$3
`tsx
import { useWontumPlayer } from "@obipascal/player"function CustomPlayer() {
const { containerRef, player, state } = useWontumPlayer({
src: "https://media.example.com/video/playlist.m3u8",
controls: false, // Build your own custom controls
})
const handleSkipForward = () => player?.skipForward(10)
const handleSkipBackward = () => player?.skipBackward(10)
return (
{state && (
{Math.floor(state.currentTime)}s / {Math.floor(state.duration)}s
Status: {state.playing ? "Playing" : "Paused"}
)}
)
}
``$3
`tsx
import { WontumPlayerProvider, useWontumPlayerContext } from "@obipascal/player"function App() {
return (
)
}
function VideoSection() {
const { containerRef } = useWontumPlayerContext()
return
}function ControlPanel() {
const { player, state } = useWontumPlayerContext()
return (
Playing: {state?.playing ? "Yes" : "No"}
)
}
`$3
If you're using Apollo Client or other GraphQL clients for URL signing, use
useQuery instead of useLazyQuery to avoid abort errors:`tsx
import React from "react"
import { S3Config, WontumPlayerReact } from "@obipascal/player"
import { useQuery } from "@apollo/client"
import { GET_MEDIA_SIGNED_URL } from "@/graphql/queries/media.queries"interface VideoPlayerProps {
videoUrl: string
}
function VideoPlayer({ videoUrl }: VideoPlayerProps) {
// โ
Use useQuery with skip option instead of useLazyQuery
const { refetch } = useQuery(GET_MEDIA_SIGNED_URL, {
skip: true, // Don't run on mount
fetchPolicy: "no-cache", // Always fetch fresh signed URLs
})
const url = new URL(videoUrl)
const s3config: S3Config = {
cloudFrontDomains: [url.hostname],
withCredentials: true, // Enable cookies for CloudFront signed cookies
signUrl: async (resourceUrl: string) => {
try {
const { data } = await refetch({
signingMediaInput: {
resourceUrl,
isPublic: false,
type: "COOKIES",
},
})
console.log("Signed URL result:", data)
// For cookie-based signing, return the original URL
// The server sets cookies in the response
return resourceUrl
} catch (error) {
console.error("Failed to sign URL:", error)
throw error
}
},
}
return (
src={videoUrl}
width="100%"
height="500px"
autoplay={false}
controls
s3Config={s3config}
theme={{
primaryColor: "#3b82f6",
accentColor: "#60a5fa",
}}
/>
)
}
`Why
useQuery with skip instead of useLazyQuery?-
useLazyQuery creates a new AbortController on each call, which can be aborted during React lifecycle
- useQuery with skip: true and refetch persists across renders, avoiding abort issues
- The SDK includes retry logic for AbortErrors, but using useQuery prevents them entirely๐ CloudFront & S3 Integration
This player supports three video hosting scenarios. Choose the one that fits your needs:
$3
When to use: Your videos are publicly accessible and don't require user authentication.
Setup: Just provide the video URL!
`typescript
import { WontumPlayer } from "@obipascal/player"const player = new WontumPlayer({
src: "https://d1234567890.cloudfront.net/video/playlist.m3u8",
container: "#player",
})
`โ
That's it! No backend needed. Works for public S3 buckets or CloudFront distributions.
---
$3
When to use: You want to restrict video access to authorized users (e.g., paid courses, premium content).
How it works:
1. User logs into your app
2. Your backend verifies the user and sets CloudFront signed cookies
3. Player automatically sends these cookies with every video request
4. CloudFront checks the cookies and allows/denies access
Frontend Setup:
`typescript
import { WontumPlayer } from "@obipascal/player"// STEP 1: Call your backend to set signed cookies BEFORE creating the player
async function initializePlayer() {
// This endpoint sets CloudFront cookies in the browser
await fetch("/api/auth/video-access", {
credentials: "include", // Important: include cookies
})
// STEP 2: Create player - it will automatically use the cookies
const player = new WontumPlayer({
src: "https://media.yourdomain.com/videos/lesson-1/playlist.m3u8",
container: "#player",
s3Config: {
cloudFrontDomains: ["media.yourdomain.com"], // Your CloudFront domain
withCredentials: true, // Enable cookies for all HLS requests (required for CloudFront signed cookies)
signUrl: async (url) => {
// This function is called when player needs to access a video
// Call your backend to refresh/set cookies if needed
const response = await fetch("/api/auth/sign-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ url }),
})
if (!response.ok) {
throw new Error("Failed to authenticate video access")
}
// Backend sets cookies, return the URL
return url
},
},
})
}
initializePlayer()
`Backend Setup (Node.js/Express):
`typescript
import express from "express"
import { getSignedCookies } from "@aws-sdk/cloudfront-signer"
import fs from "fs"const app = express()
// STEP 1: Create endpoint that sets CloudFront signed cookies
app.get("/api/auth/video-access", (req, res) => {
// Check if user is logged in (your authentication logic)
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" })
}
// Define what resources user can access
const policy = {
Statement: [
{
Resource: "https://media.yourdomain.com/*", // All videos on this domain
Condition: {
DateLessThan: {
"AWS:EpochTime": Math.floor(Date.now() / 1000) + 3600, // Expires in 1 hour
},
},
},
],
}
// Generate CloudFront signed cookies
const cookies = getSignedCookies({
keyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID!, // Your CloudFront key pair ID
privateKey: fs.readFileSync("./cloudfront-private-key.pem", "utf8"), // Your private key
policy: JSON.stringify(policy),
})
// Set the three required cookies
res.cookie("CloudFront-Policy", cookies["CloudFront-Policy"], {
domain: ".yourdomain.com", // Use your domain
path: "/",
secure: true, // HTTPS only
httpOnly: true, // Prevent JavaScript access
sameSite: "none",
})
res.cookie("CloudFront-Signature", cookies["CloudFront-Signature"], {
domain: ".yourdomain.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "none",
})
res.cookie("CloudFront-Key-Pair-Id", cookies["CloudFront-Key-Pair-Id"], {
domain: ".yourdomain.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "none",
})
res.json({ success: true })
})
// STEP 2: Optional endpoint for on-demand signing (called by signUrl function)
app.post("/api/auth/sign-url", (req, res) => {
const { url } = req.body
// Verify user is authorized
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" })
}
// You can add additional authorization logic here
// For example, check if user has access to this specific video
// Refresh cookies (same code as above)
const policy = {
Statement: [
{
Resource: "https://media.yourdomain.com/*",
Condition: {
DateLessThan: {
"AWS:EpochTime": Math.floor(Date.now() / 1000) + 3600,
},
},
},
],
}
const cookies = getSignedCookies({
keyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID!,
privateKey: fs.readFileSync("./cloudfront-private-key.pem", "utf8"),
policy: JSON.stringify(policy),
})
res.cookie("CloudFront-Policy", cookies["CloudFront-Policy"], {
domain: ".yourdomain.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "none",
})
res.cookie("CloudFront-Signature", cookies["CloudFront-Signature"], {
domain: ".yourdomain.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "none",
})
res.cookie("CloudFront-Key-Pair-Id", cookies["CloudFront-Key-Pair-Id"], {
domain: ".yourdomain.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "none",
})
res.json({ success: true })
})
`AWS CloudFront Setup:
1. Create a CloudFront key pair in AWS Console โ CloudFront โ Key pairs
2. Download the private key file
3. Set up environment variables:
`bash
CLOUDFRONT_KEY_PAIR_ID=APKA...
`
4. Configure your CloudFront distribution to require signed cookies---
$3
When to use: Videos are in private S3 buckets without CloudFront.
How it works:
1. Your backend generates temporary presigned URLs for S3 objects
2. Player uses these URLs to access videos
3. URLs expire after a set time (e.g., 1 hour)
Frontend Setup:
`typescript
import { WontumPlayer } from "@obipascal/player"const player = new WontumPlayer({
src: "s3://my-bucket/videos/lesson-1/playlist.m3u8", // S3 URI
container: "#player",
s3Config: {
getPresignedUrl: async (s3Key) => {
// Call your backend to generate presigned URL
const response = await fetch("/api/s3/presigned-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: s3Key }),
})
if (!response.ok) {
throw new Error("Failed to get presigned URL")
}
const data = await response.json()
return data.url // Return the presigned URL
},
},
})
`Backend Setup (Node.js):
`typescript
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"const s3Client = new S3Client({
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
})
app.post("/api/s3/presigned-url", async (req, res) => {
const { key } = req.body
// Verify user is authorized to access this video
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" })
}
try {
// Generate presigned URL
const command = new GetObjectCommand({
Bucket: "my-bucket",
Key: key, // e.g., "videos/lesson-1/playlist.m3u8"
})
const url = await getSignedUrl(s3Client, command, {
expiresIn: 3600, // URL valid for 1 hour
})
res.json({ url })
} catch (error) {
console.error("Error generating presigned URL:", error)
res.status(500).json({ error: "Failed to generate presigned URL" })
}
})
`---
$3
| Method | Best For | Complexity | Performance |
| ---------------------- | ---------------------------------------------- | ----------- | ------------ |
| Public Videos | Free content, marketing videos | โญ Easy | โก Fast |
| CloudFront Cookies | โญ Recommended for paid courses, premium | โญโญ Medium | โกโก Fastest |
| S3 Presigned URLs | Direct S3 access, simple private video hosting | โญโญ Medium | โก Good |
๐ก Tip: Use CloudFront with signed cookies for production. It's more secure and performant for HLS videos (which have many file segments).
๐ Subtitle Support
$3
`typescript
const player = new WontumPlayer({
src: "https://media.example.com/video/playlist.m3u8",
container: "#player",
subtitles: [
{
label: "English",
src: "https://example.com/subtitles/en.vtt",
srclang: "en",
default: true, // Default track
},
{
label: "Spanish",
src: "https://example.com/subtitles/es.vtt",
srclang: "es",
},
{
label: "French",
src: "https://example.com/subtitles/fr.vtt",
srclang: "fr",
},
],
})
`$3
`typescript
// Enable specific subtitle track by index
player.enableSubtitles(0) // Enable first track (English)// Disable all subtitles
player.disableSubtitles()
// Toggle subtitles on/off
player.toggleSubtitles()
// Get all subtitle tracks
const tracks = player.getSubtitleTracks()
console.log(tracks)
// [
// { label: 'English', src: '...', srclang: 'en', default: true },
// { label: 'Spanish', src: '...', srclang: 'es' }
// ]
// Check if subtitles are enabled
const enabled = player.areSubtitlesEnabled()
console.log(enabled) // true or false
`$3
`typescript
player.on("subtitlechange", (event) => {
console.log("Subtitle changed:", event.data.track)
})
`โ๏ธ Settings & Controls
$3
Keep controls visible at all times:
`typescript
const player = new WontumPlayer({
src: "https://media.example.com/video/playlist.m3u8",
container: "#player",
stickyControls: true, // Controls always visible
})
`Users can also toggle sticky controls from the settings menu in the player UI.
$3
10-second skip buttons with circular arrow icons are automatically included:
`typescript
// Programmatic skip
player.skipForward(10) // Skip 10 seconds forward
player.skipBackward(10) // Skip 10 seconds backward// Custom skip duration
player.seek(player.getCurrentTime() + 30) // Skip 30 seconds forward
`$3
Clicking anywhere on the video toggles play/pause automatically.
$3
Modern vertical volume slider with popup interface - hover over volume button to adjust.
๐ Analytics
Track video engagement and quality metrics with HTTP endpoints or real-time WebSocket/Socket.IO streaming:
Features:
- โ
HTTP endpoint support for traditional analytics
- โ
Native WebSocket support for real-time streaming
- โ
Socket.IO support with full TypeScript types
- โ
Dual streaming (HTTP + Socket simultaneously)
- โ
Event transformation and filtering
- โ
Auto-reconnection with configurable delays
- โ
Quality of Experience (QoE) metrics included in every event
$3
`typescript
const player = new WontumPlayer({
src: "https://example.com/video.m3u8",
container: "#player",
analytics: {
enabled: true,
endpoint: "https://your-analytics-endpoint.com/events",
sessionId: "session_123",
userId: "user_456",
videoId: "video_789",
},
})// Get analytics metrics
const metrics = player.analytics.getMetrics()
console.log(metrics)
// {
// sessionId: 'session_123',
// totalPlayTime: 120000,
// totalBufferTime: 2000,
// bufferingRatio: 0.017,
// rebufferCount: 3,
// seekCount: 5,
// eventCount: 42
// }
`$3
Stream analytics events in real-time using native WebSocket for live dashboards and monitoring:
`typescript
const player = new WontumPlayer({
src: "https://example.com/video.m3u8",
container: "#player",
analytics: {
enabled: true,
userId: "user_456",
videoId: "video_789",
// Native WebSocket configuration
webSocket: {
type: "websocket", // Specify native WebSocket
connection: "wss://analytics.example.com/stream",
// Optional: Transform events before sending
transform: (event) => ({
type: event.eventType,
video_id: event.videoId,
user_id: event.userId,
timestamp: event.timestamp,
metrics: event.data,
}),
// Optional: Handle errors
onError: (error) => {
console.error("Analytics WebSocket error:", error)
},
// Optional: Connection opened
onOpen: (event) => {
console.log("Analytics WebSocket connected")
},
// Optional: Connection closed
onClose: (event) => {
console.log("Analytics WebSocket disconnected")
},
// Auto-reconnect on disconnect (default: true)
autoReconnect: true,
// Reconnect delay in milliseconds (default: 3000)
reconnectDelay: 3000,
},
},
})
`$3
For Socket.IO-based real-time analytics (requires
socket.io-client to be loaded):`typescript
// Option 1: Let the SDK create the Socket.IO connection
const player = new WontumPlayer({
src: "https://example.com/video.m3u8",
container: "#player",
analytics: {
enabled: true,
userId: "user_456",
videoId: "video_789",
webSocket: {
type: "socket.io",
connection: "https://analytics.example.com", // Socket.IO server URL
options: {
path: "/socket.io/",
transports: ["websocket", "polling"],
auth: {
token: "your-auth-token",
},
reconnection: true,
reconnectionDelay: 1000,
},
eventName: "video_analytics", // Event name to emit (default: "analytics")
transform: (event) => ({
event: event.eventType,
video: event.videoId,
user: event.userId,
data: event.data,
}),
onConnect: () => {
console.log("Socket.IO connected")
},
onDisconnect: (reason) => {
console.log("Socket.IO disconnected:", reason)
},
onError: (error) => {
console.error("Socket.IO error:", error)
},
},
},
})
``typescript
// Option 2: Use existing Socket.IO connection
import { io } from "socket.io-client"const socket = io("https://analytics.example.com", {
auth: {
token: "your-auth-token",
},
})
const player = new WontumPlayer({
src: "https://example.com/video.m3u8",
container: "#player",
analytics: {
enabled: true,
userId: "user_456",
videoId: "video_789",
webSocket: {
type: "socket.io",
connection: socket, // Use existing Socket.IO instance
eventName: "analytics",
},
},
})
`$3
`typescript
// Create your own WebSocket connection
const ws = new WebSocket("wss://analytics.example.com/stream")// Configure authentication or custom headers before connecting
ws.addEventListener("open", () => {
// Send authentication message
ws.send(
JSON.stringify({
type: "auth",
token: "your-auth-token",
}),
)
})
const player = new WontumPlayer({
src: "https://example.com/video.m3u8",
container: "#player",
analytics: {
enabled: true,
userId: "user_456",
videoId: "video_789",
webSocket: {
type: "websocket",
connection: ws, // Use existing connection
transform: (event) => ({
// Custom format for your backend
action: "video_event",
payload: {
event: event.eventType,
data: event.data,
},
}),
},
},
})
`$3
Send analytics to both HTTP endpoint and real-time socket simultaneously:
`typescript
const player = new WontumPlayer({
src: "https://example.com/video.m3u8",
container: "#player",
analytics: {
enabled: true,
endpoint: "https://api.example.com/analytics", // HTTP fallback/storage
webSocket: {
type: "socket.io",
connection: "https://realtime.example.com", // Real-time monitoring
eventName: "video_analytics",
},
userId: "user_456",
videoId: "video_789",
},
})
`$3
The SDK automatically tracks these events:
- Session:
session_start, session_end
- Playback: play, pause, ended, playing
- Buffering: buffering_start, buffering_end, waiting, stalled
- Seeking: seeking, seeked
- Quality: qualitychange, renditionchange
- Errors: error
- User Actions: Volume changes, fullscreen, playback rate changesEach event includes Quality of Experience (QoE) metrics:
-
sessionDuration - Total session time
- totalPlayTime - Actual video play time
- totalBufferTime - Time spent buffering
- bufferingRatio - Buffer time / play time ratio
- rebufferCount - Number of rebuffer events
- seekCount - Number of seek operations$3
For React applications, use the
useAnalytics hook for automatic lifecycle management:`tsx
import { useAnalytics } from "@obipascal/player"
import { useEffect } from "react"function VideoAnalyticsDashboard() {
const { trackEvent, getMetrics, connected, sessionId } = useAnalytics({
enabled: true,
endpoint: "https://api.example.com/analytics",
videoId: "video-123",
userId: "user-456",
webSocket: {
type: "socket.io",
url: "https://analytics.example.com",
auth: { token: "your-auth-token" },
eventName: "video_event",
},
})
// Track custom events
const handleShareClick = () => {
trackEvent("share_clicked", {
platform: "twitter",
videoTime: 125.5,
})
}
const handleBookmark = () => {
trackEvent("bookmark_added", {
timestamp: Date.now(),
})
}
// Display metrics
useEffect(() => {
const interval = setInterval(() => {
const metrics = getMetrics()
console.log("Session Metrics:", metrics)
}, 5000)
return () => clearInterval(interval)
}, [getMetrics])
return (
Analytics Dashboard
Session ID: {sessionId}
WebSocket Status: {connected ? "๐ข Connected" : "๐ด Disconnected"}
{/ The hook automatically tracks session_start and session_end /}
{/ It cleans up on component unmount /}
)
}
`Hook Features:
- โ
Automatic lifecycle management (initialization and cleanup)
- โ
WebSocket/Socket.IO connection status monitoring
- โ
Track custom events with
trackEvent()
- โ
Access metrics with getMetrics()
- โ
Access all events with getEvents()
- โ
Session ID available immediatelyAPI Reference
$3
#### Constructor Options
`typescript
interface WontumPlayerConfig {
src: string // Video source URL (HLS manifest)
container: HTMLElement | string // Container element or selector
autoplay?: boolean // Auto-play on load (default: false)
muted?: boolean // Start muted (default: false)
controls?: boolean // Show controls (default: true)
poster?: string // Poster image URL
preload?: "none" | "metadata" | "auto" // Preload strategy
theme?: PlayerTheme // Custom theme
s3Config?: S3Config // S3/CloudFront configuration
analytics?: AnalyticsConfig // Analytics configuration
hlsConfig?: Partial // HLS.js config override
subtitles?: SubtitleTrack[] // Subtitle tracks
stickyControls?: boolean // Keep controls always visible
}interface S3Config {
signUrl?: (url: string) => Promise // Sign URL and set cookies
cloudFrontDomains?: string[] // CloudFront domains (e.g., ['media.example.com'])
withCredentials?: boolean // Enable cookies for HLS requests (default: false, required for CloudFront signed cookies)
region?: string // S3 region
endpoint?: string // Custom S3 endpoint
}
interface AnalyticsConfig {
enabled?: boolean // Enable analytics tracking
endpoint?: string // HTTP endpoint for analytics events
webSocket?: WebSocketAnalyticsHandler | SocketIOAnalyticsHandler // Real-time streaming
sessionId?: string // Session identifier
userId?: string // User identifier
videoId?: string // Video identifier
}
// Native WebSocket Configuration
interface WebSocketAnalyticsHandler {
type: "websocket"
connection: WebSocket | string // WebSocket instance or URL
transform?: (event: AnalyticsEvent) => any // Transform before sending
onError?: (error: Event) => void
onOpen?: (event: Event) => void
onClose?: (event: CloseEvent) => void
autoReconnect?: boolean // Default: true
reconnectDelay?: number // Default: 3000ms
}
// Socket.IO Configuration
interface SocketIOAnalyticsHandler {
type: "socket.io"
connection: Socket | string // Socket.IO instance or URL
options?: Partial // Socket.IO options
eventName?: string // Event name to emit (default: "analytics")
transform?: (event: AnalyticsEvent) => any
onError?: (error: Error) => void
onConnect?: () => void
onDisconnect?: (reason: string) => void
}
`#### Methods
`typescript
// Playback control
player.play(): Promise
player.pause(): void
player.seek(time: number): void// Volume control
player.setVolume(volume: number): void // 0-1
player.mute(): void
player.unmute(): void
// Playback rate
player.setPlaybackRate(rate: number): void // 0.5, 1, 1.5, 2, etc.
// Quality control
player.setQuality(qualityIndex: number): void
player.getQualities(): QualityLevel[]
// Fullscreen
player.enterFullscreen(): void
player.exitFullscreen(): void
// Picture-in-Picture
player.enterPictureInPicture(): Promise
player.exitPictureInPicture(): Promise
player.togglePictureInPicture(): Promise
// State
player.getState(): PlayerState
// Events
player.on(eventType: PlayerEventType, callback: (event: PlayerEvent) => void): void
player.off(eventType: PlayerEventType, callback: (event: PlayerEvent) => void): void
// Cleanup
player.destroy(): void
`#### Events
`typescript
type PlayerEventType =
| "play"
| "pause"
| "ended"
| "timeupdate"
| "volumechange"
| "ratechange"
| "seeked"
| "seeking"
| "waiting"
| "canplay"
| "loadedmetadata"
| "error"
| "qualitychange"
| "fullscreenchange"
| "pictureinpictureenter"
| "pictureinpictureexit"
`$3
#### WontumPlayerReact
`tsx
src="https://example.com/video.m3u8"
width="100%"
height="500px"
autoplay={false}
muted={false}
controls={true}
poster="https://example.com/poster.jpg"
onReady={(player) => console.log("Player ready", player)}
onPlay={() => console.log("Playing")}
onPause={() => console.log("Paused")}
onEnded={() => console.log("Ended")}
onTimeUpdate={(time) => console.log("Time:", time)}
onVolumeChange={(volume, muted) => console.log("Volume:", volume, muted)}
onError={(error) => console.error("Error:", error)}
theme={{
primaryColor: "#3b82f6",
accentColor: "#60a5fa",
fontFamily: "Inter, sans-serif",
}}
analytics={{
enabled: true,
videoId: "video_123",
userId: "user_456",
}}
/>
`Important: Changing Video Source
The
WontumPlayerReact component properly handles video source changes. When you update the src prop, the player will:- โ
Clean up the previous player instance completely
- โ
Remove all DOM elements (controls, progress bars)
- โ
Reinitialize with the new video source
- โ
Maintain control visibility and functionality
`tsx
function VideoModal() {
const [currentVideo, setCurrentVideo] = useState("video1.m3u8") return (
src={currentVideo} // โ
Simply change the src - no need for React key tricks!
width="100%"
height="100%"
controls
stickyControls
/>
)
}
`Advanced: Using updateSource() for Better Performance
For even better performance when changing sources, you can use the
updateSource() method via the onReady callback:`tsx
function VideoPlayer() {
const [videos] = useState(["https://example.com/video1.m3u8", "https://example.com/video2.m3u8", "https://example.com/video3.m3u8"])
const [currentIndex, setCurrentIndex] = useState(0)
const playerRef = useRef(null) const handleReady = (player: WontumPlayer) => {
playerRef.current = player
}
const switchVideo = async (index: number) => {
if (playerRef.current) {
// Use updateSource for efficient source changes (no full reinitialization)
await playerRef.current.updateSource(videos[index])
setCurrentIndex(index)
}
}
return (
{videos.map((_, index) => (
))}
)
}
`#### useWontumPlayer Hook
`tsx
function CustomPlayer() {
const { containerRef, player, state } = useWontumPlayer({
src: "https://example.com/video.m3u8",
controls: false, // Build custom controls
}) return (
{state && (
Time: {state.currentTime} / {state.duration}
Status: {state.playing ? "Playing" : "Paused"}
)}
)
}
`๐จ Theming
$3
Wontum Player comes with 7 beautiful pre-made themes:
`typescript
import { netflixTheme, youtubeTheme, modernTheme, greenTheme, cyberpunkTheme, pastelTheme, educationTheme } from "@obipascal/player"const player = new WontumPlayer({
src: "https://media.example.com/video/playlist.m3u8",
container: "#player",
theme: netflixTheme(), // Netflix-inspired dark theme
})
`Available Themes:
-
netflixTheme() - Netflix-inspired red and black
- youtubeTheme() - YouTube-inspired red and white
- modernTheme() - Modern blue gradient
- greenTheme() - Nature-inspired green
- cyberpunkTheme() - Neon pink and purple
- pastelTheme() - Soft pastel colors
- educationTheme() - Professional education platform$3
Create your own custom theme with 8 customizable properties:
`typescript
const player = new WontumPlayer({
src: "https://media.example.com/video/playlist.m3u8",
container: "#player",
theme: {
primaryColor: "#3b82f6", // Primary brand color
accentColor: "#60a5fa", // Accent/hover color
backgroundColor: "#1f2937", // Control background
textColor: "#ffffff", // Text color
fontFamily: "Inter, sans-serif", // Font
borderRadius: "8px", // Corner radius
controlHeight: "50px", // Control bar height
iconSize: "24px", // Icon size
},
})
`$3
Quick brand color presets:
`typescript
import { BrandPresets } from "@obipascal/player"const player = new WontumPlayer({
src: "https://media.example.com/video/playlist.m3u8",
container: "#player",
theme: {
...modernTheme(),
primaryColor: BrandPresets.blue,
accentColor: BrandPresets.lightBlue,
},
})
`Available Brand Colors:
-
blue, lightBlue, darkBlue
- red, lightRed, darkRed
- green, lightGreen, darkGreen
- purple, lightPurple, darkPurple
- pink, lightPink, darkPink
- orange, lightOrange, darkOrange๐ง Advanced Usage
$3
Pass custom HLS.js configuration:
`typescript
const player = new WontumPlayer({
src: "https://example.com/video.m3u8",
container: "#player",
hlsConfig: {
maxBufferLength: 30,
maxMaxBufferLength: 600,
startLevel: -1, // Auto quality
capLevelToPlayerSize: true,
enableWorker: true,
lowLatencyMode: false,
},
})
`$3
`typescript
const player1 = new WontumPlayer({
src: "https://example.com/video1.m3u8",
container: "#player-1",
theme: netflixTheme(),
})const player2 = new WontumPlayer({
src: "https://example.com/video2.m3u8",
container: "#player-2",
theme: youtubeTheme(),
})
// Each player operates independently
player1.play()
player2.pause()
`$3
`typescript
const player = new WontumPlayer({
src: "https://media.example.com/video/playlist.m3u8",
container: "#player",
})// Playback events
player.on("play", () => console.log("Playing"))
player.on("pause", () => console.log("Paused"))
player.on("ended", () => console.log("Video ended"))
// Time tracking
player.on("timeupdate", (event) => {
const { currentTime, duration } = event.data
console.log(
${currentTime}s / ${duration}s)
})// Quality changes
player.on("qualitychange", (event) => {
console.log("Quality changed to:", event.data.quality)
})
// Picture-in-Picture events
player.on("pictureinpictureenter", () => {
console.log("Entered Picture-in-Picture mode")
})
player.on("pictureinpictureexit", () => {
console.log("Exited Picture-in-Picture mode")
})
// Buffer events
player.on("waiting", () => console.log("Buffering..."))
player.on("canplay", () => console.log("Ready to play"))
// Error handling
player.on("error", (event) => {
console.error("Player error:", event.data)
})
// Subtitle changes
player.on("subtitlechange", (event) => {
console.log("Subtitle track:", event.data.track)
})
// Remove event listener
const handlePlay = () => console.log("Playing")
player.on("play", handlePlay)
player.off("play", handlePlay)
`$3
`typescript
// Get current player state
const state = player.getState()
console.log(state)
// {
// playing: false,
// currentTime: 45.2,
// duration: 300,
// volume: 0.8,
// muted: false,
// playbackRate: 1,
// buffered: [...],
// qualities: [...],
// currentQuality: 2
// }// Track specific properties
const currentTime = player.getCurrentTime() // 45.2
const duration = player.getDuration() // 300
const isPlaying = player.getState().playing // false
`$3
`typescript
// Playback control
await player.play()
player.pause()
player.seek(60) // Seek to 60 seconds
player.skipForward(10) // Skip 10 seconds forward
player.skipBackward(10) // Skip 10 seconds backward// Volume control
player.setVolume(0.5) // Set to 50%
player.mute()
player.unmute()
// Playback speed
player.setPlaybackRate(1.5) // 1.5x speed
player.setPlaybackRate(0.5) // 0.5x speed
// Quality selection
const qualities = player.getQualities()
player.setQuality(2) // Set to quality index 2
// Fullscreen
player.enterFullscreen()
player.exitFullscreen()
// Picture-in-Picture
await player.enterPictureInPicture()
await player.exitPictureInPicture()
await player.togglePictureInPicture()
// Cleanup
player.destroy() // Remove player and clean up resources
`$3
Enable floating video that stays on top while users work in other apps:
`typescript
const player = new WontumPlayer({
src: "https://example.com/video.m3u8",
container: "#player",
})// Enter PiP mode
await player.enterPictureInPicture()
// Listen for PiP events
player.on("pictureinpictureenter", () => {
console.log("Video is now floating!")
})
player.on("pictureinpictureexit", () => {
console.log("Back to normal mode")
})
// Custom button to toggle PiP
const pipButton = document.getElementById("pip-btn")
pipButton.addEventListener("click", async () => {
await player.togglePictureInPicture()
})
`Note: Picture-in-Picture is supported in most modern browsers. The player includes a built-in PiP button in the controls.
$3
The SDK includes a
WontumFileInfo utility class to extract metadata from video files before uploading or processing them.#### React Hook (useVideoFileInfo)
For React applications, use the
useVideoFileInfo hook for automatic state management:`tsx
import { useVideoFileInfo } from "@obipascal/player"
import { useState } from "react"function VideoUploader() {
const [selectedFile, setSelectedFile] = useState(null)
const { info, loading, error, refetch } = useVideoFileInfo(selectedFile)
const handleFileChange = (event: React.ChangeEvent) => {
const file = event.target.files?.[0]
setSelectedFile(file || null)
}
return (
{loading &&
Analyzing video...
} {error && (
Error: {error}
)} {info && (
Video Information
Resolution: {info.width} ร {info.height}
- Aspect Ratio: {info.aspectRatio}
- Quality: {info.quality}
- Duration: {info.durationFormatted}
- Size: {info.sizeFormatted}
- Bitrate: {info.bitrate} kbps
- Frame Rate: {info.frameRate} fps
- Audio: {info.hasAudio ? ${info.audioChannels} channels
: "No audio"}
{/ Validation example /}
{info.aspectRatio !== "16:9" &&
โ ๏ธ Video should be 16:9 aspect ratio
}
{info.height < 720 && โ Minimum resolution is 720p
}
{!info.hasAudio && โ Video must have audio
}
{info.audioChannels !== 2 && โ ๏ธ Audio should be stereo (2 channels)
}
)}
)
}
`#### Vanilla JavaScript
`typescript
import { WontumFileInfo } from "@obipascal/player"// Example: File input handling
const fileInput = document.querySelector("#video-upload")
fileInput.addEventListener("change", async (event) => {
const file = event.target.files?.[0]
if (!file) return
try {
// Create instance (validates it's a video file)
const videoInfo = new WontumFileInfo(file)
// Extract metadata
await videoInfo.extract()
// Access properties
console.log("Video Information:")
console.log("- Width:", videoInfo.width) // e.g., 1920
console.log("- Height:", videoInfo.height) // e.g., 1080
console.log("- Aspect Ratio:", videoInfo.aspectRatio) // e.g., "16:9"
console.log("- Quality:", videoInfo.quality) // e.g., "Full HD (1080p)"
console.log("- Duration (raw):", videoInfo.durationInSeconds, "seconds") // e.g., 125.5
console.log("- Formatted Duration:", videoInfo.durationFormatted) // e.g., "02:05"
console.log("- File Size (raw):", videoInfo.sizeInBytes, "bytes") // e.g., 52428800
console.log("- Formatted Size:", videoInfo.sizeFormatted) // e.g., "50 MB"
console.log("- MIME Type:", videoInfo.mimeType) // e.g., "video/mp4"
console.log("- File Name:", videoInfo.fileName) // e.g., "my-video.mp4"
console.log("- Extension:", videoInfo.fileExtension) // e.g., ".mp4"
console.log("- Bitrate:", videoInfo.bitrate, "kbps") // e.g., 3500
console.log("- Frame Rate:", videoInfo.frameRate, "fps") // e.g., 30 or 60
console.log("- Has Audio:", videoInfo.hasAudio) // e.g., true
console.log("- Audio Channels:", videoInfo.audioChannels) // e.g., 2 (stereo)
// Get all info as object
const allInfo = videoInfo.getInfo()
console.log(allInfo)
// Validate against platform requirements
const isValid = validateVideo(videoInfo)
if (!isValid.valid) {
console.error("Validation errors:", isValid.errors)
}
// Clean up when done
videoInfo.destroy()
} catch (error) {
console.error("Error extracting video info:", error.message)
// Throws error if file is not a video
}
})
// Example validation function for educational platform
function validateVideo(info: VideoFileInfo) {
const errors: string[] = []
// Aspect Ratio: 16:9 required
if (info.aspectRatio !== "16:9") {
errors.push(
Aspect ratio must be 16:9, got ${info.aspectRatio})
} // Resolution: Minimum 1280ร720
if (info.height < 720 || info.width < 1280) {
errors.push(
Minimum resolution is 1280ร720, got ${info.width}ร${info.height})
} // File Format: .MP4 or .MOV
if (![".mp4", ".mov"].includes(info.fileExtension.toLowerCase())) {
errors.push(
File format must be MP4 or MOV, got ${info.fileExtension})
} // Bitrate: 5-10 Mbps
if (info.bitrate && (info.bitrate < 5000 || info.bitrate > 10000)) {
errors.push(
Bitrate should be 5-10 Mbps, got ${info.bitrate} kbps)
} // Audio: Must be stereo (2 channels)
if (!info.hasAudio) {
errors.push("Video must have audio track")
} else if (info.audioChannels && info.audioChannels !== 2) {
errors.push(
Audio must be stereo (2 channels), got ${info.audioChannels})
} // File Size: โค4.0 GB
const maxSize = 4 1024 1024 * 1024 // 4GB in bytes
if (info.sizeInBytes > maxSize) {
errors.push(
File size must be โค4GB, got ${info.sizeFormatted})
} // Duration: 2 minutes to 2 hours
if (info.durationInSeconds < 120 || info.durationInSeconds > 7200) {
errors.push(
Duration must be 2min-2hrs, got ${info.durationFormatted})
} // Frame Rate: 30 or 60 fps
if (info.frameRate && ![30, 60].includes(info.frameRate)) {
errors.push(
Frame rate should be 30 or 60 fps, got ${info.frameRate})
} return { valid: errors.length === 0, errors }
}
`#### WontumFileInfo API
Constructor:
`typescript
new WontumFileInfo(file: File)
`Throws an error if the file is not a valid video file.
Methods:
-
extract(): Promise - Extracts metadata from the video file
- getInfo(): VideoFileInfo | null - Returns the extracted information object
- destroy(): void - Cleans up resourcesProperties (available after calling
extract()):-
width: number - Video width in pixels
- height: number - Video height in pixels
- aspectRatio: string - Aspect ratio (e.g., "16:9", "4:3", "21:9")
- quality: string - Quality description (e.g., "4K (2160p)", "Full HD (1080p)")
- size: number - File size in bytes (raw value for computation)
- sizeInBytes: number - Alias for size (raw value for computation)
- sizeFormatted: string - Human-readable size (e.g., "50 MB")
- duration: number - Duration in seconds (raw value for computation)
- durationInSeconds: number - Alias for duration (raw value for computation)
- durationFormatted: string - Formatted duration (e.g., "01:23:45")
- mimeType: string - MIME type (e.g., "video/mp4")
- fileName: string - Original file name
- fileExtension: string - File extension (e.g., ".mp4")
- bitrate: number | undefined - Estimated bitrate in kbps
- frameRate: number | undefined - Frame rate in fps (30, 60, etc.)
- hasAudio: boolean - Whether video has an audio track
- audioChannels: number | undefined - Number of audio channels (1=mono, 2=stereo)Validation Use Case:
Perfect for validating videos against platform requirements (aspect ratio, resolution, format, bitrate, audio channels, file size, duration, frame rate).
Supported Video Formats:
.mp4, .webm, .ogg, .mov, .avi, .mkv, .flv, .wmv, .m4v, .3gp, .ts, .m3u8๐ Complete API Reference
For detailed API documentation including all methods, events, types, and configuration options, see API-REFERENCE.md.
$3
Player Methods:
- Playback:
play(), pause(), seek(time), skipForward(seconds), skipBackward(seconds)
- Volume: setVolume(level), mute(), unmute()
- Subtitles: enableSubtitles(index), disableSubtitles(), toggleSubtitles(), getSubtitleTracks(), areSubtitlesEnabled()
- Quality: setQuality(index), getQualities()
- Playback Rate: setPlaybackRate(rate)
- Fullscreen: enterFullscreen(), exitFullscreen()
- Picture-in-Picture: enterPictureInPicture(), exitPictureInPicture(), togglePictureInPicture()
- Source Management: updateSource(src) - _Efficiently change video source without full reinitialization_
- State: getState(), getCurrentTime(), getDuration()
- Lifecycle: destroy()Events (26 total):
- Playback:
play, pause, ended, timeupdate, durationchange
- Loading: loadstart, loadedmetadata, loadeddata, canplay, canplaythrough
- Buffering: waiting, playing, stalled, suspend, abort
- Seeking: seeking, seeked
- Volume: volumechange
- Quality: qualitychange, renditionchange
- Source: sourcechange - _Fires when video source is changed via updateSource()_
- Errors: error
- Playback Rate: ratechange
- Fullscreen: fullscreenchange
- Resize: resize
- Subtitles: subtitlechange๐ Browser Support
| Browser | Minimum Version |
| -------------- | ----------------- |
| Chrome | Latest 2 versions |
| Edge | Latest 2 versions |
| Firefox | Latest 2 versions |
| Safari | Latest 2 versions |
| iOS Safari | iOS 12+ |
| Android Chrome | Latest 2 versions |
Note: HLS playback requires HLS.js support. Native HLS playback is supported on Safari.
๐ License
MIT ยฉ Wontum Player
๐ค Contributing
Contributions are welcome! Please follow these steps:
1. Fork the repository
2. Create your feature branch (
git checkout -b feature/amazing-feature)
3. Commit your changes (git commit -m 'Add some amazing feature')
4. Push to the branch (git push origin feature/amazing-feature`)- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: pascalobi83@gmail.com
- Inspired by Mux Player
- Powered by HLS.js
- Built with TypeScript
---
Made with โค๏ธ for educational platforms