Skip to content

Commit d63df14

Browse files
authored
feat(HLS): Add support for EXT-X-START (#6938)
1 parent 52e3864 commit d63df14

File tree

13 files changed

+87
-2
lines changed

13 files changed

+87
-2
lines changed

externs/shaka/manifest.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
* nextUrl: ?string,
2626
* periodCount: number,
2727
* gapCount: number,
28-
* isLowLatency: boolean
28+
* isLowLatency: boolean,
29+
* startTime: ?number
2930
* }}
3031
*
3132
* @description
@@ -111,6 +112,10 @@
111112
* If in src= mode or nothing is loaded, NaN.
112113
* @property {bolean} isLowLatency
113114
* If true, the manifest is Low Latency.
115+
* @property {?number} startTime
116+
* Indicate the startTime of the playback, when <code>startTime</code> is
117+
* <code>null</code>, playback will start at the default start time.
118+
* Note: It only overrides the load startTime when it is not defined.
114119
*
115120
* @exportDoc
116121
*/

lib/dash/dash_parser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ shaka.dash.DashParser = class {
790790
periodCount: periods.length,
791791
gapCount: this.gapCount_,
792792
isLowLatency: this.isLowLatency_,
793+
startTime: null,
793794
};
794795

795796
// We only need to do clock sync when we're using presentation start

lib/hls/hls_parser.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ shaka.hls.HlsParser = class {
267267

268268
/** @private {HTMLMediaElement} */
269269
this.mediaElement_ = null;
270+
271+
/** @private {?number} */
272+
this.startTime_ = null;
270273
}
271274

272275

@@ -937,6 +940,8 @@ shaka.hls.HlsParser = class {
937940
shaka.util.Error.Code.OPERATION_ABORTED);
938941
}
939942

943+
this.determineStartTime_(playlist);
944+
940945
// Single-variant streams aren't lazy-loaded, so for them we already have
941946
// enough info here to determine the presentation type and duration.
942947
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
@@ -971,6 +976,7 @@ shaka.hls.HlsParser = class {
971976
periodCount: 1,
972977
gapCount: 0,
973978
isLowLatency: false,
979+
startTime: this.startTime_,
974980
};
975981

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

24512457
const realStream = realStreamInfo.stream;
24522458

2459+
this.determineStartTime_(playlist);
2460+
24532461
if (this.isLive_() && !wasLive) {
24542462
// Now that we know that the presentation is live, convert the timeline
24552463
// to live.
@@ -2564,6 +2572,10 @@ shaka.hls.HlsParser = class {
25642572

25652573
this.processDateRangeTags_(
25662574
playlist.tags, stream.type, mediaVariables, getUris);
2575+
2576+
if (this.manifest_) {
2577+
this.manifest_.startTime = this.startTime_;
2578+
}
25672579
};
25682580

25692581
/** @type {Promise} */
@@ -3201,6 +3213,24 @@ shaka.hls.HlsParser = class {
32013213
}
32023214

32033215

3216+
/**
3217+
* @param {!shaka.hls.Playlist} playlist
3218+
* @private
3219+
*/
3220+
determineStartTime_(playlist) {
3221+
// If we already have a starttime we avoid processing this again.
3222+
if (this.startTime_ != null) {
3223+
return;
3224+
}
3225+
const startTimeTag =
3226+
shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-START');
3227+
if (startTimeTag) {
3228+
this.startTime_ =
3229+
Number(startTimeTag.getRequiredAttrValue('TIME-OFFSET'));
3230+
}
3231+
}
3232+
3233+
32043234
/**
32053235
* @param {!shaka.hls.Playlist} playlist
32063236
* @private
@@ -3346,6 +3376,11 @@ shaka.hls.HlsParser = class {
33463376
presentationDelay = this.maxTargetDuration_ * delaySegments;
33473377
}
33483378

3379+
if (this.startTime_ && this.startTime_ < 0) {
3380+
presentationDelay = Math.min(-this.startTime_, presentationDelay);
3381+
this.startTime_ += presentationDelay;
3382+
}
3383+
33493384
this.presentationTimeline_.setPresentationStartTime(0);
33503385
this.presentationTimeline_.setDelay(presentationDelay);
33513386
this.presentationTimeline_.setStatic(false);

lib/mss/mss_parser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ shaka.mss.MssParser = class {
429429
periodCount: 1,
430430
gapCount: 0,
431431
isLowLatency: false,
432+
startTime: null,
432433
};
433434

434435
// This is the first point where we have a meaningful presentation start

lib/offline/manifest_converter.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ shaka.offline.ManifestConverter = class {
9797
periodCount: 1,
9898
gapCount: 0,
9999
isLowLatency: false,
100+
startTime: null,
100101
};
101102
}
102103

lib/player.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2509,7 +2509,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
25092509
};
25102510

25112511
if (!this.config_.streaming.startAtSegmentBoundary) {
2512-
setupPlayhead(this.startTime_);
2512+
let startTime = this.startTime_;
2513+
if (startTime == null && this.manifest_.startTime) {
2514+
startTime = this.manifest_.startTime;
2515+
}
2516+
setupPlayhead(startTime);
25132517
}
25142518

25152519
// Now we can switch to the initial variant.
@@ -2522,6 +2526,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
25222526
if (this.config_.streaming.startAtSegmentBoundary) {
25232527
const timeline = this.manifest_.presentationTimeline;
25242528
let initialTime = this.startTime_ || this.video_.currentTime;
2529+
if (this.startTime_ == null && this.manifest_.startTime) {
2530+
initialTime = this.manifest_.startTime;
2531+
}
25252532
const seekRangeStart = timeline.getSeekRangeStart();
25262533
const seekRangeEnd = timeline.getSeekRangeEnd();
25272534
if (initialTime < seekRangeStart) {

roadmap.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ v5.0 - 2024 Q4
2222

2323
v4.11 - 2024 Q3
2424
- HLS improvements
25+
- HLS: EXT-X-START support
2526

2627
=====
2728

test/hls/hls_live_unit.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,31 @@ describe('HlsParser live', () => {
570570
expect(manifest.presentationTimeline.getDelay()).toBe(15);
571571
});
572572

573+
it('sets 3 times target duration as presentation delay if not configured and clamped to the start', async () => { // eslint-disable-line max-len
574+
const media = [
575+
'#EXTM3U\n',
576+
'#EXT-X-TARGETDURATION:5\n',
577+
'#EXT-X-START:TIME-OFFSET=-10\n',
578+
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
579+
'#EXT-X-MEDIA-SEQUENCE:0\n',
580+
'#EXTINF:2,\n',
581+
'main.mp4\n',
582+
'#EXTINF:2,\n',
583+
'main.mp4\n',
584+
'#EXTINF:2,\n',
585+
'main.mp4\n',
586+
'#EXTINF:2,\n',
587+
'main.mp4\n',
588+
'#EXTINF:2,\n',
589+
'main.mp4\n',
590+
'#EXTINF:2,\n',
591+
'main.mp4\n',
592+
].join('');
593+
const manifest = await testInitialManifest(master, media);
594+
expect(manifest.presentationTimeline.getDelay()).toBe(10);
595+
expect(manifest.startTime).toBe(0);
596+
});
597+
573598
it('sets 1 times target duration as presentation delay if there are not enough segments', async () => { // eslint-disable-line max-len
574599
const media = [
575600
'#EXTM3U\n',

test/hls/hls_parser_unit.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ describe('HlsParser', () => {
156156
it('parses manifest attributes', async () => {
157157
const master = [
158158
'#EXTM3U\n',
159+
'#EXT-X-START:TIME-OFFSET=2\n',
159160
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
160161
'CHANNELS="16/JOC",SAMPLE-RATE="48000",URI="audio"\n',
161162
'#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",',
@@ -187,6 +188,7 @@ describe('HlsParser', () => {
187188
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
188189
manifest.sequenceMode = sequenceMode;
189190
manifest.type = shaka.media.ManifestParser.HLS;
191+
manifest.startTime = 2;
190192
manifest.anyTimeline();
191193
manifest.addPartialVariant((variant) => {
192194
variant.language = 'en';
@@ -5081,6 +5083,7 @@ describe('HlsParser', () => {
50815083
it('parses media playlists directly', async () => {
50825084
const media = [
50835085
'#EXTM3U\n',
5086+
'#EXT-X-START:TIME-OFFSET=-2\n',
50845087
'#EXT-X-PLAYLIST-TYPE:VOD\n',
50855088
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
50865089
'#EXTINF:5,\n',
@@ -5091,6 +5094,7 @@ describe('HlsParser', () => {
50915094
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
50925095
manifest.sequenceMode = sequenceMode;
50935096
manifest.type = shaka.media.ManifestParser.HLS;
5097+
manifest.startTime = -2;
50945098
manifest.anyTimeline();
50955099
manifest.addPartialVariant((variant) => {
50965100
variant.addPartialStream(ContentType.VIDEO, (stream) => {

test/media/playhead_unit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ describe('Playhead', () => {
141141
periodCount: 1,
142142
gapCount: 0,
143143
isLowLatency: false,
144+
startTime: null,
144145
};
145146

146147
config = shaka.util.PlayerConfiguration.createDefault().streaming;

test/media/streaming_engine_integration.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ describe('StreamingEngine', () => {
609609
periodCount: 1,
610610
gapCount: 0,
611611
isLowLatency: false,
612+
startTime: null,
612613
variants: [{
613614
id: 1,
614615
video: {

test/test/util/manifest_generator.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ shaka.test.ManifestGenerator.Manifest = class {
111111
this.gapCount = 0;
112112
/** @type {boolean} */
113113
this.isLowLatency = false;
114+
/** @type {?number} */
115+
this.startTime = null;
114116

115117

116118
/** @type {shaka.extern.Manifest} */

test/test/util/streaming_engine_util.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ shaka.test.StreamingEngineUtil = class {
324324
periodCount: 1,
325325
gapCount: 0,
326326
isLowLatency: false,
327+
startTime: null,
327328
};
328329

329330
/** @type {shaka.extern.Variant} */

0 commit comments

Comments
 (0)