Skip to content

Commit

Permalink
fix: live dash segment changes should be considered a playlist update (
Browse files Browse the repository at this point in the history
…#1065)

Currently dash content goes off of the number attribute for segments to determine the mediaSequence. This is problematic as the first segment is almost always listed as 1. So in essence when we request the playlist again the media sequence will be the same, and likely the amount of segments will be the same, so we will determine that this playlist update didn't update anything at all.
  • Loading branch information
brandonocasey authored Feb 19, 2021
1 parent cf2efcb commit 1ce7838
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 11 deletions.
68 changes: 65 additions & 3 deletions src/dash-playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
} from 'mpd-parser';
import {
refreshDelay,
updateMaster as updatePlaylist
updateMaster as updatePlaylist,
isPlaylistUnchanged
} from './playlist-loader';
import { resolveUrl, resolveManifestRedirect } from './resolve-url';
import parseSidx from 'mux.js/lib/tools/parse-sidx';
Expand All @@ -21,6 +22,67 @@ import {toUint8} from '@videojs/vhs-utils/es/byte-helpers';

const { EventTarget, mergeOptions } = videojs;

const dashPlaylistUnchanged = function(a, b) {
if (!isPlaylistUnchanged(a, b)) {
return false;
}

// for dash the above check will often return true in scenarios where
// the playlist actually has changed because mediaSequence isn't a
// dash thing, and we often set it to 1. So if the playlists have the same amount
// of segments we return true.
// So for dash we need to make sure that the underlying segments are different.

// if sidx changed then the playlists are different.
if (a.sidx && b.sidx && (a.sidx.offset !== b.sidx.offset || a.sidx.length !== b.sidx.length)) {
return false;
} else if ((!a.sidx && b.sidx) || (a.sidx && !b.sidx)) {
return false;
}

// one or the other does not have segments
// there was a change.
if (a.segments && !b.segments || !a.segments && b.segments) {
return false;
}

// neither has segments nothing changed
if (!a.segments && !b.segments) {
return true;
}

// check segments themselves
for (let i = 0; i < a.segments.length; i++) {
const aSegment = a.segments[i];
const bSegment = b.segments[i];

// if uris are different between segments there was a change
if (aSegment.uri !== bSegment.uri) {
return false;
}

// neither segment has a byterange, there will be no byterange change.
if (!aSegment.byterange && !bSegment.byterange) {
continue;
}
const aByterange = aSegment.byterange;
const bByterange = bSegment.byterange;

// if byterange only exists on one of the segments, there was a change.
if ((aByterange && !bByterange) || (!aByterange && bByterange)) {
return false;
}

// if both segments have byterange with different offsets, there was a change.
if (aByterange.offset !== bByterange.offset || aByterange.length !== bByterange.length) {
return false;
}
}

// if everything was the same with segments, this is the same playlist.
return true;
};

/**
* Parses the master XML string and updates playlist URI references.
*
Expand Down Expand Up @@ -92,7 +154,7 @@ export const updateMaster = (oldMaster, newMaster, sidxMapping) => {
addSidxSegmentsToPlaylist(playlist, sidxMapping[sidxKey].sidx, playlist.sidx.resolvedUri);
}
}
const playlistUpdate = updatePlaylist(update, playlist);
const playlistUpdate = updatePlaylist(update, playlist, dashPlaylistUnchanged);

if (playlistUpdate) {
update = playlistUpdate;
Expand All @@ -104,7 +166,7 @@ export const updateMaster = (oldMaster, newMaster, sidxMapping) => {
forEachMediaGroup(newMaster, (properties, type, group, label) => {
if (properties.playlists && properties.playlists.length) {
const id = properties.playlists[0].id;
const playlistUpdate = updatePlaylist(update, properties.playlists[0]);
const playlistUpdate = updatePlaylist(update, properties.playlists[0], dashPlaylistUnchanged);

if (playlistUpdate) {
update = playlistUpdate;
Expand Down
18 changes: 10 additions & 8 deletions src/playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export const resolveSegmentUris = (segment, baseUri) => {
}
};

// consider the playlist unchanged if the playlist object is the same or
// the number of segments is equal, the media sequence number is unchanged,
// and this playlist hasn't become the end of the playlist
export const isPlaylistUnchanged = (a, b) => a === b ||
(a.segments && b.segments && a.segments.length === b.segments.length &&
a.endList === b.endList &&
a.mediaSequence === b.mediaSequence);

/**
* Returns a new master playlist that is the result of merging an
* updated media playlist into the original version. If the
Expand All @@ -69,21 +77,15 @@ export const resolveSegmentUris = (segment, baseUri) => {
* master playlist with the updated media playlist merged in, or
* null if the merge produced no change.
*/
export const updateMaster = (master, media) => {
export const updateMaster = (master, media, unchangedCheck = isPlaylistUnchanged) => {
const result = mergeOptions(master, {});
const playlist = result.playlists[media.id];

if (!playlist) {
return null;
}

// consider the playlist unchanged if the number of segments is equal, the media
// sequence number is unchanged, and this playlist hasn't become the end of the playlist
if (playlist.segments &&
media.segments &&
playlist.segments.length === media.segments.length &&
playlist.endList === media.endList &&
playlist.mediaSequence === media.mediaSequence) {
if (unchangedCheck(playlist, media)) {
return null;
}

Expand Down
158 changes: 158 additions & 0 deletions test/dash-playlist-loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,164 @@ QUnit.test('refreshXml_: updates media playlist reference if master changed', fu
);
});

QUnit.test('refreshXml_: updates playlists if segment uri changed, but media sequence did not', function(assert) {
const loader = new DashPlaylistLoader('dash.mpd', this.fakeVhs);

loader.load();
this.standardXHRResponse(this.requests.shift());

const oldMaster = loader.master;
const oldMedia = loader.media();

// change segment uris
const newMasterXml = loader.masterXml_
.replace(/\$RepresentationID\$/g, '$RepresentationID$-foo')
.replace('media="segment-$Number$.mp4"', 'media="segment-foo$Number$.mp4"');

loader.refreshXml_();

assert.strictEqual(this.requests.length, 1, 'manifest is being requested');

this.requests.shift().respond(200, null, newMasterXml);

const newMaster = loader.master;
const newMedia = loader.media();

assert.notEqual(newMaster, oldMaster, 'master changed');
assert.notEqual(newMedia, oldMedia, 'media changed');
assert.equal(
newMedia,
newMaster.playlists[newMedia.id],
'media from updated master'
);
});

QUnit.test('refreshXml_: updates playlists if sidx changed', function(assert) {
const loader = new DashPlaylistLoader('dash-sidx.mpd', this.fakeVhs);

loader.load();
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift(), mp4VideoInitSegment().subarray(0, 10));
this.standardXHRResponse(this.requests.shift(), sidxResponse());

const oldMaster = loader.master;
const oldMedia = loader.media();

const newMasterXml = loader.masterXml_
.replace(/indexRange="200-399"/g, 'indexRange="500-699"');

loader.refreshXml_();

assert.strictEqual(this.requests.length, 1, 'manifest is being requested');

this.standardXHRResponse(this.requests.shift(), newMasterXml);

const newMaster = loader.master;
const newMedia = loader.media();

assert.notEqual(newMaster, oldMaster, 'master changed');
assert.notEqual(newMedia, oldMedia, 'media changed');
assert.equal(
newMedia,
newMaster.playlists[newMedia.id],
'media from updated master'
);
});

QUnit.test('refreshXml_: updates playlists if sidx removed', function(assert) {
const loader = new DashPlaylistLoader('dash-sidx.mpd', this.fakeVhs);

loader.load();
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift(), mp4VideoInitSegment().subarray(0, 10));
this.standardXHRResponse(this.requests.shift(), sidxResponse());

const oldMaster = loader.master;
const oldMedia = loader.media();

const newMasterXml = loader.masterXml_
.replace(/indexRange="200-399"/g, '');

loader.refreshXml_();

assert.strictEqual(this.requests.length, 1, 'manifest is being requested');

this.standardXHRResponse(this.requests.shift(), newMasterXml);

const newMaster = loader.master;
const newMedia = loader.media();

assert.notEqual(newMaster, oldMaster, 'master changed');
assert.notEqual(newMedia, oldMedia, 'media changed');
assert.equal(
newMedia,
newMaster.playlists[newMedia.id],
'media from updated master'
);
});

QUnit.test('refreshXml_: updates playlists if only segment byteranges change', function(assert) {
const loader = new DashPlaylistLoader('dashByterange.mpd', this.fakeVhs);

loader.load();
this.standardXHRResponse(this.requests.shift());

const oldMaster = loader.master;
const oldMedia = loader.media();

const newMasterXml = loader.masterXml_
.replace('mediaRange="12883295-13124492"', 'mediaRange="12883296-13124492"');

loader.refreshXml_();

assert.strictEqual(this.requests.length, 1, 'manifest is being requested');

this.standardXHRResponse(this.requests.shift(), newMasterXml);

const newMaster = loader.master;
const newMedia = loader.media();

assert.notEqual(newMaster, oldMaster, 'master changed');
assert.notEqual(newMedia, oldMedia, 'media changed');
assert.equal(
newMedia,
newMaster.playlists[newMedia.id],
'media from updated master'
);
});

QUnit.test('refreshXml_: updates playlists if sidx removed', function(assert) {
const loader = new DashPlaylistLoader('dash-sidx.mpd', this.fakeVhs);

loader.load();
this.standardXHRResponse(this.requests.shift());
this.standardXHRResponse(this.requests.shift(), mp4VideoInitSegment().subarray(0, 10));
this.standardXHRResponse(this.requests.shift(), sidxResponse());

const oldMaster = loader.master;
const oldMedia = loader.media();

const newMasterXml = loader.masterXml_
.replace(/indexRange="200-399"/g, '');

loader.refreshXml_();

assert.strictEqual(this.requests.length, 1, 'manifest is being requested');

this.standardXHRResponse(this.requests.shift(), newMasterXml);

const newMaster = loader.master;
const newMedia = loader.media();

assert.notEqual(newMaster, oldMaster, 'master changed');
assert.notEqual(newMedia, oldMedia, 'media changed');
assert.equal(
newMedia,
newMaster.playlists[newMedia.id],
'media from updated master'
);
});

QUnit.test('addSidxSegments_: updates master with sidx information', function(assert) {
const loader = new DashPlaylistLoader('dash.mpd', this.fakeVhs);
const sidxData = sidxResponse();
Expand Down
58 changes: 58 additions & 0 deletions test/manifests/dashByterange.mpd
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:mpeg:DASH:schema:MPD:2011"
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011"
profiles="urn:mpeg:dash:profile:isoff-main:2011"
type="static"
mediaPresentationDuration="PT0H9M56.46S"
minBufferTime="PT15.0S">
<BaseURL>http://www-itec.uni-klu.ac.at/ftp/datasets/mmsys12/BigBuckBunny/bunny_15s/</BaseURL>
<Period start="PT0S">
<AdaptationSet bitstreamSwitching="true">
<Representation id="3" codecs="avc1" mimeType="video/mp4" width="480" height="360" startWithSAP="1" bandwidth="176031">
<SegmentList duration="15">
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="868-347185" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="347186-664464" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="664465-1027685" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="1027686-1367784" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="1367785-1677710" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="1677711-2001517" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="2001518-2290173" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="2290174-2634238" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="2634239-2985994" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="2985995-3323725" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="3323726-3650264" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="3650265-3978004" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="3978005-4304349" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="4304350-4629741" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="4629742-4951671" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="4951672-5282910" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="5282911-5629211" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="5629212-5963914" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="5963915-6312646" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="6312647-6612703" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="6612704-6923786" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="6923787-7272547" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="7272548-7590097" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="7590098-7947017" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="7947018-8276044" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="8276045-8551338" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="8551339-8866104" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="8866105-9171839" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="9171840-9515898" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="9515899-9849503" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="9849504-10161047" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="10161048-10494887" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="10494888-10786011" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="10786012-11142570" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="11142571-11503643" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="11503644-11859498" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="11859499-12213313" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="12213314-12578158" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="12578159-12883294" />
<SegmentURL media="bunny_15s_200kbit/bunny_200kbit_dashNonSeg.mp4" mediaRange="12883295-13124492" />
</SegmentList>
</Representation>
</AdaptationSet>
</Period>
</MPD>

0 comments on commit 1ce7838

Please sign in to comment.