@livepeer-frameworks/player-core 0.0.4 → 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 +14 -1
- 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
|
@@ -143,6 +143,8 @@ export interface CreatePipelineMessage {
|
|
|
143
143
|
track: TrackInfo;
|
|
144
144
|
opts: {
|
|
145
145
|
optimizeForLatency: boolean;
|
|
146
|
+
/** Payload format: 'avcc' (length-prefixed) or 'annexb' (start-code delimited) */
|
|
147
|
+
payloadFormat?: 'avcc' | 'annexb';
|
|
146
148
|
};
|
|
147
149
|
uid?: number;
|
|
148
150
|
}
|
|
@@ -181,9 +183,10 @@ export interface ClosePipelineMessage {
|
|
|
181
183
|
}
|
|
182
184
|
export interface FrameTimingMessage {
|
|
183
185
|
type: 'frametiming';
|
|
184
|
-
action: 'setSpeed' | 'reset';
|
|
186
|
+
action: 'setSpeed' | 'reset' | 'setPaused';
|
|
185
187
|
speed?: number;
|
|
186
188
|
tweak?: number;
|
|
189
|
+
paused?: boolean;
|
|
187
190
|
uid?: number;
|
|
188
191
|
}
|
|
189
192
|
export interface SeekWorkerMessage {
|
|
@@ -196,7 +199,19 @@ export interface DebuggingMessage {
|
|
|
196
199
|
value: boolean | 'verbose';
|
|
197
200
|
uid?: number;
|
|
198
201
|
}
|
|
199
|
-
export
|
|
202
|
+
export interface FrameStepMessage {
|
|
203
|
+
type: 'framestep';
|
|
204
|
+
direction: -1 | 1;
|
|
205
|
+
uid?: number;
|
|
206
|
+
}
|
|
207
|
+
export interface WriteFrameResponseMessage {
|
|
208
|
+
type: 'writeframe';
|
|
209
|
+
idx: number;
|
|
210
|
+
uid?: number;
|
|
211
|
+
status: 'ok' | 'error';
|
|
212
|
+
error?: string;
|
|
213
|
+
}
|
|
214
|
+
export type MainToWorkerMessage = CreatePipelineMessage | ConfigurePipelineMessage | ReceiveChunkMessage | SetWritableMessage | CreateGeneratorMessage | ClosePipelineMessage | FrameTimingMessage | SeekWorkerMessage | DebuggingMessage | FrameStepMessage | WriteFrameResponseMessage;
|
|
200
215
|
export interface AddTrackMessage {
|
|
201
216
|
type: 'addtrack';
|
|
202
217
|
idx: number;
|
|
@@ -228,6 +243,7 @@ export interface SendEventMessage {
|
|
|
228
243
|
type: 'sendevent';
|
|
229
244
|
kind: string;
|
|
230
245
|
message?: string;
|
|
246
|
+
time?: number;
|
|
231
247
|
idx?: number;
|
|
232
248
|
uid?: number;
|
|
233
249
|
}
|
|
@@ -243,7 +259,13 @@ export interface AckMessage {
|
|
|
243
259
|
status?: 'ok' | 'error';
|
|
244
260
|
error?: string;
|
|
245
261
|
}
|
|
246
|
-
export
|
|
262
|
+
export interface WriteFrameMessage {
|
|
263
|
+
type: 'writeframe';
|
|
264
|
+
idx: number;
|
|
265
|
+
frame: AudioData;
|
|
266
|
+
uid?: number;
|
|
267
|
+
}
|
|
268
|
+
export type WorkerToMainMessage = AddTrackMessage | RemoveTrackMessage | SetPlaybackRateMessage | ClosedMessage | LogMessage | SendEventMessage | StatsMessage | AckMessage | WriteFrameMessage;
|
|
247
269
|
export interface FrameTimingStats {
|
|
248
270
|
/** Timestamp when frame entered decoder (microseconds) */
|
|
249
271
|
in: number;
|
|
@@ -10,6 +10,8 @@ export interface CreateMessage {
|
|
|
10
10
|
track: TrackInfo;
|
|
11
11
|
opts: {
|
|
12
12
|
optimizeForLatency: boolean;
|
|
13
|
+
/** Payload format: 'avcc' (length-prefixed) or 'annexb' (start-code delimited) */
|
|
14
|
+
payloadFormat?: 'avcc' | 'annexb';
|
|
13
15
|
};
|
|
14
16
|
uid: number;
|
|
15
17
|
}
|
|
@@ -48,9 +50,10 @@ export interface CloseMessage {
|
|
|
48
50
|
}
|
|
49
51
|
export interface FrameTimingMessage {
|
|
50
52
|
type: 'frametiming';
|
|
51
|
-
action: 'setSpeed' | 'reset';
|
|
53
|
+
action: 'setSpeed' | 'reset' | 'setPaused';
|
|
52
54
|
speed?: number;
|
|
53
55
|
tweak?: number;
|
|
56
|
+
paused?: boolean;
|
|
54
57
|
uid: number;
|
|
55
58
|
}
|
|
56
59
|
export interface SeekMessage {
|
|
@@ -58,12 +61,17 @@ export interface SeekMessage {
|
|
|
58
61
|
seekTime: number;
|
|
59
62
|
uid: number;
|
|
60
63
|
}
|
|
64
|
+
export interface FrameStepMessage {
|
|
65
|
+
type: 'framestep';
|
|
66
|
+
direction: -1 | 1;
|
|
67
|
+
uid: number;
|
|
68
|
+
}
|
|
61
69
|
export interface DebuggingMessage {
|
|
62
70
|
type: 'debugging';
|
|
63
71
|
value: boolean | 'verbose';
|
|
64
72
|
uid: number;
|
|
65
73
|
}
|
|
66
|
-
export type MainToWorkerMessage = CreateMessage | ConfigureMessage | ReceiveMessage | SetWritableMessage | CreateGeneratorMessage | CloseMessage | FrameTimingMessage | SeekMessage | DebuggingMessage;
|
|
74
|
+
export type MainToWorkerMessage = CreateMessage | ConfigureMessage | ReceiveMessage | SetWritableMessage | CreateGeneratorMessage | CloseMessage | FrameTimingMessage | SeekMessage | FrameStepMessage | DebuggingMessage;
|
|
67
75
|
export interface AddTrackMessage {
|
|
68
76
|
type: 'addtrack';
|
|
69
77
|
idx: number;
|
|
@@ -97,6 +105,7 @@ export interface SendEventMessage {
|
|
|
97
105
|
type: 'sendevent';
|
|
98
106
|
kind: string;
|
|
99
107
|
message?: string;
|
|
108
|
+
time?: number;
|
|
100
109
|
idx?: number;
|
|
101
110
|
uid: number;
|
|
102
111
|
}
|
|
@@ -159,6 +168,13 @@ export interface PipelineState {
|
|
|
159
168
|
data: Uint8Array;
|
|
160
169
|
}>;
|
|
161
170
|
outputQueue: DecodedFrame[];
|
|
171
|
+
/** Recent video frames for backward/forward stepping (video only) */
|
|
172
|
+
frameHistory?: Array<{
|
|
173
|
+
frame: VideoFrame;
|
|
174
|
+
timestamp: number;
|
|
175
|
+
}>;
|
|
176
|
+
/** Cursor into frameHistory for step navigation */
|
|
177
|
+
historyCursor?: number | null;
|
|
162
178
|
stats: {
|
|
163
179
|
framesIn: number;
|
|
164
180
|
framesDecoded: number;
|
|
@@ -172,6 +188,8 @@ export interface PipelineState {
|
|
|
172
188
|
lastChunkBytes: string;
|
|
173
189
|
};
|
|
174
190
|
optimizeForLatency: boolean;
|
|
191
|
+
/** Payload format: 'avcc' (length-prefixed) or 'annexb' (start-code delimited) */
|
|
192
|
+
payloadFormat: 'avcc' | 'annexb';
|
|
175
193
|
}
|
|
176
194
|
export interface ScheduleResult {
|
|
177
195
|
/** Whether frame should be output now */
|
package/dist/types/types.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface PlayerOptions {
|
|
|
50
50
|
hlsConfig?: HlsJsConfig;
|
|
51
51
|
/** DASH.js configuration override (merged with defaults) */
|
|
52
52
|
dashConfig?: DashJsConfig;
|
|
53
|
+
/** Video.js VHS configuration override (merged with defaults) */
|
|
54
|
+
vhsConfig?: VhsConfig;
|
|
53
55
|
/** WebRTC configuration (ICE servers, etc.) */
|
|
54
56
|
rtcConfig?: RTCConfiguration;
|
|
55
57
|
/** String to append to all request URLs (auth tokens, tracking params) */
|
|
@@ -98,6 +100,21 @@ export interface DashJsConfig {
|
|
|
98
100
|
};
|
|
99
101
|
[key: string]: unknown;
|
|
100
102
|
}
|
|
103
|
+
/** Video.js VHS (http-streaming) configuration subset */
|
|
104
|
+
export interface VhsConfig {
|
|
105
|
+
/** Start with lowest quality for faster initial playback */
|
|
106
|
+
enableLowInitialPlaylist?: boolean;
|
|
107
|
+
/** Initial bandwidth estimate in bits per second (e.g., 5_000_000 for 5 Mbps) */
|
|
108
|
+
bandwidth?: number;
|
|
109
|
+
/** Persist bandwidth estimate in localStorage across sessions */
|
|
110
|
+
useBandwidthFromLocalStorage?: boolean;
|
|
111
|
+
/** Enable partial segment appends for lower latency */
|
|
112
|
+
handlePartialData?: boolean;
|
|
113
|
+
/** Time delta for live range safety calculations (seconds) */
|
|
114
|
+
liveRangeSafeTimeDelta?: number;
|
|
115
|
+
/** Pass-through for other VHS options */
|
|
116
|
+
[key: string]: unknown;
|
|
117
|
+
}
|
|
101
118
|
export type StreamProtocol = 'WHEP' | 'HLS' | 'DASH' | 'MP4' | 'WEBM' | 'RTMP' | 'MIST_HTML';
|
|
102
119
|
export interface OutputCapabilities {
|
|
103
120
|
supportsSeek: boolean;
|
|
@@ -138,7 +155,7 @@ export interface ContentMetadata {
|
|
|
138
155
|
durationSeconds?: number;
|
|
139
156
|
thumbnailUrl?: string;
|
|
140
157
|
createdAt?: string;
|
|
141
|
-
status?: 'AVAILABLE' | 'PROCESSING' | 'ERROR' | 'OFFLINE';
|
|
158
|
+
status?: 'AVAILABLE' | 'PROCESSING' | 'ERROR' | 'OFFLINE' | 'ONLINE' | 'INITIALIZING' | 'BOOTING' | 'WAITING_FOR_DATA' | 'SHUTTING_DOWN' | 'INVALID';
|
|
142
159
|
viewers?: number;
|
|
143
160
|
isLive?: boolean;
|
|
144
161
|
recordingSizeBytes?: number;
|
|
@@ -147,6 +164,19 @@ export interface ContentMetadata {
|
|
|
147
164
|
dvrStatus?: 'recording' | 'completed';
|
|
148
165
|
/** Native container format: mp4, m3u8, webm, etc. */
|
|
149
166
|
format?: string;
|
|
167
|
+
/** MistServer authoritative snapshot (merged into this metadata) */
|
|
168
|
+
mist?: MistStreamInfo;
|
|
169
|
+
/** Parsed track summary (derived from Mist metadata when available) */
|
|
170
|
+
tracks?: Array<{
|
|
171
|
+
type: 'video' | 'audio' | 'meta';
|
|
172
|
+
codec?: string;
|
|
173
|
+
width?: number;
|
|
174
|
+
height?: number;
|
|
175
|
+
bitrate?: number;
|
|
176
|
+
fps?: number;
|
|
177
|
+
channels?: number;
|
|
178
|
+
sampleRate?: number;
|
|
179
|
+
}>;
|
|
150
180
|
}
|
|
151
181
|
export interface ContentEndpoints {
|
|
152
182
|
primary: EndpointInfo;
|
|
@@ -360,4 +390,5 @@ export interface PlayerMetadata {
|
|
|
360
390
|
channels?: number;
|
|
361
391
|
sampleRate?: number;
|
|
362
392
|
}>;
|
|
393
|
+
mist?: MistStreamInfo;
|
|
363
394
|
}
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```typescript
|
|
9
|
-
* import { FrameWorksPlayer } from '@livepeer-frameworks/player/vanilla';
|
|
10
|
-
* import '@livepeer-frameworks/player/player.css';
|
|
9
|
+
* import { FrameWorksPlayer } from '@livepeer-frameworks/player-core/vanilla';
|
|
10
|
+
* import '@livepeer-frameworks/player-core/player.css';
|
|
11
11
|
*
|
|
12
12
|
* const player = new FrameWorksPlayer('#player', {
|
|
13
|
-
* contentId: '
|
|
13
|
+
* contentId: 'pk_...',
|
|
14
14
|
* contentType: 'live',
|
|
15
15
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
16
16
|
* onStateChange: (state) => console.log('State:', state),
|
|
@@ -30,7 +30,7 @@ export interface FrameWorksPlayerOptions {
|
|
|
30
30
|
/** Content identifier (stream name) */
|
|
31
31
|
contentId: string;
|
|
32
32
|
/** Content type */
|
|
33
|
-
contentType
|
|
33
|
+
contentType?: ContentType;
|
|
34
34
|
/** Pre-resolved endpoints (skip gateway) */
|
|
35
35
|
endpoints?: ContentEndpoints;
|
|
36
36
|
/** Gateway URL (required if endpoints not provided) */
|
|
@@ -57,7 +57,7 @@ export interface FrameWorksPlayerOptions {
|
|
|
57
57
|
}
|
|
58
58
|
interface LegacyConfig {
|
|
59
59
|
contentId: string;
|
|
60
|
-
contentType
|
|
60
|
+
contentType?: ContentType;
|
|
61
61
|
thumbnailUrl?: string | null;
|
|
62
62
|
options?: {
|
|
63
63
|
gatewayUrl?: string;
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @example
|
|
5
5
|
* ```typescript
|
|
6
|
-
* import { FrameWorksPlayer } from '@livepeer-frameworks/player/vanilla';
|
|
7
|
-
* import '@livepeer-frameworks/player/player.css';
|
|
6
|
+
* import { FrameWorksPlayer } from '@livepeer-frameworks/player-core/vanilla';
|
|
7
|
+
* import '@livepeer-frameworks/player-core/player.css';
|
|
8
8
|
*
|
|
9
9
|
* const player = new FrameWorksPlayer('#player', {
|
|
10
|
-
* contentId: '
|
|
10
|
+
* contentId: 'pk_...',
|
|
11
11
|
* contentType: 'live',
|
|
12
12
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
13
13
|
* });
|
|
@@ -57,6 +57,65 @@
|
|
|
57
57
|
trackBaseTimes.clear();
|
|
58
58
|
log(`Reset all track baseTimes`);
|
|
59
59
|
}
|
|
60
|
+
function cloneVideoFrame(frame) {
|
|
61
|
+
try {
|
|
62
|
+
if ('clone' in frame) {
|
|
63
|
+
return frame.clone();
|
|
64
|
+
}
|
|
65
|
+
return new VideoFrame(frame);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function pushFrameHistory(pipeline, frame, timestamp) {
|
|
72
|
+
if (pipeline.track.type !== 'video')
|
|
73
|
+
return;
|
|
74
|
+
if (!pipeline.frameHistory)
|
|
75
|
+
pipeline.frameHistory = [];
|
|
76
|
+
const cloned = cloneVideoFrame(frame);
|
|
77
|
+
if (!cloned)
|
|
78
|
+
return;
|
|
79
|
+
pipeline.frameHistory.push({ frame: cloned, timestamp });
|
|
80
|
+
// Trim history
|
|
81
|
+
while (pipeline.frameHistory.length > MAX_FRAME_HISTORY) {
|
|
82
|
+
const entry = pipeline.frameHistory.shift();
|
|
83
|
+
if (entry) {
|
|
84
|
+
try {
|
|
85
|
+
entry.frame.close();
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
pipeline.historyCursor = pipeline.frameHistory.length - 1;
|
|
91
|
+
}
|
|
92
|
+
function alignHistoryCursorToLastOutput(pipeline) {
|
|
93
|
+
if (!pipeline.frameHistory || pipeline.frameHistory.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
const lastTs = pipeline.stats.lastOutputTimestamp;
|
|
96
|
+
if (!Number.isFinite(lastTs)) {
|
|
97
|
+
pipeline.historyCursor = pipeline.frameHistory.length - 1;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Find first history entry greater than last output, then step back one
|
|
101
|
+
const idx = pipeline.frameHistory.findIndex(entry => entry.timestamp > lastTs);
|
|
102
|
+
if (idx === -1) {
|
|
103
|
+
pipeline.historyCursor = pipeline.frameHistory.length - 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
pipeline.historyCursor = Math.max(0, idx - 1);
|
|
107
|
+
}
|
|
108
|
+
function getPrimaryVideoPipeline() {
|
|
109
|
+
let selected = null;
|
|
110
|
+
for (const pipeline of pipelines.values()) {
|
|
111
|
+
if (pipeline.track.type === 'video') {
|
|
112
|
+
if (!selected || pipeline.idx < selected.idx) {
|
|
113
|
+
selected = pipeline;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return selected;
|
|
118
|
+
}
|
|
60
119
|
// Stats update interval
|
|
61
120
|
let statsTimer = null;
|
|
62
121
|
const STATS_INTERVAL_MS = 250;
|
|
@@ -64,6 +123,9 @@
|
|
|
64
123
|
// Per Chrome WebCodecs best practices: drop when decodeQueueSize > 2
|
|
65
124
|
// This ensures decoder doesn't fall too far behind before corrective action
|
|
66
125
|
const MAX_DECODER_QUEUE_SIZE = 2;
|
|
126
|
+
const MAX_FRAME_HISTORY = 60;
|
|
127
|
+
const MAX_PAUSED_OUTPUT_QUEUE = 120;
|
|
128
|
+
const MAX_PAUSED_INPUT_QUEUE = 600;
|
|
67
129
|
// ============================================================================
|
|
68
130
|
// Logging
|
|
69
131
|
// ============================================================================
|
|
@@ -113,6 +175,9 @@
|
|
|
113
175
|
case 'seek':
|
|
114
176
|
handleSeek(msg);
|
|
115
177
|
break;
|
|
178
|
+
case 'framestep':
|
|
179
|
+
handleFrameStep(msg);
|
|
180
|
+
break;
|
|
116
181
|
case 'debugging':
|
|
117
182
|
debugging = msg.value;
|
|
118
183
|
log(`Debugging set to: ${msg.value}`);
|
|
@@ -137,6 +202,8 @@
|
|
|
137
202
|
writer: null,
|
|
138
203
|
inputQueue: [],
|
|
139
204
|
outputQueue: [],
|
|
205
|
+
frameHistory: track.type === 'video' ? [] : undefined,
|
|
206
|
+
historyCursor: track.type === 'video' ? null : undefined,
|
|
140
207
|
stats: {
|
|
141
208
|
framesIn: 0,
|
|
142
209
|
framesDecoded: 0,
|
|
@@ -151,6 +218,7 @@
|
|
|
151
218
|
lastChunkBytes: '',
|
|
152
219
|
},
|
|
153
220
|
optimizeForLatency: opts.optimizeForLatency,
|
|
221
|
+
payloadFormat: opts.payloadFormat || 'avcc',
|
|
154
222
|
};
|
|
155
223
|
pipelines.set(idx, pipeline);
|
|
156
224
|
// Start stats reporting if not already running
|
|
@@ -230,7 +298,11 @@
|
|
|
230
298
|
hardwareAcceleration: 'prefer-hardware',
|
|
231
299
|
};
|
|
232
300
|
// Pass description directly from WebSocket INIT data (per reference rawws.js line 1052)
|
|
233
|
-
|
|
301
|
+
// For Annex B format (ws/video/h264), SPS/PPS comes inline in the bitstream - skip description
|
|
302
|
+
if (pipeline.payloadFormat === 'annexb') {
|
|
303
|
+
log(`Annex B mode - SPS/PPS inline in bitstream, no description needed`);
|
|
304
|
+
}
|
|
305
|
+
else if (description && description.byteLength > 0) {
|
|
234
306
|
config.description = description;
|
|
235
307
|
log(`Configuring with description (${description.byteLength} bytes)`);
|
|
236
308
|
}
|
|
@@ -374,7 +446,7 @@
|
|
|
374
446
|
// Frame Input/Output
|
|
375
447
|
// ============================================================================
|
|
376
448
|
function handleReceive(msg) {
|
|
377
|
-
const { idx, chunk
|
|
449
|
+
const { idx, chunk } = msg;
|
|
378
450
|
const pipeline = pipelines.get(idx);
|
|
379
451
|
if (!pipeline) {
|
|
380
452
|
logVerbose(`Received chunk for unknown pipeline ${idx}`);
|
|
@@ -386,6 +458,15 @@
|
|
|
386
458
|
logVerbose(`Queued chunk for track ${idx} (configured=${pipeline.configured}, decoder=${!!pipeline.decoder})`);
|
|
387
459
|
return;
|
|
388
460
|
}
|
|
461
|
+
// If paused and output queue is saturated, queue input to preserve per-frame stepping
|
|
462
|
+
if (frameTiming.paused && pipeline.outputQueue.length >= MAX_PAUSED_OUTPUT_QUEUE) {
|
|
463
|
+
pipeline.inputQueue.push(chunk);
|
|
464
|
+
if (pipeline.inputQueue.length > MAX_PAUSED_INPUT_QUEUE) {
|
|
465
|
+
pipeline.inputQueue.splice(0, pipeline.inputQueue.length - MAX_PAUSED_INPUT_QUEUE);
|
|
466
|
+
logVerbose(`Trimmed paused input queue for track ${idx} to ${MAX_PAUSED_INPUT_QUEUE}`);
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
389
470
|
// Log only first 3 chunks per track to confirm receiving
|
|
390
471
|
if (pipeline.stats.framesIn < 3) {
|
|
391
472
|
log(`Received chunk ${pipeline.stats.framesIn} for track ${idx}: type=${chunk.type}, ts=${chunk.timestamp / 1000}ms, size=${chunk.data.byteLength}`);
|
|
@@ -410,6 +491,8 @@
|
|
|
410
491
|
* Based on Chrome WebCodecs best practices: drop when decodeQueueSize > 2
|
|
411
492
|
*/
|
|
412
493
|
function shouldDropFramesDueToDecoderPressure(pipeline) {
|
|
494
|
+
if (frameTiming.paused)
|
|
495
|
+
return false;
|
|
413
496
|
if (!pipeline.decoder)
|
|
414
497
|
return false;
|
|
415
498
|
const queueSize = pipeline.decoder.decodeQueueSize;
|
|
@@ -515,6 +598,9 @@
|
|
|
515
598
|
}
|
|
516
599
|
}
|
|
517
600
|
function processOutputQueue(pipeline) {
|
|
601
|
+
if (frameTiming.paused) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
518
604
|
// Check if pipeline is closed (e.g., player destroyed) - clean up queued frames
|
|
519
605
|
if (pipeline.closed) {
|
|
520
606
|
while (pipeline.outputQueue.length > 0) {
|
|
@@ -618,7 +704,7 @@
|
|
|
618
704
|
// Schedule check for when frame should be ready
|
|
619
705
|
return { shouldOutput: false, earliness: -delay, checkDelayMs: Math.max(1, Math.floor(delay)) };
|
|
620
706
|
}
|
|
621
|
-
function outputFrame(pipeline, entry) {
|
|
707
|
+
function outputFrame(pipeline, entry, options) {
|
|
622
708
|
if (!pipeline.writer || pipeline.closed) {
|
|
623
709
|
entry.frame.close();
|
|
624
710
|
return;
|
|
@@ -631,6 +717,10 @@
|
|
|
631
717
|
if (pipeline.stats.framesOut <= 3) {
|
|
632
718
|
log(`Output frame ${pipeline.stats.framesOut} for track ${pipeline.idx}: ts=${entry.timestamp}μs`);
|
|
633
719
|
}
|
|
720
|
+
// Store history for frame stepping (video only)
|
|
721
|
+
if (pipeline.track.type === 'video' && !(options?.skipHistory)) {
|
|
722
|
+
pushFrameHistory(pipeline, entry.frame, entry.timestamp);
|
|
723
|
+
}
|
|
634
724
|
// Write returns a Promise - handle rejection to avoid unhandled promise errors
|
|
635
725
|
// Frame ownership is transferred to the stream, so we don't need to close() on success
|
|
636
726
|
pipeline.writer.write(entry.frame).then(() => {
|
|
@@ -639,6 +729,7 @@
|
|
|
639
729
|
type: 'sendevent',
|
|
640
730
|
kind: 'timeupdate',
|
|
641
731
|
idx: pipeline.idx,
|
|
732
|
+
time: entry.timestamp / 1e6,
|
|
642
733
|
uid: uidCounter++,
|
|
643
734
|
};
|
|
644
735
|
self.postMessage(message);
|
|
@@ -742,13 +833,14 @@
|
|
|
742
833
|
}
|
|
743
834
|
};
|
|
744
835
|
self.addEventListener('message', handler);
|
|
745
|
-
// Send frame to main thread
|
|
746
|
-
|
|
836
|
+
// Send frame to main thread (transfer AudioData)
|
|
837
|
+
const msg = {
|
|
747
838
|
type: 'writeframe',
|
|
748
839
|
idx,
|
|
749
840
|
frame,
|
|
750
841
|
uid: frameUid,
|
|
751
|
-
}
|
|
842
|
+
};
|
|
843
|
+
self.postMessage(msg, { transfer: [frame] });
|
|
752
844
|
});
|
|
753
845
|
},
|
|
754
846
|
close: () => Promise.resolve(),
|
|
@@ -763,6 +855,7 @@
|
|
|
763
855
|
self.postMessage(message);
|
|
764
856
|
log(`Set up frame relay for track ${idx} (Safari audio)`);
|
|
765
857
|
}
|
|
858
|
+
// @ts-ignore - MediaStreamTrackGenerator may not be in standard types
|
|
766
859
|
}
|
|
767
860
|
else if (typeof MediaStreamTrackGenerator !== 'undefined') {
|
|
768
861
|
// Chrome/Edge: use MediaStreamTrackGenerator in worker
|
|
@@ -832,12 +925,96 @@
|
|
|
832
925
|
frameTiming.speed.combined = frameTiming.speed.main * frameTiming.speed.tweak;
|
|
833
926
|
log(`Speed set to ${frameTiming.speed.combined} (main: ${frameTiming.speed.main}, tweak: ${frameTiming.speed.tweak})`);
|
|
834
927
|
}
|
|
928
|
+
else if (action === 'setPaused') {
|
|
929
|
+
frameTiming.paused = msg.paused === true;
|
|
930
|
+
log(`Frame timing paused=${frameTiming.paused}`);
|
|
931
|
+
}
|
|
835
932
|
else if (action === 'reset') {
|
|
836
933
|
frameTiming.seeking = false;
|
|
837
934
|
log('Frame timing reset (seek complete)');
|
|
838
935
|
}
|
|
839
936
|
sendAck(uid);
|
|
840
937
|
}
|
|
938
|
+
function handleFrameStep(msg) {
|
|
939
|
+
const { direction, uid } = msg;
|
|
940
|
+
log(`FrameStep request dir=${direction} paused=${frameTiming.paused}`);
|
|
941
|
+
if (!frameTiming.paused) {
|
|
942
|
+
log(`FrameStep ignored (not paused)`);
|
|
943
|
+
sendAck(uid);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const pipeline = getPrimaryVideoPipeline();
|
|
947
|
+
if (!pipeline || !pipeline.writer || pipeline.closed) {
|
|
948
|
+
log(`FrameStep ignored (pipeline missing or closed)`);
|
|
949
|
+
sendAck(uid);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
pipeline.frameHistory = pipeline.frameHistory ?? [];
|
|
953
|
+
if (pipeline.historyCursor === null || pipeline.historyCursor === undefined) {
|
|
954
|
+
alignHistoryCursorToLastOutput(pipeline);
|
|
955
|
+
}
|
|
956
|
+
log(`FrameStep pipeline idx=${pipeline.idx} outQueue=${pipeline.outputQueue.length} history=${pipeline.frameHistory.length} cursor=${pipeline.historyCursor}`);
|
|
957
|
+
if (direction < 0) {
|
|
958
|
+
const nextIndex = (pipeline.historyCursor ?? 0) - 1;
|
|
959
|
+
if (nextIndex < 0 || pipeline.frameHistory.length === 0) {
|
|
960
|
+
log(`FrameStep back: no history`);
|
|
961
|
+
sendAck(uid);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
pipeline.historyCursor = nextIndex;
|
|
965
|
+
const entry = pipeline.frameHistory[nextIndex];
|
|
966
|
+
const clone = entry ? cloneVideoFrame(entry.frame) : null;
|
|
967
|
+
if (!clone) {
|
|
968
|
+
log(`FrameStep back: failed to clone frame`);
|
|
969
|
+
sendAck(uid);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
log(`FrameStep back: output ts=${entry.timestamp}`);
|
|
973
|
+
outputFrame(pipeline, { frame: clone, timestamp: entry.timestamp, decodedAt: performance.now() }, { skipHistory: true });
|
|
974
|
+
sendAck(uid);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (direction > 0) {
|
|
978
|
+
// If we're stepping forward within history (after stepping back), use history
|
|
979
|
+
const cursor = pipeline.historyCursor;
|
|
980
|
+
if (cursor !== null && cursor !== undefined && cursor < pipeline.frameHistory.length - 1) {
|
|
981
|
+
pipeline.historyCursor = cursor + 1;
|
|
982
|
+
const entry = pipeline.frameHistory[pipeline.historyCursor];
|
|
983
|
+
const clone = entry ? cloneVideoFrame(entry.frame) : null;
|
|
984
|
+
if (!clone) {
|
|
985
|
+
log(`FrameStep forward: failed to clone frame`);
|
|
986
|
+
sendAck(uid);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
log(`FrameStep forward (history): output ts=${entry.timestamp}`);
|
|
990
|
+
outputFrame(pipeline, { frame: clone, timestamp: entry.timestamp, decodedAt: performance.now() }, { skipHistory: true });
|
|
991
|
+
sendAck(uid);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
// Otherwise, output the next queued frame
|
|
995
|
+
if (pipeline.outputQueue.length > 1) {
|
|
996
|
+
const wasSorted = pipeline.outputQueue.every((entry, i, arr) => i === 0 || arr[i - 1].timestamp <= entry.timestamp);
|
|
997
|
+
if (!wasSorted) {
|
|
998
|
+
pipeline.outputQueue.sort((a, b) => a.timestamp - b.timestamp);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const lastTs = pipeline.stats.lastOutputTimestamp;
|
|
1002
|
+
let idx = pipeline.outputQueue.findIndex(e => e.timestamp > lastTs);
|
|
1003
|
+
if (idx === -1 && pipeline.outputQueue.length > 0)
|
|
1004
|
+
idx = 0;
|
|
1005
|
+
if (idx === -1) {
|
|
1006
|
+
log(`FrameStep forward: no queued frame available`);
|
|
1007
|
+
sendAck(uid);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const entry = pipeline.outputQueue.splice(idx, 1)[0];
|
|
1011
|
+
log(`FrameStep forward (queue): output ts=${entry.timestamp}`);
|
|
1012
|
+
outputFrame(pipeline, entry);
|
|
1013
|
+
sendAck(uid);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
sendAck(uid);
|
|
1017
|
+
}
|
|
841
1018
|
// ============================================================================
|
|
842
1019
|
// Cleanup
|
|
843
1020
|
// ============================================================================
|