Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: live dash segment changes should be considered a playlist update #1065

Merged
merged 3 commits into from
Feb 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while I don't like returning booleans directly, this does seem like it's more readable than trying to convert all this into a single expression (even if it is possible)

} 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe worth checking a and b's segment's length first? If it changed, the manifest has updated, no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, isPlaylistUnchanged does this check.

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"?>
gkatsev marked this conversation as resolved.
Show resolved Hide resolved
<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>