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

feat: add support for dash manifests describing sidx boxes #455

Merged
merged 51 commits into from
Apr 12, 2019
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
3a9a95d
Adding initial tests to ensure coverage of states and events
Jan 24, 2019
13c2f0a
Initial implementation of code changes:
Feb 5, 2019
5aa5d16
fixes live manifests
Feb 6, 2019
0fd7f87
loadedplaylist should happen twice, once when the master manifest ret…
Feb 6, 2019
85049e8
Fix eme setup:
Feb 7, 2019
ff40727
Fixes:
Feb 7, 2019
67c92cc
Cleanup:
Feb 8, 2019
eb14f52
Cleanup:
Feb 8, 2019
1bb3dd4
only trigger loadedplaylist and loadedmetadata when loading a new VOD…
Feb 12, 2019
9aab16d
Fix: return to using zero delays to mimic asynchronous code in start …
Feb 12, 2019
65b9d0e
docs:
Feb 12, 2019
02c8595
remove comment
Feb 12, 2019
6723894
fix: media selection fallback
Feb 13, 2019
6ebf1d2
fix: mock loader.hasPendingRequest for functional tests
Feb 14, 2019
fafa47e
both haveMaster and haveMetadata can use zero delays. Update docs to …
Feb 14, 2019
b5f24dd
initial work
Nov 2, 2018
8a34323
add scaffolding
Nov 10, 2018
f7195dc
move sidx requesting into the dash playlist loader
Nov 30, 2018
ecd9699
first master playlist load complete
Dec 3, 2018
e3109dc
make parseMasterXml async
Dec 7, 2018
55e02c2
fix test
Dec 7, 2018
38c11a3
slight change to make things clearer
Jan 3, 2019
e320129
request segment base sidxes for media groups also
Jan 4, 2019
c3d39f1
clean up code a bit
Jan 5, 2019
97e37d6
CR comments
Jan 11, 2019
31c8ad8
fix:
Feb 14, 2019
420fe73
- update to request sidx on a playlist basis
Feb 14, 2019
77a8e9f
fix: store sidxInfo reference before making request for sidx
Feb 19, 2019
7c74d4b
clean up request code
Feb 22, 2019
e80ad0a
add integration tests for sidx
Feb 25, 2019
4dd41fd
tests: add unit tests for:
Feb 25, 2019
3d101f8
tests:
Feb 25, 2019
795f420
tests: added test for asynch haveMaster handling
Feb 26, 2019
089d874
fixing rebase mistake
Feb 27, 2019
94fb586
tests: update handleSidxResponse tests to use class method and check …
Mar 4, 2019
859d111
add support for sidx info changing in a live playlist
Mar 5, 2019
ca459b4
tests: refreshXml_
Mar 6, 2019
d42cf8a
fixes for filterSidxMapping and utilities and tests
Mar 11, 2019
870c8cc
linting updates
Mar 11, 2019
e2b8754
add redirect support for sidx requests
Mar 11, 2019
3bf1d12
use pre-release of mpd-parser
gkatsev Apr 5, 2019
7454652
store response, if any. We aren't allowed to read responseText since …
gkatsev Apr 8, 2019
0dafa87
switch test to use response as well
gkatsev Apr 8, 2019
0fe8dbb
fix erroring response test case
gkatsev Apr 8, 2019
4291943
update mpd-parser to latest
gkatsev Apr 11, 2019
9e00023
exit early if not sidx, one less block
gkatsev Apr 11, 2019
a9fc3b0
only one will
gkatsev Apr 11, 2019
ef6f39c
use boolean operations for equivalentSidx
gkatsev Apr 11, 2019
60c009e
comment and rename filterSidxMapping to filterChangedSidxMappings
gkatsev Apr 11, 2019
daa8c0f
rename handleSidxResponse to sidxRequestFinished to match segment loader
gkatsev Apr 11, 2019
e4155c6
Merge branch 'master' into request-sidx
gkatsev Apr 11, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"aes-decrypter": "3.0.0",
"global": "^4.3.0",
"m3u8-parser": "4.3.0",
"mpd-parser": "0.7.0",
"mpd-parser": "0.8.0-0",
gkatsev marked this conversation as resolved.
Show resolved Hide resolved
"mux.js": "5.1.0",
"url-toolkit": "^2.1.3",
"video.js": "^6.8.0 || ^7.0.0"
Expand Down
2 changes: 1 addition & 1 deletion scripts/segments-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module.exports = {
const file = path.resolve(segmentsDir, files.shift());
const extname = path.extname(file);

if (extname === '.ts') {
if (extname === '.ts' || extname === '.mp4') {
// read the file directly as a buffer before converting to base64
const base64Segment = fs.readFileSync(file).toString('base64');

Expand Down
248 changes: 236 additions & 12 deletions src/dash-playlist-loader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import videojs from 'video.js';
import { parse as parseMpd, parseUTCTiming } from 'mpd-parser';
import {
parse as parseMpd,
parseUTCTiming
} from 'mpd-parser';
import {
refreshDelay,
setupMediaPlaylists,
Expand All @@ -8,6 +11,8 @@ import {
forEachMediaGroup
} from './playlist-loader';
import { resolveUrl, resolveManifestRedirect } from './resolve-url';
import mp4Inspector from 'mux.js/lib/tools/mp4-inspector';
import { segmentXhrHeaders } from './xhr';
import window from 'global/window';

const { EventTarget, mergeOptions } = videojs;
Expand Down Expand Up @@ -65,6 +70,103 @@ export const updateMaster = (oldMaster, newMaster) => {
return update;
};

export const generateSidxKey = (sidxInfo) => {
// should be non-inclusive
const sidxByteRangeEnd =
sidxInfo.byterange.offset +
sidxInfo.byterange.length -
1;

return sidxInfo.uri + '-' +
sidxInfo.byterange.offset + '-' +
sidxByteRangeEnd;
};

const equivalentSidx = (a, b) => {
let equivalentMap = true;

if (a.map && b.map) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if one of them has a map and the other does not? Doesn't that make them not equivalent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, the map here is the init segment for the playlist

Copy link
Contributor

Choose a reason for hiding this comment

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

So should the check be (a.map || b.map) so that we check if either has a map?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suppose the if condition is unnecessary as the value of equivalentMap would be falsy if either a or b did not have map

Copy link
Member

Choose a reason for hiding this comment

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

equivalentMap already checks that both have .map, so, I'm going to remove the if statement.

Copy link
Member

Choose a reason for hiding this comment

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

Improved

equivalentMap = a.map && b.map &&
a.map.byterange.offset === b.map.byterange.offset &&
a.map.byterange.length === b.map.byterange.length;
}

return equivalentMap &&
a.uri === b.uri &&
a.byterange.offset === b.byterange.offset &&
a.byterange.length === b.byterange.length;
};

// exported for testing
export const compareSidxEntry = (playlists, oldSidxMapping) => {
const newSidxMapping = {};

for (const uri in playlists) {
const playlist = playlists[uri];
const currentSidxInfo = playlist.sidx;

if (currentSidxInfo) {
const key = generateSidxKey(currentSidxInfo);

if (!oldSidxMapping[key]) {
break;
}

const savedSidxInfo = oldSidxMapping[key].sidxInfo;

if (equivalentSidx(savedSidxInfo, currentSidxInfo)) {
newSidxMapping[key] = oldSidxMapping[key];
}
}
}

return newSidxMapping;
};

// exported for testing
export const filterSidxMapping = (masterXml, srcUrl, clientOffset, oldSidxMapping) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't quite understand what this function is filtering. It seems to be merging a new sidx mapping with an old one?

Copy link
Contributor Author

@ldayananda ldayananda Apr 8, 2019

Choose a reason for hiding this comment

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

it's removing sidx mappings that have changed (either url or indexRange) from the video and audio playlists

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe rename it to removeChangedMappings for clarity?

Copy link
Member

Choose a reason for hiding this comment

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

I think filter is fine there because it parallels Array#filter. However, a comment can be added to clarify.

Copy link
Member

Choose a reason for hiding this comment

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

Updated the name a bit as well.

// Don't pass current sidx mapping
const master = parseMpd(masterXml, {
manifestUri: srcUrl,
clientOffset
});

const videoSidx = compareSidxEntry(master.playlists, oldSidxMapping);
let mediaGroupSidx = videoSidx;

forEachMediaGroup(master, (properties, mediaType, groupKey, labelKey) => {
if (properties.playlists && properties.playlists.length) {
const playlists = properties.playlists;

mediaGroupSidx = mergeOptions(
mediaGroupSidx,
compareSidxEntry(playlists, oldSidxMapping)
);
}
});

return mediaGroupSidx;
};

// exported for testing
export const requestSidx_ = (sidxRange, playlist, xhr, options, finishProcessingFn) => {
const sidxInfo = {
// resolve the segment URL relative to the playlist
uri: resolveManifestRedirect(options.handleManifestRedirects, sidxRange.resolvedUri),
// resolvedUri: sidxRange.resolvedUri,
byterange: sidxRange.byterange,
// the segment's playlist
playlist
};

const sidxRequestOptions = videojs.mergeOptions(sidxInfo, {
responseType: 'arraybuffer',
headers: segmentXhrHeaders(sidxInfo)
});

return xhr(sidxRequestOptions, finishProcessingFn);
};

export default class DashPlaylistLoader extends EventTarget {
// DashPlaylistLoader must accept either a src url or a playlist because subsequent
// playlist loader setups from media groups will expect to be able to pass a playlist
Expand All @@ -89,7 +191,7 @@ export default class DashPlaylistLoader extends EventTarget {

// live playlist staleness timeout
this.on('mediaupdatetimeout', () => {
this.refreshMedia_();
this.refreshMedia_(this.media().uri);
});

this.state = 'HAVE_NOTHING';
Expand All @@ -99,6 +201,9 @@ export default class DashPlaylistLoader extends EventTarget {
// The masterPlaylistLoader will be created with a string
if (typeof srcUrlOrPlaylist === 'string') {
this.srcUrl = srcUrlOrPlaylist;
// TODO: reset sidxMapping between period changes
// once multi-period is refactored
this.sidxMapping_ = {};
return;
}

Expand Down Expand Up @@ -130,6 +235,39 @@ export default class DashPlaylistLoader extends EventTarget {
}
}

handleSidxResponse_(playlist, master, startingState, doneFn) {
return (err, request) => {
// disposed
if (!this.request) {
return;
}

// pending request is cleared
this.request = null;

if (err) {
this.error = {
status: request.status,
message: 'DASH playlist request error at URL: ' + playlist.uri,
response: request.response,
// MEDIA_ERR_NETWORK
code: 2
};
if (startingState) {
this.state = startingState;
}

this.trigger('error');
return doneFn(master, null);
}

const bytes = new Uint8Array(request.response);
const sidx = mp4Inspector.parseSidx(bytes.subarray(8));

return doneFn(master, sidx);
};
}

media(playlist) {
// getter
if (!playlist) {
Expand Down Expand Up @@ -178,7 +316,48 @@ export default class DashPlaylistLoader extends EventTarget {
this.trigger('mediachanging');
}

// TODO: check for sidx here
if (playlist.sidx) {
Copy link
Contributor

Choose a reason for hiding this comment

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

perhaps it would make more sense to more the mediaRequest below this if statement into if (!playlist.sidx) and then return inside of it? That would keep similar code together and get rid of a big if scope.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense to me

let oldMaster;
let sidxMapping;

// sidxMapping is used when parsing the masterXml, so store
// it on the masterPlaylistLoader
if (this.masterPlaylistLoader_) {
oldMaster = this.masterPlaylistLoader_.master;
sidxMapping = this.masterPlaylistLoader_.sidxMapping_;
} else {
oldMaster = this.master;
sidxMapping = this.sidxMapping_;
}

const sidxKey = generateSidxKey(playlist.sidx);

sidxMapping[sidxKey] = {
sidxInfo: playlist.sidx
};

this.request = requestSidx_(
playlist.sidx,
playlist,
this.hls_.xhr,
{ handleManifestRedirects: this.handleManifestRedirects },
this.handleSidxResponse_(playlist, oldMaster, startingState, (newMaster, sidx) => {
if (!newMaster || !sidx) {
throw new Error('failed to request sidx');
}

// update loader's sidxMapping with parsed sidx box
sidxMapping[sidxKey].sidx = sidx;

// everything is ready just continue to haveMetadata
this.haveMetadata({
startingState,
playlist: newMaster.playlists[playlist.uri]
});
})
);
return;
}

// Continue asynchronously if there is no sidx
// wait one tick to allow haveMaster to run first on a child loader
Expand All @@ -190,12 +369,11 @@ export default class DashPlaylistLoader extends EventTarget {

haveMetadata({startingState, playlist}) {
this.state = 'HAVE_METADATA';
this.media_ = playlist;
this.loadedPlaylists_[playlist.uri] = playlist;
this.mediaRequest_ = null;

// This will trigger loadedplaylist
this.refreshMedia_();
this.refreshMedia_(playlist.uri);

// fire loadedmetadata the first time a media playlist is loaded
// to resolve setup of media groups
Expand Down Expand Up @@ -248,7 +426,8 @@ export default class DashPlaylistLoader extends EventTarget {
parseMasterXml() {
const master = parseMpd(this.masterXml_, {
manifestUri: this.srcUrl,
clientOffset: this.clientOffset_
clientOffset: this.clientOffset_,
sidxMapping: this.sidxMapping_
});

master.uri = this.srcUrl;
Expand Down Expand Up @@ -472,11 +651,51 @@ export default class DashPlaylistLoader extends EventTarget {

this.masterXml_ = req.responseText;

const newMaster = this.parseMasterXml();
const updatedMaster = updateMaster(this.master, newMaster);
// This will filter out updated sidx info from the mapping
this.sidxMapping_ = filterSidxMapping(
this.masterXml_,
this.srcUrl,
this.clientOffset_,
this.sidxMapping_
);

const master = this.parseMasterXml();
const updatedMaster = updateMaster(this.master, master);

if (updatedMaster) {
this.master = updatedMaster;
const sidxKey = generateSidxKey(this.media().sidx);

// the sidx was updated, so the previous mapping was removed
if (!this.sidxMapping_[sidxKey]) {
const playlist = this.media();

this.request = requestSidx_(
playlist.sidx,
playlist,
this.hls_.xhr,
{ handleManifestRedirects: this.handleManifestRedirects },
this.handleSidxResponse_(playlist, master, this.state, (newMaster, sidx) => {
if (!newMaster || !sidx) {
brandonocasey marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('failed to request sidx on minimumUpdatePeriod');
}

// update loader's sidxMapping with parsed sidx box
this.sidxMapping_[sidxKey].sidx = sidx;

window.setTimeout(() => {
this.trigger('minimumUpdatePeriod');
}, this.master.minimumUpdatePeriod);

// TODO: do we need to reload the current playlist?
this.refreshMedia_(this.media().uri);

return;
})
);
} else {

this.master = updatedMaster;
}
}

window.setTimeout(() => {
Expand All @@ -490,7 +709,11 @@ export default class DashPlaylistLoader extends EventTarget {
* references. If this is an alternate loader, the updated parsed manifest is retrieved
* from the master loader.
*/
refreshMedia_() {
refreshMedia_(mediaUri) {
if (!mediaUri) {
throw new Error('refreshMedia_ must take a media uri');
}

let oldMaster;
let newMaster;

Expand All @@ -510,13 +733,14 @@ export default class DashPlaylistLoader extends EventTarget {
} else {
this.master = updatedMaster;
}
this.media_ = updatedMaster.playlists[this.media_.uri];
this.media_ = updatedMaster.playlists[mediaUri];
} else {
this.media_ = newMaster.playlists[mediaUri];
this.trigger('playlistunchanged');
}

if (!this.media().endList) {
this.mediaUpdateTimeout = window.setTimeout(()=> {
this.mediaUpdateTimeout = window.setTimeout(() => {
this.trigger('mediaupdatetimeout');
}, refreshDelay(this.media(), !!updatedMaster));
}
Expand Down