@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
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { TypedEventEmitter } from './EventEmitter';
|
|
12
|
-
import { GatewayClient
|
|
12
|
+
import { GatewayClient } from './GatewayClient';
|
|
13
13
|
import { StreamStateClient } from './StreamStateClient';
|
|
14
14
|
import { PlayerManager } from './PlayerManager';
|
|
15
15
|
import { globalPlayerManager, ensurePlayersRegistered } from './PlayerRegistry';
|
|
16
16
|
import { ABRController } from './ABRController';
|
|
17
|
-
import { InteractionController
|
|
17
|
+
import { InteractionController } from './InteractionController';
|
|
18
18
|
import { MistReporter } from './MistReporter';
|
|
19
19
|
import { QualityMonitor } from './QualityMonitor';
|
|
20
20
|
import { MetaTrackManager } from './MetaTrackManager';
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
canSeekStream,
|
|
26
26
|
isMediaStreamSource,
|
|
27
27
|
supportsPlaybackRate,
|
|
28
|
-
isLiveContent,
|
|
29
28
|
getLatencyTier,
|
|
30
29
|
type LatencyTier,
|
|
31
30
|
type LiveThresholds,
|
|
@@ -35,6 +34,7 @@ import type {
|
|
|
35
34
|
ContentEndpoints,
|
|
36
35
|
ContentMetadata,
|
|
37
36
|
EndpointInfo,
|
|
37
|
+
MistStreamInfo,
|
|
38
38
|
OutputEndpoint,
|
|
39
39
|
OutputCapabilities,
|
|
40
40
|
PlayerState,
|
|
@@ -51,7 +51,7 @@ export interface PlayerControllerConfig {
|
|
|
51
51
|
/** Content identifier (stream name) */
|
|
52
52
|
contentId: string;
|
|
53
53
|
/** Content type */
|
|
54
|
-
contentType
|
|
54
|
+
contentType?: ContentType;
|
|
55
55
|
|
|
56
56
|
/** Pre-resolved endpoints (skip gateway) */
|
|
57
57
|
endpoints?: ContentEndpoints;
|
|
@@ -182,6 +182,10 @@ export interface PlayerControllerEvents {
|
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Content Type Resolution Helpers
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
185
189
|
// ============================================================================
|
|
186
190
|
// MistServer Source Type Mapping
|
|
187
191
|
// ============================================================================
|
|
@@ -206,6 +210,8 @@ export const MIST_SOURCE_TYPES: Record<string, { hrn: string; player: string; su
|
|
|
206
210
|
// ===== WEBSOCKET STREAMING =====
|
|
207
211
|
'ws/video/mp4': { hrn: 'MP4 WebSocket', player: 'mews', supported: true },
|
|
208
212
|
'wss/video/mp4': { hrn: 'MP4 WebSocket (SSL)', player: 'mews', supported: true },
|
|
213
|
+
'ws/video/webm': { hrn: 'WebM WebSocket', player: 'mews', supported: true },
|
|
214
|
+
'wss/video/webm': { hrn: 'WebM WebSocket (SSL)', player: 'mews', supported: true },
|
|
209
215
|
'ws/video/raw': { hrn: 'Raw WebSocket', player: 'webcodecs', supported: true },
|
|
210
216
|
'wss/video/raw': { hrn: 'Raw WebSocket (SSL)', player: 'webcodecs', supported: true },
|
|
211
217
|
'ws/video/h264': { hrn: 'Annex B WebSocket', player: 'webcodecs', supported: true },
|
|
@@ -214,6 +220,7 @@ export const MIST_SOURCE_TYPES: Record<string, { hrn: string; player: string; su
|
|
|
214
220
|
// ===== WEBRTC =====
|
|
215
221
|
'whep': { hrn: 'WebRTC (WHEP)', player: 'native', supported: true },
|
|
216
222
|
'webrtc': { hrn: 'WebRTC (WebSocket)', player: 'mist-webrtc', supported: true },
|
|
223
|
+
'mist/webrtc': { hrn: 'MistServer WebRTC', player: 'mist-webrtc', supported: true },
|
|
217
224
|
|
|
218
225
|
// ===== AUDIO ONLY =====
|
|
219
226
|
'html5/audio/aac': { hrn: 'AAC progressive', player: 'native', supported: true },
|
|
@@ -259,13 +266,18 @@ export const PROTOCOL_TO_MIME: Record<string, string> = {
|
|
|
259
266
|
'WEBM': 'html5/video/webm',
|
|
260
267
|
'WHEP': 'whep',
|
|
261
268
|
'WebRTC': 'webrtc',
|
|
269
|
+
'MIST_WEBRTC': 'mist/webrtc', // MistServer native WebRTC signaling
|
|
262
270
|
|
|
263
271
|
// WebSocket variants
|
|
264
272
|
'MEWS': 'ws/video/mp4',
|
|
265
273
|
'MEWS_WS': 'ws/video/mp4',
|
|
266
274
|
'MEWS_WSS': 'wss/video/mp4',
|
|
275
|
+
'MEWS_WEBM': 'ws/video/webm',
|
|
276
|
+
'MEWS_WEBM_SSL': 'wss/video/webm',
|
|
267
277
|
'RAW_WS': 'ws/video/raw',
|
|
268
278
|
'RAW_WSS': 'wss/video/raw',
|
|
279
|
+
'H264_WS': 'ws/video/h264',
|
|
280
|
+
'H264_WSS': 'wss/video/h264',
|
|
269
281
|
|
|
270
282
|
// Audio
|
|
271
283
|
'AAC': 'html5/audio/aac',
|
|
@@ -355,7 +367,7 @@ export function buildStreamInfoFromEndpoints(
|
|
|
355
367
|
if (typeof primary.outputs === 'string') {
|
|
356
368
|
try {
|
|
357
369
|
outputs = JSON.parse(primary.outputs);
|
|
358
|
-
} catch
|
|
370
|
+
} catch {
|
|
359
371
|
console.warn('[buildStreamInfoFromEndpoints] Failed to parse outputs JSON');
|
|
360
372
|
outputs = {};
|
|
361
373
|
}
|
|
@@ -367,7 +379,6 @@ export function buildStreamInfoFromEndpoints(
|
|
|
367
379
|
const sources: StreamSource[] = [];
|
|
368
380
|
const oKeys = Object.keys(outputs);
|
|
369
381
|
|
|
370
|
-
// Helper to attach MistServer sources
|
|
371
382
|
const attachMistSource = (html?: string, playerJs?: string) => {
|
|
372
383
|
if (!html && !playerJs) return;
|
|
373
384
|
const src: StreamSource = {
|
|
@@ -473,7 +484,7 @@ export function buildStreamInfoFromEndpoints(
|
|
|
473
484
|
* @example
|
|
474
485
|
* ```typescript
|
|
475
486
|
* const controller = new PlayerController({
|
|
476
|
-
* contentId: '
|
|
487
|
+
* contentId: 'pk_...', // playbackId (view key)
|
|
477
488
|
* contentType: 'live',
|
|
478
489
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
479
490
|
* });
|
|
@@ -493,7 +504,6 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
493
504
|
private state: PlayerState = 'booting';
|
|
494
505
|
private lastEmittedState: PlayerState | null = null;
|
|
495
506
|
private suppressPlayPauseEventsUntil = 0;
|
|
496
|
-
private suppressPlayPauseEventsUntil = 0;
|
|
497
507
|
|
|
498
508
|
private gatewayClient: GatewayClient | null = null;
|
|
499
509
|
private streamStateClient: StreamStateClient | null = null;
|
|
@@ -508,6 +518,10 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
508
518
|
private streamState: StreamState | null = null;
|
|
509
519
|
/** Tracks parsed from MistServer JSON response (used for direct MistServer mode) */
|
|
510
520
|
private mistTracks: StreamTrack[] | null = null;
|
|
521
|
+
/** Gateway-seeded metadata (used as base for Mist enrichment) */
|
|
522
|
+
private metadataSeed: ContentMetadata | null = null;
|
|
523
|
+
/** Merged metadata (gateway seed + Mist enrichment) */
|
|
524
|
+
private metadata: ContentMetadata | null = null;
|
|
511
525
|
|
|
512
526
|
private cleanupFns: Array<() => void> = [];
|
|
513
527
|
private isDestroyed: boolean = false;
|
|
@@ -698,6 +712,8 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
698
712
|
this.endpoints = null;
|
|
699
713
|
this.streamInfo = null;
|
|
700
714
|
this.streamState = null;
|
|
715
|
+
this.metadataSeed = null;
|
|
716
|
+
this.metadata = null;
|
|
701
717
|
this.videoElement = null;
|
|
702
718
|
this.currentPlayer = null;
|
|
703
719
|
this.lastEmittedState = null;
|
|
@@ -738,7 +754,130 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
738
754
|
|
|
739
755
|
/** Get content metadata (title, description, duration, etc.) */
|
|
740
756
|
getMetadata(): ContentMetadata | null {
|
|
741
|
-
return this.
|
|
757
|
+
return this.metadata ?? null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ============================================================================
|
|
761
|
+
// Metadata Merge (Gateway seed + Mist enrichment)
|
|
762
|
+
// ============================================================================
|
|
763
|
+
|
|
764
|
+
private setMetadataSeed(seed: ContentMetadata | null | undefined): void {
|
|
765
|
+
this.metadataSeed = seed ? { ...seed } : null;
|
|
766
|
+
this.refreshMergedMetadata();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private refreshMergedMetadata(): void {
|
|
770
|
+
const seed = this.metadataSeed ? { ...this.metadataSeed } : null;
|
|
771
|
+
const mist = this.streamState?.streamInfo;
|
|
772
|
+
const streamStatus = this.streamState?.status;
|
|
773
|
+
|
|
774
|
+
if (!seed && !mist) {
|
|
775
|
+
this.metadata = null;
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const merged: ContentMetadata = seed ? { ...seed } : {};
|
|
780
|
+
|
|
781
|
+
if (mist) {
|
|
782
|
+
merged.mist = this.sanitizeMistInfo(mist);
|
|
783
|
+
if (mist.type) {
|
|
784
|
+
merged.contentType = mist.type;
|
|
785
|
+
merged.isLive = mist.type === 'live';
|
|
786
|
+
}
|
|
787
|
+
if (streamStatus) {
|
|
788
|
+
merged.status = streamStatus;
|
|
789
|
+
}
|
|
790
|
+
if (mist.meta?.duration && (!merged.durationSeconds || merged.durationSeconds <= 0)) {
|
|
791
|
+
merged.durationSeconds = Math.round(mist.meta.duration);
|
|
792
|
+
}
|
|
793
|
+
if (mist.meta?.tracks) {
|
|
794
|
+
merged.tracks = this.buildMetadataTracks(mist.meta.tracks);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
this.metadata = merged;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private buildMetadataTracks(tracksObj: Record<string, unknown>): ContentMetadata['tracks'] {
|
|
802
|
+
const tracks: NonNullable<ContentMetadata['tracks']> = [];
|
|
803
|
+
for (const [, trackData] of Object.entries(tracksObj)) {
|
|
804
|
+
const t = trackData as Record<string, unknown>;
|
|
805
|
+
const trackType = t.type as string;
|
|
806
|
+
if (trackType !== 'video' && trackType !== 'audio' && trackType !== 'meta') {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const bitrate = typeof t.bps === 'number' ? Math.round(t.bps) : undefined;
|
|
811
|
+
const fps = typeof t.fpks === 'number' ? t.fpks / 1000 : undefined;
|
|
812
|
+
|
|
813
|
+
tracks.push({
|
|
814
|
+
type: trackType,
|
|
815
|
+
codec: t.codec as string | undefined,
|
|
816
|
+
width: t.width as number | undefined,
|
|
817
|
+
height: t.height as number | undefined,
|
|
818
|
+
bitrate,
|
|
819
|
+
fps,
|
|
820
|
+
channels: t.channels as number | undefined,
|
|
821
|
+
sampleRate: t.rate as number | undefined,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
return tracks.length ? tracks : undefined;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private sanitizeMistInfo(info: MistStreamInfo): MistStreamInfo {
|
|
828
|
+
const sanitized: MistStreamInfo = {
|
|
829
|
+
error: info.error,
|
|
830
|
+
on_error: info.on_error,
|
|
831
|
+
perc: info.perc,
|
|
832
|
+
type: info.type,
|
|
833
|
+
hasVideo: info.hasVideo,
|
|
834
|
+
hasAudio: info.hasAudio,
|
|
835
|
+
unixoffset: info.unixoffset,
|
|
836
|
+
lastms: info.lastms,
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
if (info.source) {
|
|
840
|
+
sanitized.source = info.source.map((src) => ({
|
|
841
|
+
url: src.url,
|
|
842
|
+
type: src.type,
|
|
843
|
+
priority: src.priority,
|
|
844
|
+
simul_tracks: src.simul_tracks,
|
|
845
|
+
relurl: src.relurl,
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (info.meta) {
|
|
850
|
+
sanitized.meta = {
|
|
851
|
+
buffer_window: info.meta.buffer_window,
|
|
852
|
+
duration: info.meta.duration,
|
|
853
|
+
mistUrl: info.meta.mistUrl,
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
if (info.meta.tracks) {
|
|
857
|
+
const tracks: Record<string, any> = {};
|
|
858
|
+
for (const [key, track] of Object.entries(info.meta.tracks)) {
|
|
859
|
+
tracks[key] = {
|
|
860
|
+
type: track.type,
|
|
861
|
+
codec: track.codec,
|
|
862
|
+
width: track.width,
|
|
863
|
+
height: track.height,
|
|
864
|
+
bps: track.bps,
|
|
865
|
+
fpks: track.fpks,
|
|
866
|
+
codecstring: track.codecstring,
|
|
867
|
+
firstms: track.firstms,
|
|
868
|
+
lastms: track.lastms,
|
|
869
|
+
lang: track.lang,
|
|
870
|
+
idx: track.idx,
|
|
871
|
+
channels: track.channels,
|
|
872
|
+
rate: track.rate,
|
|
873
|
+
size: track.size,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
sanitized.meta.tracks = tracks;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return sanitized;
|
|
742
881
|
}
|
|
743
882
|
|
|
744
883
|
/** Get stream info (sources + tracks for player selection) */
|
|
@@ -905,6 +1044,29 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
905
1044
|
return this._supportsPlaybackRate;
|
|
906
1045
|
}
|
|
907
1046
|
|
|
1047
|
+
/** Resolve content type from config override or Gateway metadata */
|
|
1048
|
+
private getResolvedContentType(): ContentType | null {
|
|
1049
|
+
if (this.config.contentType) {
|
|
1050
|
+
return this.config.contentType;
|
|
1051
|
+
}
|
|
1052
|
+
const metadata = this.getMetadata();
|
|
1053
|
+
const metaType = metadata?.contentType?.toLowerCase();
|
|
1054
|
+
if (metaType === 'live' || metaType === 'clip' || metaType === 'dvr' || metaType === 'vod') {
|
|
1055
|
+
return metaType as ContentType;
|
|
1056
|
+
}
|
|
1057
|
+
const mistType = this.streamState?.streamInfo?.type;
|
|
1058
|
+
if (mistType === 'live' || mistType === 'vod') {
|
|
1059
|
+
return mistType;
|
|
1060
|
+
}
|
|
1061
|
+
if (metadata?.isLive === true) {
|
|
1062
|
+
return 'live';
|
|
1063
|
+
}
|
|
1064
|
+
if (metadata?.isLive === false) {
|
|
1065
|
+
return 'vod';
|
|
1066
|
+
}
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
908
1070
|
/** Check if source is WebRTC/MediaStream */
|
|
909
1071
|
isWebRTCSource(): boolean {
|
|
910
1072
|
return this._isWebRTC;
|
|
@@ -918,14 +1080,33 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
918
1080
|
|
|
919
1081
|
/** Check if content is effectively live (live or DVR still recording) */
|
|
920
1082
|
isEffectivelyLive(): boolean {
|
|
921
|
-
const
|
|
1083
|
+
const contentType = this.getResolvedContentType() ?? 'live';
|
|
922
1084
|
const metadata = this.getMetadata();
|
|
923
|
-
|
|
1085
|
+
|
|
1086
|
+
// Explicit VOD content types are never live
|
|
1087
|
+
if (contentType === 'vod' || contentType === 'clip') {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// If Gateway metadata says it's not live, trust it
|
|
1092
|
+
if (metadata?.isLive === false) {
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// DVR that's finished recording is not live
|
|
1097
|
+
if (contentType === 'dvr' && metadata?.dvrStatus === 'completed') {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Default: trust contentType or duration-based detection
|
|
1102
|
+
return contentType === 'live' ||
|
|
1103
|
+
(contentType === 'dvr' && metadata?.dvrStatus === 'recording') ||
|
|
1104
|
+
!Number.isFinite(this.getDuration());
|
|
924
1105
|
}
|
|
925
1106
|
|
|
926
1107
|
/** Check if content is strictly live (not DVR/clip/vod) */
|
|
927
1108
|
isLive(): boolean {
|
|
928
|
-
return this.
|
|
1109
|
+
return (this.getResolvedContentType() ?? 'live') === 'live';
|
|
929
1110
|
}
|
|
930
1111
|
|
|
931
1112
|
/**
|
|
@@ -935,7 +1116,9 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
935
1116
|
* DVR-while-recording needs cold start because MistServer may not be serving the VOD yet
|
|
936
1117
|
*/
|
|
937
1118
|
needsColdStart(): boolean {
|
|
938
|
-
|
|
1119
|
+
const contentType = this.getResolvedContentType();
|
|
1120
|
+
if (!contentType) return true;
|
|
1121
|
+
return contentType !== 'live';
|
|
939
1122
|
}
|
|
940
1123
|
|
|
941
1124
|
/**
|
|
@@ -1509,7 +1692,6 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
1509
1692
|
|
|
1510
1693
|
// Check if values changed
|
|
1511
1694
|
const seekableChanged = this._seekableStart !== seekableStart || this._liveEdge !== liveEdge;
|
|
1512
|
-
const canSeekChanged = this._canSeek !== this._canSeek; // Already updated above
|
|
1513
1695
|
|
|
1514
1696
|
this._seekableStart = seekableStart;
|
|
1515
1697
|
this._liveEdge = liveEdge;
|
|
@@ -1933,11 +2115,12 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
1933
2115
|
// ============================================================================
|
|
1934
2116
|
|
|
1935
2117
|
private async resolveEndpoints(): Promise<void> {
|
|
1936
|
-
const { endpoints, gatewayUrl, mistUrl,
|
|
2118
|
+
const { endpoints, gatewayUrl, mistUrl, contentId, authToken } = this.config;
|
|
1937
2119
|
|
|
1938
2120
|
// Priority 1: Use pre-resolved endpoints if provided
|
|
1939
2121
|
if (endpoints?.primary) {
|
|
1940
2122
|
this.endpoints = endpoints;
|
|
2123
|
+
this.setMetadataSeed(endpoints.metadata ?? null);
|
|
1941
2124
|
this.setState('gateway_ready', { gatewayStatus: 'ready' });
|
|
1942
2125
|
return;
|
|
1943
2126
|
}
|
|
@@ -1950,7 +2133,7 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
1950
2133
|
|
|
1951
2134
|
// Priority 3: Gateway resolution
|
|
1952
2135
|
if (gatewayUrl) {
|
|
1953
|
-
await this.resolveFromGateway(gatewayUrl,
|
|
2136
|
+
await this.resolveFromGateway(gatewayUrl, contentId, authToken);
|
|
1954
2137
|
return;
|
|
1955
2138
|
}
|
|
1956
2139
|
|
|
@@ -1973,7 +2156,13 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
1973
2156
|
throw new Error(`MistServer HTTP ${response.status}`);
|
|
1974
2157
|
}
|
|
1975
2158
|
|
|
1976
|
-
|
|
2159
|
+
// MistServer can return JSONP: callback({...}); - strip wrapper if present
|
|
2160
|
+
let text = await response.text();
|
|
2161
|
+
const jsonpMatch = text.match(/^[^(]+\(([\s\S]*)\);?$/);
|
|
2162
|
+
if (jsonpMatch) {
|
|
2163
|
+
text = jsonpMatch[1];
|
|
2164
|
+
}
|
|
2165
|
+
const data = JSON.parse(text);
|
|
1977
2166
|
|
|
1978
2167
|
if (data.error) {
|
|
1979
2168
|
throw new Error(data.error);
|
|
@@ -2008,6 +2197,7 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2008
2197
|
};
|
|
2009
2198
|
|
|
2010
2199
|
this.endpoints = { primary, fallbacks: [] };
|
|
2200
|
+
this.setMetadataSeed(null);
|
|
2011
2201
|
|
|
2012
2202
|
// Parse track metadata from MistServer response
|
|
2013
2203
|
if (data.meta?.tracks && typeof data.meta.tracks === 'object') {
|
|
@@ -2031,11 +2221,17 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2031
2221
|
*/
|
|
2032
2222
|
private mapMistTypeToProtocol(mistType: string): string {
|
|
2033
2223
|
// WebCodecs raw streams - check BEFORE generic ws/ catch-all
|
|
2034
|
-
// MistServer rawws.js uses 'ws/video/raw', mews.js uses 'ws/video/mp4' and 'ws/video/webm'
|
|
2035
2224
|
if (mistType === 'ws/video/raw') return 'RAW_WS';
|
|
2036
2225
|
if (mistType === 'wss/video/raw') return 'RAW_WSS';
|
|
2037
|
-
//
|
|
2038
|
-
if (mistType
|
|
2226
|
+
// Annex B H264 over WebSocket (video-only, uses same 12-byte header as raw)
|
|
2227
|
+
if (mistType === 'ws/video/h264') return 'H264_WS';
|
|
2228
|
+
if (mistType === 'wss/video/h264') return 'H264_WSS';
|
|
2229
|
+
// WebM over WebSocket - check BEFORE generic ws/ catch-all
|
|
2230
|
+
if (mistType === 'ws/video/webm') return 'MEWS_WEBM';
|
|
2231
|
+
if (mistType === 'wss/video/webm') return 'MEWS_WEBM_SSL';
|
|
2232
|
+
// MEWS MP4 over WebSocket - catch remaining ws/* types (defaults to mp4)
|
|
2233
|
+
if (mistType.startsWith('ws/')) return 'MEWS_WS';
|
|
2234
|
+
if (mistType.startsWith('wss/')) return 'MEWS_WSS';
|
|
2039
2235
|
if (mistType.includes('webrtc')) return 'MIST_WEBRTC';
|
|
2040
2236
|
if (mistType.includes('mpegurl') || mistType.includes('m3u8')) return 'HLS';
|
|
2041
2237
|
if (mistType.includes('dash') || mistType.includes('mpd')) return 'DASH';
|
|
@@ -2064,7 +2260,6 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2064
2260
|
*/
|
|
2065
2261
|
private async resolveFromGateway(
|
|
2066
2262
|
gatewayUrl: string,
|
|
2067
|
-
contentType: ContentType,
|
|
2068
2263
|
contentId: string,
|
|
2069
2264
|
authToken?: string
|
|
2070
2265
|
): Promise<void> {
|
|
@@ -2072,7 +2267,6 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2072
2267
|
|
|
2073
2268
|
this.gatewayClient = new GatewayClient({
|
|
2074
2269
|
gatewayUrl,
|
|
2075
|
-
contentType,
|
|
2076
2270
|
contentId,
|
|
2077
2271
|
authToken,
|
|
2078
2272
|
});
|
|
@@ -2088,6 +2282,7 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2088
2282
|
|
|
2089
2283
|
try {
|
|
2090
2284
|
this.endpoints = await this.gatewayClient.resolve();
|
|
2285
|
+
this.setMetadataSeed(this.endpoints?.metadata ?? null);
|
|
2091
2286
|
this.setState('gateway_ready', { gatewayStatus: 'ready' });
|
|
2092
2287
|
} catch (error) {
|
|
2093
2288
|
const message = error instanceof Error ? error.message : 'Gateway resolution failed';
|
|
@@ -2097,10 +2292,17 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2097
2292
|
}
|
|
2098
2293
|
|
|
2099
2294
|
private startStreamStatePolling(): void {
|
|
2100
|
-
const {
|
|
2295
|
+
const { contentId, mistUrl } = this.config;
|
|
2296
|
+
const contentType = this.getResolvedContentType();
|
|
2101
2297
|
|
|
2102
2298
|
// Only poll for live-like content. DVR should only poll while recording.
|
|
2103
|
-
|
|
2299
|
+
// If contentType is unknown but mistUrl is provided, still poll so we can
|
|
2300
|
+
// detect when a stream comes online and initialize playback.
|
|
2301
|
+
if (contentType == null) {
|
|
2302
|
+
if (!mistUrl) return;
|
|
2303
|
+
} else if (contentType !== 'live' && contentType !== 'dvr') {
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2104
2306
|
if (contentType === 'dvr') {
|
|
2105
2307
|
const dvrStatus = this.getMetadata()?.dvrStatus;
|
|
2106
2308
|
if (dvrStatus && dvrStatus !== 'recording') return;
|
|
@@ -2145,6 +2347,9 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2145
2347
|
}
|
|
2146
2348
|
}
|
|
2147
2349
|
|
|
2350
|
+
// Merge Mist metadata into the unified metadata surface
|
|
2351
|
+
this.refreshMergedMetadata();
|
|
2352
|
+
|
|
2148
2353
|
this.emit('streamStateChange', { state });
|
|
2149
2354
|
|
|
2150
2355
|
// Auto-play when stream transitions from offline to online
|
|
@@ -2213,6 +2418,7 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2213
2418
|
};
|
|
2214
2419
|
|
|
2215
2420
|
this.endpoints = { primary, fallbacks: [] };
|
|
2421
|
+
this.setMetadataSeed(this.endpoints.metadata ?? null);
|
|
2216
2422
|
|
|
2217
2423
|
// Parse track metadata from stream info
|
|
2218
2424
|
if (streamInfo.meta?.tracks && typeof streamInfo.meta.tracks === 'object') {
|
|
@@ -2373,7 +2579,7 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2373
2579
|
this.initializeSubControllers();
|
|
2374
2580
|
this.emit('ready', { videoElement: el });
|
|
2375
2581
|
},
|
|
2376
|
-
onTimeUpdate: (
|
|
2582
|
+
onTimeUpdate: (_t) => {
|
|
2377
2583
|
if (this.isDestroyed) return;
|
|
2378
2584
|
// Defensive: keep video element attached even if some other lifecycle cleared the container.
|
|
2379
2585
|
// (Playback can continue even when detached, which looks like "audio only".)
|
|
@@ -2663,7 +2869,7 @@ export class PlayerController extends TypedEventEmitter<PlayerControllerEvents>
|
|
|
2663
2869
|
if (playerName === 'webcodecs') {
|
|
2664
2870
|
this.suppressPlayPauseEvents(250);
|
|
2665
2871
|
}
|
|
2666
|
-
if (hasFrameStep && player) {
|
|
2872
|
+
if (hasFrameStep && player && player.frameStep) {
|
|
2667
2873
|
player.frameStep(direction, seconds);
|
|
2668
2874
|
return true;
|
|
2669
2875
|
}
|
|
@@ -61,6 +61,12 @@ export interface PlayerOptions {
|
|
|
61
61
|
onPlaying?: () => void;
|
|
62
62
|
onCanPlay?: () => void;
|
|
63
63
|
onDurationChange?: (duration: number) => void;
|
|
64
|
+
/** HLS.js configuration override (merged with defaults) */
|
|
65
|
+
hlsConfig?: Record<string, unknown>;
|
|
66
|
+
/** DASH.js configuration override (merged with defaults) */
|
|
67
|
+
dashConfig?: Record<string, unknown>;
|
|
68
|
+
/** Video.js VHS configuration override (merged with defaults) */
|
|
69
|
+
vhsConfig?: Record<string, unknown>;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
export interface PlayerCapability {
|
|
@@ -349,6 +349,14 @@ export class PlayerManager {
|
|
|
349
349
|
selectionSources.forEach((s, idx) => selectionIndexBySource.set(s, idx));
|
|
350
350
|
const totalSources = selectionSources.length;
|
|
351
351
|
|
|
352
|
+
const requiredTracks: Array<'video' | 'audio'> = [];
|
|
353
|
+
if (streamInfo.meta.tracks.some((t) => t.type === 'video')) {
|
|
354
|
+
requiredTracks.push('video');
|
|
355
|
+
}
|
|
356
|
+
if (streamInfo.meta.tracks.some((t) => t.type === 'audio')) {
|
|
357
|
+
requiredTracks.push('audio');
|
|
358
|
+
}
|
|
359
|
+
|
|
352
360
|
// Track seen player+sourceType pairs to avoid duplicates
|
|
353
361
|
const seenPairs = new Set<string>();
|
|
354
362
|
|
|
@@ -439,6 +447,47 @@ export class PlayerManager {
|
|
|
439
447
|
continue;
|
|
440
448
|
}
|
|
441
449
|
|
|
450
|
+
if (Array.isArray(tracktypes) && requiredTracks.length > 0) {
|
|
451
|
+
const missing = requiredTracks.filter((t) => !tracktypes.includes(t));
|
|
452
|
+
if (missing.length > 0) {
|
|
453
|
+
const priorityScore =
|
|
454
|
+
1 - player.capability.priority / Math.max(maxPriority, 1);
|
|
455
|
+
const sourceScore =
|
|
456
|
+
1 - sourceListIndex / Math.max(totalSources - 1, 1);
|
|
457
|
+
const playerScore = scorePlayer(
|
|
458
|
+
tracktypes,
|
|
459
|
+
player.capability.priority,
|
|
460
|
+
sourceListIndex,
|
|
461
|
+
{
|
|
462
|
+
maxPriority,
|
|
463
|
+
totalSources,
|
|
464
|
+
playerShortname: player.capability.shortname,
|
|
465
|
+
mimeType: source.type,
|
|
466
|
+
playbackMode: effectiveMode,
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
combinations.push({
|
|
471
|
+
player: player.capability.shortname,
|
|
472
|
+
playerName: player.capability.name,
|
|
473
|
+
source,
|
|
474
|
+
sourceIndex,
|
|
475
|
+
sourceType: source.type,
|
|
476
|
+
score: playerScore.total,
|
|
477
|
+
compatible: false,
|
|
478
|
+
incompatibleReason: `Missing required tracks: ${missing.join(', ')}`,
|
|
479
|
+
scoreBreakdown: {
|
|
480
|
+
trackScore: 0,
|
|
481
|
+
trackTypes: tracktypes,
|
|
482
|
+
priorityScore,
|
|
483
|
+
sourceScore,
|
|
484
|
+
weights: { tracks: 0.5, priority: 0.1, source: 0.05 },
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
442
491
|
// Compatible - calculate full score
|
|
443
492
|
const trackScore = Array.isArray(tracktypes)
|
|
444
493
|
? tracktypes.reduce(
|
|
@@ -114,7 +114,7 @@ function getStatusMessage(status: StreamStatus, percentage?: number): string {
|
|
|
114
114
|
* ```typescript
|
|
115
115
|
* const client = new StreamStateClient({
|
|
116
116
|
* mistBaseUrl: 'https://mist.example.com',
|
|
117
|
-
* streamName: '
|
|
117
|
+
* streamName: 'pk_...', // playbackId (view key)
|
|
118
118
|
* });
|
|
119
119
|
*
|
|
120
120
|
* client.on('stateChange', ({ state }) => console.log('State:', state));
|
|
@@ -291,7 +291,7 @@ export class StreamStateClient extends TypedEventEmitter<StreamStateClientEvents
|
|
|
291
291
|
.replace(/^https:/, 'wss:')
|
|
292
292
|
.replace(/\/$/, '');
|
|
293
293
|
|
|
294
|
-
const ws = new WebSocket(`${wsUrl}/json_${encodeURIComponent(streamName)}.js`);
|
|
294
|
+
const ws = new WebSocket(`${wsUrl}/json_${encodeURIComponent(streamName)}.js?metaeverywhere=1&inclzero=1`);
|
|
295
295
|
this.ws = ws;
|
|
296
296
|
|
|
297
297
|
ws.onopen = () => {
|
|
@@ -337,7 +337,7 @@ export class StreamStateClient extends TypedEventEmitter<StreamStateClientEvents
|
|
|
337
337
|
const { mistBaseUrl, streamName, pollInterval } = this.config;
|
|
338
338
|
|
|
339
339
|
try {
|
|
340
|
-
const url = `${mistBaseUrl.replace(/\/$/, '')}/json_${encodeURIComponent(streamName)}.js`;
|
|
340
|
+
const url = `${mistBaseUrl.replace(/\/$/, '')}/json_${encodeURIComponent(streamName)}.js?metaeverywhere=1&inclzero=1`;
|
|
341
341
|
const response = await fetch(url, {
|
|
342
342
|
method: 'GET',
|
|
343
343
|
headers: { 'Accept': 'application/json' },
|
package/src/core/TimerManager.ts
CHANGED
|
@@ -140,7 +140,7 @@ export class TimerManager {
|
|
|
140
140
|
stopAll(): void {
|
|
141
141
|
const count = this.timers.size;
|
|
142
142
|
|
|
143
|
-
for (const [
|
|
143
|
+
for (const [_internalId, entry] of this.timers) {
|
|
144
144
|
if (entry.isInterval) {
|
|
145
145
|
clearInterval(entry.id);
|
|
146
146
|
} else {
|
package/src/core/scorer.ts
CHANGED
|
@@ -182,6 +182,9 @@ export const PROTOCOL_PENALTIES: Record<string, number> = {
|
|
|
182
182
|
// MEWS - heavy penalty, prefer HLS/WebRTC (reference mews.js has issues)
|
|
183
183
|
'ws/video/mp4': 0.50,
|
|
184
184
|
'wss/video/mp4': 0.50,
|
|
185
|
+
// Native Mist WebRTC signaling - treat like MEWS (legacy/less stable than WHEP)
|
|
186
|
+
'webrtc': 0.50,
|
|
187
|
+
'mist/webrtc': 0.50,
|
|
185
188
|
// DASH - heavy penalty, broken implementation
|
|
186
189
|
'dash/video/mp4': 0.90, // Below legacy
|
|
187
190
|
'dash/video/webm': 0.95,
|
|
@@ -258,8 +261,8 @@ export const MODE_PROTOCOL_BONUSES: Record<PlaybackMode, Record<string, number>>
|
|
|
258
261
|
'wss/video/h264': 0.52,
|
|
259
262
|
// WHEP/WebRTC: sub-second latency
|
|
260
263
|
'whep': 0.50,
|
|
261
|
-
'webrtc': 0.
|
|
262
|
-
'mist/webrtc': 0.
|
|
264
|
+
'webrtc': 0.25,
|
|
265
|
+
'mist/webrtc': 0.25,
|
|
263
266
|
// MP4/WS (MEWS): 2-5s latency, good fallback
|
|
264
267
|
'ws/video/mp4': 0.30,
|
|
265
268
|
'wss/video/mp4': 0.30,
|
|
@@ -283,6 +286,7 @@ export const MODE_PROTOCOL_BONUSES: Record<PlaybackMode, Record<string, number>>
|
|
|
283
286
|
// WebRTC: minimal for quality mode
|
|
284
287
|
'whep': 0.05,
|
|
285
288
|
'webrtc': 0.05,
|
|
289
|
+
'mist/webrtc': 0.05,
|
|
286
290
|
},
|
|
287
291
|
'vod': {
|
|
288
292
|
// VOD/Clip: Prefer seekable protocols, EXCLUDE WebRTC (no seek support)
|
|
@@ -306,8 +310,8 @@ export const MODE_PROTOCOL_BONUSES: Record<PlaybackMode, Record<string, number>>
|
|
|
306
310
|
'html5/video/mp4': 0.42,
|
|
307
311
|
// WHEP/WebRTC: good for low latency
|
|
308
312
|
'whep': 0.38,
|
|
309
|
-
'webrtc': 0.
|
|
310
|
-
'mist/webrtc': 0.
|
|
313
|
+
'webrtc': 0.20,
|
|
314
|
+
'mist/webrtc': 0.20,
|
|
311
315
|
// MP4/WS (MEWS): lower latency than HLS
|
|
312
316
|
'ws/video/mp4': 0.30,
|
|
313
317
|
'wss/video/mp4': 0.30,
|