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(HLS): Add support for EXT-X-START #6938

Merged
merged 4 commits into from
Jul 2, 2024
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
7 changes: 6 additions & 1 deletion externs/shaka/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
* nextUrl: ?string,
* periodCount: number,
* gapCount: number,
* isLowLatency: boolean
* isLowLatency: boolean,
* startTime: ?number
* }}
*
* @description
Expand Down Expand Up @@ -111,6 +112,10 @@
* If in src= mode or nothing is loaded, NaN.
* @property {bolean} isLowLatency
* If true, the manifest is Low Latency.
* @property {?number} startTime
* Indicate the startTime of the playback, when <code>startTime</code> is
* <code>null</code>, playback will start at the default start time.
* Note: It only overrides the load startTime when it is not defined.
*
* @exportDoc
*/
Expand Down
1 change: 1 addition & 0 deletions lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,7 @@ shaka.dash.DashParser = class {
periodCount: periods.length,
gapCount: this.gapCount_,
isLowLatency: this.isLowLatency_,
startTime: null,
};

// We only need to do clock sync when we're using presentation start
Expand Down
35 changes: 35 additions & 0 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ shaka.hls.HlsParser = class {

/** @private {HTMLMediaElement} */
this.mediaElement_ = null;

/** @private {?number} */
this.startTime_ = null;
}


Expand Down Expand Up @@ -937,6 +940,8 @@ shaka.hls.HlsParser = class {
shaka.util.Error.Code.OPERATION_ABORTED);
}

this.determineStartTime_(playlist);

// Single-variant streams aren't lazy-loaded, so for them we already have
// enough info here to determine the presentation type and duration.
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
Expand Down Expand Up @@ -971,6 +976,7 @@ shaka.hls.HlsParser = class {
periodCount: 1,
gapCount: 0,
isLowLatency: false,
startTime: this.startTime_,
};

// If there is no 'CODECS' attribute in the manifest and codec guessing is
Expand Down Expand Up @@ -2450,6 +2456,8 @@ shaka.hls.HlsParser = class {

const realStream = realStreamInfo.stream;

this.determineStartTime_(playlist);

if (this.isLive_() && !wasLive) {
// Now that we know that the presentation is live, convert the timeline
// to live.
Expand Down Expand Up @@ -2564,6 +2572,10 @@ shaka.hls.HlsParser = class {

this.processDateRangeTags_(
playlist.tags, stream.type, mediaVariables, getUris);

if (this.manifest_) {
this.manifest_.startTime = this.startTime_;
}
};

/** @type {Promise} */
Expand Down Expand Up @@ -3201,6 +3213,24 @@ shaka.hls.HlsParser = class {
}


/**
* @param {!shaka.hls.Playlist} playlist
* @private
*/
determineStartTime_(playlist) {
// If we already have a starttime we avoid processing this again.
if (this.startTime_ != null) {
return;
}
const startTimeTag =
shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-START');
if (startTimeTag) {
this.startTime_ =
Number(startTimeTag.getRequiredAttrValue('TIME-OFFSET'));
}
}


/**
* @param {!shaka.hls.Playlist} playlist
* @private
Expand Down Expand Up @@ -3346,6 +3376,11 @@ shaka.hls.HlsParser = class {
presentationDelay = this.maxTargetDuration_ * delaySegments;
}

if (this.startTime_ && this.startTime_ < 0) {
presentationDelay = Math.min(-this.startTime_, presentationDelay);
avelad marked this conversation as resolved.
Show resolved Hide resolved
this.startTime_ += presentationDelay;
}

this.presentationTimeline_.setPresentationStartTime(0);
this.presentationTimeline_.setDelay(presentationDelay);
this.presentationTimeline_.setStatic(false);
Expand Down
1 change: 1 addition & 0 deletions lib/mss/mss_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ shaka.mss.MssParser = class {
periodCount: 1,
gapCount: 0,
isLowLatency: false,
startTime: null,
};

// This is the first point where we have a meaningful presentation start
Expand Down
1 change: 1 addition & 0 deletions lib/offline/manifest_converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ shaka.offline.ManifestConverter = class {
periodCount: 1,
gapCount: 0,
isLowLatency: false,
startTime: null,
};
}

Expand Down
9 changes: 8 additions & 1 deletion lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -2508,7 +2508,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
};

if (!this.config_.streaming.startAtSegmentBoundary) {
setupPlayhead(this.startTime_);
let startTime = this.startTime_;
if (startTime == null && this.manifest_.startTime) {
startTime = this.manifest_.startTime;
}
setupPlayhead(startTime);
}

// Now we can switch to the initial variant.
Expand All @@ -2521,6 +2525,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
if (this.config_.streaming.startAtSegmentBoundary) {
const timeline = this.manifest_.presentationTimeline;
let initialTime = this.startTime_ || this.video_.currentTime;
if (this.startTime_ == null && this.manifest_.startTime) {
initialTime = this.manifest_.startTime;
}
const seekRangeStart = timeline.getSeekRangeStart();
const seekRangeEnd = timeline.getSeekRangeEnd();
if (initialTime < seekRangeStart) {
Expand Down
1 change: 1 addition & 0 deletions roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ v5.0 - 2024 Q4

v4.11 - 2024 Q3
- HLS improvements
- HLS: EXT-X-START support

=====

Expand Down
25 changes: 25 additions & 0 deletions test/hls/hls_live_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,31 @@ describe('HlsParser live', () => {
expect(manifest.presentationTimeline.getDelay()).toBe(15);
});

it('sets 3 times target duration as presentation delay if not configured and clamped to the start', async () => { // eslint-disable-line max-len
const media = [
'#EXTM3U\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-START:TIME-OFFSET=-10\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXT-X-MEDIA-SEQUENCE:0\n',
'#EXTINF:2,\n',
'main.mp4\n',
'#EXTINF:2,\n',
'main.mp4\n',
'#EXTINF:2,\n',
'main.mp4\n',
'#EXTINF:2,\n',
'main.mp4\n',
'#EXTINF:2,\n',
'main.mp4\n',
'#EXTINF:2,\n',
'main.mp4\n',
].join('');
const manifest = await testInitialManifest(master, media);
expect(manifest.presentationTimeline.getDelay()).toBe(10);
expect(manifest.startTime).toBe(0);
});

it('sets 1 times target duration as presentation delay if there are not enough segments', async () => { // eslint-disable-line max-len
const media = [
'#EXTM3U\n',
Expand Down
4 changes: 4 additions & 0 deletions test/hls/hls_parser_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ describe('HlsParser', () => {
it('parses manifest attributes', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-START:TIME-OFFSET=2\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'CHANNELS="16/JOC",SAMPLE-RATE="48000",URI="audio"\n',
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
Expand Down Expand Up @@ -187,6 +188,7 @@ describe('HlsParser', () => {
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.sequenceMode = sequenceMode;
manifest.type = shaka.media.ManifestParser.HLS;
manifest.startTime = 2;
manifest.anyTimeline();
manifest.addPartialVariant((variant) => {
variant.language = 'en';
Expand Down Expand Up @@ -5081,6 +5083,7 @@ describe('HlsParser', () => {
it('parses media playlists directly', async () => {
const media = [
'#EXTM3U\n',
'#EXT-X-START:TIME-OFFSET=-2\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
Expand All @@ -5091,6 +5094,7 @@ describe('HlsParser', () => {
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.sequenceMode = sequenceMode;
manifest.type = shaka.media.ManifestParser.HLS;
manifest.startTime = -2;
manifest.anyTimeline();
manifest.addPartialVariant((variant) => {
variant.addPartialStream(ContentType.VIDEO, (stream) => {
Expand Down
1 change: 1 addition & 0 deletions test/media/playhead_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ describe('Playhead', () => {
periodCount: 1,
gapCount: 0,
isLowLatency: false,
startTime: null,
};

config = shaka.util.PlayerConfiguration.createDefault().streaming;
Expand Down
1 change: 1 addition & 0 deletions test/media/streaming_engine_integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ describe('StreamingEngine', () => {
periodCount: 1,
gapCount: 0,
isLowLatency: false,
startTime: null,
variants: [{
id: 1,
video: {
Expand Down
2 changes: 2 additions & 0 deletions test/test/util/manifest_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ shaka.test.ManifestGenerator.Manifest = class {
this.gapCount = 0;
/** @type {boolean} */
this.isLowLatency = false;
/** @type {?number} */
this.startTime = null;


/** @type {shaka.extern.Manifest} */
Expand Down
1 change: 1 addition & 0 deletions test/test/util/streaming_engine_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ shaka.test.StreamingEngineUtil = class {
periodCount: 1,
gapCount: 0,
isLowLatency: false,
startTime: null,
};

/** @type {shaka.extern.Variant} */
Expand Down
Loading