@100mslive/hls-player 0.0.1-alpha.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/dist/controllers/HMSHLSPlayer.d.ts +101 -0
- package/dist/controllers/HMSHLSTimedMetadata.d.ts +23 -0
- package/dist/error/HMSHLSErrorFactory.d.ts +21 -0
- package/dist/error/HMSHLSException.d.ts +16 -0
- package/dist/index.cjs.js +8 -0
- package/dist/index.cjs.js.map +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +7 -0
- package/dist/interfaces/IHMSHLSPlayer.d.ts +43 -0
- package/dist/interfaces/ILevel.d.ts +9 -0
- package/dist/interfaces/events.d.ts +48 -0
- package/dist/utilies/constants.d.ts +28 -0
- package/dist/utilies/utils.d.ts +16 -0
- package/package.json +38 -0
- package/src/controllers/HMSHLSPlayer.ts +338 -0
- package/src/controllers/HMSHLSTimedMetadata.ts +118 -0
- package/src/error/HMSHLSErrorFactory.ts +88 -0
- package/src/error/HMSHLSException.ts +38 -0
- package/src/index.ts +7 -0
- package/src/interfaces/IHMSHLSPlayer.ts +46 -0
- package/src/interfaces/ILevel.ts +10 -0
- package/src/interfaces/events.ts +69 -0
- package/src/utilies/constants.ts +36 -0
- package/src/utilies/utils.ts +38 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { HlsPlayerStats, HlsStats } from '@100mslive/hls-stats';
|
|
2
|
+
import Hls, { ErrorData, HlsConfig, Level, LevelParsed } from 'hls.js';
|
|
3
|
+
import { HMSHLSTimedMetadata } from './HMSHLSTimedMetadata';
|
|
4
|
+
import { HMSHLSErrorFactory } from '../error/HMSHLSErrorFactory';
|
|
5
|
+
import { HMSHLSPlayerEventEmitter, HMSHLSPlayerListeners, IHMSHLSPlayerEventEmitter } from '../interfaces/events';
|
|
6
|
+
import IHMSHLSPlayer from '../interfaces/IHMSHLSPlayer';
|
|
7
|
+
import { ILevel } from '../interfaces/ILevel';
|
|
8
|
+
import { HLS_DEFAULT_ALLOWED_MAX_LATENCY_DELAY, HLSPlaybackState, HMSHLSPlayerEvents } from '../utilies/constants';
|
|
9
|
+
import { mapLevel, mapLevels } from '../utilies/utils';
|
|
10
|
+
|
|
11
|
+
export class HMSHLSPlayer implements IHMSHLSPlayer, IHMSHLSPlayerEventEmitter {
|
|
12
|
+
private _hls: Hls;
|
|
13
|
+
private _hlsUrl: string;
|
|
14
|
+
private _hlsStats: HlsStats;
|
|
15
|
+
private _videoEl: HTMLVideoElement;
|
|
16
|
+
private _emitter: HMSHLSPlayerEventEmitter;
|
|
17
|
+
private _subscribeHlsStats?: (() => void) | null = null;
|
|
18
|
+
private _isLive: boolean;
|
|
19
|
+
private _volume: number;
|
|
20
|
+
private _metaData: HMSHLSTimedMetadata;
|
|
21
|
+
private readonly TAG = '[HMSHLSPlayer]';
|
|
22
|
+
/**
|
|
23
|
+
* Initiliaze the player with hlsUrl and video element
|
|
24
|
+
* @remarks If video element is not passed, we will create one and call a method getVideoElement get element
|
|
25
|
+
* @param hlsUrl required - Pass hls url to
|
|
26
|
+
* @param videoEl optional field - HTML video element
|
|
27
|
+
*/
|
|
28
|
+
constructor(hlsUrl: string, videoEl?: HTMLVideoElement) {
|
|
29
|
+
this._hls = new Hls(this.getPlayerConfig());
|
|
30
|
+
this._emitter = new HMSHLSPlayerEventEmitter();
|
|
31
|
+
this._hlsUrl = hlsUrl;
|
|
32
|
+
this._videoEl = videoEl || this.createVideoElement();
|
|
33
|
+
if (!hlsUrl) {
|
|
34
|
+
throw HMSHLSErrorFactory.HLSMediaError.hlsURLNotFound();
|
|
35
|
+
} else if (!hlsUrl.endsWith('m3u8')) {
|
|
36
|
+
throw HMSHLSErrorFactory.HLSMediaError.hlsURLNotFound('Invalid URL, pass m3u8 url');
|
|
37
|
+
}
|
|
38
|
+
this._hls.loadSource(hlsUrl);
|
|
39
|
+
this._hls.attachMedia(this._videoEl);
|
|
40
|
+
this._isLive = true;
|
|
41
|
+
this._volume = this._videoEl.volume * 100;
|
|
42
|
+
this._hlsStats = new HlsStats(this._hls, this._videoEl);
|
|
43
|
+
this.listenHLSEvent();
|
|
44
|
+
this._metaData = new HMSHLSTimedMetadata(this._hls, this._videoEl, this.emit);
|
|
45
|
+
this.seekToLivePosition();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @remarks It will create a video element with playiniline true.
|
|
49
|
+
* @returns HTML video element
|
|
50
|
+
*/
|
|
51
|
+
private createVideoElement(): HTMLVideoElement {
|
|
52
|
+
if (this._videoEl) {
|
|
53
|
+
return this._videoEl;
|
|
54
|
+
}
|
|
55
|
+
const video: HTMLVideoElement = document.createElement('video');
|
|
56
|
+
video.playsInline = true;
|
|
57
|
+
video.controls = false;
|
|
58
|
+
video.autoplay = true;
|
|
59
|
+
return video;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @returns get html video element
|
|
63
|
+
*/
|
|
64
|
+
getVideoElement(): HTMLVideoElement {
|
|
65
|
+
return this._videoEl;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Subscribe to hls stats
|
|
69
|
+
*/
|
|
70
|
+
private subscribeStats = (interval = 2000) => {
|
|
71
|
+
this._subscribeHlsStats = this._hlsStats.subscribe((state: HlsPlayerStats) => {
|
|
72
|
+
this.emit(HMSHLSPlayerEvents.STATS, state);
|
|
73
|
+
}, interval);
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Unsubscribe to hls stats
|
|
77
|
+
*/
|
|
78
|
+
private unsubscribeStats = () => {
|
|
79
|
+
if (this._subscribeHlsStats) {
|
|
80
|
+
this._subscribeHlsStats();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
// reset the controller
|
|
84
|
+
reset() {
|
|
85
|
+
if (this._hls && this._hls.media) {
|
|
86
|
+
this._hls.detachMedia();
|
|
87
|
+
this.unsubscribeStats();
|
|
88
|
+
}
|
|
89
|
+
if (this._metaData) {
|
|
90
|
+
this._metaData.unregisterListener();
|
|
91
|
+
}
|
|
92
|
+
if (Hls.isSupported()) {
|
|
93
|
+
this._hls.off(Hls.Events.MANIFEST_LOADED, this.manifestLoadedHandler);
|
|
94
|
+
this._hls.off(Hls.Events.LEVEL_UPDATED, this.levelUpdatedHandler);
|
|
95
|
+
this._hls.off(Hls.Events.ERROR, this.handleHLSException);
|
|
96
|
+
}
|
|
97
|
+
if (this._videoEl) {
|
|
98
|
+
this._videoEl.removeEventListener('play', this.playEventHandler);
|
|
99
|
+
this._videoEl.removeEventListener('pause', this.pauseEventHandler);
|
|
100
|
+
this._videoEl.removeEventListener('timeupdate', this.handleTimeUpdateListener);
|
|
101
|
+
this._videoEl.removeEventListener('volumechange', this.volumeEventHandler);
|
|
102
|
+
}
|
|
103
|
+
this.removeAllListeners();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
on<E extends HMSHLSPlayerEvents>(eventName: E, listener: HMSHLSPlayerListeners<E>) {
|
|
107
|
+
this._emitter.on(eventName, listener);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
off<E extends HMSHLSPlayerEvents>(eventName: E, listener: HMSHLSPlayerListeners<E>) {
|
|
111
|
+
this._emitter.off(eventName, listener);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
emit<E extends HMSHLSPlayerEvents>(eventName: E, eventObject: Parameters<HMSHLSPlayerListeners<E>>[0]): boolean {
|
|
115
|
+
return this._emitter.emit(eventName, eventObject);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private removeAllListeners<E extends HMSHLSPlayerEvents>(eventName?: E): void {
|
|
119
|
+
this._emitter.removeAllListeners(eventName);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* get current video volume
|
|
123
|
+
*/
|
|
124
|
+
public get volume(): number {
|
|
125
|
+
return this._volume;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* set video volumne
|
|
129
|
+
* @param { volume } - define volume in range [1,100]
|
|
130
|
+
*/
|
|
131
|
+
setVolume(volume: number) {
|
|
132
|
+
this._videoEl.volume = volume / 100;
|
|
133
|
+
this._volume = volume;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
*
|
|
137
|
+
* @returns returns a ILevel which represents current
|
|
138
|
+
* quality level. -1 if currentlevel is set to "Auto"
|
|
139
|
+
*/
|
|
140
|
+
getCurrentLevel(): ILevel | null {
|
|
141
|
+
if (this._hls && this._hls.currentLevel !== -1) {
|
|
142
|
+
const currentLevel = this._hls?.levels.at(this._hls?.currentLevel);
|
|
143
|
+
return currentLevel ? mapLevel(currentLevel) : null;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
*
|
|
150
|
+
* @param { ILevel } currentLevel - currentLevel we want to
|
|
151
|
+
* set the stream to -1 for Auto
|
|
152
|
+
*/
|
|
153
|
+
setCurrentLevel(currentLevel: ILevel) {
|
|
154
|
+
if (this._hls) {
|
|
155
|
+
const current = this._hls.levels.findIndex((level: Level) => {
|
|
156
|
+
return level?.attrs?.RESOLUTION === currentLevel?.resolution;
|
|
157
|
+
});
|
|
158
|
+
this._hls.currentLevel = current;
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* set current stream to Live
|
|
164
|
+
*/
|
|
165
|
+
async seekToLivePosition() {
|
|
166
|
+
let end = 0;
|
|
167
|
+
if (this._videoEl?.buffered.length > 0) {
|
|
168
|
+
end = this._videoEl?.buffered.end(this._videoEl?.buffered.length - 1);
|
|
169
|
+
}
|
|
170
|
+
this._videoEl.currentTime = this._hls?.liveSyncPosition || end;
|
|
171
|
+
if (this._videoEl.paused) {
|
|
172
|
+
try {
|
|
173
|
+
await this.playVideo();
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(this.TAG, 'Attempt to jump to live position Failed.', err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Play stream
|
|
181
|
+
*/
|
|
182
|
+
play = async () => {
|
|
183
|
+
await this.playVideo();
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Pause stream
|
|
187
|
+
*/
|
|
188
|
+
pause = () => {
|
|
189
|
+
this.pauseVideo();
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* It will update the video element current time
|
|
193
|
+
* @param seekValue Pass currentTime in second
|
|
194
|
+
*/
|
|
195
|
+
seekTo = (seekValue: number) => {
|
|
196
|
+
this._videoEl.currentTime = seekValue;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
private playVideo = async () => {
|
|
200
|
+
try {
|
|
201
|
+
if (this._videoEl.paused) {
|
|
202
|
+
await this._videoEl.play();
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.debug(this.TAG, 'Play failed with error', (error as Error).message);
|
|
206
|
+
if ((error as Error).name === 'NotAllowedError') {
|
|
207
|
+
this.emit(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, HMSHLSErrorFactory.HLSMediaError.autoplayFailed());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
private pauseVideo = () => {
|
|
212
|
+
if (!this._videoEl.paused) {
|
|
213
|
+
this._videoEl.pause();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
private playEventHandler = () => {
|
|
217
|
+
this.emit(HMSHLSPlayerEvents.PLAYBACK_STATE, {
|
|
218
|
+
state: HLSPlaybackState.playing,
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
private pauseEventHandler = () => {
|
|
222
|
+
this.emit(HMSHLSPlayerEvents.PLAYBACK_STATE, {
|
|
223
|
+
state: HLSPlaybackState.paused,
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
private volumeEventHandler = () => {
|
|
227
|
+
this._volume = this._videoEl.volume;
|
|
228
|
+
};
|
|
229
|
+
// eslint-disable-next-line complexity
|
|
230
|
+
private handleHLSException = (_: any, data: ErrorData) => {
|
|
231
|
+
console.error(this.TAG, data);
|
|
232
|
+
const details = data.error?.message || data.err?.message || '';
|
|
233
|
+
const detail = {
|
|
234
|
+
details: details,
|
|
235
|
+
fatal: data.fatal,
|
|
236
|
+
};
|
|
237
|
+
switch (data.details) {
|
|
238
|
+
case Hls.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR: {
|
|
239
|
+
const error = HMSHLSErrorFactory.HLSMediaError.manifestIncompatibleCodecsError(detail);
|
|
240
|
+
this.emit(HMSHLSPlayerEvents.ERROR, error);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case Hls.ErrorDetails.FRAG_DECRYPT_ERROR: {
|
|
244
|
+
const error = HMSHLSErrorFactory.HLSMediaError.fragDecryptError(detail);
|
|
245
|
+
this.emit(HMSHLSPlayerEvents.ERROR, error);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case Hls.ErrorDetails.BUFFER_INCOMPATIBLE_CODECS_ERROR: {
|
|
249
|
+
const error = HMSHLSErrorFactory.HLSMediaError.bufferIncompatibleCodecsError(detail);
|
|
250
|
+
this.emit(HMSHLSPlayerEvents.ERROR, error);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
// Below one are network related errors
|
|
254
|
+
case Hls.ErrorDetails.MANIFEST_LOAD_ERROR: {
|
|
255
|
+
const error = HMSHLSErrorFactory.HLSNetworkError.manifestLoadError(detail);
|
|
256
|
+
this.emit(HMSHLSPlayerEvents.ERROR, error);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case Hls.ErrorDetails.MANIFEST_PARSING_ERROR: {
|
|
260
|
+
const error = HMSHLSErrorFactory.HLSNetworkError.manifestParsingError(detail);
|
|
261
|
+
this.emit(HMSHLSPlayerEvents.ERROR, error);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case Hls.ErrorDetails.LEVEL_LOAD_ERROR: {
|
|
265
|
+
const error = HMSHLSErrorFactory.HLSNetworkError.levelLoadError(detail);
|
|
266
|
+
this.emit(HMSHLSPlayerEvents.ERROR, error);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
default: {
|
|
270
|
+
const error = HMSHLSErrorFactory.UnknownError(detail);
|
|
271
|
+
this.emit(HMSHLSPlayerEvents.ERROR, error);
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
private manifestLoadedHandler = (_: any, { levels }: { levels: LevelParsed[] }) => {
|
|
277
|
+
const level: ILevel[] = mapLevels(this.removeAudioLevels(levels));
|
|
278
|
+
this.emit(HMSHLSPlayerEvents.MANIFEST_LOADED, {
|
|
279
|
+
levels: level,
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
private levelUpdatedHandler = (_: any, { level }: { level: number }) => {
|
|
283
|
+
const qualityLevel: ILevel = mapLevel(this._hls.levels[level]);
|
|
284
|
+
this.emit(HMSHLSPlayerEvents.LEVEL_UPDATED, {
|
|
285
|
+
level: qualityLevel,
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
private handleTimeUpdateListener = (_: Event) => {
|
|
290
|
+
if (!this._videoEl) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.emit(HMSHLSPlayerEvents.CURRENT_TIME, this._videoEl.currentTime);
|
|
294
|
+
const live = this._hls.liveSyncPosition
|
|
295
|
+
? this._hls.liveSyncPosition - this._videoEl.currentTime <= HLS_DEFAULT_ALLOWED_MAX_LATENCY_DELAY
|
|
296
|
+
: false;
|
|
297
|
+
if (this._isLive !== live) {
|
|
298
|
+
this._isLive = live;
|
|
299
|
+
this.emit(HMSHLSPlayerEvents.SEEK_POS_BEHIND_LIVE_EDGE, {
|
|
300
|
+
isLive: this._isLive,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Listen to hlsjs and video related events
|
|
306
|
+
*/
|
|
307
|
+
private listenHLSEvent() {
|
|
308
|
+
if (Hls.isSupported()) {
|
|
309
|
+
this._hls.on(Hls.Events.MANIFEST_LOADED, this.manifestLoadedHandler);
|
|
310
|
+
this._hls.on(Hls.Events.LEVEL_UPDATED, this.levelUpdatedHandler);
|
|
311
|
+
this._hls.on(Hls.Events.ERROR, this.handleHLSException);
|
|
312
|
+
this.subscribeStats();
|
|
313
|
+
} else if (this._videoEl.canPlayType('application/vnd.apple.mpegurl')) {
|
|
314
|
+
// code for ios safari, mseNot Supported.
|
|
315
|
+
this._videoEl.src = this._hlsUrl;
|
|
316
|
+
}
|
|
317
|
+
this._videoEl.addEventListener('timeupdate', this.handleTimeUpdateListener);
|
|
318
|
+
this._videoEl.addEventListener('play', this.playEventHandler);
|
|
319
|
+
this._videoEl.addEventListener('pause', this.pauseEventHandler);
|
|
320
|
+
this._videoEl.addEventListener('volumechange', this.volumeEventHandler);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private getPlayerConfig(): Partial<HlsConfig> {
|
|
324
|
+
return {
|
|
325
|
+
enableWorker: true,
|
|
326
|
+
maxBufferLength: 20,
|
|
327
|
+
backBufferLength: 10,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {Array} levels array from hlsJS
|
|
333
|
+
* @returns a new array with only video levels.
|
|
334
|
+
*/
|
|
335
|
+
private removeAudioLevels(levels: LevelParsed[]) {
|
|
336
|
+
return levels.filter(({ videoCodec, width, height }) => !!videoCodec || !!(width && height));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import Hls, { Fragment } from 'hls.js';
|
|
2
|
+
import { HMSHLSErrorFactory } from '../error/HMSHLSErrorFactory';
|
|
3
|
+
import { HMSHLSPlayerListeners } from '../interfaces/events';
|
|
4
|
+
import { HMSHLSPlayerEvents } from '../utilies/constants';
|
|
5
|
+
import { metadataPayloadParser } from '../utilies/utils';
|
|
6
|
+
|
|
7
|
+
export class HMSHLSTimedMetadata {
|
|
8
|
+
private hls: Hls;
|
|
9
|
+
constructor(
|
|
10
|
+
hls: Hls,
|
|
11
|
+
private videoEl: HTMLVideoElement,
|
|
12
|
+
private emit: <E extends HMSHLSPlayerEvents>(
|
|
13
|
+
eventName: E,
|
|
14
|
+
eventObject: Parameters<HMSHLSPlayerListeners<E>>[0],
|
|
15
|
+
) => boolean,
|
|
16
|
+
) {
|
|
17
|
+
this.hls = hls;
|
|
18
|
+
this.registerListner();
|
|
19
|
+
}
|
|
20
|
+
extractMetaTextTrack = (): TextTrack | null => {
|
|
21
|
+
const textTrackListCount = this.videoEl.textTracks.length || 0;
|
|
22
|
+
for (let trackIndex = 0; trackIndex < textTrackListCount; trackIndex++) {
|
|
23
|
+
const textTrack = this.videoEl.textTracks[trackIndex];
|
|
24
|
+
if (textTrack?.kind !== 'metadata') {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
textTrack.mode = 'showing';
|
|
28
|
+
return textTrack;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// sync time with cue and trigger event
|
|
34
|
+
fireCues = (currentAbsTime: number, tolerance: number) => {
|
|
35
|
+
const cues = this.extractMetaTextTrack()?.cues;
|
|
36
|
+
if (!cues) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const cuesLength = cues.length;
|
|
40
|
+
let cueIndex = 0;
|
|
41
|
+
while (cueIndex < cuesLength) {
|
|
42
|
+
const cue = cues[cueIndex] as TextTrackCue & {
|
|
43
|
+
queued: boolean;
|
|
44
|
+
value: { data: string };
|
|
45
|
+
};
|
|
46
|
+
if (cue.queued) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// here we are converting base64 to actual data.
|
|
50
|
+
const data: Record<string, any> = metadataPayloadParser(cue.value.data);
|
|
51
|
+
const startDate = data.start_date;
|
|
52
|
+
const endDate = data.end_date;
|
|
53
|
+
const timeDiff = new Date(startDate).getTime() - currentAbsTime;
|
|
54
|
+
const duration = new Date(endDate).getTime() - new Date(startDate).getTime();
|
|
55
|
+
if (timeDiff <= tolerance) {
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
this.emit(HMSHLSPlayerEvents.TIMED_METADATA_LOADED, {
|
|
58
|
+
id: cue?.id,
|
|
59
|
+
payload: data.payload,
|
|
60
|
+
duration: duration,
|
|
61
|
+
startDate: new Date(startDate),
|
|
62
|
+
endDate: new Date(endDate),
|
|
63
|
+
});
|
|
64
|
+
}, timeDiff);
|
|
65
|
+
cue.queued = true;
|
|
66
|
+
}
|
|
67
|
+
cueIndex++;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// handle time update listener
|
|
72
|
+
handleTimeUpdateListener = () => {
|
|
73
|
+
// extract timed metadata text track
|
|
74
|
+
const metaTextTrack: TextTrack | null = this.extractMetaTextTrack();
|
|
75
|
+
if (!metaTextTrack || !metaTextTrack.cues) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
const firstFragProgramDateTime = this.videoEl?.getStartDate() || 0;
|
|
80
|
+
const currentAbsTime = new Date(firstFragProgramDateTime).getTime() + (this.videoEl.currentTime || 0) * 1000;
|
|
81
|
+
// fire cue for timed meta data extract
|
|
82
|
+
this.fireCues(currentAbsTime, 0.25);
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Metadata are automatically parsed and added to the video element's
|
|
86
|
+
* textTrack cue by hlsjs as they come through the stream.
|
|
87
|
+
* in FRAG_CHANGED, we read the cues and emit HLS_METADATA_LOADED
|
|
88
|
+
* when the current fragment has a metadata to play.
|
|
89
|
+
*/
|
|
90
|
+
fragChangeHandler = (_: any, { frag }: { frag: Fragment }) => {
|
|
91
|
+
if (!this.videoEl) {
|
|
92
|
+
const error = HMSHLSErrorFactory.HLSMediaError.videoElementNotFound();
|
|
93
|
+
this.emit(HMSHLSPlayerEvents.ERROR, error);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
if (this.videoEl.textTracks.length === 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const fragStartTime = frag.programDateTime || 0;
|
|
100
|
+
const fragmentDuration = frag.end - frag.start;
|
|
101
|
+
this.fireCues(fragStartTime, fragmentDuration);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error('FRAG_CHANGED event error', e);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
private registerListner = () => {
|
|
107
|
+
if (Hls.isSupported()) {
|
|
108
|
+
this.hls.on(Hls.Events.FRAG_CHANGED, this.fragChangeHandler);
|
|
109
|
+
} else if (this.videoEl.canPlayType('application/vnd.apple.mpegurl')) {
|
|
110
|
+
this.videoEl.addEventListener('timeupdate', this.handleTimeUpdateListener);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
unregisterListener = () => {
|
|
115
|
+
this.hls.off(Hls.Events.FRAG_CHANGED, this.fragChangeHandler);
|
|
116
|
+
this.videoEl.removeEventListener('timeupdate', this.handleTimeUpdateListener);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { HMSHLSException } from './HMSHLSException';
|
|
2
|
+
import { HMSHLSExceptionEvents } from '../utilies/constants';
|
|
3
|
+
|
|
4
|
+
export type HMSHLSErrorDetails = {
|
|
5
|
+
details: string;
|
|
6
|
+
fatal?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export const HMSHLSErrorFactory = {
|
|
9
|
+
HLSNetworkError: {
|
|
10
|
+
manifestLoadError(data: HMSHLSErrorDetails): HMSHLSException {
|
|
11
|
+
return new HMSHLSException(
|
|
12
|
+
HMSHLSExceptionEvents.MANIFEST_LOAD_ERROR,
|
|
13
|
+
data.details,
|
|
14
|
+
'Unable to load manifest file',
|
|
15
|
+
data.fatal,
|
|
16
|
+
);
|
|
17
|
+
},
|
|
18
|
+
manifestParsingError(data: HMSHLSErrorDetails): HMSHLSException {
|
|
19
|
+
return new HMSHLSException(
|
|
20
|
+
HMSHLSExceptionEvents.MANIFEST_PARSING_ERROR,
|
|
21
|
+
data.details,
|
|
22
|
+
'Unable to parse manifest file',
|
|
23
|
+
data.fatal,
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
levelLoadError(data: HMSHLSErrorDetails): HMSHLSException {
|
|
27
|
+
return new HMSHLSException(
|
|
28
|
+
HMSHLSExceptionEvents.LEVEL_LOAD_ERROR,
|
|
29
|
+
data.details,
|
|
30
|
+
'Unable to load levels',
|
|
31
|
+
data.fatal,
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
HLSMediaError: {
|
|
36
|
+
manifestIncompatibleCodecsError(data: HMSHLSErrorDetails): HMSHLSException {
|
|
37
|
+
return new HMSHLSException(
|
|
38
|
+
HMSHLSExceptionEvents.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
|
|
39
|
+
data.details,
|
|
40
|
+
'Incompatible manifest codecs',
|
|
41
|
+
data.fatal,
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
fragDecryptError(data: HMSHLSErrorDetails): HMSHLSException {
|
|
45
|
+
return new HMSHLSException(
|
|
46
|
+
HMSHLSExceptionEvents.FRAG_DECRYPT_ERROR,
|
|
47
|
+
data.details,
|
|
48
|
+
'Unable to decrypt fragment',
|
|
49
|
+
data.fatal,
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
bufferIncompatibleCodecsError(data: HMSHLSErrorDetails): HMSHLSException {
|
|
53
|
+
return new HMSHLSException(
|
|
54
|
+
HMSHLSExceptionEvents.BUFFER_INCOMPATIBLE_CODECS_ERROR,
|
|
55
|
+
data.details,
|
|
56
|
+
'Incompatible buffer codecs',
|
|
57
|
+
data.fatal,
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
videoElementNotFound(): HMSHLSException {
|
|
61
|
+
return new HMSHLSException(
|
|
62
|
+
HMSHLSExceptionEvents.VIDEO_ELEMENT_NOT_FOUND,
|
|
63
|
+
'Video element not found',
|
|
64
|
+
'Video element not found',
|
|
65
|
+
false,
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
autoplayFailed(): HMSHLSException {
|
|
69
|
+
return new HMSHLSException(
|
|
70
|
+
HMSHLSExceptionEvents.HLS_AUTOPLAY_FAILED,
|
|
71
|
+
'Failed to autoplay',
|
|
72
|
+
'Failed to autoplay',
|
|
73
|
+
false,
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
hlsURLNotFound(msg?: string): HMSHLSException {
|
|
77
|
+
return new HMSHLSException(
|
|
78
|
+
HMSHLSExceptionEvents.HLS_URL_NOT_FOUND,
|
|
79
|
+
msg || 'hls url not found',
|
|
80
|
+
msg || 'hls url not found',
|
|
81
|
+
true,
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
UnknownError: (data: HMSHLSErrorDetails): HMSHLSException => {
|
|
86
|
+
return new HMSHLSException(HMSHLSExceptionEvents.UNKNOWN_ERROR, data.details, 'Unknown error', data.fatal);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class HMSHLSException extends Error {
|
|
2
|
+
nativeError?: Error;
|
|
3
|
+
|
|
4
|
+
constructor(
|
|
5
|
+
public name: string,
|
|
6
|
+
public message: string,
|
|
7
|
+
public description: string,
|
|
8
|
+
public isTerminal: boolean = false,
|
|
9
|
+
) {
|
|
10
|
+
super(message);
|
|
11
|
+
|
|
12
|
+
// Ref: https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
|
13
|
+
Object.setPrototypeOf(this, HMSHLSException.prototype);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toAnalyticsProperties() {
|
|
17
|
+
return {
|
|
18
|
+
error_name: this.name,
|
|
19
|
+
error_message: this.message,
|
|
20
|
+
error_description: this.description,
|
|
21
|
+
is_terminal: this.isTerminal,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addNativeError(error: Error) {
|
|
26
|
+
this.nativeError = error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
toString() {
|
|
30
|
+
return `{
|
|
31
|
+
name: ${this.name};
|
|
32
|
+
message: ${this.message};
|
|
33
|
+
description: ${this.description};
|
|
34
|
+
isTerminal: ${this.isTerminal};
|
|
35
|
+
nativeError: ${this.nativeError?.message};
|
|
36
|
+
}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { HlsPlayerStats } from '@100mslive/hls-stats';
|
|
2
|
+
import { HMSHLSPlayer } from './controllers/HMSHLSPlayer';
|
|
3
|
+
import { HMSHLSException } from './error/HMSHLSException';
|
|
4
|
+
import { ILevel } from './interfaces/ILevel';
|
|
5
|
+
import { HLSPlaybackState, HMSHLSPlayerEvents } from './utilies/constants';
|
|
6
|
+
export type { ILevel, HMSHLSException, HlsPlayerStats };
|
|
7
|
+
export { HMSHLSPlayer, HLSPlaybackState, HMSHLSPlayerEvents };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ILevel } from './ILevel';
|
|
2
|
+
interface IHMSHLSPlayer {
|
|
3
|
+
/**
|
|
4
|
+
* @returns get html video element
|
|
5
|
+
*/
|
|
6
|
+
getVideoElement(): HTMLVideoElement;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* set video volumne
|
|
10
|
+
* @param { volume } - define volume in range [1,100]
|
|
11
|
+
*/
|
|
12
|
+
setVolume(volume: number): void;
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @returns returns a ILevel which represents current
|
|
16
|
+
* quality level. -1 if currentlevel is set to "Auto"
|
|
17
|
+
*/
|
|
18
|
+
getCurrentLevel(): ILevel | null;
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param { ILevel } currentLevel - currentLevel we want to
|
|
22
|
+
* set the stream to. -1 for Auto
|
|
23
|
+
*/
|
|
24
|
+
setCurrentLevel(currentLevel: ILevel): void;
|
|
25
|
+
/**
|
|
26
|
+
* move the video to Live
|
|
27
|
+
*/
|
|
28
|
+
seekToLivePosition(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* play stream
|
|
31
|
+
* call this when autoplay error is received
|
|
32
|
+
*/
|
|
33
|
+
play(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* pause stream
|
|
36
|
+
*/
|
|
37
|
+
pause(): void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* It will update the video element current time
|
|
41
|
+
* @param seekValue Pass currentTime in second
|
|
42
|
+
*/
|
|
43
|
+
seekTo(seekValue: number): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default IHMSHLSPlayer;
|