@stinkycomputing/web-live-player 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,7 @@ A framework-agnostic video streaming library for playing back **Sesame** video s
9
9
  - **MoQ support** - Native Media over QUIC protocol support via `stinky-moq-js`
10
10
  - **Pluggable stream sources** - Use dependency injection to provide video data from any transport
11
11
  - **Frame scheduling** - Automatic buffering and drift correction for smooth playback
12
+ - **Optimized file loading** - Range-based chunked loading for fast playback of large MP4 files
12
13
  - **No framework dependencies** - Works with vanilla JS, React, Three.js, or any other framework
13
14
 
14
15
  ## Installation
@@ -17,12 +18,6 @@ A framework-agnostic video streaming library for playing back **Sesame** video s
17
18
  npm install @stinkycomputing/web-live-player
18
19
  ```
19
20
 
20
- For MoQ support, also install:
21
-
22
- ```bash
23
- npm install stinky-moq-js
24
- ```
25
-
26
21
  ## Quick Start
27
22
 
28
23
  ### Using with MoQ (Standalone)
@@ -33,7 +28,7 @@ import { createPlayer, createStandaloneMoQSource } from '@stinkycomputing/web-li
33
28
  // Create player
34
29
  const player = createPlayer({
35
30
  preferredDecoder: 'webcodecs-hw',
36
- bufferSizeFrames: 3,
31
+ bufferDelayMs: 100,
37
32
  });
38
33
 
39
34
  // Create MoQ source
@@ -42,6 +37,7 @@ const moqSource = createStandaloneMoQSource({
42
37
  namespace: 'live/stream',
43
38
  subscriptions: [
44
39
  { trackName: 'video', streamType: 'video' },
40
+ { trackName: 'audio', streamType: 'audio' },
45
41
  ],
46
42
  });
47
43
 
@@ -62,23 +58,6 @@ function render(timestamp) {
62
58
  requestAnimationFrame(render);
63
59
  ```
64
60
 
65
- ### Using with Elmo's MoQSession
66
-
67
- ```typescript
68
- import { createPlayer } from '@stinkycomputing/web-live-player';
69
- import { MoQDiscoveryUtils } from '@elmo/core';
70
-
71
- // Find session from Elmo's node tree
72
- // MoQSessionNode implements IStreamSource directly
73
- const session = MoQDiscoveryUtils.findMoQSession(currentNode, 'my-session');
74
-
75
- // Create and configure player - session can be used directly as stream source
76
- const player = createPlayer();
77
- player.setStreamSource(session);
78
- player.setTrackFilter('video-track');
79
- player.play();
80
- ```
81
-
82
61
  ### Custom Stream Source
83
62
 
84
63
  ```typescript
@@ -109,6 +88,60 @@ player.setStreamSource(source);
109
88
  player.play();
110
89
  ```
111
90
 
91
+ ### File Playback
92
+
93
+ For playing MP4 files from URLs or local files:
94
+
95
+ ```typescript
96
+ import { createFilePlayer } from '@stinkycomputing/web-live-player';
97
+
98
+ const filePlayer = createFilePlayer({
99
+ preferredDecoder: 'webcodecs-hw',
100
+ enableAudio: true,
101
+ debugLogging: false,
102
+ playMode: 'once', // or 'loop' for continuous playback
103
+ });
104
+
105
+ // Load from URL (with optimized chunked loading)
106
+ await filePlayer.loadFromUrl('https://example.com/video.mp4');
107
+
108
+ // Or load from File object (e.g., from file input)
109
+ const file = fileInput.files[0];
110
+ await filePlayer.loadFromFile(file);
111
+
112
+ // Play the file
113
+ filePlayer.play();
114
+
115
+ // Render loop
116
+ function render() {
117
+ const frame = filePlayer.getVideoFrame();
118
+ if (frame) {
119
+ ctx.drawImage(frame, 0, 0);
120
+ frame.close();
121
+ }
122
+ requestAnimationFrame(render);
123
+ }
124
+ requestAnimationFrame(render);
125
+
126
+ // Seek to position (in seconds)
127
+ await filePlayer.seek(30);
128
+
129
+ // Listen to events
130
+ filePlayer.on('ready', (info) => {
131
+ console.log(`Video loaded: ${info.width}x${info.height}, ${info.duration}s`);
132
+ });
133
+
134
+ filePlayer.on('progress', (loaded, total) => {
135
+ console.log(`Loading: ${(loaded / total * 100).toFixed(1)}%`);
136
+ });
137
+ ```
138
+
139
+ **Optimized Loading**: The file player uses HTTP Range requests to load large files in chunks (1MB each). This means:
140
+ - Playback starts as soon as metadata is available (~1-2MB typically)
141
+ - Remaining file loads in the background during playback
142
+ - 10-30x faster time-to-first-frame for large files
143
+ - Automatic fallback to full download if server doesn't support ranges
144
+
112
145
  ## API Reference
113
146
 
114
147
  ### `createPlayer(config?)`
@@ -116,10 +149,51 @@ player.play();
116
149
  Creates a new player instance.
117
150
 
118
151
  **Config options:**
119
- - `preferredDecoder`: `'webcodecs-hw'` | `'webcodecs-sw'` | `'wasm'` - Decoder preference
120
- - `bufferSizeFrames`: `number` - Target buffer size (default: 3)
152
+ - `preferredDecoder`: `'webcodecs-hw'` | `'webcodecs-sw'` | `'wasm'` - Decoder preference (default: `'webcodecs-sw'`). Note: WASM decoder only supports H.264 Baseline profile.
153
+ - `bufferDelayMs`: `number` - Buffer delay in milliseconds (default: 100)
154
+ - `enableAudio`: `boolean` - Enable audio playback (default: true)
155
+ - `videoTrackName`: `string | null` - Video track name for MoQ streams (default: `'video'`)
156
+ - `audioTrackName`: `string | null` - Audio track name for MoQ streams (default: `'audio'`)
157
+ - `debugLogging`: `boolean` - Enable debug logging
158
+
159
+ ### `createFilePlayer(config?)`
160
+
161
+ Creates a file player instance for MP4 playback.
162
+
163
+ **Config options:**
164
+ - `preferredDecoder`: `'webcodecs-hw'` | `'webcodecs-sw'` | `'wasm'` - Decoder preference (default: `'webcodecs-sw'`)
165
+ - `enableAudio`: `boolean` - Enable audio playback (default: true)
166
+ - `audioContext`: `AudioContext` - Optional audio context (creates one if not provided)
167
+ - `playMode`: `'once'` | `'loop'` - Play mode (default: `'once'`)
121
168
  - `debugLogging`: `boolean` - Enable debug logging
122
169
 
170
+ ### `FileVideoPlayer`
171
+
172
+ File player class.
173
+
174
+ **Methods:**
175
+ - `loadFromUrl(url: string)` - Load MP4 from URL (uses range-based chunked loading)
176
+ - `loadFromFile(file: File)` - Load MP4 from File object
177
+ - `play()` - Start playback
178
+ - `pause()` - Pause playback
179
+ - `seek(timeSeconds: number)` - Seek to position
180
+ - `getVideoFrame()` - Get current video frame for rendering
181
+ - `getPosition()` - Get current position in seconds
182
+ - `getDuration()` - Get duration in seconds
183
+ - `getStats()` - Get playback statistics
184
+ - `setVolume(volume: number)` - Set audio volume (0-1)
185
+ - `setPlayMode(mode: 'once' | 'loop')` - Set play mode
186
+ - `dispose()` - Clean up resources
187
+
188
+ **Events:**
189
+ - `ready` - Emitted when file is loaded and ready to play
190
+ - `progress` - Emitted during file loading with (loaded, total) bytes
191
+ - `statechange` - Emitted when player state changes
192
+ - `ended` - Emitted when playback ends (in 'once' mode)
193
+ - `loop` - Emitted when video loops (in 'loop' mode)
194
+ - `seeked` - Emitted after seeking completes
195
+ - `error` - Emitted on errors
196
+
123
197
  ### `LiveVideoPlayer`
124
198
 
125
199
  Main player class.
@@ -127,10 +201,13 @@ Main player class.
127
201
  **Methods:**
128
202
  - `setStreamSource(source: IStreamSource)` - Set the stream data source
129
203
  - `setTrackFilter(trackName: string)` - Filter for specific track
204
+ - `connectToMoQRelay(relayUrl, namespace, options?)` - Connect directly to a MoQ relay
130
205
  - `play()` - Start playback
131
206
  - `pause()` - Pause playback
132
207
  - `getVideoFrame(timestampMs: number)` - Get frame for current render timestamp
133
208
  - `getStats()` - Get playback statistics
209
+ - `setVolume(volume: number)` - Set audio volume (0-1)
210
+ - `setDebugLogging(enabled: boolean)` - Enable/disable debug logging at runtime
134
211
  - `dispose()` - Clean up resources
135
212
 
136
213
  **Events:**
@@ -210,6 +287,8 @@ function render(timestamp: number) {
210
287
 
211
288
  ### Handling YUV Frames (WASM Decoder)
212
289
 
290
+ > **Note:** The WASM decoder only supports **H.264 Baseline profile**. For Main or High profile streams, use `'webcodecs-hw'` or `'webcodecs-sw'` instead.
291
+
213
292
  When using the WASM decoder, the library automatically converts YUV frames to `VideoFrame` objects using the browser's native I420 support. The GPU handles YUV→RGB conversion, so you can use the same rendering code regardless of decoder:
214
293
 
215
294
  ```typescript
@@ -298,7 +377,6 @@ const player = createPlayer({
298
377
  Run the demo application:
299
378
 
300
379
  ```bash
301
- cd video-player
302
380
  npm install
303
381
  npm run dev
304
382
  ```
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export * from './sources/stream-source';
12
12
  export { BasePlayer } from './player/base-player';
13
13
  export type { BasePlayerConfig } from './player/base-player';
14
14
  export { LiveVideoPlayer, createPlayer } from './player/live-player';
15
- export type { PlayerConfig, PlayerStats, PlayerState } from './player/live-player';
15
+ export type { PlayerConfig, PlayerStats, PlayerState, BandwidthStats } from './player/live-player';
16
16
  export { FileVideoPlayer, createFilePlayer } from './player/file-player';
17
17
  export type { FilePlayerConfig, FilePlayerState, FilePlayerStats, FilePlayMode } from './player/file-player';
18
18
  export { createStandaloneMoQSource, StandaloneMoQSource } from './sources/standalone-moq-source';
@@ -30,6 +30,6 @@ export { FileAudioPlayer } from './audio/file-audio-player';
30
30
  export { LiveAudioPlayer } from './audio/live-audio-player';
31
31
  export type { LiveAudioConfig } from './audio/live-audio-player';
32
32
  export { FrameScheduler } from './scheduling/frame-scheduler';
33
- export type { FrameTiming, LatencyStats, SchedulerStatus, SchedulerConfig } from './scheduling/frame-scheduler';
33
+ export type { FrameTiming, LatencyStats, SchedulerStatus, SchedulerConfig, PacketTimingEntry } from './scheduling/frame-scheduler';
34
34
  export { SesameBinaryProtocol } from './protocol/sesame-binary-protocol';
35
35
  export type { ParsedData, HeaderData, HeaderCodecData } from './protocol/sesame-binary-protocol';
@@ -21,6 +21,14 @@ export interface PlayerConfig {
21
21
  * Player state
22
22
  */
23
23
  export type PlayerState = 'idle' | 'playing' | 'paused' | 'error';
24
+ /**
25
+ * Bandwidth statistics
26
+ */
27
+ export interface BandwidthStats {
28
+ videoBytesPerSecond: number;
29
+ audioBytesPerSecond: number;
30
+ totalBytesPerSecond: number;
31
+ }
24
32
  /**
25
33
  * Player statistics
26
34
  */
@@ -36,6 +44,7 @@ export interface PlayerStats {
36
44
  streamHeight: number;
37
45
  frameRate: number;
38
46
  latency: LatencyStats | null;
47
+ bandwidth: BandwidthStats | null;
39
48
  }
40
49
  /**
41
50
  * Player event types
@@ -84,6 +93,13 @@ export declare class LiveVideoPlayer extends BasePlayer<PlayerState> {
84
93
  private ownsAudioContext;
85
94
  private audioCodecData;
86
95
  private arrivalTimes;
96
+ private keyframeStatus;
97
+ private videoBytesReceived;
98
+ private audioBytesReceived;
99
+ private lastBandwidthUpdateTime;
100
+ private lastVideoBytesReceived;
101
+ private lastAudioBytesReceived;
102
+ private currentBandwidth;
87
103
  constructor(config?: PlayerConfig);
88
104
  /**
89
105
  * Enable or disable debug logging at runtime
@@ -164,6 +180,14 @@ export declare class LiveVideoPlayer extends BasePlayer<PlayerState> {
164
180
  * Get player statistics
165
181
  */
166
182
  getStats(): PlayerStats;
183
+ /**
184
+ * Update bandwidth statistics
185
+ */
186
+ private updateBandwidthStats;
187
+ /**
188
+ * Get packet timing history for visualization/debugging
189
+ */
190
+ getPacketTimingHistory(): import('..').PacketTimingEntry[];
167
191
  /**
168
192
  * Subscribe to player events (typed overload)
169
193
  */
@@ -42,6 +42,23 @@ export interface SchedulerStatus {
42
42
  driftCorrections: number;
43
43
  latency: LatencyStats | null;
44
44
  }
45
+ /**
46
+ * Packet timing entry for visualization
47
+ */
48
+ export interface PacketTimingEntry {
49
+ /** Time packet arrived (performance.now()) */
50
+ arrivalTime: number;
51
+ /** Time since previous packet (ms) */
52
+ intervalMs: number;
53
+ /** Stream timestamp (us) */
54
+ streamTimestampUs: number;
55
+ /** Whether this was a keyframe */
56
+ isKeyframe: boolean;
57
+ /** Decode latency (ms) */
58
+ decodeLatencyMs: number;
59
+ /** Whether frame was dropped */
60
+ wasDropped: boolean;
61
+ }
45
62
  export interface SchedulerConfig<T> {
46
63
  /** Target buffer delay in milliseconds (0 = bypass mode, always return latest) */
47
64
  bufferDelayMs?: number;
@@ -81,6 +98,9 @@ export declare class FrameScheduler<T> {
81
98
  private dequeueCount;
82
99
  private latencyHistory;
83
100
  private latencyHistorySize;
101
+ private packetTimingHistory;
102
+ private packetTimingHistorySize;
103
+ private lastPacketArrivalTime;
84
104
  private stats;
85
105
  private logger;
86
106
  private onFrameDropped?;
@@ -90,7 +110,7 @@ export declare class FrameScheduler<T> {
90
110
  /** Effective drift threshold - scales with buffer target for low-latency mode */
91
111
  private get effectiveDriftThresholdMs();
92
112
  /** Enqueue a decoded frame with timing information */
93
- enqueue(frame: T, timestampUs: number, timing: FrameTiming): void;
113
+ enqueue(frame: T, timestampUs: number, timing: FrameTiming, isKeyframe?: boolean): void;
94
114
  /** Dequeue frame for rendering at given real time (milliseconds) */
95
115
  dequeue(realTimeMs: number): T | null;
96
116
  /** Bypass mode: return latest frame, drop rest */
@@ -119,4 +139,6 @@ export declare class FrameScheduler<T> {
119
139
  logStatus(): void;
120
140
  /** Reset statistics */
121
141
  resetStats(): void;
142
+ /** Get packet timing history for visualization */
143
+ getPacketTimingHistory(): PacketTimingEntry[];
122
144
  }
@@ -18,6 +18,7 @@ export interface MP4FileInfo {
18
18
  bitrate?: number;
19
19
  audioChannels?: number;
20
20
  audioSampleRate?: number;
21
+ isMoovAtStart?: boolean;
21
22
  }
22
23
  /**
23
24
  * Sample ready for decoding
@@ -54,6 +55,7 @@ export declare class MP4FileSource {
54
55
  private totalVideoSamples;
55
56
  private totalAudioSamples;
56
57
  private samplesRequested;
58
+ private isProgressiveLoading;
57
59
  private fileSize;
58
60
  private loadedBytes;
59
61
  private videoDescription;
@@ -73,9 +75,14 @@ export declare class MP4FileSource {
73
75
  */
74
76
  getAudioDescription(): Uint8Array | null;
75
77
  /**
76
- * Load an MP4 file from a URL
78
+ * Load an MP4 file from a URL using range-based loading for better performance.
79
+ * Automatically falls back to full download if server doesn't support ranges.
77
80
  */
78
81
  loadFromUrl(url: string): Promise<MP4FileInfo>;
82
+ /**
83
+ * Continue loading remaining chunks in background after initial metadata is ready
84
+ */
85
+ private continueLoadingInBackground;
79
86
  /**
80
87
  * Load an MP4 file from a File object (e.g., from file input)
81
88
  */