UNPKG

26.4 kBJavaScriptView Raw
1import videojs from 'video.js';
2import {
3 parse as parseMpd,
4 addSidxSegmentsToPlaylist,
5 generateSidxKey,
6 parseUTCTiming
7} from 'mpd-parser';
8import {
9 refreshDelay,
10 updateMaster as updatePlaylist,
11 isPlaylistUnchanged
12} from './playlist-loader';
13import { resolveUrl, resolveManifestRedirect } from './resolve-url';
14import parseSidx from 'mux.js/lib/tools/parse-sidx';
15import { segmentXhrHeaders } from './xhr';
16import window from 'global/window';
17import {
18 forEachMediaGroup,
19 addPropertiesToMaster
20} from './manifest';
21import containerRequest from './util/container-request.js';
22import {toUint8} from '@videojs/vhs-utils/es/byte-helpers';
23import logger from './util/logger';
24
25const { EventTarget, mergeOptions } = videojs;
26
27const dashPlaylistUnchanged = function(a, b) {
28 if (!isPlaylistUnchanged(a, b)) {
29 return false;
30 }
31
32 // for dash the above check will often return true in scenarios where
33 // the playlist actually has changed because mediaSequence isn't a
34 // dash thing, and we often set it to 1. So if the playlists have the same amount
35 // of segments we return true.
36 // So for dash we need to make sure that the underlying segments are different.
37
38 // if sidx changed then the playlists are different.
39 if (a.sidx && b.sidx && (a.sidx.offset !== b.sidx.offset || a.sidx.length !== b.sidx.length)) {
40 return false;
41 } else if ((!a.sidx && b.sidx) || (a.sidx && !b.sidx)) {
42 return false;
43 }
44
45 // one or the other does not have segments
46 // there was a change.
47 if (a.segments && !b.segments || !a.segments && b.segments) {
48 return false;
49 }
50
51 // neither has segments nothing changed
52 if (!a.segments && !b.segments) {
53 return true;
54 }
55
56 // check segments themselves
57 for (let i = 0; i < a.segments.length; i++) {
58 const aSegment = a.segments[i];
59 const bSegment = b.segments[i];
60
61 // if uris are different between segments there was a change
62 if (aSegment.uri !== bSegment.uri) {
63 return false;
64 }
65
66 // neither segment has a byterange, there will be no byterange change.
67 if (!aSegment.byterange && !bSegment.byterange) {
68 continue;
69 }
70 const aByterange = aSegment.byterange;
71 const bByterange = bSegment.byterange;
72
73 // if byterange only exists on one of the segments, there was a change.
74 if ((aByterange && !bByterange) || (!aByterange && bByterange)) {
75 return false;
76 }
77
78 // if both segments have byterange with different offsets, there was a change.
79 if (aByterange.offset !== bByterange.offset || aByterange.length !== bByterange.length) {
80 return false;
81 }
82 }
83
84 // if everything was the same with segments, this is the same playlist.
85 return true;
86};
87
88/**
89 * Parses the master XML string and updates playlist URI references.
90 *
91 * @param {Object} config
92 * Object of arguments
93 * @param {string} config.masterXml
94 * The mpd XML
95 * @param {string} config.srcUrl
96 * The mpd URL
97 * @param {Date} config.clientOffset
98 * A time difference between server and client
99 * @param {Object} config.sidxMapping
100 * SIDX mappings for moof/mdat URIs and byte ranges
101 * @return {Object}
102 * The parsed mpd manifest object
103 */
104export const parseMasterXml = ({
105 masterXml,
106 srcUrl,
107 clientOffset,
108 sidxMapping,
109 previousManifest
110}) => {
111 const manifest = parseMpd(masterXml, {
112 manifestUri: srcUrl,
113 clientOffset,
114 sidxMapping,
115 previousManifest
116 });
117
118 addPropertiesToMaster(manifest, srcUrl);
119
120 return manifest;
121};
122
123/**
124 * Returns a new master manifest that is the result of merging an updated master manifest
125 * into the original version.
126 *
127 * @param {Object} oldMaster
128 * The old parsed mpd object
129 * @param {Object} newMaster
130 * The updated parsed mpd object
131 * @return {Object}
132 * A new object representing the original master manifest with the updated media
133 * playlists merged in
134 */
135export const updateMaster = (oldMaster, newMaster, sidxMapping) => {
136 let noChanges = true;
137 let update = mergeOptions(oldMaster, {
138 // These are top level properties that can be updated
139 duration: newMaster.duration,
140 minimumUpdatePeriod: newMaster.minimumUpdatePeriod,
141 timelineStarts: newMaster.timelineStarts
142 });
143
144 // First update the playlists in playlist list
145 for (let i = 0; i < newMaster.playlists.length; i++) {
146 const playlist = newMaster.playlists[i];
147
148 if (playlist.sidx) {
149 const sidxKey = generateSidxKey(playlist.sidx);
150
151 // add sidx segments to the playlist if we have all the sidx info already
152 if (sidxMapping && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx) {
153 addSidxSegmentsToPlaylist(playlist, sidxMapping[sidxKey].sidx, playlist.sidx.resolvedUri);
154 }
155 }
156 const playlistUpdate = updatePlaylist(update, playlist, dashPlaylistUnchanged);
157
158 if (playlistUpdate) {
159 update = playlistUpdate;
160 noChanges = false;
161 }
162 }
163
164 // Then update media group playlists
165 forEachMediaGroup(newMaster, (properties, type, group, label) => {
166 if (properties.playlists && properties.playlists.length) {
167 const id = properties.playlists[0].id;
168 const playlistUpdate = updatePlaylist(update, properties.playlists[0], dashPlaylistUnchanged);
169
170 if (playlistUpdate) {
171 update = playlistUpdate;
172 // update the playlist reference within media groups
173 update.mediaGroups[type][group][label].playlists[0] = update.playlists[id];
174 noChanges = false;
175 }
176 }
177 });
178
179 if (newMaster.minimumUpdatePeriod !== oldMaster.minimumUpdatePeriod) {
180 noChanges = false;
181 }
182
183 if (noChanges) {
184 return null;
185 }
186
187 return update;
188};
189
190// SIDX should be equivalent if the URI and byteranges of the SIDX match.
191// If the SIDXs have maps, the two maps should match,
192// both `a` and `b` missing SIDXs is considered matching.
193// If `a` or `b` but not both have a map, they aren't matching.
194const equivalentSidx = (a, b) => {
195 const neitherMap = Boolean(!a.map && !b.map);
196
197 const equivalentMap = neitherMap || Boolean(a.map && b.map &&
198 a.map.byterange.offset === b.map.byterange.offset &&
199 a.map.byterange.length === b.map.byterange.length);
200
201 return equivalentMap &&
202 a.uri === b.uri &&
203 a.byterange.offset === b.byterange.offset &&
204 a.byterange.length === b.byterange.length;
205};
206
207// exported for testing
208export const compareSidxEntry = (playlists, oldSidxMapping) => {
209 const newSidxMapping = {};
210
211 for (const id in playlists) {
212 const playlist = playlists[id];
213 const currentSidxInfo = playlist.sidx;
214
215 if (currentSidxInfo) {
216 const key = generateSidxKey(currentSidxInfo);
217
218 if (!oldSidxMapping[key]) {
219 break;
220 }
221
222 const savedSidxInfo = oldSidxMapping[key].sidxInfo;
223
224 if (equivalentSidx(savedSidxInfo, currentSidxInfo)) {
225 newSidxMapping[key] = oldSidxMapping[key];
226 }
227 }
228 }
229
230 return newSidxMapping;
231};
232
233/**
234 * A function that filters out changed items as they need to be requested separately.
235 *
236 * The method is exported for testing
237 *
238 * @param {Object} master the parsed mpd XML returned via mpd-parser
239 * @param {Object} oldSidxMapping the SIDX to compare against
240 */
241export const filterChangedSidxMappings = (master, oldSidxMapping) => {
242 const videoSidx = compareSidxEntry(master.playlists, oldSidxMapping);
243 let mediaGroupSidx = videoSidx;
244
245 forEachMediaGroup(master, (properties, mediaType, groupKey, labelKey) => {
246 if (properties.playlists && properties.playlists.length) {
247 const playlists = properties.playlists;
248
249 mediaGroupSidx = mergeOptions(
250 mediaGroupSidx,
251 compareSidxEntry(playlists, oldSidxMapping)
252 );
253 }
254 });
255
256 return mediaGroupSidx;
257};
258
259export default class DashPlaylistLoader extends EventTarget {
260 // DashPlaylistLoader must accept either a src url or a playlist because subsequent
261 // playlist loader setups from media groups will expect to be able to pass a playlist
262 // (since there aren't external URLs to media playlists with DASH)
263 constructor(srcUrlOrPlaylist, vhs, options = { }, masterPlaylistLoader) {
264 super();
265
266 this.masterPlaylistLoader_ = masterPlaylistLoader || this;
267 if (!masterPlaylistLoader) {
268 this.isMaster_ = true;
269 }
270
271 const { withCredentials = false, handleManifestRedirects = false } = options;
272
273 this.vhs_ = vhs;
274 this.withCredentials = withCredentials;
275 this.handleManifestRedirects = handleManifestRedirects;
276
277 if (!srcUrlOrPlaylist) {
278 throw new Error('A non-empty playlist URL or object is required');
279 }
280
281 // event naming?
282 this.on('minimumUpdatePeriod', () => {
283 this.refreshXml_();
284 });
285
286 // live playlist staleness timeout
287 this.on('mediaupdatetimeout', () => {
288 this.refreshMedia_(this.media().id);
289 });
290
291 this.state = 'HAVE_NOTHING';
292 this.loadedPlaylists_ = {};
293 this.logger_ = logger('DashPlaylistLoader');
294
295 // initialize the loader state
296 // The masterPlaylistLoader will be created with a string
297 if (this.isMaster_) {
298 this.masterPlaylistLoader_.srcUrl = srcUrlOrPlaylist;
299 // TODO: reset sidxMapping between period changes
300 // once multi-period is refactored
301 this.masterPlaylistLoader_.sidxMapping_ = {};
302 } else {
303 this.childPlaylist_ = srcUrlOrPlaylist;
304 }
305 }
306
307 requestErrored_(err, request, startingState) {
308 // disposed
309 if (!this.request) {
310 return true;
311 }
312
313 // pending request is cleared
314 this.request = null;
315
316 if (err) {
317 // use the provided error object or create one
318 // based on the request/response
319 this.error = typeof err === 'object' && !(err instanceof Error) ? err : {
320 status: request.status,
321 message: 'DASH request error at URL: ' + request.uri,
322 response: request.response,
323 // MEDIA_ERR_NETWORK
324 code: 2
325 };
326 if (startingState) {
327 this.state = startingState;
328 }
329
330 this.trigger('error');
331 return true;
332 }
333 }
334
335 /**
336 * Verify that the container of the sidx segment can be parsed
337 * and if it can, get and parse that segment.
338 */
339 addSidxSegments_(playlist, startingState, cb) {
340 const sidxKey = playlist.sidx && generateSidxKey(playlist.sidx);
341
342 // playlist lacks sidx or sidx segments were added to this playlist already.
343 if (!playlist.sidx || !sidxKey || this.masterPlaylistLoader_.sidxMapping_[sidxKey]) {
344 // keep this function async
345 this.mediaRequest_ = window.setTimeout(() => cb(false), 0);
346 return;
347 }
348
349 // resolve the segment URL relative to the playlist
350 const uri = resolveManifestRedirect(this.handleManifestRedirects, playlist.sidx.resolvedUri);
351
352 const fin = (err, request) => {
353 if (this.requestErrored_(err, request, startingState)) {
354 return;
355 }
356
357 const sidxMapping = this.masterPlaylistLoader_.sidxMapping_;
358 let sidx;
359
360 try {
361 sidx = parseSidx(toUint8(request.response).subarray(8));
362 } catch (e) {
363 // sidx parsing failed.
364 this.requestErrored_(e, request, startingState);
365 return;
366 }
367
368 sidxMapping[sidxKey] = {
369 sidxInfo: playlist.sidx,
370 sidx
371 };
372
373 addSidxSegmentsToPlaylist(playlist, sidx, playlist.sidx.resolvedUri);
374
375 return cb(true);
376 };
377
378 this.request = containerRequest(uri, this.vhs_.xhr, (err, request, container, bytes) => {
379 if (err) {
380 return fin(err, request);
381 }
382
383 if (!container || container !== 'mp4') {
384 return fin({
385 status: request.status,
386 message: `Unsupported ${container || 'unknown'} container type for sidx segment at URL: ${uri}`,
387 // response is just bytes in this case
388 // but we really don't want to return that.
389 response: '',
390 playlist,
391 internal: true,
392 blacklistDuration: Infinity,
393 // MEDIA_ERR_NETWORK
394 code: 2
395 }, request);
396 }
397
398 // if we already downloaded the sidx bytes in the container request, use them
399 const {offset, length} = playlist.sidx.byterange;
400
401 if (bytes.length >= (length + offset)) {
402 return fin(err, {
403 response: bytes.subarray(offset, offset + length),
404 status: request.status,
405 uri: request.uri
406 });
407 }
408
409 // otherwise request sidx bytes
410 this.request = this.vhs_.xhr({
411 uri,
412 responseType: 'arraybuffer',
413 headers: segmentXhrHeaders({byterange: playlist.sidx.byterange})
414 }, fin);
415 });
416 }
417
418 dispose() {
419 this.trigger('dispose');
420 this.stopRequest();
421 this.loadedPlaylists_ = {};
422 window.clearTimeout(this.minimumUpdatePeriodTimeout_);
423 window.clearTimeout(this.mediaRequest_);
424 window.clearTimeout(this.mediaUpdateTimeout);
425 this.mediaUpdateTimeout = null;
426 this.mediaRequest_ = null;
427 this.minimumUpdatePeriodTimeout_ = null;
428
429 if (this.masterPlaylistLoader_.createMupOnMedia_) {
430 this.off('loadedmetadata', this.masterPlaylistLoader_.createMupOnMedia_);
431 this.masterPlaylistLoader_.createMupOnMedia_ = null;
432 }
433
434 this.off();
435 }
436
437 hasPendingRequest() {
438 return this.request || this.mediaRequest_;
439 }
440
441 stopRequest() {
442 if (this.request) {
443 const oldRequest = this.request;
444
445 this.request = null;
446 oldRequest.onreadystatechange = null;
447 oldRequest.abort();
448 }
449 }
450
451 media(playlist) {
452 // getter
453 if (!playlist) {
454 return this.media_;
455 }
456
457 // setter
458 if (this.state === 'HAVE_NOTHING') {
459 throw new Error('Cannot switch media playlist from ' + this.state);
460 }
461
462 const startingState = this.state;
463
464 // find the playlist object if the target playlist has been specified by URI
465 if (typeof playlist === 'string') {
466 if (!this.masterPlaylistLoader_.master.playlists[playlist]) {
467 throw new Error('Unknown playlist URI: ' + playlist);
468 }
469 playlist = this.masterPlaylistLoader_.master.playlists[playlist];
470 }
471
472 const mediaChange = !this.media_ || playlist.id !== this.media_.id;
473
474 // switch to previously loaded playlists immediately
475 if (mediaChange &&
476 this.loadedPlaylists_[playlist.id] &&
477 this.loadedPlaylists_[playlist.id].endList) {
478 this.state = 'HAVE_METADATA';
479 this.media_ = playlist;
480
481 // trigger media change if the active media has been updated
482 if (mediaChange) {
483 this.trigger('mediachanging');
484 this.trigger('mediachange');
485 }
486 return;
487 }
488
489 // switching to the active playlist is a no-op
490 if (!mediaChange) {
491 return;
492 }
493
494 // switching from an already loaded playlist
495 if (this.media_) {
496 this.trigger('mediachanging');
497 }
498 this.addSidxSegments_(playlist, startingState, (sidxChanged) => {
499 // everything is ready just continue to haveMetadata
500 this.haveMetadata({startingState, playlist});
501 });
502 }
503
504 haveMetadata({startingState, playlist}) {
505 this.state = 'HAVE_METADATA';
506 this.loadedPlaylists_[playlist.id] = playlist;
507 this.mediaRequest_ = null;
508
509 // This will trigger loadedplaylist
510 this.refreshMedia_(playlist.id);
511
512 // fire loadedmetadata the first time a media playlist is loaded
513 // to resolve setup of media groups
514 if (startingState === 'HAVE_MASTER') {
515 this.trigger('loadedmetadata');
516 } else {
517 // trigger media change if the active media has been updated
518 this.trigger('mediachange');
519 }
520 }
521
522 pause() {
523 if (this.masterPlaylistLoader_.createMupOnMedia_) {
524 this.off('loadedmetadata', this.masterPlaylistLoader_.createMupOnMedia_);
525 this.masterPlaylistLoader_.createMupOnMedia_ = null;
526 }
527 this.stopRequest();
528 window.clearTimeout(this.mediaUpdateTimeout);
529 this.mediaUpdateTimeout = null;
530 if (this.isMaster_) {
531 window.clearTimeout(this.masterPlaylistLoader_.minimumUpdatePeriodTimeout_);
532 this.masterPlaylistLoader_.minimumUpdatePeriodTimeout_ = null;
533 }
534 if (this.state === 'HAVE_NOTHING') {
535 // If we pause the loader before any data has been retrieved, its as if we never
536 // started, so reset to an unstarted state.
537 this.started = false;
538 }
539 }
540
541 load(isFinalRendition) {
542 window.clearTimeout(this.mediaUpdateTimeout);
543 this.mediaUpdateTimeout = null;
544
545 const media = this.media();
546
547 if (isFinalRendition) {
548 const delay = media ? (media.targetDuration / 2) * 1000 : 5 * 1000;
549
550 this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay);
551 return;
552 }
553
554 // because the playlists are internal to the manifest, load should either load the
555 // main manifest, or do nothing but trigger an event
556 if (!this.started) {
557 this.start();
558 return;
559 }
560
561 if (media && !media.endList) {
562 // Check to see if this is the master loader and the MUP was cleared (this happens
563 // when the loader was paused). `media` should be set at this point since one is always
564 // set during `start()`.
565 if (this.isMaster_ && !this.minimumUpdatePeriodTimeout_) {
566 // Trigger minimumUpdatePeriod to refresh the master manifest
567 this.trigger('minimumUpdatePeriod');
568 // Since there was no prior minimumUpdatePeriodTimeout it should be recreated
569 this.updateMinimumUpdatePeriodTimeout_();
570 }
571 this.trigger('mediaupdatetimeout');
572 } else {
573 this.trigger('loadedplaylist');
574 }
575 }
576
577 start() {
578 this.started = true;
579
580 // We don't need to request the master manifest again
581 // Call this asynchronously to match the xhr request behavior below
582 if (!this.isMaster_) {
583 this.mediaRequest_ = window.setTimeout(() => this.haveMaster_(), 0);
584 return;
585 }
586
587 this.requestMaster_((req, masterChanged) => {
588 this.haveMaster_();
589
590 if (!this.hasPendingRequest() && !this.media_) {
591 this.media(this.masterPlaylistLoader_.master.playlists[0]);
592 }
593 });
594 }
595
596 requestMaster_(cb) {
597 this.request = this.vhs_.xhr({
598 uri: this.masterPlaylistLoader_.srcUrl,
599 withCredentials: this.withCredentials
600 }, (error, req) => {
601 if (this.requestErrored_(error, req)) {
602 if (this.state === 'HAVE_NOTHING') {
603 this.started = false;
604 }
605 return;
606 }
607
608 const masterChanged = req.responseText !== this.masterPlaylistLoader_.masterXml_;
609
610 this.masterPlaylistLoader_.masterXml_ = req.responseText;
611
612 if (req.responseHeaders && req.responseHeaders.date) {
613 this.masterLoaded_ = Date.parse(req.responseHeaders.date);
614 } else {
615 this.masterLoaded_ = Date.now();
616 }
617
618 this.masterPlaylistLoader_.srcUrl = resolveManifestRedirect(this.handleManifestRedirects, this.masterPlaylistLoader_.srcUrl, req);
619
620 if (masterChanged) {
621 this.handleMaster_();
622 this.syncClientServerClock_(() => {
623 return cb(req, masterChanged);
624 });
625 return;
626 }
627
628 return cb(req, masterChanged);
629 });
630
631 }
632
633 /**
634 * Parses the master xml for UTCTiming node to sync the client clock to the server
635 * clock. If the UTCTiming node requires a HEAD or GET request, that request is made.
636 *
637 * @param {Function} done
638 * Function to call when clock sync has completed
639 */
640 syncClientServerClock_(done) {
641 const utcTiming = parseUTCTiming(this.masterPlaylistLoader_.masterXml_);
642
643 // No UTCTiming element found in the mpd. Use Date header from mpd request as the
644 // server clock
645 if (utcTiming === null) {
646 this.masterPlaylistLoader_.clientOffset_ = this.masterLoaded_ - Date.now();
647 return done();
648 }
649
650 if (utcTiming.method === 'DIRECT') {
651 this.masterPlaylistLoader_.clientOffset_ = utcTiming.value - Date.now();
652 return done();
653 }
654
655 this.request = this.vhs_.xhr({
656 uri: resolveUrl(this.masterPlaylistLoader_.srcUrl, utcTiming.value),
657 method: utcTiming.method,
658 withCredentials: this.withCredentials
659 }, (error, req) => {
660 // disposed
661 if (!this.request) {
662 return;
663 }
664
665 if (error) {
666 // sync request failed, fall back to using date header from mpd
667 // TODO: log warning
668 this.masterPlaylistLoader_.clientOffset_ = this.masterLoaded_ - Date.now();
669 return done();
670 }
671
672 let serverTime;
673
674 if (utcTiming.method === 'HEAD') {
675 if (!req.responseHeaders || !req.responseHeaders.date) {
676 // expected date header not preset, fall back to using date header from mpd
677 // TODO: log warning
678 serverTime = this.masterLoaded_;
679 } else {
680 serverTime = Date.parse(req.responseHeaders.date);
681 }
682 } else {
683 serverTime = Date.parse(req.responseText);
684 }
685
686 this.masterPlaylistLoader_.clientOffset_ = serverTime - Date.now();
687
688 done();
689 });
690 }
691
692 haveMaster_() {
693 this.state = 'HAVE_MASTER';
694 if (this.isMaster_) {
695 // We have the master playlist at this point, so
696 // trigger this to allow MasterPlaylistController
697 // to make an initial playlist selection
698 this.trigger('loadedplaylist');
699 } else if (!this.media_) {
700 // no media playlist was specifically selected so select
701 // the one the child playlist loader was created with
702 this.media(this.childPlaylist_);
703 }
704 }
705
706 handleMaster_() {
707 // clear media request
708 this.mediaRequest_ = null;
709
710 const oldMaster = this.masterPlaylistLoader_.master;
711
712 let newMaster = parseMasterXml({
713 masterXml: this.masterPlaylistLoader_.masterXml_,
714 srcUrl: this.masterPlaylistLoader_.srcUrl,
715 clientOffset: this.masterPlaylistLoader_.clientOffset_,
716 sidxMapping: this.masterPlaylistLoader_.sidxMapping_,
717 previousManifest: oldMaster
718 });
719
720 // if we have an old master to compare the new master against
721 if (oldMaster) {
722 newMaster = updateMaster(oldMaster, newMaster, this.masterPlaylistLoader_.sidxMapping_);
723 }
724
725 // only update master if we have a new master
726 this.masterPlaylistLoader_.master = newMaster ? newMaster : oldMaster;
727 const location = this.masterPlaylistLoader_.master.locations && this.masterPlaylistLoader_.master.locations[0];
728
729 if (location && location !== this.masterPlaylistLoader_.srcUrl) {
730 this.masterPlaylistLoader_.srcUrl = location;
731 }
732
733 if (!oldMaster || (newMaster && newMaster.minimumUpdatePeriod !== oldMaster.minimumUpdatePeriod)) {
734 this.updateMinimumUpdatePeriodTimeout_();
735 }
736
737 return Boolean(newMaster);
738 }
739
740 updateMinimumUpdatePeriodTimeout_() {
741 const mpl = this.masterPlaylistLoader_;
742
743 // cancel any pending creation of mup on media
744 // a new one will be added if needed.
745 if (mpl.createMupOnMedia_) {
746 mpl.off('loadedmetadata', mpl.createMupOnMedia_);
747 mpl.createMupOnMedia_ = null;
748 }
749
750 // clear any pending timeouts
751 if (mpl.minimumUpdatePeriodTimeout_) {
752 window.clearTimeout(mpl.minimumUpdatePeriodTimeout_);
753 mpl.minimumUpdatePeriodTimeout_ = null;
754 }
755
756 let mup = mpl.master && mpl.master.minimumUpdatePeriod;
757
758 // If the minimumUpdatePeriod has a value of 0, that indicates that the current
759 // MPD has no future validity, so a new one will need to be acquired when new
760 // media segments are to be made available. Thus, we use the target duration
761 // in this case
762 if (mup === 0) {
763 if (mpl.media()) {
764 mup = mpl.media().targetDuration * 1000;
765 } else {
766 mpl.createMupOnMedia_ = mpl.updateMinimumUpdatePeriodTimeout_;
767 mpl.one('loadedmetadata', mpl.createMupOnMedia_);
768 }
769 }
770
771 // if minimumUpdatePeriod is invalid or <= zero, which
772 // can happen when a live video becomes VOD. skip timeout
773 // creation.
774 if (typeof mup !== 'number' || mup <= 0) {
775 if (mup < 0) {
776 this.logger_(`found invalid minimumUpdatePeriod of ${mup}, not setting a timeout`);
777 }
778 return;
779 }
780
781 this.createMUPTimeout_(mup);
782 }
783
784 createMUPTimeout_(mup) {
785 const mpl = this.masterPlaylistLoader_;
786
787 mpl.minimumUpdatePeriodTimeout_ = window.setTimeout(() => {
788 mpl.minimumUpdatePeriodTimeout_ = null;
789 mpl.trigger('minimumUpdatePeriod');
790 mpl.createMUPTimeout_(mup);
791 }, mup);
792 }
793
794 /**
795 * Sends request to refresh the master xml and updates the parsed master manifest
796 */
797 refreshXml_() {
798 this.requestMaster_((req, masterChanged) => {
799 if (!masterChanged) {
800 return;
801 }
802
803 if (this.media_) {
804 this.media_ = this.masterPlaylistLoader_.master.playlists[this.media_.id];
805 }
806
807 // This will filter out updated sidx info from the mapping
808 this.masterPlaylistLoader_.sidxMapping_ = filterChangedSidxMappings(
809 this.masterPlaylistLoader_.master,
810 this.masterPlaylistLoader_.sidxMapping_
811 );
812
813 this.addSidxSegments_(this.media(), this.state, (sidxChanged) => {
814 // TODO: do we need to reload the current playlist?
815 this.refreshMedia_(this.media().id);
816 });
817 });
818 }
819
820 /**
821 * Refreshes the media playlist by re-parsing the master xml and updating playlist
822 * references. If this is an alternate loader, the updated parsed manifest is retrieved
823 * from the master loader.
824 */
825 refreshMedia_(mediaID) {
826 if (!mediaID) {
827 throw new Error('refreshMedia_ must take a media id');
828 }
829
830 // for master we have to reparse the master xml
831 // to re-create segments based on current timing values
832 // which may change media. We only skip updating master
833 // if this is the first time this.media_ is being set.
834 // as master was just parsed in that case.
835 if (this.media_ && this.isMaster_) {
836 this.handleMaster_();
837 }
838
839 const playlists = this.masterPlaylistLoader_.master.playlists;
840 const mediaChanged = !this.media_ || this.media_ !== playlists[mediaID];
841
842 if (mediaChanged) {
843 this.media_ = playlists[mediaID];
844 } else {
845 this.trigger('playlistunchanged');
846 }
847
848 if (!this.mediaUpdateTimeout) {
849 const createMediaUpdateTimeout = () => {
850 if (this.media().endList) {
851 return;
852 }
853
854 this.mediaUpdateTimeout = window.setTimeout(() => {
855 this.trigger('mediaupdatetimeout');
856 createMediaUpdateTimeout();
857 }, refreshDelay(this.media(), Boolean(mediaChanged)));
858 };
859
860 createMediaUpdateTimeout();
861 }
862
863 this.trigger('loadedplaylist');
864 }
865}