1 | import videojs from 'video.js';
|
2 | import PlaylistLoader from './playlist-loader';
|
3 |
|
4 | const noop = () => {};
|
5 |
|
6 | /**
|
7 | * Convert the properties of an HLS track into an audioTrackKind.
|
8 | *
|
9 | * @private
|
10 | */
|
11 | const audioTrackKind_ = (properties) => {
|
12 | let kind = properties.default ? 'main' : 'alternative';
|
13 |
|
14 | if (properties.characteristics &&
|
15 | properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) {
|
16 | kind = 'main-desc';
|
17 | }
|
18 |
|
19 | return kind;
|
20 | };
|
21 |
|
22 | /**
|
23 | * Pause provided segment loader and playlist loader if active
|
24 | *
|
25 | * @param {SegmentLoader} segmentLoader
|
26 | * SegmentLoader to pause
|
27 | * @param {Object} mediaType
|
28 | * Active media type
|
29 | * @function stopLoaders
|
30 | */
|
31 | export const stopLoaders = (segmentLoader, mediaType) => {
|
32 | segmentLoader.abort();
|
33 | segmentLoader.pause();
|
34 |
|
35 | if (mediaType && mediaType.activePlaylistLoader) {
|
36 | mediaType.activePlaylistLoader.pause();
|
37 | mediaType.activePlaylistLoader = null;
|
38 | }
|
39 | };
|
40 |
|
41 | /**
|
42 | * Start loading provided segment loader and playlist loader
|
43 | *
|
44 | * @param {PlaylistLoader} playlistLoader
|
45 | * PlaylistLoader to start loading
|
46 | * @param {Object} mediaType
|
47 | * Active media type
|
48 | * @function startLoaders
|
49 | */
|
50 | export const startLoaders = (playlistLoader, mediaType) => {
|
51 | // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the
|
52 | // playlist loader
|
53 | mediaType.activePlaylistLoader = playlistLoader;
|
54 | playlistLoader.load();
|
55 | };
|
56 |
|
57 | /**
|
58 | * Returns a function to be called when the media group changes. It performs a
|
59 | * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a
|
60 | * change of group is merely a rendition switch of the same content at another encoding,
|
61 | * rather than a change of content, such as switching audio from English to Spanish.
|
62 | *
|
63 | * @param {String} type
|
64 | * MediaGroup type
|
65 | * @param {Object} settings
|
66 | * Object containing required information for media groups
|
67 | * @return {Function}
|
68 | * Handler for a non-destructive resync of SegmentLoader when the active media
|
69 | * group changes.
|
70 | * @function onGroupChanged
|
71 | */
|
72 | export const onGroupChanged = (type, settings) => () => {
|
73 | const {
|
74 | segmentLoaders: {
|
75 | [type]: segmentLoader,
|
76 | main: mainSegmentLoader
|
77 | },
|
78 | mediaTypes: { [type]: mediaType }
|
79 | } = settings;
|
80 | const activeTrack = mediaType.activeTrack();
|
81 | const activeGroup = mediaType.activeGroup(activeTrack);
|
82 | const previousActiveLoader = mediaType.activePlaylistLoader;
|
83 |
|
84 | stopLoaders(segmentLoader, mediaType);
|
85 |
|
86 | if (!activeGroup) {
|
87 | // there is no group active
|
88 | return;
|
89 | }
|
90 |
|
91 | if (!activeGroup.playlistLoader) {
|
92 | if (previousActiveLoader) {
|
93 | // The previous group had a playlist loader but the new active group does not
|
94 | // this means we are switching from demuxed to muxed audio. In this case we want to
|
95 | // do a destructive reset of the main segment loader and not restart the audio
|
96 | // loaders.
|
97 | mainSegmentLoader.resetEverything();
|
98 | }
|
99 | return;
|
100 | }
|
101 |
|
102 | // Non-destructive resync
|
103 | segmentLoader.resyncLoader();
|
104 |
|
105 | startLoaders(activeGroup.playlistLoader, mediaType);
|
106 | };
|
107 |
|
108 | /**
|
109 | * Returns a function to be called when the media track changes. It performs a
|
110 | * destructive reset of the SegmentLoader to ensure we start loading as close to
|
111 | * currentTime as possible.
|
112 | *
|
113 | * @param {String} type
|
114 | * MediaGroup type
|
115 | * @param {Object} settings
|
116 | * Object containing required information for media groups
|
117 | * @return {Function}
|
118 | * Handler for a destructive reset of SegmentLoader when the active media
|
119 | * track changes.
|
120 | * @function onTrackChanged
|
121 | */
|
122 | export const onTrackChanged = (type, settings) => () => {
|
123 | const {
|
124 | segmentLoaders: {
|
125 | [type]: segmentLoader,
|
126 | main: mainSegmentLoader
|
127 | },
|
128 | mediaTypes: { [type]: mediaType }
|
129 | } = settings;
|
130 | const activeTrack = mediaType.activeTrack();
|
131 | const activeGroup = mediaType.activeGroup(activeTrack);
|
132 | const previousActiveLoader = mediaType.activePlaylistLoader;
|
133 |
|
134 | stopLoaders(segmentLoader, mediaType);
|
135 |
|
136 | if (!activeGroup) {
|
137 | // there is no group active so we do not want to restart loaders
|
138 | return;
|
139 | }
|
140 |
|
141 | if (!activeGroup.playlistLoader) {
|
142 | // when switching from demuxed audio/video to muxed audio/video (noted by no playlist
|
143 | // loader for the audio group), we want to do a destructive reset of the main segment
|
144 | // loader and not restart the audio loaders
|
145 | mainSegmentLoader.resetEverything();
|
146 | return;
|
147 | }
|
148 |
|
149 | if (previousActiveLoader === activeGroup.playlistLoader) {
|
150 | // Nothing has actually changed. This can happen because track change events can fire
|
151 | // multiple times for a "single" change. One for enabling the new active track, and
|
152 | // one for disabling the track that was active
|
153 | startLoaders(activeGroup.playlistLoader, mediaType);
|
154 | return;
|
155 | }
|
156 |
|
157 | if (segmentLoader.track) {
|
158 | // For WebVTT, set the new text track in the segmentloader
|
159 | segmentLoader.track(activeTrack);
|
160 | }
|
161 |
|
162 | // destructive reset
|
163 | segmentLoader.resetEverything();
|
164 |
|
165 | startLoaders(activeGroup.playlistLoader, mediaType);
|
166 | };
|
167 |
|
168 | export const onError = {
|
169 | /**
|
170 | * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
|
171 | * an error.
|
172 | *
|
173 | * @param {String} type
|
174 | * MediaGroup type
|
175 | * @param {Object} settings
|
176 | * Object containing required information for media groups
|
177 | * @return {Function}
|
178 | * Error handler. Logs warning (or error if the playlist is blacklisted) to
|
179 | * console and switches back to default audio track.
|
180 | * @function onError.AUDIO
|
181 | */
|
182 | AUDIO: (type, settings) => () => {
|
183 | const {
|
184 | segmentLoaders: { [type]: segmentLoader},
|
185 | mediaTypes: { [type]: mediaType },
|
186 | blacklistCurrentPlaylist
|
187 | } = settings;
|
188 |
|
189 | stopLoaders(segmentLoader, mediaType);
|
190 |
|
191 | // switch back to default audio track
|
192 | const activeTrack = mediaType.activeTrack();
|
193 | const activeGroup = mediaType.activeGroup();
|
194 | const id = (activeGroup.filter(group => group.default)[0] || activeGroup[0]).id;
|
195 | const defaultTrack = mediaType.tracks[id];
|
196 |
|
197 | if (activeTrack === defaultTrack) {
|
198 | // Default track encountered an error. All we can do now is blacklist the current
|
199 | // rendition and hope another will switch audio groups
|
200 | blacklistCurrentPlaylist({
|
201 | message: 'Problem encountered loading the default audio track.'
|
202 | });
|
203 | return;
|
204 | }
|
205 |
|
206 | videojs.log.warn('Problem encountered loading the alternate audio track.' +
|
207 | 'Switching back to default.');
|
208 |
|
209 | for (let trackId in mediaType.tracks) {
|
210 | mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack;
|
211 | }
|
212 |
|
213 | mediaType.onTrackChanged();
|
214 | },
|
215 | /**
|
216 | * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
|
217 | * an error.
|
218 | *
|
219 | * @param {String} type
|
220 | * MediaGroup type
|
221 | * @param {Object} settings
|
222 | * Object containing required information for media groups
|
223 | * @return {Function}
|
224 | * Error handler. Logs warning to console and disables the active subtitle track
|
225 | * @function onError.SUBTITLES
|
226 | */
|
227 | SUBTITLES: (type, settings) => () => {
|
228 | const {
|
229 | segmentLoaders: { [type]: segmentLoader},
|
230 | mediaTypes: { [type]: mediaType }
|
231 | } = settings;
|
232 |
|
233 | videojs.log.warn('Problem encountered loading the subtitle track.' +
|
234 | 'Disabling subtitle track.');
|
235 |
|
236 | stopLoaders(segmentLoader, mediaType);
|
237 |
|
238 | const track = mediaType.activeTrack();
|
239 |
|
240 | if (track) {
|
241 | track.mode = 'disabled';
|
242 | }
|
243 |
|
244 | mediaType.onTrackChanged();
|
245 | }
|
246 | };
|
247 |
|
248 | export const setupListeners = {
|
249 | /**
|
250 | * Setup event listeners for audio playlist loader
|
251 | *
|
252 | * @param {String} type
|
253 | * MediaGroup type
|
254 | * @param {PlaylistLoader|null} playlistLoader
|
255 | * PlaylistLoader to register listeners on
|
256 | * @param {Object} settings
|
257 | * Object containing required information for media groups
|
258 | * @function setupListeners.AUDIO
|
259 | */
|
260 | AUDIO: (type, playlistLoader, settings) => {
|
261 | if (!playlistLoader) {
|
262 | // no playlist loader means audio will be muxed with the video
|
263 | return;
|
264 | }
|
265 |
|
266 | const {
|
267 | tech,
|
268 | requestOptions,
|
269 | segmentLoaders: { [type]: segmentLoader }
|
270 | } = settings;
|
271 |
|
272 | playlistLoader.on('loadedmetadata', () => {
|
273 | const media = playlistLoader.media();
|
274 |
|
275 | segmentLoader.playlist(media, requestOptions);
|
276 |
|
277 | // if the video is already playing, or if this isn't a live video and preload
|
278 | // permits, start downloading segments
|
279 | if (!tech.paused() || (media.endList && tech.preload() !== 'none')) {
|
280 | segmentLoader.load();
|
281 | }
|
282 | });
|
283 |
|
284 | playlistLoader.on('loadedplaylist', () => {
|
285 | segmentLoader.playlist(playlistLoader.media(), requestOptions);
|
286 |
|
287 | // If the player isn't paused, ensure that the segment loader is running
|
288 | if (!tech.paused()) {
|
289 | segmentLoader.load();
|
290 | }
|
291 | });
|
292 |
|
293 | playlistLoader.on('error', onError[type](type, settings));
|
294 | },
|
295 | /**
|
296 | * Setup event listeners for subtitle playlist loader
|
297 | *
|
298 | * @param {String} type
|
299 | * MediaGroup type
|
300 | * @param {PlaylistLoader|null} playlistLoader
|
301 | * PlaylistLoader to register listeners on
|
302 | * @param {Object} settings
|
303 | * Object containing required information for media groups
|
304 | * @function setupListeners.SUBTITLES
|
305 | */
|
306 | SUBTITLES: (type, playlistLoader, settings) => {
|
307 | const {
|
308 | tech,
|
309 | requestOptions,
|
310 | segmentLoaders: { [type]: segmentLoader },
|
311 | mediaTypes: { [type]: mediaType }
|
312 | } = settings;
|
313 |
|
314 | playlistLoader.on('loadedmetadata', () => {
|
315 | const media = playlistLoader.media();
|
316 |
|
317 | segmentLoader.playlist(media, requestOptions);
|
318 | segmentLoader.track(mediaType.activeTrack());
|
319 |
|
320 | // if the video is already playing, or if this isn't a live video and preload
|
321 | // permits, start downloading segments
|
322 | if (!tech.paused() || (media.endList && tech.preload() !== 'none')) {
|
323 | segmentLoader.load();
|
324 | }
|
325 | });
|
326 |
|
327 | playlistLoader.on('loadedplaylist', () => {
|
328 | segmentLoader.playlist(playlistLoader.media(), requestOptions);
|
329 |
|
330 | // If the player isn't paused, ensure that the segment loader is running
|
331 | if (!tech.paused()) {
|
332 | segmentLoader.load();
|
333 | }
|
334 | });
|
335 |
|
336 | playlistLoader.on('error', onError[type](type, settings));
|
337 | }
|
338 | };
|
339 |
|
340 | export const initialize = {
|
341 | /**
|
342 | * Setup PlaylistLoaders and AudioTracks for the audio groups
|
343 | *
|
344 | * @param {String} type
|
345 | * MediaGroup type
|
346 | * @param {Object} settings
|
347 | * Object containing required information for media groups
|
348 | * @function initialize.AUDIO
|
349 | */
|
350 | 'AUDIO': (type, settings) => {
|
351 | const {
|
352 | mode,
|
353 | hls,
|
354 | segmentLoaders: { [type]: segmentLoader },
|
355 | requestOptions: { withCredentials },
|
356 | master: { mediaGroups },
|
357 | mediaTypes: {
|
358 | [type]: {
|
359 | groups,
|
360 | tracks
|
361 | }
|
362 | }
|
363 | } = settings;
|
364 |
|
365 | // force a default if we have none or we are not
|
366 | // in html5 mode (the only mode to support more than one
|
367 | // audio track)
|
368 | if (!mediaGroups[type] ||
|
369 | Object.keys(mediaGroups[type]).length === 0 ||
|
370 | mode !== 'html5') {
|
371 | mediaGroups[type] = { main: { default: { default: true } } };
|
372 | }
|
373 |
|
374 | for (let groupId in mediaGroups[type]) {
|
375 | if (!groups[groupId]) {
|
376 | groups[groupId] = [];
|
377 | }
|
378 |
|
379 | for (let variantLabel in mediaGroups[type][groupId]) {
|
380 | let properties = mediaGroups[type][groupId][variantLabel];
|
381 | let playlistLoader;
|
382 |
|
383 | if (properties.resolvedUri) {
|
384 | playlistLoader = new PlaylistLoader(properties.resolvedUri,
|
385 | hls,
|
386 | withCredentials);
|
387 | } else {
|
388 | // no resolvedUri means the audio is muxed with the video when using this
|
389 | // audio track
|
390 | playlistLoader = null;
|
391 | }
|
392 |
|
393 | properties = videojs.mergeOptions({ id: variantLabel, playlistLoader },
|
394 | properties);
|
395 |
|
396 | setupListeners[type](type, properties.playlistLoader, settings);
|
397 |
|
398 | groups[groupId].push(properties);
|
399 |
|
400 | if (typeof tracks[variantLabel] === 'undefined') {
|
401 | const track = new videojs.AudioTrack({
|
402 | id: variantLabel,
|
403 | kind: audioTrackKind_(properties),
|
404 | enabled: false,
|
405 | language: properties.language,
|
406 | default: properties.default,
|
407 | label: variantLabel
|
408 | });
|
409 |
|
410 | tracks[variantLabel] = track;
|
411 | }
|
412 | }
|
413 | }
|
414 |
|
415 | // setup single error event handler for the segment loader
|
416 | segmentLoader.on('error', onError[type](type, settings));
|
417 | },
|
418 | /**
|
419 | * Setup PlaylistLoaders and TextTracks for the subtitle groups
|
420 | *
|
421 | * @param {String} type
|
422 | * MediaGroup type
|
423 | * @param {Object} settings
|
424 | * Object containing required information for media groups
|
425 | * @function initialize.SUBTITLES
|
426 | */
|
427 | 'SUBTITLES': (type, settings) => {
|
428 | const {
|
429 | tech,
|
430 | hls,
|
431 | segmentLoaders: { [type]: segmentLoader },
|
432 | requestOptions: { withCredentials },
|
433 | master: { mediaGroups },
|
434 | mediaTypes: {
|
435 | [type]: {
|
436 | groups,
|
437 | tracks
|
438 | }
|
439 | }
|
440 | } = settings;
|
441 |
|
442 | for (let groupId in mediaGroups[type]) {
|
443 | if (!groups[groupId]) {
|
444 | groups[groupId] = [];
|
445 | }
|
446 |
|
447 | for (let variantLabel in mediaGroups[type][groupId]) {
|
448 | if (mediaGroups[type][groupId][variantLabel].forced) {
|
449 | // Subtitle playlists with the forced attribute are not selectable in Safari.
|
450 | // According to Apple's HLS Authoring Specification:
|
451 | // If content has forced subtitles and regular subtitles in a given language,
|
452 | // the regular subtitles track in that language MUST contain both the forced
|
453 | // subtitles and the regular subtitles for that language.
|
454 | // Because of this requirement and that Safari does not add forced subtitles,
|
455 | // forced subtitles are skipped here to maintain consistent experience across
|
456 | // all platforms
|
457 | continue;
|
458 | }
|
459 |
|
460 | let properties = mediaGroups[type][groupId][variantLabel];
|
461 |
|
462 | properties = videojs.mergeOptions({
|
463 | id: variantLabel,
|
464 | playlistLoader: new PlaylistLoader(properties.resolvedUri,
|
465 | hls,
|
466 | withCredentials)
|
467 | }, properties);
|
468 |
|
469 | setupListeners[type](type, properties.playlistLoader, settings);
|
470 |
|
471 | groups[groupId].push(properties);
|
472 |
|
473 | if (typeof tracks[variantLabel] === 'undefined') {
|
474 | const track = tech.addRemoteTextTrack({
|
475 | id: variantLabel,
|
476 | kind: 'subtitles',
|
477 | enabled: false,
|
478 | language: properties.language,
|
479 | label: variantLabel
|
480 | }, false).track;
|
481 |
|
482 | tracks[variantLabel] = track;
|
483 | }
|
484 | }
|
485 | }
|
486 |
|
487 | // setup single error event handler for the segment loader
|
488 | segmentLoader.on('error', onError[type](type, settings));
|
489 | },
|
490 | /**
|
491 | * Setup TextTracks for the closed-caption groups
|
492 | *
|
493 | * @param {String} type
|
494 | * MediaGroup type
|
495 | * @param {Object} settings
|
496 | * Object containing required information for media groups
|
497 | * @function initialize['CLOSED-CAPTIONS']
|
498 | */
|
499 | 'CLOSED-CAPTIONS': (type, settings) => {
|
500 | const {
|
501 | tech,
|
502 | master: { mediaGroups },
|
503 | mediaTypes: {
|
504 | [type]: {
|
505 | groups,
|
506 | tracks
|
507 | }
|
508 | }
|
509 | } = settings;
|
510 |
|
511 | for (let groupId in mediaGroups[type]) {
|
512 | if (!groups[groupId]) {
|
513 | groups[groupId] = [];
|
514 | }
|
515 |
|
516 | for (let variantLabel in mediaGroups[type][groupId]) {
|
517 | let properties = mediaGroups[type][groupId][variantLabel];
|
518 |
|
519 | // We only support CEA608 captions for now, so ignore anything that
|
520 | // doesn't use a CCx INSTREAM-ID
|
521 | if (!properties.instreamId.match(/CC\d/)) {
|
522 | continue;
|
523 | }
|
524 |
|
525 | // No PlaylistLoader is required for Closed-Captions because the captions are
|
526 | // embedded within the video stream
|
527 | groups[groupId].push(videojs.mergeOptions({ id: variantLabel }, properties));
|
528 |
|
529 | if (typeof tracks[variantLabel] === 'undefined') {
|
530 | const track = tech.addRemoteTextTrack({
|
531 | id: properties.instreamId,
|
532 | kind: 'captions',
|
533 | enabled: false,
|
534 | language: properties.language,
|
535 | label: variantLabel
|
536 | }, false).track;
|
537 |
|
538 | tracks[variantLabel] = track;
|
539 | }
|
540 | }
|
541 | }
|
542 | }
|
543 | };
|
544 |
|
545 | /**
|
546 | * Returns a function used to get the active group of the provided type
|
547 | *
|
548 | * @param {String} type
|
549 | * MediaGroup type
|
550 | * @param {Object} settings
|
551 | * Object containing required information for media groups
|
552 | * @return {Function}
|
553 | * Function that returns the active media group for the provided type. Takes an
|
554 | * optional parameter {TextTrack} track. If no track is provided, a list of all
|
555 | * variants in the group, otherwise the variant corresponding to the provided
|
556 | * track is returned.
|
557 | * @function activeGroup
|
558 | */
|
559 | export const activeGroup = (type, settings) => (track) => {
|
560 | const {
|
561 | masterPlaylistLoader,
|
562 | mediaTypes: { [type]: { groups } }
|
563 | } = settings;
|
564 |
|
565 | const media = masterPlaylistLoader.media();
|
566 |
|
567 | if (!media) {
|
568 | return null;
|
569 | }
|
570 |
|
571 | let variants = null;
|
572 |
|
573 | if (media.attributes[type]) {
|
574 | variants = groups[media.attributes[type]];
|
575 | }
|
576 |
|
577 | variants = variants || groups.main;
|
578 |
|
579 | if (typeof track === 'undefined') {
|
580 | return variants;
|
581 | }
|
582 |
|
583 | if (track === null) {
|
584 | // An active track was specified so a corresponding group is expected. track === null
|
585 | // means no track is currently active so there is no corresponding group
|
586 | return null;
|
587 | }
|
588 |
|
589 | return variants.filter((props) => props.id === track.id)[0] || null;
|
590 | };
|
591 |
|
592 | export const activeTrack = {
|
593 | /**
|
594 | * Returns a function used to get the active track of type provided
|
595 | *
|
596 | * @param {String} type
|
597 | * MediaGroup type
|
598 | * @param {Object} settings
|
599 | * Object containing required information for media groups
|
600 | * @return {Function}
|
601 | * Function that returns the active media track for the provided type. Returns
|
602 | * null if no track is active
|
603 | * @function activeTrack.AUDIO
|
604 | */
|
605 | AUDIO: (type, settings) => () => {
|
606 | const { mediaTypes: { [type]: { tracks } } } = settings;
|
607 |
|
608 | for (let id in tracks) {
|
609 | if (tracks[id].enabled) {
|
610 | return tracks[id];
|
611 | }
|
612 | }
|
613 |
|
614 | return null;
|
615 | },
|
616 | /**
|
617 | * Returns a function used to get the active track of type provided
|
618 | *
|
619 | * @param {String} type
|
620 | * MediaGroup type
|
621 | * @param {Object} settings
|
622 | * Object containing required information for media groups
|
623 | * @return {Function}
|
624 | * Function that returns the active media track for the provided type. Returns
|
625 | * null if no track is active
|
626 | * @function activeTrack.SUBTITLES
|
627 | */
|
628 | SUBTITLES: (type, settings) => () => {
|
629 | const { mediaTypes: { [type]: { tracks } } } = settings;
|
630 |
|
631 | for (let id in tracks) {
|
632 | if (tracks[id].mode === 'showing') {
|
633 | return tracks[id];
|
634 | }
|
635 | }
|
636 |
|
637 | return null;
|
638 | }
|
639 | };
|
640 |
|
641 | /**
|
642 | * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles,
|
643 | * Closed-Captions) specified in the master manifest.
|
644 | *
|
645 | * @param {Object} settings
|
646 | * Object containing required information for setting up the media groups
|
647 | * @param {SegmentLoader} settings.segmentLoaders.AUDIO
|
648 | * Audio segment loader
|
649 | * @param {SegmentLoader} settings.segmentLoaders.SUBTITLES
|
650 | * Subtitle segment loader
|
651 | * @param {SegmentLoader} settings.segmentLoaders.main
|
652 | * Main segment loader
|
653 | * @param {Tech} settings.tech
|
654 | * The tech of the player
|
655 | * @param {Object} settings.requestOptions
|
656 | * XHR request options used by the segment loaders
|
657 | * @param {PlaylistLoader} settings.masterPlaylistLoader
|
658 | * PlaylistLoader for the master source
|
659 | * @param {String} mode
|
660 | * Mode of the hls source handler. Can be 'auto', 'html5', or 'flash'
|
661 | * @param {HlsHandler} settings.hls
|
662 | * HLS SourceHandler
|
663 | * @param {Object} settings.master
|
664 | * The parsed master manifest
|
665 | * @param {Object} settings.mediaTypes
|
666 | * Object to store the loaders, tracks, and utility methods for each media type
|
667 | * @param {Function} settings.blacklistCurrentPlaylist
|
668 | * Blacklists the current rendition and forces a rendition switch.
|
669 | * @function setupMediaGroups
|
670 | */
|
671 | export const setupMediaGroups = (settings) => {
|
672 | ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => {
|
673 | initialize[type](type, settings);
|
674 | });
|
675 |
|
676 | const {
|
677 | mediaTypes,
|
678 | masterPlaylistLoader,
|
679 | tech,
|
680 | hls
|
681 | } = settings;
|
682 |
|
683 | // setup active group and track getters and change event handlers
|
684 | ['AUDIO', 'SUBTITLES'].forEach((type) => {
|
685 | mediaTypes[type].activeGroup = activeGroup(type, settings);
|
686 | mediaTypes[type].activeTrack = activeTrack[type](type, settings);
|
687 | mediaTypes[type].onGroupChanged = onGroupChanged(type, settings);
|
688 | mediaTypes[type].onTrackChanged = onTrackChanged(type, settings);
|
689 | });
|
690 |
|
691 | // DO NOT enable the default subtitle or caption track.
|
692 | // DO enable the default audio track
|
693 | const audioGroup = mediaTypes.AUDIO.activeGroup();
|
694 | const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id;
|
695 |
|
696 | mediaTypes.AUDIO.tracks[groupId].enabled = true;
|
697 | mediaTypes.AUDIO.onTrackChanged();
|
698 |
|
699 | masterPlaylistLoader.on('mediachange', () => {
|
700 | ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanged());
|
701 | });
|
702 |
|
703 | // custom audio track change event handler for usage event
|
704 | const onAudioTrackChanged = () => {
|
705 | mediaTypes.AUDIO.onTrackChanged();
|
706 | tech.trigger({ type: 'usage', name: 'hls-audio-change' });
|
707 | };
|
708 |
|
709 | tech.audioTracks().addEventListener('change', onAudioTrackChanged);
|
710 | tech.remoteTextTracks().addEventListener('change',
|
711 | mediaTypes.SUBTITLES.onTrackChanged);
|
712 |
|
713 | hls.on('dispose', () => {
|
714 | tech.audioTracks().removeEventListener('change', onAudioTrackChanged);
|
715 | tech.remoteTextTracks().removeEventListener('change',
|
716 | mediaTypes.SUBTITLES.onTrackChanged);
|
717 | });
|
718 |
|
719 | // clear existing audio tracks and add the ones we just created
|
720 | tech.clearTracks('audio');
|
721 |
|
722 | for (let id in mediaTypes.AUDIO.tracks) {
|
723 | tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]);
|
724 | }
|
725 | };
|
726 |
|
727 | /**
|
728 | * Creates skeleton object used to store the loaders, tracks, and utility methods for each
|
729 | * media type
|
730 | *
|
731 | * @return {Object}
|
732 | * Object to store the loaders, tracks, and utility methods for each media type
|
733 | * @function createMediaTypes
|
734 | */
|
735 | export const createMediaTypes = () => {
|
736 | const mediaTypes = {};
|
737 |
|
738 | ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => {
|
739 | mediaTypes[type] = {
|
740 | groups: {},
|
741 | tracks: {},
|
742 | activePlaylistLoader: null,
|
743 | activeGroup: noop,
|
744 | activeTrack: noop,
|
745 | onGroupChanged: noop,
|
746 | onTrackChanged: noop
|
747 | };
|
748 | });
|
749 |
|
750 | return mediaTypes;
|
751 | };
|