Skip to content

Commit

Permalink
feat(HLS): Add new config to get codecs from media segment for playli…
Browse files Browse the repository at this point in the history
…sts without CODECS attribute (#5772)

Closes #5769
  • Loading branch information
zangue committed Oct 20, 2023
1 parent 9f7576b commit 80630bb
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 11 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -21,6 +21,7 @@ Alugha GmbH <*@alugha.com>
Alvaro Velad Galvan <ladvan91@hotmail.com>
Amila Sampath <lucksy@gmail.com>
Anthony Stansbridge <github@anthonystansbridge.co.uk>
Armand Zangue <armand.zangue@gmail.com>
Benjamin Wallberg <me@bwallberg.com>
Bonnier Broadcasting <*@bonnierbroadcasting.com>
Bryan Huh <bhh1988@gmail.com>
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS
Expand Up @@ -32,6 +32,7 @@ Alvaro Velad Galvan <ladvan91@hotmail.com>
Amila Sampath <lucksy@gmail.com>
Andy Hochhaus <ahochhaus@samegoal.com>
Anthony Stansbridge <github@anthonystansbridge.co.uk>
Armand Zangue <armand.zangue@gmail.com>
Ashutosh Kumar Mukhiya <ashukm4@gmail.com>
Benjamin Wallberg <benjamin.wallberg@bonnierbroadcasting.com>
Benjamin Wallberg <me@bwallberg.com>
Expand Down
2 changes: 2 additions & 0 deletions demo/config.js
Expand Up @@ -207,6 +207,8 @@ shakaDemo.Config = class {
.addBoolInput_('Enable HLS sequence mode', 'manifest.hls.sequenceMode')
.addBoolInput_('Ignore Manifest Timestamps in Segments Mode',
'manifest.hls.ignoreManifestTimestampsInSegmentsMode')
.addBoolInput_('Disable codec guessing',
'manifest.hls.disableCodecGuessing')
.addNumberInput_('Availability Window Override',
'manifest.availabilityWindowOverride',
/* canBeDecimal= */ true,
Expand Down
10 changes: 9 additions & 1 deletion externs/shaka/player.js
Expand Up @@ -920,7 +920,8 @@ shaka.extern.DashManifestConfiguration;
* useSafariBehaviorForLive: boolean,
* liveSegmentsDelay: number,
* sequenceMode: boolean,
* ignoreManifestTimestampsInSegmentsMode: boolean
* ignoreManifestTimestampsInSegmentsMode: boolean,
* disableCodecGuessing: boolean
* }}
*
* @property {boolean} ignoreTextStreamFailures
Expand Down Expand Up @@ -973,6 +974,13 @@ shaka.extern.DashManifestConfiguration;
* to the SourceBuffer, even if the manifest and segment times disagree.
* Only applies when sequenceMode is <code>false</code>.
* <i>Defaults to <code>false</code>.</i>
* @property {boolean} disableCodecGuessing
* If set to true, the HLS parser won't automatically guess or assume default
* codec for playlists with no "CODECS" attribute. Instead, it will attempt to
* extract the missing information from the media segment.
* As a consequence, lazy-loading media playlists won't be possible for this
* use case, which may result in longer video startup times.
* <i>Defaults to <code>false</code>.</i>
* @exportDoc
*/
shaka.extern.HlsManifestConfiguration;
Expand Down
67 changes: 57 additions & 10 deletions lib/hls/hls_parser.js
Expand Up @@ -155,6 +155,13 @@ shaka.hls.HlsParser = class {
*/
this.streamsFinalized_ = false;

/**
* Whether the manifest informs about the codec to use.
*
* @private
*/
this.codecInfoInManifest_ = false;

/**
* This timer is used to trigger the start of a manifest update. A manifest
* update is async. Once the update is finished, the timer will be restarted
Expand Down Expand Up @@ -806,6 +813,24 @@ shaka.hls.HlsParser = class {
type: shaka.media.ManifestParser.HLS,
serviceDescription: null,
};

// If there is no 'CODECS' attribute in the manifest and codec guessing is
// disabled, we need to create the segment indexes now so that missing info
// can be parsed from the media data and added to the stream objects.
if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
const createIndexes = [];
for (const variant of this.manifest_.variants) {
if (variant.audio && variant.audio.codecs === '') {
createIndexes.push(variant.audio.createSegmentIndex());
}
if (variant.video && variant.video.codecs === '') {
createIndexes.push(variant.video.createSegmentIndex());
}
}

await Promise.all(createIndexes);
}

this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
}

Expand Down Expand Up @@ -1293,17 +1318,24 @@ shaka.hls.HlsParser = class {
* @private
*/
getCodecsForVariantTag_(tag) {
// These are the default codecs to assume if none are specified.
const defaultCodecsArray = [];
if (!this.config_.disableVideo) {
defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
}
if (!this.config_.disableAudio) {
defaultCodecsArray.push(this.config_.hls.defaultAudioCodec);
let codecsString = tag.getAttributeValue('CODECS') || '';

this.codecInfoInManifest_ = codecsString.length > 0;

if (!this.codecInfoInManifest_ && !this.config_.hls.disableCodecGuessing) {
// These are the default codecs to assume if none are specified.
const defaultCodecsArray = [];

if (!this.config_.disableVideo) {
defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
}
if (!this.config_.disableAudio) {
defaultCodecsArray.push(this.config_.hls.defaultAudioCodec);
}

codecsString = defaultCodecsArray.join(',');
}
const defaultCodecs = defaultCodecsArray.join(',');

const codecsString = tag.getAttributeValue('CODECS', defaultCodecs);
// Strip out internal whitespace while splitting on commas:
/** @type {!Array.<string>} */
const codecs = codecsString.split(/\s*,\s*/);
Expand Down Expand Up @@ -1829,11 +1861,25 @@ shaka.hls.HlsParser = class {
const playlist = this.manifestTextParser_.parsePlaylist(
response.data, absoluteMediaPlaylistUri);

let mimeType = undefined;

// If no codec info was provided in the manifest and codec guessing is
// disabled we try to get necessary info from the media data.
if (!this.codecInfoInManifest_ && this.config_.hls.disableCodecGuessing) {
const basicInfo = await this.getMediaPlaylistBasicInfo_(playlist);

goog.asserts.assert(
type === basicInfo.type, 'Media types should match!');

mimeType = basicInfo.mimeType;
codecs = basicInfo.codecs;
}

const wasLive = this.isLive_();
const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs,
type, languageValue, primary, name, channelsCount, closedCaptions,
characteristics, forced, sampleRate, spatialAudio);
characteristics, forced, sampleRate, spatialAudio, mimeType);
if (abortSignal.aborted) {
return;
}
Expand Down Expand Up @@ -1864,6 +1910,7 @@ shaka.hls.HlsParser = class {
stream.keyIds = realStream.keyIds;
stream.mimeType = realStream.mimeType;
stream.bandwidth = realStream.bandwidth;
stream.codecs = realStream.codecs || stream.codecs;

// Since we lazy-loaded this content, the player may need to create new
// sessions for the DRM info in this stream.
Expand Down
1 change: 1 addition & 0 deletions lib/util/player_configuration.js
Expand Up @@ -148,6 +148,7 @@ shaka.util.PlayerConfiguration = class {
liveSegmentsDelay: 3,
sequenceMode: shaka.util.Platform.supportsSequenceMode(),
ignoreManifestTimestampsInSegmentsMode: false,
disableCodecGuessing: false,
},
mss: {
manifestPreprocessor: (element) => {
Expand Down
100 changes: 100 additions & 0 deletions test/hls/hls_parser_unit.js
Expand Up @@ -2587,6 +2587,106 @@ describe('HlsParser', () => {
expect(actual).toEqual(manifest);
});

describe('When config.hls.disableCodecGuessing is set to true', () => {
beforeEach(() => {
const config = shaka.util.PlayerConfiguration.createDefault().manifest;
config.hls.disableCodecGuessing = true;
parser.configure(config);
});

it('gets codec info from media if omitted in playlist', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-VERSION:3\n',
'#EXT-X-STREAM-INF:BANDWIDTH=2000000\n',
'video\n',
].join('');

const media = [
'#EXTM3U\n',
'#EXT-X-VERSION:3\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MEDIA-SEQUENCE:0\n',
'#EXTINF:5,\n',
'video-0.ts\n',
].join('');

fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/video-0.ts', tsSegmentData);

const actual = await parser.start('test:/master', playerInterface);
const variant = actual.variants[0];

expect(variant.audio).toBe(null);
expect(variant.video).toBeDefined();
expect(variant.video.codecs).toBe('avc1.42C01E');
});

it('gets codecs from playlist if CODECS attribute present', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-VERSION:3\n',
'#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="foo"\n',
'video\n',
].join('');

const media = [
'#EXTM3U\n',
'#EXT-X-VERSION:3\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MEDIA-SEQUENCE:0\n',
'#EXTINF:5,\n',
'video-0.ts\n',
].join('');

fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/video-0.ts', tsSegmentData);

const actual = await parser.start('test:/master', playerInterface);
const variant = actual.variants[0];

expect(variant.audio).toBe(null);
expect(variant.video).toBeDefined();
expect(variant.video.codecs).toBe('foo');
});

it('falls back to default codecs if it could not find codec', async () => {
const master = [
'#EXTM3U\n',
'#EXT-X-VERSION:3\n',
'#EXT-X-STREAM-INF:BANDWIDTH=2000000\n',
'video\n',
].join('');

const media = [
'#EXTM3U\n',
'#EXT-X-VERSION:3\n',
'#EXT-X-TARGETDURATION:5\n',
'#EXT-X-MEDIA-SEQUENCE:0\n',
'#EXTINF:5,\n',
'video-0.ts\n',
].join('');

fakeNetEngine
.setResponseText('test:/master', master)
.setResponseText('test:/video', media)
.setResponseValue('test:/video-0.ts', new Uint8Array([]));

const actual = await parser.start('test:/master', playerInterface);
const variant = actual.variants[0];

expect(variant.video).toBeDefined();

const codecs = variant.video.codecs.split(',').map((c) => c.trim());

expect(codecs).toEqual(['avc1.42E01E', 'mp4a.40.2']);
});
});

describe('produces syncTime', () => {
// Corresponds to "2000-01-01T00:00:00.00Z".
// All the PROGRAM-DATE-TIME values in the tests below are at or after this.
Expand Down

0 comments on commit 80630bb

Please sign in to comment.