@mux/playback-core 0.7.1-canary.0-556dabf
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/CHANGELOG.md +91 -0
- package/README.md +9 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +7 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +7 -0
- package/dist/playback-core.js +30 -0
- package/dist/playback-core.js.map +7 -0
- package/dist/playback-core.mjs +30 -0
- package/dist/playback-core.mjs.map +7 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/autoplay.d.ts +15 -0
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/index.d.ts +70 -0
- package/dist/types/tracks.d.ts +2 -0
- package/dist/types/util.d.ts +3 -0
- package/dist/types-ts3.4/autoplay.d.ts +15 -0
- package/dist/types-ts3.4/errors.d.ts +14 -0
- package/dist/types-ts3.4/index.d.ts +73 -0
- package/dist/types-ts3.4/tracks.d.ts +2 -0
- package/dist/types-ts3.4/util.d.ts +3 -0
- package/package.json +61 -0
- package/src/autoplay.ts +167 -0
- package/src/errors.ts +33 -0
- package/src/index.ts +503 -0
- package/src/tracks.ts +132 -0
- package/src/util.ts +6 -0
- package/tsconfig.json +22 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import '@mux/polyfills';
|
|
2
|
+
import mux, { Options, ErrorEvent } from 'mux-embed';
|
|
3
|
+
|
|
4
|
+
import Hls, { HlsConfig } from 'hls.js';
|
|
5
|
+
import { AutoplayTypes, setupAutoplay } from './autoplay';
|
|
6
|
+
import { MediaError } from './errors';
|
|
7
|
+
import { setupTracks } from './tracks';
|
|
8
|
+
import { isKeyOf } from './util';
|
|
9
|
+
import type { Autoplay, UpdateAutoplay } from './autoplay';
|
|
10
|
+
|
|
11
|
+
export type ValueOf<T> = T[keyof T];
|
|
12
|
+
export type Metadata = Partial<Options['data']>;
|
|
13
|
+
export type PlaybackEngine = Hls;
|
|
14
|
+
export { mux, Hls, MediaError, Autoplay, UpdateAutoplay, setupAutoplay };
|
|
15
|
+
|
|
16
|
+
const MUX_VIDEO_DOMAIN = 'mux.com';
|
|
17
|
+
|
|
18
|
+
export const generatePlayerInitTime = () => {
|
|
19
|
+
return mux.utils.now();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type StreamTypes = {
|
|
23
|
+
VOD: 'on-demand';
|
|
24
|
+
ON_DEMAND: 'on-demand';
|
|
25
|
+
LIVE: 'live';
|
|
26
|
+
LL_LIVE: 'll-live';
|
|
27
|
+
DVR: 'live:dvr';
|
|
28
|
+
LL_DVR: 'll-live:dvr';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const StreamTypes: StreamTypes = {
|
|
32
|
+
VOD: 'on-demand',
|
|
33
|
+
ON_DEMAND: 'on-demand',
|
|
34
|
+
LIVE: 'live',
|
|
35
|
+
LL_LIVE: 'll-live',
|
|
36
|
+
DVR: 'live:dvr',
|
|
37
|
+
LL_DVR: 'll-live:dvr',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ExtensionMimeTypeMap = {
|
|
41
|
+
M3U8: 'application/vnd.apple.mpegurl';
|
|
42
|
+
MP4: 'video/mp4';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const ExtensionMimeTypeMap: ExtensionMimeTypeMap = {
|
|
46
|
+
M3U8: 'application/vnd.apple.mpegurl',
|
|
47
|
+
MP4: 'video/mp4',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type MimeTypeShorthandMap = {
|
|
51
|
+
HLS: ExtensionMimeTypeMap['M3U8'];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const MimeTypeShorthandMap: MimeTypeShorthandMap = {
|
|
55
|
+
HLS: ExtensionMimeTypeMap.M3U8,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const shorthandKeys = Object.keys(MimeTypeShorthandMap);
|
|
59
|
+
|
|
60
|
+
export type MediaTypes =
|
|
61
|
+
| ValueOf<ExtensionMimeTypeMap>
|
|
62
|
+
| keyof MimeTypeShorthandMap
|
|
63
|
+
/** @TODO Figure out a way to "downgrade" derived types below to early TS syntax (e.g. 3.4) instead of explicit versions here (CJP) */
|
|
64
|
+
| 'hls';
|
|
65
|
+
// | `${Lowercase<keyof MimeTypeShorthandMap>}`
|
|
66
|
+
// | `${Uppercase<keyof MimeTypeShorthandMap>}`;
|
|
67
|
+
|
|
68
|
+
export const allMediaTypes = [
|
|
69
|
+
...(Object.values(ExtensionMimeTypeMap) as ValueOf<ExtensionMimeTypeMap>[]),
|
|
70
|
+
/** @TODO Figure out a way to "downgrade" derived types below to early TS syntax (e.g. 3.4) instead of explicit versions here (CJP) */
|
|
71
|
+
'hls',
|
|
72
|
+
'HLS',
|
|
73
|
+
// ...(shorthandKeys as (keyof MimeTypeShorthandMap)[]),
|
|
74
|
+
// ...(shorthandKeys.map((k) => k.toUpperCase()) as `${Uppercase<keyof MimeTypeShorthandMap>}`[]),
|
|
75
|
+
// ...(shorthandKeys.map((k) => k.toLowerCase()) as `${Lowercase<keyof MimeTypeShorthandMap>}`[]),
|
|
76
|
+
] as MediaTypes[];
|
|
77
|
+
|
|
78
|
+
export type MuxMediaPropTypes = {
|
|
79
|
+
envKey: Options['data']['env_key'];
|
|
80
|
+
debug: Options['debug'] & Hls['config']['debug'];
|
|
81
|
+
metadata: Partial<Options['data']>;
|
|
82
|
+
customDomain: string;
|
|
83
|
+
beaconCollectionDomain: Options['beaconCollectionDomain'];
|
|
84
|
+
errorTranslator: Options['errorTranslator'];
|
|
85
|
+
playbackId: string;
|
|
86
|
+
playerInitTime: Options['data']['player_init_time'];
|
|
87
|
+
preferMse: boolean;
|
|
88
|
+
type: MediaTypes;
|
|
89
|
+
streamType: ValueOf<StreamTypes>;
|
|
90
|
+
startTime: HlsConfig['startPosition'];
|
|
91
|
+
autoPlay: boolean | ValueOf<AutoplayTypes>;
|
|
92
|
+
autoplay: boolean | ValueOf<AutoplayTypes>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type HTMLMediaElementProps = Partial<Pick<HTMLMediaElement, 'src'>>;
|
|
96
|
+
|
|
97
|
+
export type MuxMediaProps = HTMLMediaElementProps & MuxMediaPropTypes;
|
|
98
|
+
export type MuxMediaPropsInternal = MuxMediaProps & {
|
|
99
|
+
playerSoftwareName: Options['data']['player_software_name'];
|
|
100
|
+
playerSoftwareVersion: Options['data']['player_software_version'];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const toPlaybackIdParts = (playbackIdWithOptionalParams: string): [string, string?] => {
|
|
104
|
+
const qIndex = playbackIdWithOptionalParams.indexOf('?');
|
|
105
|
+
if (qIndex < 0) return [playbackIdWithOptionalParams];
|
|
106
|
+
const idPart = playbackIdWithOptionalParams.slice(0, qIndex);
|
|
107
|
+
const queryPart = playbackIdWithOptionalParams.slice(qIndex);
|
|
108
|
+
return [idPart, queryPart];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const toMuxVideoURL = (playbackId?: string, { domain = MUX_VIDEO_DOMAIN } = {}) => {
|
|
112
|
+
if (!playbackId) return undefined;
|
|
113
|
+
const [idPart, queryPart = ''] = toPlaybackIdParts(playbackId);
|
|
114
|
+
return `https://stream.${domain}/${idPart}.m3u8${queryPart}`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const inferMimeTypeFromURL = (url: string) => {
|
|
118
|
+
let pathname = '';
|
|
119
|
+
try {
|
|
120
|
+
pathname = new URL(url).pathname;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error('invalid url');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const extDelimIdx = pathname.lastIndexOf('.');
|
|
126
|
+
if (extDelimIdx < 0) return '';
|
|
127
|
+
|
|
128
|
+
const ext = pathname.slice(extDelimIdx + 1);
|
|
129
|
+
const upperExt = ext.toUpperCase();
|
|
130
|
+
|
|
131
|
+
return isKeyOf(upperExt, ExtensionMimeTypeMap) ? ExtensionMimeTypeMap[upperExt] : '';
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const getType = (props: Partial<Pick<MuxMediaProps, 'type' | 'src'>>) => {
|
|
135
|
+
const type = props.type;
|
|
136
|
+
|
|
137
|
+
if (type) {
|
|
138
|
+
const upperType = type.toUpperCase();
|
|
139
|
+
|
|
140
|
+
return isKeyOf(upperType, MimeTypeShorthandMap) ? MimeTypeShorthandMap[upperType] : type;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { src } = props;
|
|
144
|
+
|
|
145
|
+
if (!src) return '';
|
|
146
|
+
|
|
147
|
+
return inferMimeTypeFromURL(src);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const getStreamTypeConfig = (streamType?: ValueOf<StreamTypes>) => {
|
|
151
|
+
if ([StreamTypes.LIVE, StreamTypes.LL_LIVE].includes(streamType as any)) {
|
|
152
|
+
const liveConfig = {
|
|
153
|
+
backBufferLength: 12,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (streamType === StreamTypes.LL_LIVE) {
|
|
157
|
+
return {
|
|
158
|
+
...liveConfig,
|
|
159
|
+
maxFragLookUpTolerance: 0.001,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return liveConfig;
|
|
164
|
+
}
|
|
165
|
+
return {};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
let muxMediaState: WeakMap<HTMLMediaElement, { error?: MediaError }> = new WeakMap();
|
|
169
|
+
|
|
170
|
+
export const getError = (mediaEl: HTMLMediaElement) => {
|
|
171
|
+
return muxMediaState.get(mediaEl)?.error;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const teardown = (mediaEl?: HTMLMediaElement | null, hls?: Pick<Hls, 'detachMedia' | 'destroy'>) => {
|
|
175
|
+
if (hls) {
|
|
176
|
+
hls.detachMedia();
|
|
177
|
+
hls.destroy();
|
|
178
|
+
}
|
|
179
|
+
if (mediaEl?.mux && !mediaEl.mux.deleted) {
|
|
180
|
+
mediaEl.mux.destroy();
|
|
181
|
+
mediaEl.mux;
|
|
182
|
+
}
|
|
183
|
+
if (mediaEl) {
|
|
184
|
+
mediaEl.removeEventListener('error', handleNativeError);
|
|
185
|
+
mediaEl.removeEventListener('error', handleInternalError);
|
|
186
|
+
muxMediaState.delete(mediaEl);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const setupHls = (
|
|
191
|
+
props: Partial<Pick<MuxMediaProps, 'debug' | 'preferMse' | 'streamType' | 'type' | 'src' | 'startTime'>>,
|
|
192
|
+
mediaEl?: Pick<HTMLMediaElement, 'canPlayType'> | null
|
|
193
|
+
) => {
|
|
194
|
+
const { debug, preferMse, streamType, startTime: startPosition = -1 } = props;
|
|
195
|
+
const type = getType(props);
|
|
196
|
+
const hlsType = type === ExtensionMimeTypeMap.M3U8;
|
|
197
|
+
|
|
198
|
+
const canUseNative = !type || (mediaEl?.canPlayType(type) ?? true);
|
|
199
|
+
const hlsSupported = Hls.isSupported();
|
|
200
|
+
// NOTE: Native HLS playback on Android for LL-HLS has been flaky, so we're prefering
|
|
201
|
+
// MSE for those conditions for now. (CJP)
|
|
202
|
+
const userAgentStr = window?.navigator?.userAgent ?? '';
|
|
203
|
+
const isAndroid = userAgentStr.toLowerCase().indexOf('android') !== -1;
|
|
204
|
+
const defaultPreferMse = isAndroid && streamType === StreamTypes.LL_LIVE;
|
|
205
|
+
|
|
206
|
+
// We should use native playback for hls media sources if we a) can use native playback and don't also b) prefer to use MSE/hls.js if/when it's supported
|
|
207
|
+
const shouldUseNative = !hlsType || (canUseNative && !((preferMse || defaultPreferMse) && hlsSupported));
|
|
208
|
+
|
|
209
|
+
// 1. if we are trying to play an hls media source create hls if we should be using it "under the hood"
|
|
210
|
+
if (hlsType && !shouldUseNative && hlsSupported) {
|
|
211
|
+
const defaultConfig = {
|
|
212
|
+
backBufferLength: 30,
|
|
213
|
+
renderTextTracksNatively: false,
|
|
214
|
+
liveDurationInfinity: true,
|
|
215
|
+
};
|
|
216
|
+
const streamTypeConfig = getStreamTypeConfig(streamType);
|
|
217
|
+
const hls = new Hls({
|
|
218
|
+
// Kind of like preload metadata, but causes spinner.
|
|
219
|
+
// autoStartLoad: false,
|
|
220
|
+
debug,
|
|
221
|
+
startPosition,
|
|
222
|
+
...defaultConfig,
|
|
223
|
+
...streamTypeConfig,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return hls;
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const isMuxVideoSrc = ({
|
|
232
|
+
playbackId,
|
|
233
|
+
src,
|
|
234
|
+
customDomain,
|
|
235
|
+
}: Partial<Pick<MuxMediaPropsInternal, 'playbackId' | 'src' | 'customDomain'>>) => {
|
|
236
|
+
if (!!playbackId) return true;
|
|
237
|
+
// having no playback id and no src string should never actually happen, but could
|
|
238
|
+
if (typeof src !== 'string') return false;
|
|
239
|
+
const hostname = new URL(src).hostname.toLocaleLowerCase();
|
|
240
|
+
return hostname.includes(MUX_VIDEO_DOMAIN) || (!!customDomain && hostname.includes(customDomain.toLocaleLowerCase()));
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const setupMux = (
|
|
244
|
+
props: Partial<
|
|
245
|
+
Pick<
|
|
246
|
+
MuxMediaPropsInternal,
|
|
247
|
+
| 'envKey'
|
|
248
|
+
| 'playerInitTime'
|
|
249
|
+
| 'beaconCollectionDomain'
|
|
250
|
+
| 'errorTranslator'
|
|
251
|
+
| 'metadata'
|
|
252
|
+
| 'debug'
|
|
253
|
+
| 'playerSoftwareName'
|
|
254
|
+
| 'playerSoftwareVersion'
|
|
255
|
+
| 'playbackId'
|
|
256
|
+
| 'src'
|
|
257
|
+
| 'customDomain'
|
|
258
|
+
>
|
|
259
|
+
>,
|
|
260
|
+
mediaEl?: HTMLMediaElement | null,
|
|
261
|
+
hlsjs?: Hls
|
|
262
|
+
) => {
|
|
263
|
+
const { envKey: env_key } = props;
|
|
264
|
+
const inferredEnv = isMuxVideoSrc(props);
|
|
265
|
+
|
|
266
|
+
if ((env_key || inferredEnv) && mediaEl) {
|
|
267
|
+
const {
|
|
268
|
+
playerInitTime: player_init_time,
|
|
269
|
+
playerSoftwareName: player_software_name,
|
|
270
|
+
playerSoftwareVersion: player_software_version,
|
|
271
|
+
beaconCollectionDomain,
|
|
272
|
+
metadata,
|
|
273
|
+
debug,
|
|
274
|
+
} = props;
|
|
275
|
+
|
|
276
|
+
const muxEmbedErrorTranslator = (error: ErrorEvent) => {
|
|
277
|
+
// mux-embed auto tracks fatal hls.js errors, turn it off.
|
|
278
|
+
// playback-core will emit errors with a numeric code manually to mux-embed.
|
|
279
|
+
if (typeof error.player_error_code === 'string') return false;
|
|
280
|
+
|
|
281
|
+
if (typeof props.errorTranslator === 'function') {
|
|
282
|
+
return props.errorTranslator(error);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return error;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
mux.monitor(mediaEl, {
|
|
289
|
+
debug,
|
|
290
|
+
beaconCollectionDomain,
|
|
291
|
+
hlsjs,
|
|
292
|
+
Hls: hlsjs ? Hls : undefined,
|
|
293
|
+
automaticErrorTracking: false,
|
|
294
|
+
errorTranslator: muxEmbedErrorTranslator,
|
|
295
|
+
data: {
|
|
296
|
+
...(env_key ? { env_key } : {}),
|
|
297
|
+
// Metadata fields
|
|
298
|
+
player_software_name,
|
|
299
|
+
player_software_version,
|
|
300
|
+
player_init_time,
|
|
301
|
+
// Use any metadata passed in programmatically (which may override the defaults above)
|
|
302
|
+
...metadata,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const loadMedia = (
|
|
309
|
+
props: Partial<Pick<MuxMediaProps, 'preferMse' | 'src' | 'type' | 'startTime' | 'streamType' | 'autoplay'>>,
|
|
310
|
+
mediaEl?: HTMLMediaElement | null,
|
|
311
|
+
hls?: Pick<
|
|
312
|
+
Hls,
|
|
313
|
+
| 'config'
|
|
314
|
+
| 'on'
|
|
315
|
+
| 'once'
|
|
316
|
+
| 'startLoad'
|
|
317
|
+
| 'stopLoad'
|
|
318
|
+
| 'recoverMediaError'
|
|
319
|
+
| 'destroy'
|
|
320
|
+
| 'loadSource'
|
|
321
|
+
| 'attachMedia'
|
|
322
|
+
| 'liveSyncPosition'
|
|
323
|
+
| 'subtitleTracks'
|
|
324
|
+
| 'subtitleTrack'
|
|
325
|
+
>
|
|
326
|
+
) => {
|
|
327
|
+
if (!mediaEl) {
|
|
328
|
+
console.warn('attempting to load media before mediaEl exists');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const { preferMse, streamType } = props;
|
|
332
|
+
const type = getType(props);
|
|
333
|
+
const hlsType = type === ExtensionMimeTypeMap.M3U8;
|
|
334
|
+
|
|
335
|
+
const canUseNative = !type || (mediaEl?.canPlayType(type) ?? true);
|
|
336
|
+
const hlsSupported = Hls.isSupported();
|
|
337
|
+
const userAgentStr = window?.navigator?.userAgent ?? '';
|
|
338
|
+
// NOTE: Native HLS playback on Android for LL-HLS has been flaky, so we're prefering
|
|
339
|
+
// MSE for those conditions for now. (CJP)
|
|
340
|
+
const isAndroid = userAgentStr.toLowerCase().indexOf('android') !== -1;
|
|
341
|
+
const defaultPreferMse = isAndroid && streamType === StreamTypes.LL_LIVE;
|
|
342
|
+
|
|
343
|
+
// We should use native playback for hls media sources if we a) can use native playback and don't also b) prefer to use MSE/hls.js if/when it's supported
|
|
344
|
+
const shouldUseNative = !hlsType || (canUseNative && !((preferMse || defaultPreferMse) && hlsSupported));
|
|
345
|
+
|
|
346
|
+
const { src } = props;
|
|
347
|
+
if (mediaEl && canUseNative && shouldUseNative) {
|
|
348
|
+
if (typeof src === 'string') {
|
|
349
|
+
const { startTime } = props;
|
|
350
|
+
mediaEl.setAttribute('src', src);
|
|
351
|
+
if (startTime) {
|
|
352
|
+
const setStartTimeOnLoad = ({ target }: HTMLMediaElementEventMap['loadedmetadata']) => {
|
|
353
|
+
(target as HTMLMediaElement).currentTime = startTime;
|
|
354
|
+
(target as HTMLMediaElement).removeEventListener('loadedmetadata', setStartTimeOnLoad);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
mediaEl.addEventListener('loadedmetadata', setStartTimeOnLoad);
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
mediaEl.removeAttribute('src');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
mediaEl.addEventListener('error', handleNativeError);
|
|
364
|
+
mediaEl.addEventListener('error', handleInternalError);
|
|
365
|
+
} else if (hls && src) {
|
|
366
|
+
hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
367
|
+
// if (data.fatal) {
|
|
368
|
+
// switch (data.type) {
|
|
369
|
+
// case Hls.ErrorTypes.NETWORK_ERROR:
|
|
370
|
+
// // try to recover network error
|
|
371
|
+
// console.error("fatal network error encountered, try to recover");
|
|
372
|
+
// hls.startLoad();
|
|
373
|
+
// break;
|
|
374
|
+
// case Hls.ErrorTypes.MEDIA_ERROR:
|
|
375
|
+
// console.error("fatal media error encountered, try to recover");
|
|
376
|
+
// hls.recoverMediaError();
|
|
377
|
+
// break;
|
|
378
|
+
// default:
|
|
379
|
+
// // cannot recover
|
|
380
|
+
// console.error(
|
|
381
|
+
// "unrecoverable fatal error encountered, cannot recover (check logs for more info)"
|
|
382
|
+
// );
|
|
383
|
+
// hls.destroy();
|
|
384
|
+
// break;
|
|
385
|
+
// }
|
|
386
|
+
// }
|
|
387
|
+
|
|
388
|
+
const errorCodeMap: Record<string, number> = {
|
|
389
|
+
[Hls.ErrorTypes.NETWORK_ERROR]: MediaError.MEDIA_ERR_NETWORK,
|
|
390
|
+
[Hls.ErrorTypes.MEDIA_ERROR]: MediaError.MEDIA_ERR_DECODE,
|
|
391
|
+
};
|
|
392
|
+
const error = new MediaError('', errorCodeMap[data.type]);
|
|
393
|
+
error.fatal = data.fatal;
|
|
394
|
+
error.data = data;
|
|
395
|
+
mediaEl.dispatchEvent(
|
|
396
|
+
new CustomEvent('error', {
|
|
397
|
+
detail: error,
|
|
398
|
+
})
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
mediaEl.addEventListener('error', handleInternalError);
|
|
402
|
+
|
|
403
|
+
setupTracks(mediaEl, hls);
|
|
404
|
+
|
|
405
|
+
switch (mediaEl.preload) {
|
|
406
|
+
case 'none':
|
|
407
|
+
// when preload is none, load the source on first play
|
|
408
|
+
mediaEl.addEventListener('play', () => hls.loadSource(src), { once: true });
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case 'metadata':
|
|
412
|
+
const originalLength = hls.config.maxBufferLength;
|
|
413
|
+
const originalSize = hls.config.maxBufferSize;
|
|
414
|
+
|
|
415
|
+
// load the least amount of data possible
|
|
416
|
+
hls.config.maxBufferLength = 1;
|
|
417
|
+
hls.config.maxBufferSize = 1;
|
|
418
|
+
// and once a user has player, allow for it to load data as normal
|
|
419
|
+
mediaEl.addEventListener(
|
|
420
|
+
'play',
|
|
421
|
+
() => {
|
|
422
|
+
hls.config.maxBufferLength = originalLength;
|
|
423
|
+
hls.config.maxBufferSize = originalSize;
|
|
424
|
+
},
|
|
425
|
+
{ once: true }
|
|
426
|
+
);
|
|
427
|
+
hls.loadSource(src);
|
|
428
|
+
break;
|
|
429
|
+
|
|
430
|
+
default:
|
|
431
|
+
// load source immediately for any other preload value
|
|
432
|
+
hls.loadSource(src);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
hls.attachMedia(mediaEl);
|
|
436
|
+
} else {
|
|
437
|
+
console.error(
|
|
438
|
+
"It looks like the video you're trying to play will not work on this system! If possible, try upgrading to the newest versions of your browser or software."
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
async function handleNativeError(event: Event) {
|
|
444
|
+
// Return if the event was created or modified by a script or dispatched
|
|
445
|
+
// via EventTarget.dispatchEvent() preventing an infinite loop.
|
|
446
|
+
if (!event.isTrusted) return;
|
|
447
|
+
|
|
448
|
+
// Stop immediate propagation of the native error event, re-dispatch below!
|
|
449
|
+
event.stopImmediatePropagation();
|
|
450
|
+
|
|
451
|
+
const mediaEl = event.target as HTMLMediaElement;
|
|
452
|
+
// Safari sometimes throws an error event with a null error.
|
|
453
|
+
if (!mediaEl?.error) return;
|
|
454
|
+
|
|
455
|
+
const { message, code } = mediaEl.error;
|
|
456
|
+
const error = new MediaError(message, code);
|
|
457
|
+
|
|
458
|
+
if (mediaEl.src && (code !== MediaError.MEDIA_ERR_DECODE || code !== undefined)) {
|
|
459
|
+
// Attempt to get the response code from the video src url.
|
|
460
|
+
const { status } = await fetch(mediaEl.src as RequestInfo);
|
|
461
|
+
// Use the same hls.js data structure.
|
|
462
|
+
error.data = { response: { code: status } };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
mediaEl.dispatchEvent(
|
|
466
|
+
new CustomEvent('error', {
|
|
467
|
+
detail: error,
|
|
468
|
+
})
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Use a event listener instead of a function call when dispatching the Custom error
|
|
474
|
+
* event so consumers are still able to disable or intercept this error event.
|
|
475
|
+
* @param {Event} event
|
|
476
|
+
*/
|
|
477
|
+
function handleInternalError(event: Event) {
|
|
478
|
+
if (!(event instanceof CustomEvent) || !(event.detail instanceof MediaError)) return;
|
|
479
|
+
|
|
480
|
+
const mediaEl = event.target as HTMLMediaElement;
|
|
481
|
+
const error = event.detail;
|
|
482
|
+
// Prevent tracking non-fatal errors in Mux data.
|
|
483
|
+
if (!error || !error.fatal) return;
|
|
484
|
+
|
|
485
|
+
const state = muxMediaState.get(mediaEl);
|
|
486
|
+
if (state) state.error = error;
|
|
487
|
+
|
|
488
|
+
// Only pass valid mux-embed props: player_error_code, player_error_message
|
|
489
|
+
mediaEl.mux?.emit('error', {
|
|
490
|
+
player_error_code: error.code,
|
|
491
|
+
player_error_message: error.message,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export const initialize = (props: Partial<MuxMediaPropsInternal>, mediaEl?: HTMLMediaElement | null, hls?: Hls) => {
|
|
496
|
+
// Automatically tear down previously initialized mux data & hls instance if it exists.
|
|
497
|
+
teardown(mediaEl, hls);
|
|
498
|
+
muxMediaState.set(mediaEl as HTMLMediaElement, {});
|
|
499
|
+
const nextHlsInstance = setupHls(props, mediaEl);
|
|
500
|
+
setupMux(props, mediaEl, nextHlsInstance);
|
|
501
|
+
loadMedia(props, mediaEl, nextHlsInstance);
|
|
502
|
+
return nextHlsInstance;
|
|
503
|
+
};
|
package/src/tracks.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import Hls from 'hls.js';
|
|
2
|
+
import type { MediaPlaylist } from 'hls.js';
|
|
3
|
+
|
|
4
|
+
export function setupTracks(
|
|
5
|
+
mediaEl: HTMLMediaElement,
|
|
6
|
+
hls: Pick<Hls, 'on' | 'once' | 'subtitleTracks' | 'subtitleTrack'>
|
|
7
|
+
) {
|
|
8
|
+
hls.on(Hls.Events.NON_NATIVE_TEXT_TRACKS_FOUND, (_type, { tracks }) => {
|
|
9
|
+
tracks.forEach((trackObj) => {
|
|
10
|
+
const baseTrackObj = trackObj.subtitleTrack ?? trackObj.closedCaptions;
|
|
11
|
+
const idx = hls.subtitleTracks.findIndex(({ lang, name, type }) => {
|
|
12
|
+
return lang == baseTrackObj?.lang && name === trackObj.label && type.toLowerCase() === trackObj.kind;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
createTextTrack(
|
|
16
|
+
mediaEl,
|
|
17
|
+
trackObj.kind as TextTrackKind,
|
|
18
|
+
trackObj.label,
|
|
19
|
+
baseTrackObj?.lang,
|
|
20
|
+
`${trackObj.kind}${idx}`
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const changeHandler = () => {
|
|
26
|
+
if (!hls.subtitleTracks.length) return;
|
|
27
|
+
|
|
28
|
+
const showingTrack = Array.from(mediaEl.textTracks).find((textTrack) => {
|
|
29
|
+
return textTrack.id && textTrack.mode === 'showing' && ['subtitles', 'captions'].includes(textTrack.kind);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// If hls.subtitleTrack is -1 or its id changed compared to the one that is showing load the new subtitle track.
|
|
33
|
+
const hlsTrackId = `${hls.subtitleTracks[hls.subtitleTrack]?.type.toLowerCase()}${hls.subtitleTrack}`;
|
|
34
|
+
if (showingTrack && (hls.subtitleTrack < 0 || showingTrack?.id !== hlsTrackId)) {
|
|
35
|
+
const idx = hls.subtitleTracks.findIndex(({ lang, name, type }) => {
|
|
36
|
+
return lang == showingTrack.language && name === showingTrack.label && type.toLowerCase() === showingTrack.kind;
|
|
37
|
+
});
|
|
38
|
+
// After the subtitleTrack is set here, hls.js will load the playlist and CUES_PARSED events will be fired below.
|
|
39
|
+
hls.subtitleTrack = idx;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (showingTrack && showingTrack?.id === hlsTrackId) {
|
|
43
|
+
// Refresh the cues after a texttrack mode change to fix a Chrome bug causing the captions not to render.
|
|
44
|
+
if (showingTrack.cues) {
|
|
45
|
+
Array.from(showingTrack.cues).forEach((cue) => {
|
|
46
|
+
showingTrack.addCue(cue);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
mediaEl.textTracks.addEventListener('change', changeHandler);
|
|
53
|
+
|
|
54
|
+
hls.on(Hls.Events.CUES_PARSED, (_type, { track, type, cues }) => {
|
|
55
|
+
const textTrack = mediaEl.textTracks.getTrackById(track);
|
|
56
|
+
if (!textTrack) return;
|
|
57
|
+
|
|
58
|
+
const disabled = textTrack.mode === 'disabled';
|
|
59
|
+
if (disabled) {
|
|
60
|
+
textTrack.mode = 'hidden';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
cues.forEach((cue: VTTCue) => {
|
|
64
|
+
if (textTrack.cues?.getCueById(cue.id)) return;
|
|
65
|
+
textTrack.addCue(cue);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (disabled) {
|
|
69
|
+
textTrack.mode = 'disabled';
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
hls.on(Hls.Events.DESTROYING, () => {
|
|
74
|
+
mediaEl.textTracks.removeEventListener('change', changeHandler);
|
|
75
|
+
|
|
76
|
+
const trackEls = mediaEl.querySelectorAll('track');
|
|
77
|
+
trackEls.forEach((trackEl) => {
|
|
78
|
+
if (!(trackEl.id && ['subtitles', 'captions'].includes(trackEl.kind))) return;
|
|
79
|
+
if (!hls.subtitleTracks.some(({ type }, idx) => trackEl.id === `${type.toLowerCase()}${idx}`)) return;
|
|
80
|
+
|
|
81
|
+
mediaEl.removeChild(trackEl);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const forceHiddenThumbnails = () => {
|
|
86
|
+
// Keeping this a forEach in case we want to expand the scope of this.
|
|
87
|
+
Array.from(mediaEl.textTracks).forEach((track) => {
|
|
88
|
+
if (['subtitles', 'caption'].includes(track.kind)) return;
|
|
89
|
+
if (track.label !== 'thumbnails') return;
|
|
90
|
+
if (!track.cues?.length) {
|
|
91
|
+
const trackEl = mediaEl.querySelector('track[label="thumbnails"]');
|
|
92
|
+
// Force a reload of the cues if they've been removed
|
|
93
|
+
const src = trackEl?.getAttribute('src') ?? '';
|
|
94
|
+
trackEl?.removeAttribute('src');
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
trackEl?.setAttribute('src', src);
|
|
97
|
+
}, 0);
|
|
98
|
+
}
|
|
99
|
+
// Force hidden mode if it's not hidden
|
|
100
|
+
if (track.mode !== 'hidden') {
|
|
101
|
+
track.mode = 'hidden';
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// hls.js will forcibly clear all cues from tracks on manifest loads or media attaches.
|
|
107
|
+
// This ensures that we re-load them after it's done that.
|
|
108
|
+
hls.once(Hls.Events.MANIFEST_LOADED, forceHiddenThumbnails);
|
|
109
|
+
hls.once(Hls.Events.MEDIA_ATTACHED, forceHiddenThumbnails);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createTextTrack(
|
|
113
|
+
mediaEl: HTMLMediaElement,
|
|
114
|
+
kind: TextTrackKind,
|
|
115
|
+
label: string,
|
|
116
|
+
lang?: string,
|
|
117
|
+
id?: string
|
|
118
|
+
): TextTrack | undefined {
|
|
119
|
+
const trackEl = document.createElement('track');
|
|
120
|
+
trackEl.kind = kind;
|
|
121
|
+
trackEl.label = label;
|
|
122
|
+
if (lang) {
|
|
123
|
+
// This attribute must be present if the element's kind attribute is in the subtitles state.
|
|
124
|
+
trackEl.srclang = lang;
|
|
125
|
+
}
|
|
126
|
+
if (id) {
|
|
127
|
+
trackEl.id = id;
|
|
128
|
+
}
|
|
129
|
+
trackEl.track.mode = 'disabled';
|
|
130
|
+
mediaEl.appendChild(trackEl);
|
|
131
|
+
return trackEl.track;
|
|
132
|
+
}
|
package/src/util.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "src",
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"incremental": true,
|
|
6
|
+
"target": "ES2019",
|
|
7
|
+
"module": "es6",
|
|
8
|
+
"jsx": "react",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"composite": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noImplicitAny": true,
|
|
14
|
+
"moduleResolution": "Node",
|
|
15
|
+
"baseUrl": "./packages",
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"typeRoots": ["node_modules/@types", "../../types"]
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*", "../../types"]
|
|
22
|
+
}
|