@livepeer-frameworks/player-core 0.0.3 → 0.1.0
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 +78 -0
- package/dist/cjs/index.js +792 -146
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +792 -146
- package/dist/esm/index.js.map +1 -1
- package/dist/player.css +3 -331
- package/dist/types/core/GatewayClient.d.ts +3 -4
- package/dist/types/core/InteractionController.d.ts +12 -0
- package/dist/types/core/MetaTrackManager.d.ts +1 -1
- package/dist/types/core/PlayerController.d.ts +18 -2
- package/dist/types/core/PlayerInterface.d.ts +10 -0
- package/dist/types/core/SeekingUtils.d.ts +3 -1
- package/dist/types/core/StreamStateClient.d.ts +1 -1
- package/dist/types/players/HlsJsPlayer.d.ts +8 -0
- package/dist/types/players/MewsWsPlayer/index.d.ts +1 -1
- package/dist/types/players/VideoJsPlayer.d.ts +12 -4
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +1 -1
- package/dist/types/players/WebCodecsPlayer/index.d.ts +11 -0
- package/dist/types/players/WebCodecsPlayer/types.d.ts +25 -3
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +20 -2
- package/dist/types/types.d.ts +32 -1
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +5 -5
- package/dist/types/vanilla/index.d.ts +3 -3
- package/dist/workers/decoder.worker.js +183 -6
- package/dist/workers/decoder.worker.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ABRController.ts +1 -1
- package/src/core/CodecUtils.ts +1 -1
- package/src/core/GatewayClient.ts +8 -10
- package/src/core/LiveDurationProxy.ts +0 -1
- package/src/core/MetaTrackManager.ts +1 -1
- package/src/core/PlayerController.ts +232 -26
- package/src/core/PlayerInterface.ts +6 -0
- package/src/core/PlayerManager.ts +49 -0
- package/src/core/StreamStateClient.ts +3 -3
- package/src/core/SubtitleManager.ts +1 -1
- package/src/core/TelemetryReporter.ts +1 -1
- package/src/core/TimerManager.ts +1 -1
- package/src/core/scorer.ts +8 -4
- package/src/players/DashJsPlayer.ts +23 -11
- package/src/players/HlsJsPlayer.ts +29 -5
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +3 -3
- package/src/players/MewsWsPlayer/WebSocketManager.ts +0 -1
- package/src/players/MewsWsPlayer/index.ts +7 -5
- package/src/players/MistPlayer.ts +1 -1
- package/src/players/MistWebRTCPlayer/index.ts +1 -1
- package/src/players/NativePlayer.ts +2 -2
- package/src/players/VideoJsPlayer.ts +33 -31
- package/src/players/WebCodecsPlayer/SyncController.ts +1 -2
- package/src/players/WebCodecsPlayer/WebSocketController.ts +1 -1
- package/src/players/WebCodecsPlayer/index.ts +25 -7
- package/src/players/WebCodecsPlayer/types.ts +31 -3
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +20 -13
- package/src/players/WebCodecsPlayer/worker/types.ts +4 -0
- package/src/styles/player.css +0 -314
- package/src/types.ts +43 -1
- package/src/vanilla/FrameWorksPlayer.ts +5 -5
- package/src/vanilla/index.ts +3 -3
|
@@ -296,16 +296,17 @@ export class DashJsPlayerImpl extends BasePlayer {
|
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
// Configure dashjs v5 streaming settings BEFORE initialization
|
|
299
|
+
// AGGRESSIVE settings for fastest startup and low latency
|
|
299
300
|
this.dashPlayer.updateSettings({
|
|
300
301
|
streaming: {
|
|
301
|
-
//
|
|
302
|
+
// AGGRESSIVE: Minimal buffers for fastest startup
|
|
302
303
|
buffer: {
|
|
303
304
|
fastSwitchEnabled: true,
|
|
304
|
-
stableBufferTime: 16
|
|
305
|
-
bufferTimeAtTopQuality: 30
|
|
306
|
-
bufferTimeAtTopQualityLongForm: 60
|
|
307
|
-
bufferToKeep: 30
|
|
308
|
-
bufferPruningInterval: 30
|
|
305
|
+
stableBufferTime: 4, // Reduced from 16 (aggressive!)
|
|
306
|
+
bufferTimeAtTopQuality: 8, // Reduced from 30
|
|
307
|
+
bufferTimeAtTopQualityLongForm: 15, // Reduced from 60
|
|
308
|
+
bufferToKeep: 10, // Reduced from 30
|
|
309
|
+
bufferPruningInterval: 10, // Reduced from 30
|
|
309
310
|
},
|
|
310
311
|
// Gaps/stall handling
|
|
311
312
|
gaps: {
|
|
@@ -314,12 +315,23 @@ export class DashJsPlayerImpl extends BasePlayer {
|
|
|
314
315
|
smallGapLimit: 1.5,
|
|
315
316
|
threshold: 0.3,
|
|
316
317
|
},
|
|
317
|
-
// ABR
|
|
318
|
+
// AGGRESSIVE: ABR with high initial bitrate estimate
|
|
318
319
|
abr: {
|
|
319
320
|
autoSwitchBitrate: { video: true, audio: true },
|
|
320
321
|
limitBitrateByPortal: false,
|
|
321
322
|
useDefaultABRRules: true,
|
|
322
|
-
initialBitrate: { video:
|
|
323
|
+
initialBitrate: { video: 5_000_000, audio: 128_000 }, // 5Mbps initial (was -1)
|
|
324
|
+
},
|
|
325
|
+
// LIVE CATCHUP - critical for maintaining live edge (was missing!)
|
|
326
|
+
liveCatchup: {
|
|
327
|
+
enabled: true,
|
|
328
|
+
maxDrift: 1.5, // Seek to live if drift > 1.5s
|
|
329
|
+
playbackRate: {
|
|
330
|
+
max: 0.15, // Speed up by max 15%
|
|
331
|
+
min: -0.15, // Slow down by max 15%
|
|
332
|
+
},
|
|
333
|
+
playbackBufferMin: 0.3, // Min buffer before catchup
|
|
334
|
+
mode: 'liveCatchupModeDefault',
|
|
323
335
|
},
|
|
324
336
|
// Retry settings - more aggressive
|
|
325
337
|
retryAttempts: {
|
|
@@ -354,11 +366,11 @@ export class DashJsPlayerImpl extends BasePlayer {
|
|
|
354
366
|
abandonLoadTimeout: 5000, // 5 seconds instead of default 10
|
|
355
367
|
xhrWithCredentials: false,
|
|
356
368
|
text: { defaultEnabled: false },
|
|
357
|
-
//
|
|
369
|
+
// AGGRESSIVE: Tighter live delay
|
|
358
370
|
delay: {
|
|
359
|
-
liveDelay:
|
|
371
|
+
liveDelay: 2, // Reduced from 4 (2s behind live edge)
|
|
360
372
|
liveDelayFragmentCount: null,
|
|
361
|
-
useSuggestedPresentationDelay:
|
|
373
|
+
useSuggestedPresentationDelay: false, // Ignore manifest suggestions
|
|
362
374
|
},
|
|
363
375
|
},
|
|
364
376
|
debug: {
|
|
@@ -3,6 +3,7 @@ import { checkProtocolMismatch, getBrowserInfo } from '../core/detector';
|
|
|
3
3
|
import { translateCodec } from '../core/CodecUtils';
|
|
4
4
|
import { LiveDurationProxy } from '../core/LiveDurationProxy';
|
|
5
5
|
import type { StreamSource, StreamInfo, PlayerOptions, PlayerCapability } from '../core/PlayerInterface';
|
|
6
|
+
import type { HlsJsConfig } from '../types';
|
|
6
7
|
|
|
7
8
|
// Player implementation class
|
|
8
9
|
export class HlsJsPlayerImpl extends BasePlayer {
|
|
@@ -148,13 +149,36 @@ export class HlsJsPlayerImpl extends BasePlayer {
|
|
|
148
149
|
console.log('[HLS.js] hls.js module imported, Hls.isSupported():', Hls.isSupported?.());
|
|
149
150
|
|
|
150
151
|
if (Hls.isSupported()) {
|
|
151
|
-
|
|
152
|
+
// Build optimized HLS.js config with user overrides
|
|
153
|
+
const hlsConfig: HlsJsConfig = {
|
|
154
|
+
// Worker disabled for lower latency (per HLS.js maintainer recommendation)
|
|
152
155
|
enableWorker: false,
|
|
156
|
+
|
|
157
|
+
// LL-HLS support
|
|
153
158
|
lowLatencyMode: true,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
|
|
160
|
+
// AGGRESSIVE: Assume 5 Mbps initially (not 500kbps default)
|
|
161
|
+
// This dramatically improves startup time by selecting appropriate quality faster
|
|
162
|
+
abrEwmaDefaultEstimate: 5_000_000,
|
|
163
|
+
|
|
164
|
+
// AGGRESSIVE: Minimal buffers for fastest startup
|
|
165
|
+
maxBufferLength: 6, // Reduced from 15 (just 2 segments @ 3s)
|
|
166
|
+
maxMaxBufferLength: 15, // Reduced from 60
|
|
167
|
+
backBufferLength: Infinity, // Let browser manage (per maintainer advice)
|
|
168
|
+
|
|
169
|
+
// Stay close to live edge but not too aggressive
|
|
170
|
+
liveSyncDuration: 4, // Target 4 seconds behind live edge
|
|
171
|
+
liveMaxLatencyDuration: 8, // Max 8 seconds before seeking to live
|
|
172
|
+
|
|
173
|
+
// Faster ABR adaptation for live
|
|
174
|
+
abrEwmaFastLive: 2.0, // Faster than default 3.0
|
|
175
|
+
abrEwmaSlowLive: 6.0, // Faster than default 9.0
|
|
176
|
+
|
|
177
|
+
// Allow user overrides
|
|
178
|
+
...options.hlsConfig,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.hls = new Hls(hlsConfig);
|
|
158
182
|
|
|
159
183
|
this.hls.attachMedia(video);
|
|
160
184
|
|
|
@@ -314,7 +314,7 @@ export class SourceBufferManager {
|
|
|
314
314
|
try {
|
|
315
315
|
// Make sure end time is never 0 (mews.js:376)
|
|
316
316
|
this.sourceBuffer.remove(0, Math.max(0.1, currentTime - keepaway));
|
|
317
|
-
} catch
|
|
317
|
+
} catch {
|
|
318
318
|
// Ignore errors during cleanup
|
|
319
319
|
}
|
|
320
320
|
});
|
|
@@ -343,7 +343,7 @@ export class SourceBufferManager {
|
|
|
343
343
|
if (!isNaN(this.mediaSource.duration)) {
|
|
344
344
|
this.sourceBuffer.remove(0, Infinity);
|
|
345
345
|
}
|
|
346
|
-
} catch
|
|
346
|
+
} catch {
|
|
347
347
|
// Ignore
|
|
348
348
|
}
|
|
349
349
|
|
|
@@ -371,7 +371,7 @@ export class SourceBufferManager {
|
|
|
371
371
|
if (this.sourceBuffer && this.mediaSource.readyState === 'open') {
|
|
372
372
|
try {
|
|
373
373
|
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
|
|
374
|
-
} catch
|
|
374
|
+
} catch {
|
|
375
375
|
// Ignore
|
|
376
376
|
}
|
|
377
377
|
}
|
|
@@ -121,7 +121,7 @@ export class MewsWsPlayerImpl extends BasePlayer {
|
|
|
121
121
|
return Object.keys(playableTracks);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
async initialize(container: HTMLElement, source: StreamSource, options: PlayerOptions): Promise<HTMLVideoElement> {
|
|
124
|
+
async initialize(container: HTMLElement, source: StreamSource, options: PlayerOptions, streamInfo?: StreamInfo): Promise<HTMLVideoElement> {
|
|
125
125
|
this.container = container;
|
|
126
126
|
container.classList.add('fw-player-container');
|
|
127
127
|
|
|
@@ -153,12 +153,14 @@ export class MewsWsPlayerImpl extends BasePlayer {
|
|
|
153
153
|
endpoint: anyOpts.analytics?.endpoint || null
|
|
154
154
|
};
|
|
155
155
|
|
|
156
|
-
// Get stream type from
|
|
157
|
-
|
|
156
|
+
// Get stream type from streamInfo if available
|
|
157
|
+
// Note: source.type is a MIME string (e.g., 'ws/video/mp4'), not 'live'/'vod'
|
|
158
|
+
if (streamInfo?.type === 'live') {
|
|
158
159
|
this.streamType = 'live';
|
|
159
|
-
} else if (
|
|
160
|
+
} else if (streamInfo?.type === 'vod') {
|
|
160
161
|
this.streamType = 'vod';
|
|
161
162
|
}
|
|
163
|
+
// Fallback: will be determined by server on_time messages (end === 0 means live)
|
|
162
164
|
|
|
163
165
|
try {
|
|
164
166
|
// Initialize MediaSource (mews.js:138-196)
|
|
@@ -917,7 +919,7 @@ export class MewsWsPlayerImpl extends BasePlayer {
|
|
|
917
919
|
this.sbManager?._do(() => {
|
|
918
920
|
try {
|
|
919
921
|
// Clear buffer for clean loop
|
|
920
|
-
} catch
|
|
922
|
+
} catch {}
|
|
921
923
|
});
|
|
922
924
|
}
|
|
923
925
|
});
|
|
@@ -499,7 +499,7 @@ export class MistWebRTCPlayerImpl extends BasePlayer {
|
|
|
499
499
|
|
|
500
500
|
// Private methods
|
|
501
501
|
|
|
502
|
-
private async setupWebRTC(video: HTMLVideoElement, source: StreamSource,
|
|
502
|
+
private async setupWebRTC(video: HTMLVideoElement, source: StreamSource, _options: PlayerOptions): Promise<void> {
|
|
503
503
|
const sourceAny = source as any;
|
|
504
504
|
const iceServers: RTCIceServer[] = sourceAny?.iceServers || [];
|
|
505
505
|
|
|
@@ -109,7 +109,7 @@ export class NativePlayerImpl extends BasePlayer {
|
|
|
109
109
|
|
|
110
110
|
// Safari cannot play WebM - skip entirely
|
|
111
111
|
// Reference: html5.js:28-29
|
|
112
|
-
if (mimetype === 'html5/video/webm' && browser.
|
|
112
|
+
if (mimetype === 'html5/video/webm' && browser.isSafari) {
|
|
113
113
|
return false;
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -262,7 +262,7 @@ export class NativePlayerImpl extends BasePlayer {
|
|
|
262
262
|
// Use LiveDurationProxy for all live streams (non-WHEP)
|
|
263
263
|
// WHEP handles its own live edge via signaling
|
|
264
264
|
// This enables seeking and jump-to-live for native MP4/WebM/HLS live streams
|
|
265
|
-
const isLiveStream = streamInfo?.type === 'live'
|
|
265
|
+
const isLiveStream = streamInfo?.type === 'live';
|
|
266
266
|
if (source.type !== 'whep' && isLiveStream) {
|
|
267
267
|
this.setupLiveDurationProxy(video);
|
|
268
268
|
this.setupAutoRecovery(video);
|
|
@@ -154,6 +154,10 @@ export class VideoJsPlayerImpl extends BasePlayer {
|
|
|
154
154
|
// When using custom controls (controls: false), disable ALL VideoJS UI elements
|
|
155
155
|
const useVideoJsControls = options.controls === true;
|
|
156
156
|
|
|
157
|
+
// Android < 7 workaround: enable overrideNative for HLS
|
|
158
|
+
const androidMatch = navigator.userAgent.match(/android\s([\d.]*)/i);
|
|
159
|
+
const androidVersion = androidMatch ? parseFloat(androidMatch[1]) : null;
|
|
160
|
+
|
|
157
161
|
// Build VideoJS options
|
|
158
162
|
// NOTE: We disable UI components but NOT children array - that breaks playback
|
|
159
163
|
const vjsOptions: Record<string, any> = {
|
|
@@ -169,17 +173,36 @@ export class VideoJsPlayerImpl extends BasePlayer {
|
|
|
169
173
|
controlBar: useVideoJsControls,
|
|
170
174
|
liveTracker: useVideoJsControls,
|
|
171
175
|
// Don't set children: [] - that can break internal VideoJS components
|
|
172
|
-
};
|
|
173
176
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
// VHS (http-streaming) configuration - AGGRESSIVE for fastest startup
|
|
178
|
+
html5: {
|
|
179
|
+
vhs: {
|
|
180
|
+
// AGGRESSIVE: Start with lower quality for instant playback
|
|
181
|
+
enableLowInitialPlaylist: true,
|
|
182
|
+
|
|
183
|
+
// AGGRESSIVE: Assume 5 Mbps initially
|
|
184
|
+
bandwidth: 5_000_000,
|
|
185
|
+
|
|
186
|
+
// Persist bandwidth across sessions for returning users
|
|
187
|
+
useBandwidthFromLocalStorage: true,
|
|
188
|
+
|
|
189
|
+
// Enable partial segment processing for lower latency
|
|
190
|
+
handlePartialData: true,
|
|
191
|
+
|
|
192
|
+
// AGGRESSIVE: Very tight live range
|
|
193
|
+
liveRangeSafeTimeDelta: 0.3,
|
|
194
|
+
|
|
195
|
+
// Allow user overrides via options.vhsConfig
|
|
196
|
+
...options.vhsConfig,
|
|
197
|
+
},
|
|
198
|
+
// Android < 7 workaround
|
|
199
|
+
...(androidVersion && androidVersion < 7 ? {
|
|
200
|
+
hls: { overrideNative: true }
|
|
201
|
+
} : {}),
|
|
202
|
+
},
|
|
203
|
+
nativeAudioTracks: androidVersion && androidVersion < 7 ? false : undefined,
|
|
204
|
+
nativeVideoTracks: androidVersion && androidVersion < 7 ? false : undefined,
|
|
205
|
+
};
|
|
183
206
|
|
|
184
207
|
console.debug('[VideoJS] Creating player with options:', vjsOptions);
|
|
185
208
|
this.videojsPlayer = videojs(video, vjsOptions);
|
|
@@ -410,27 +433,6 @@ export class VideoJsPlayerImpl extends BasePlayer {
|
|
|
410
433
|
return v?.currentTime ?? 0;
|
|
411
434
|
}
|
|
412
435
|
|
|
413
|
-
getDuration(): number {
|
|
414
|
-
const v = this.proxyElement || this.videoElement;
|
|
415
|
-
return v?.duration ?? 0;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
getSeekableRange(): { start: number; end: number } | null {
|
|
419
|
-
if (this.videojsPlayer?.seekable) {
|
|
420
|
-
try {
|
|
421
|
-
const seekable = this.videojsPlayer.seekable();
|
|
422
|
-
if (seekable && seekable.length > 0) {
|
|
423
|
-
const start = seekable.start(0) + this.timeCorrection;
|
|
424
|
-
const end = seekable.end(seekable.length - 1) + this.timeCorrection;
|
|
425
|
-
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
|
|
426
|
-
return { start, end };
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
} catch {}
|
|
430
|
-
}
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
436
|
/**
|
|
435
437
|
* Seek to time using VideoJS API (fixes backwards seeking in HLS).
|
|
436
438
|
* Time should be in the corrected coordinate space (with firstms offset applied).
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
import type {
|
|
18
18
|
LatencyProfile,
|
|
19
|
-
BufferState,
|
|
20
19
|
SyncState,
|
|
21
20
|
TrackInfo,
|
|
22
21
|
} from './types';
|
|
@@ -383,7 +382,7 @@ export class SyncController {
|
|
|
383
382
|
/**
|
|
384
383
|
* Register a new track
|
|
385
384
|
*/
|
|
386
|
-
addTrack(
|
|
385
|
+
addTrack(_trackIndex: number, _track: TrackInfo): void {
|
|
387
386
|
// Jitter tracking will be initialized on first chunk
|
|
388
387
|
}
|
|
389
388
|
|
|
@@ -27,7 +27,6 @@ import type {
|
|
|
27
27
|
InfoMessage,
|
|
28
28
|
OnTimeMessage,
|
|
29
29
|
RawChunk,
|
|
30
|
-
LatencyProfileName,
|
|
31
30
|
WebCodecsPlayerOptions,
|
|
32
31
|
WebCodecsStats,
|
|
33
32
|
MainToWorkerMessage,
|
|
@@ -36,7 +35,7 @@ import type {
|
|
|
36
35
|
import { WebSocketController } from './WebSocketController';
|
|
37
36
|
import { SyncController } from './SyncController';
|
|
38
37
|
import { getPresentationTimestamp, isInitData } from './RawChunkParser';
|
|
39
|
-
import {
|
|
38
|
+
import { mergeLatencyProfile, selectDefaultProfile } from './LatencyProfiles';
|
|
40
39
|
import { createTrackGenerator, hasNativeMediaStreamTrackGenerator } from './polyfills/MediaStreamTrackGenerator';
|
|
41
40
|
|
|
42
41
|
/**
|
|
@@ -114,11 +113,13 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
114
113
|
name: 'WebCodecs Player',
|
|
115
114
|
shortname: 'webcodecs',
|
|
116
115
|
priority: 0, // Highest priority - lowest latency option
|
|
117
|
-
// Raw WebSocket (12-byte header +
|
|
116
|
+
// Raw WebSocket (12-byte header + codec frames) - NOT MP4-muxed
|
|
118
117
|
// MistServer's output_wsraw.cpp provides full codec negotiation (audio + video)
|
|
118
|
+
// MistServer's output_h264.cpp uses same 12-byte header but Annex B payload (video-only)
|
|
119
119
|
// NOTE: ws/video/mp4 is MP4-fragmented which needs MEWS player (uses MSE)
|
|
120
120
|
mimes: [
|
|
121
|
-
'ws/video/raw', 'wss/video/raw', // Raw codec frames (audio + video)
|
|
121
|
+
'ws/video/raw', 'wss/video/raw', // Raw codec frames - AVCC format (audio + video)
|
|
122
|
+
'ws/video/h264', 'wss/video/h264', // Annex B H264/HEVC (video-only, same 12-byte header)
|
|
122
123
|
],
|
|
123
124
|
};
|
|
124
125
|
|
|
@@ -136,6 +137,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
136
137
|
private debugging = false;
|
|
137
138
|
private verboseDebugging = false;
|
|
138
139
|
private streamType: 'live' | 'vod' = 'live';
|
|
140
|
+
/** Payload format: 'avcc' for ws/video/raw, 'annexb' for ws/video/h264 */
|
|
141
|
+
private payloadFormat: 'avcc' | 'annexb' = 'avcc';
|
|
139
142
|
private workerUidCounter = 0;
|
|
140
143
|
private workerListeners = new Map<number, (msg: WorkerToMainMessage) => void>();
|
|
141
144
|
|
|
@@ -209,7 +212,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
209
212
|
}
|
|
210
213
|
} else {
|
|
211
214
|
// Use VideoDecoder.isConfigSupported()
|
|
212
|
-
|
|
215
|
+
const videoResult = await VideoDecoder.isConfigSupported(config as VideoDecoderConfig);
|
|
216
|
+
result = { supported: videoResult.supported === true, config: videoResult.config };
|
|
213
217
|
}
|
|
214
218
|
break;
|
|
215
219
|
}
|
|
@@ -217,7 +221,8 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
217
221
|
// Audio requires numberOfChannels and sampleRate
|
|
218
222
|
config.numberOfChannels = track.channels ?? 2;
|
|
219
223
|
config.sampleRate = track.rate ?? 48000;
|
|
220
|
-
|
|
224
|
+
const audioResult = await AudioDecoder.isConfigSupported(config as AudioDecoderConfig);
|
|
225
|
+
result = { supported: audioResult.supported === true, config: audioResult.config };
|
|
221
226
|
break;
|
|
222
227
|
}
|
|
223
228
|
default:
|
|
@@ -311,6 +316,11 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
311
316
|
}
|
|
312
317
|
}
|
|
313
318
|
|
|
319
|
+
// Annex B H264 WebSocket is video-only (no audio payloads)
|
|
320
|
+
if (mimetype.includes('video/h264')) {
|
|
321
|
+
delete playableTracks.audio;
|
|
322
|
+
}
|
|
323
|
+
|
|
314
324
|
if (Object.keys(playableTracks).length === 0) {
|
|
315
325
|
return false;
|
|
316
326
|
}
|
|
@@ -341,6 +351,13 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
341
351
|
this._bytesReceived = 0;
|
|
342
352
|
this._messagesReceived = 0;
|
|
343
353
|
|
|
354
|
+
// Detect payload format from source MIME type
|
|
355
|
+
// ws/video/h264 uses Annex B (start code delimited NALs), ws/video/raw uses AVCC (length-prefixed)
|
|
356
|
+
this.payloadFormat = source.type?.includes('h264') ? 'annexb' : 'avcc';
|
|
357
|
+
if (this.payloadFormat === 'annexb') {
|
|
358
|
+
this.log('Using Annex B payload format (ws/video/h264)');
|
|
359
|
+
}
|
|
360
|
+
|
|
344
361
|
this.container = container;
|
|
345
362
|
container.classList.add('fw-player-container');
|
|
346
363
|
|
|
@@ -932,7 +949,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
932
949
|
const tracksObj = msg.meta.tracks;
|
|
933
950
|
this.log(`Info contains ${Object.keys(tracksObj).length} tracks`);
|
|
934
951
|
|
|
935
|
-
for (const [
|
|
952
|
+
for (const [_name, track] of Object.entries(tracksObj)) {
|
|
936
953
|
// Store track by its index for lookup when chunks arrive
|
|
937
954
|
if (track.idx !== undefined) {
|
|
938
955
|
this.tracksByIndex.set(track.idx, track);
|
|
@@ -1180,6 +1197,7 @@ export class WebCodecsPlayerImpl extends BasePlayer {
|
|
|
1180
1197
|
track,
|
|
1181
1198
|
opts: {
|
|
1182
1199
|
optimizeForLatency: this.streamType === 'live',
|
|
1200
|
+
payloadFormat: this.payloadFormat, // 'avcc' for ws/video/raw, 'annexb' for ws/video/h264
|
|
1183
1201
|
},
|
|
1184
1202
|
uid: this.workerUidCounter++,
|
|
1185
1203
|
});
|
|
@@ -213,6 +213,8 @@ export interface CreatePipelineMessage {
|
|
|
213
213
|
track: TrackInfo;
|
|
214
214
|
opts: {
|
|
215
215
|
optimizeForLatency: boolean;
|
|
216
|
+
/** Payload format: 'avcc' (length-prefixed) or 'annexb' (start-code delimited) */
|
|
217
|
+
payloadFormat?: 'avcc' | 'annexb';
|
|
216
218
|
};
|
|
217
219
|
uid?: number;
|
|
218
220
|
}
|
|
@@ -257,9 +259,10 @@ export interface ClosePipelineMessage {
|
|
|
257
259
|
|
|
258
260
|
export interface FrameTimingMessage {
|
|
259
261
|
type: 'frametiming';
|
|
260
|
-
action: 'setSpeed' | 'reset';
|
|
262
|
+
action: 'setSpeed' | 'reset' | 'setPaused';
|
|
261
263
|
speed?: number;
|
|
262
264
|
tweak?: number;
|
|
265
|
+
paused?: boolean;
|
|
263
266
|
uid?: number;
|
|
264
267
|
}
|
|
265
268
|
|
|
@@ -275,6 +278,20 @@ export interface DebuggingMessage {
|
|
|
275
278
|
uid?: number;
|
|
276
279
|
}
|
|
277
280
|
|
|
281
|
+
export interface FrameStepMessage {
|
|
282
|
+
type: 'framestep';
|
|
283
|
+
direction: -1 | 1;
|
|
284
|
+
uid?: number;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface WriteFrameResponseMessage {
|
|
288
|
+
type: 'writeframe';
|
|
289
|
+
idx: number;
|
|
290
|
+
uid?: number;
|
|
291
|
+
status: 'ok' | 'error';
|
|
292
|
+
error?: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
278
295
|
export type MainToWorkerMessage =
|
|
279
296
|
| CreatePipelineMessage
|
|
280
297
|
| ConfigurePipelineMessage
|
|
@@ -284,7 +301,9 @@ export type MainToWorkerMessage =
|
|
|
284
301
|
| ClosePipelineMessage
|
|
285
302
|
| FrameTimingMessage
|
|
286
303
|
| SeekWorkerMessage
|
|
287
|
-
| DebuggingMessage
|
|
304
|
+
| DebuggingMessage
|
|
305
|
+
| FrameStepMessage
|
|
306
|
+
| WriteFrameResponseMessage;
|
|
288
307
|
|
|
289
308
|
// Worker -> Main thread messages
|
|
290
309
|
export interface AddTrackMessage {
|
|
@@ -323,6 +342,7 @@ export interface SendEventMessage {
|
|
|
323
342
|
type: 'sendevent';
|
|
324
343
|
kind: string;
|
|
325
344
|
message?: string;
|
|
345
|
+
time?: number;
|
|
326
346
|
idx?: number;
|
|
327
347
|
uid?: number;
|
|
328
348
|
}
|
|
@@ -341,6 +361,13 @@ export interface AckMessage {
|
|
|
341
361
|
error?: string;
|
|
342
362
|
}
|
|
343
363
|
|
|
364
|
+
export interface WriteFrameMessage {
|
|
365
|
+
type: 'writeframe';
|
|
366
|
+
idx: number;
|
|
367
|
+
frame: AudioData;
|
|
368
|
+
uid?: number;
|
|
369
|
+
}
|
|
370
|
+
|
|
344
371
|
export type WorkerToMainMessage =
|
|
345
372
|
| AddTrackMessage
|
|
346
373
|
| RemoveTrackMessage
|
|
@@ -349,7 +376,8 @@ export type WorkerToMainMessage =
|
|
|
349
376
|
| LogMessage
|
|
350
377
|
| SendEventMessage
|
|
351
378
|
| StatsMessage
|
|
352
|
-
| AckMessage
|
|
379
|
+
| AckMessage
|
|
380
|
+
| WriteFrameMessage;
|
|
353
381
|
|
|
354
382
|
// ============================================================================
|
|
355
383
|
// Stats Types
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
VideoDecoderInit,
|
|
21
21
|
AudioDecoderInit,
|
|
22
22
|
} from './types';
|
|
23
|
-
import type {
|
|
23
|
+
import type { PipelineStats, FrameTrackerStats } from '../types';
|
|
24
24
|
|
|
25
25
|
// ============================================================================
|
|
26
26
|
// Global State
|
|
@@ -139,7 +139,7 @@ let statsTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
139
139
|
const STATS_INTERVAL_MS = 250;
|
|
140
140
|
|
|
141
141
|
// Frame dropping stats (Phase 2B)
|
|
142
|
-
let
|
|
142
|
+
let _totalFramesDropped = 0;
|
|
143
143
|
|
|
144
144
|
// Chrome-recommended decoder queue threshold
|
|
145
145
|
// Per Chrome WebCodecs best practices: drop when decodeQueueSize > 2
|
|
@@ -259,6 +259,7 @@ function handleCreate(msg: MainToWorkerMessage & { type: 'create' }): void {
|
|
|
259
259
|
lastChunkBytes: '' as string,
|
|
260
260
|
},
|
|
261
261
|
optimizeForLatency: opts.optimizeForLatency,
|
|
262
|
+
payloadFormat: opts.payloadFormat || 'avcc',
|
|
262
263
|
};
|
|
263
264
|
|
|
264
265
|
pipelines.set(idx, pipeline);
|
|
@@ -349,7 +350,10 @@ function configureVideoDecoder(pipeline: PipelineState, description?: Uint8Array
|
|
|
349
350
|
};
|
|
350
351
|
|
|
351
352
|
// Pass description directly from WebSocket INIT data (per reference rawws.js line 1052)
|
|
352
|
-
|
|
353
|
+
// For Annex B format (ws/video/h264), SPS/PPS comes inline in the bitstream - skip description
|
|
354
|
+
if (pipeline.payloadFormat === 'annexb') {
|
|
355
|
+
log(`Annex B mode - SPS/PPS inline in bitstream, no description needed`);
|
|
356
|
+
} else if (description && description.byteLength > 0) {
|
|
353
357
|
config.description = description;
|
|
354
358
|
log(`Configuring with description (${description.byteLength} bytes)`);
|
|
355
359
|
} else {
|
|
@@ -517,7 +521,7 @@ function resetPipelineAfterError(pipeline: PipelineState): void {
|
|
|
517
521
|
// ============================================================================
|
|
518
522
|
|
|
519
523
|
function handleReceive(msg: MainToWorkerMessage & { type: 'receive' }): void {
|
|
520
|
-
const { idx, chunk
|
|
524
|
+
const { idx, chunk } = msg;
|
|
521
525
|
const pipeline = pipelines.get(idx);
|
|
522
526
|
|
|
523
527
|
if (!pipeline) {
|
|
@@ -555,7 +559,7 @@ function handleReceive(msg: MainToWorkerMessage & { type: 'receive' }): void {
|
|
|
555
559
|
} else {
|
|
556
560
|
// Drop delta frames when decoder is overwhelmed
|
|
557
561
|
pipeline.stats.framesDropped++;
|
|
558
|
-
|
|
562
|
+
_totalFramesDropped++;
|
|
559
563
|
logVerbose(`Dropped delta frame @ ${chunk.timestamp / 1000}ms (decoder queue: ${pipeline.decoder.decodeQueueSize})`);
|
|
560
564
|
}
|
|
561
565
|
return;
|
|
@@ -583,7 +587,7 @@ function shouldDropFramesDueToDecoderPressure(pipeline: PipelineState): boolean
|
|
|
583
587
|
* Drop all frames up to the next keyframe in the input queue
|
|
584
588
|
* Called when decoder is severely backed up
|
|
585
589
|
*/
|
|
586
|
-
function
|
|
590
|
+
function _dropToNextKeyframe(pipeline: PipelineState): number {
|
|
587
591
|
if (pipeline.inputQueue.length === 0) return 0;
|
|
588
592
|
|
|
589
593
|
// Find next keyframe in queue
|
|
@@ -597,7 +601,7 @@ function dropToNextKeyframe(pipeline: PipelineState): number {
|
|
|
597
601
|
// Drop all frames before keyframe
|
|
598
602
|
const dropped = pipeline.inputQueue.splice(0, keyframeIdx);
|
|
599
603
|
pipeline.stats.framesDropped += dropped.length;
|
|
600
|
-
|
|
604
|
+
_totalFramesDropped += dropped.length;
|
|
601
605
|
|
|
602
606
|
log(`Dropped ${dropped.length} frames to next keyframe`, 'warn');
|
|
603
607
|
|
|
@@ -994,13 +998,14 @@ function handleCreateGenerator(msg: MainToWorkerMessage & { type: 'creategenerat
|
|
|
994
998
|
};
|
|
995
999
|
self.addEventListener('message', handler);
|
|
996
1000
|
|
|
997
|
-
// Send frame to main thread
|
|
998
|
-
|
|
1001
|
+
// Send frame to main thread (transfer AudioData)
|
|
1002
|
+
const msg = {
|
|
999
1003
|
type: 'writeframe',
|
|
1000
1004
|
idx,
|
|
1001
1005
|
frame,
|
|
1002
1006
|
uid: frameUid,
|
|
1003
|
-
}
|
|
1007
|
+
};
|
|
1008
|
+
self.postMessage(msg, { transfer: [frame] });
|
|
1004
1009
|
});
|
|
1005
1010
|
},
|
|
1006
1011
|
close: () => Promise.resolve(),
|
|
@@ -1016,6 +1021,7 @@ function handleCreateGenerator(msg: MainToWorkerMessage & { type: 'creategenerat
|
|
|
1016
1021
|
self.postMessage(message);
|
|
1017
1022
|
log(`Set up frame relay for track ${idx} (Safari audio)`);
|
|
1018
1023
|
}
|
|
1024
|
+
// @ts-ignore - MediaStreamTrackGenerator may not be in standard types
|
|
1019
1025
|
} else if (typeof MediaStreamTrackGenerator !== 'undefined') {
|
|
1020
1026
|
// Chrome/Edge: use MediaStreamTrackGenerator in worker
|
|
1021
1027
|
// @ts-ignore
|
|
@@ -1149,9 +1155,10 @@ function handleFrameStep(msg: MainToWorkerMessage & { type: 'framestep' }): void
|
|
|
1149
1155
|
|
|
1150
1156
|
if (direction > 0) {
|
|
1151
1157
|
// If we're stepping forward within history (after stepping back), use history
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1158
|
+
const cursor = pipeline.historyCursor;
|
|
1159
|
+
if (cursor !== null && cursor !== undefined && cursor < pipeline.frameHistory.length - 1) {
|
|
1160
|
+
pipeline.historyCursor = cursor + 1;
|
|
1161
|
+
const entry = pipeline.frameHistory[pipeline.historyCursor!];
|
|
1155
1162
|
const clone = entry ? cloneVideoFrame(entry.frame) : null;
|
|
1156
1163
|
if (!clone) {
|
|
1157
1164
|
log(`FrameStep forward: failed to clone frame`);
|
|
@@ -16,6 +16,8 @@ export interface CreateMessage {
|
|
|
16
16
|
track: TrackInfo;
|
|
17
17
|
opts: {
|
|
18
18
|
optimizeForLatency: boolean;
|
|
19
|
+
/** Payload format: 'avcc' (length-prefixed) or 'annexb' (start-code delimited) */
|
|
20
|
+
payloadFormat?: 'avcc' | 'annexb';
|
|
19
21
|
};
|
|
20
22
|
uid: number;
|
|
21
23
|
}
|
|
@@ -240,6 +242,8 @@ export interface PipelineState {
|
|
|
240
242
|
lastChunkBytes: string;
|
|
241
243
|
};
|
|
242
244
|
optimizeForLatency: boolean;
|
|
245
|
+
/** Payload format: 'avcc' (length-prefixed) or 'annexb' (start-code delimited) */
|
|
246
|
+
payloadFormat: 'avcc' | 'annexb';
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
// ============================================================================
|