@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 +105 -27
- package/dist/index.d.ts +2 -2
- package/dist/player/live-player.d.ts +24 -0
- package/dist/scheduling/frame-scheduler.d.ts +23 -1
- package/dist/sources/mp4-file-source.d.ts +8 -1
- package/dist/web-live-player.cjs +3 -3
- package/dist/web-live-player.mjs +3027 -2861
- package/package.json +1 -1
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
|
-
|
|
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
|
-
- `
|
|
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
|
*/
|