Skip to content

Commit

Permalink
feat(hls): parse EXT-X-GAP (#4134)
Browse files Browse the repository at this point in the history
Parse EXT-X-GAP HLS tag and add a status enum to shaka.media.SegmentReference.

shaka.media.SegmentReference.Status.AVAILABLE --> Normal behaviour
shaka.media.SegmentReference.Status. UNAVAILABLE --> Related to #2541
shaka.media.SegmentReference.Status. MISSING --> EXT-X-GAP in HLS

Note: only the parsing is added, but the functionality is not yet implemented.

Issue #1308
  • Loading branch information
Álvaro Velad Galván committed Apr 18, 2022
1 parent 4fecfb9 commit 42eecc8
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 1 deletion.
6 changes: 6 additions & 0 deletions lib/hls/hls_parser.js
Expand Up @@ -1865,6 +1865,11 @@ shaka.hls.HlsParser = class {
}
}

let status = shaka.media.SegmentReference.Status.AVAILABLE;
if (shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-GAP')) {
status = shaka.media.SegmentReference.Status.MISSING;
}

// Create SegmentReferences for the partial segments.
const partialSegmentRefs = [];
if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
Expand Down Expand Up @@ -1978,6 +1983,7 @@ shaka.hls.HlsParser = class {
tilesLayout,
tileDuration,
syncTime,
status,
);
}

Expand Down
41 changes: 40 additions & 1 deletion lib/media/segment_reference.js
Expand Up @@ -162,12 +162,15 @@ shaka.media.SegmentReference = class {
* @param {?number=} syncTime
* A time value, expressed in the same scale as the start and end time, which
* is used to synchronize between streams.
* @param {shaka.media.SegmentReference.Status=} status
* The segment status is used to indicate that a segment does not exist or is
* not available.
*/
constructor(
startTime, endTime, uris, startByte, endByte, initSegmentReference,
timestampOffset, appendWindowStart, appendWindowEnd,
partialReferences = [], tilesLayout = '', tileDuration = null,
syncTime = null) {
syncTime = null, status = shaka.media.SegmentReference.Status.AVAILABLE) {
// A preload hinted Partial Segment has the same startTime and endTime.
goog.asserts.assert(startTime <= endTime,
'startTime must be less than or equal to endTime');
Expand Down Expand Up @@ -220,6 +223,9 @@ shaka.media.SegmentReference = class {

/** @type {?number} */
this.syncTime = syncTime;

/** @type {shaka.media.SegmentReference.Status} */
this.status = status;
}

/**
Expand Down Expand Up @@ -315,6 +321,39 @@ shaka.media.SegmentReference = class {
getTileDuration() {
return this.tileDuration;
}

/**
* Returns the segment's status.
*
* @return {shaka.media.SegmentReference.Status}
* @export
*/
getStatus() {
return this.status;
}

/**
* Mark the reference as unavailable.
*
* @export
*/
markAsUnavailable() {
this.status = shaka.media.SegmentReference.Status.UNAVAILABLE;
}
};


/**
* Rather than using booleans to communicate what the state of the reference,
* we have this enum.
*
* @enum {number}
* @export
*/
shaka.media.SegmentReference.Status = {
AVAILABLE: 0,
UNAVAILABLE: 1,
MISSING: 2,
};


Expand Down
99 changes: 99 additions & 0 deletions test/hls/hls_parser_unit.js
Expand Up @@ -1424,6 +1424,105 @@ describe('HlsParser', () => {
}
});

it('parse EXT-X-GAP', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
'CHANNELS="2",URI="audio"\n',
'#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
'video\n',
].join('');

const video = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'main.mp4\n',
'#EXTINF:5,\n',
'#EXT-X-GAP\n',
'main.mp4\n',
'#EXTINF:5,\n',
'main.mp4\n',
].join('');

const audio = [
'#EXTM3U\n',
'#EXT-X-PLAYLIST-TYPE:VOD\n',
'#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
'#EXTINF:5,\n',
'main.mp4\n',
'#EXTINF:5,\n',
'#EXT-X-GAP\n',
'main.mp4\n',
'#EXTINF:5,\n',
'main.mp4\n',
].join('');

fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/audio', audio)
.setResponseText('test:/video', video)
.setResponseValue('test:/init.mp4', initSegmentData)
.setResponseValue('test:/main.mp4', segmentData);

const actual = await parser.start('test:/master', playerInterface);

expect(actual.variants.length).toBe(1);

const variant = actual.variants[0];
expect(variant.video).toBeTruthy();
expect(variant.audio).toBeTruthy();

const available = shaka.media.SegmentReference.Status.AVAILABLE;
const missing = shaka.media.SegmentReference.Status.MISSING;

await variant.video.createSegmentIndex();
goog.asserts.assert(variant.video.segmentIndex != null,
'Null segmentIndex!');

const firstVideoReference = variant.video.segmentIndex.get(0);
const secondVideoReference = variant.video.segmentIndex.get(1);
const thirdVideoReference = variant.video.segmentIndex.get(2);

expect(firstVideoReference).not.toBe(null);
expect(secondVideoReference).not.toBe(null);
expect(thirdVideoReference).not.toBe(null);

if (firstVideoReference) {
expect(firstVideoReference.getStatus()).toBe(available);
}
if (secondVideoReference) {
expect(secondVideoReference.getStatus()).toBe(missing);
}
if (thirdVideoReference) {
expect(thirdVideoReference.getStatus()).toBe(available);
}

await variant.audio.createSegmentIndex();
goog.asserts.assert(variant.audio.segmentIndex != null,
'Null segmentIndex!');

const firstAudioReference = variant.audio.segmentIndex.get(0);
const secondAudioReference = variant.audio.segmentIndex.get(1);
const thirdAudioReference = variant.audio.segmentIndex.get(2);

expect(firstAudioReference).not.toBe(null);
expect(secondAudioReference).not.toBe(null);
expect(thirdAudioReference).not.toBe(null);

if (firstAudioReference) {
expect(firstAudioReference.getStatus()).toBe(available);
}
if (secondAudioReference) {
expect(secondAudioReference.getStatus()).toBe(missing);
}
if (thirdAudioReference) {
expect(thirdAudioReference.getStatus()).toBe(available);
}
});

it('Disable audio does not create audio streams', async () => {
const master = [
'#EXTM3U\n',
Expand Down

0 comments on commit 42eecc8

Please sign in to comment.