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

fix: PERIOD_FLATTENING_FAILED error when periods have different base sample types. #4206

Merged
merged 12 commits into from May 17, 2022
4 changes: 2 additions & 2 deletions lib/dash/dash_parser.js
Expand Up @@ -765,8 +765,8 @@ shaka.dash.DashParser = class {
// currently support that. Just choose one.
// TODO: https://github.com/shaka-project/shaka-player/issues/1528
stream.trickModeVideo = trickModeSet.streams.find((trickStream) =>
shaka.util.MimeUtils.getCodecBase(stream.codecs) ==
shaka.util.MimeUtils.getCodecBase(trickStream.codecs));
shaka.util.MimeUtils.getNormalizedCodec(stream.codecs) ==
shaka.util.MimeUtils.getNormalizedCodec(trickStream.codecs));
}
}
}
Expand Down
51 changes: 50 additions & 1 deletion lib/util/mime_utils.js
Expand Up @@ -96,6 +96,55 @@ shaka.util.MimeUtils = class {
return codecs.split(',');
}

/**
* Get the normalized codec from a codec string,
* ignoring multiple names of a same codec.
*
* @param {string} codecString
* @return {string}
*/
static getNormalizedCodec(codecString) {
ghouet marked this conversation as resolved.
Show resolved Hide resolved
const parts =
shaka.util.MimeUtils.getCodecParts_(codecString.toLowerCase());
const base = parts[0];
const profile = parts[1];
ghouet marked this conversation as resolved.
Show resolved Hide resolved
switch (true) {
case base === 'mp4a' && profile === '66':
case base === 'mp4a' && profile === '67':
case base === 'mp4a' && profile === '68':
return 'mpeg2_aac';
ghouet marked this conversation as resolved.
Show resolved Hide resolved
case base === 'mp4a' && profile === '69':
case base === 'mp4a' && profile === '6b':
return 'mp3';
case base === 'mp4a' && profile === '40.2':
case base === 'mp4a' && profile === '40.02':
case base === 'mp4a' && profile === '40.5':
case base === 'mp4a' && profile === '40.05':
case base === 'mp4a' && profile === '40.29':
return 'mpeg4_aac';
ghouet marked this conversation as resolved.
Show resolved Hide resolved
case base === 'mp4a' && profile === '40.42':
return 'mpeg4_xhe_aac'; // Extended HE-AAC
ghouet marked this conversation as resolved.
Show resolved Hide resolved
case base === 'mp4a' && profile === 'a5':
return 'ac-3'; // Dolby Digital
avelad marked this conversation as resolved.
Show resolved Hide resolved
case base === 'mp4a' && profile === 'a6':
return 'ec-3'; // Dolby Digital Plus
case base === 'mp4a' && profile === 'b2':
return 'dtsx'; // DTS:X
case base === 'mp4a' && profile === 'a9':
return 'dtsc'; // DTS Digital Surround
case base === 'avc1':
case base === 'avc3':
return 'avc'; // H264
case base === 'hvc1':
case base === 'hev1':
return 'hevc'; // H265
case base === 'dvh1':
case base === 'dvhe':
return 'dovi'; // Dolby Vision
}
return base;
}

/**
* Get the base codec from a codec string.
*
Expand Down Expand Up @@ -152,7 +201,7 @@ shaka.util.MimeUtils = class {

const base = parts[0];

parts.pop();
parts.shift();
const profile = parts.join('.');

// Make sure that we always return a "base" and "profile".
Expand Down
9 changes: 5 additions & 4 deletions lib/util/periods.js
Expand Up @@ -573,8 +573,8 @@ shaka.util.PeriodCombiner = class {
// TODO(#1528): Consider changing this when we support codec switching.
const hasCodec = outputStreams.some((s) => {
return s.mimeType == stream.mimeType &&
shaka.util.MimeUtils.getCodecBase(s.codecs) ==
shaka.util.MimeUtils.getCodecBase(stream.codecs);
shaka.util.MimeUtils.getNormalizedCodec(s.codecs) ==
shaka.util.MimeUtils.getNormalizedCodec(stream.codecs);
});
if (!hasCodec) {
continue;
Expand Down Expand Up @@ -1050,10 +1050,11 @@ shaka.util.PeriodCombiner = class {
* @private
*/
static areAVStreamsCompatible_(outputStream, candidate) {
const getCodecBase = (codecs) => shaka.util.MimeUtils.getCodecBase(codecs);
const getCodec = (codecs) =>
shaka.util.MimeUtils.getNormalizedCodec(codecs);
// Check MIME type and codecs, which should always be the same.
if (candidate.mimeType != outputStream.mimeType ||
getCodecBase(candidate.codecs) != getCodecBase(outputStream.codecs)) {
getCodec(candidate.codecs) != getCodec(outputStream.codecs)) {
return false;
}

Expand Down
6 changes: 4 additions & 2 deletions lib/util/stream_utils.js
Expand Up @@ -283,12 +283,14 @@ shaka.util.StreamUtils = class {
// both be considered the same codec: avc1.42c01e, avc1.4d401f
let baseVideoCodec = '';
if (variant.video) {
baseVideoCodec = shaka.util.MimeUtils.getCodecBase(variant.video.codecs);
baseVideoCodec =
shaka.util.MimeUtils.getNormalizedCodec(variant.video.codecs);
}

let baseAudioCodec = '';
if (variant.audio) {
baseAudioCodec = shaka.util.MimeUtils.getCodecBase(variant.audio.codecs);
baseAudioCodec =
shaka.util.MimeUtils.getNormalizedCodec(variant.audio.codecs);
}

return baseVideoCodec + '-' + baseAudioCodec;
Expand Down
55 changes: 55 additions & 0 deletions test/util/mime_utils_unit.js
@@ -0,0 +1,55 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

describe('MimeUtils', () => {
const getNormalizedCodec = (codecs) =>
shaka.util.MimeUtils.getNormalizedCodec(codecs);

it('normalizes codecs', () => {
expect(getNormalizedCodec('mp4a.66')).toBe('mpeg2_aac');
expect(getNormalizedCodec('mp4a.67')).toBe('mpeg2_aac');
expect(getNormalizedCodec('mp4a.68')).toBe('mpeg2_aac');

expect(getNormalizedCodec('mp3')).toBe('mp3');
expect(getNormalizedCodec('mp4a.69')).toBe('mp3');
expect(getNormalizedCodec('mp4a.6B')).toBe('mp3');
expect(getNormalizedCodec('mp4a.6b')).toBe('mp3');

expect(getNormalizedCodec('mp4a.40.2')).toBe('mpeg4_aac');
expect(getNormalizedCodec('mp4a.40.02')).toBe('mpeg4_aac');
expect(getNormalizedCodec('mp4a.40.5')).toBe('mpeg4_aac');
expect(getNormalizedCodec('mp4a.40.05')).toBe('mpeg4_aac');
expect(getNormalizedCodec('mp4a.40.29')).toBe('mpeg4_aac');

expect(getNormalizedCodec('mp4a.40.42')).toBe('mpeg4_xhe_aac');

expect(getNormalizedCodec('ac-3')).toBe('ac-3');
expect(getNormalizedCodec('mp4a.a5')).toBe('ac-3');
expect(getNormalizedCodec('mp4a.A5')).toBe('ac-3');

expect(getNormalizedCodec('ec-3')).toBe('ec-3');
expect(getNormalizedCodec('mp4a.a6')).toBe('ec-3');
expect(getNormalizedCodec('mp4a.A6')).toBe('ec-3');

expect(getNormalizedCodec('vp8')).toBe('vp8');
expect(getNormalizedCodec('vp8.0')).toBe('vp8');

expect(getNormalizedCodec('dtsc')).toBe('dtsc');
expect(getNormalizedCodec('mp4a.a9')).toBe('dtsc');

expect(getNormalizedCodec('dtsx')).toBe('dtsx');
expect(getNormalizedCodec('mp4a.b2')).toBe('dtsx');

expect(getNormalizedCodec('avc1')).toBe('avc');
expect(getNormalizedCodec('avc3')).toBe('avc');

expect(getNormalizedCodec('hvc1')).toBe('hevc');
expect(getNormalizedCodec('hev1')).toBe('hevc');

expect(getNormalizedCodec('dvh1.05')).toBe('dovi');
expect(getNormalizedCodec('dvhe.05')).toBe('dovi');
});
});
79 changes: 79 additions & 0 deletions test/util/periods_unit.js
Expand Up @@ -996,6 +996,85 @@ describe('PeriodCombiner', () => {
expect(audio2.originalId).toBe('2,4');
});

it('Matches streams with related codecs', async () => {
const stream1 = makeVideoStream(1080);
stream1.originalId = '1';
stream1.bandwidth = 120000;
stream1.codecs = 'hvc1.1.4.L126.B0';

const stream2 = makeVideoStream(1080);
stream2.originalId = '2';
stream2.bandwidth = 120000;
stream2.codecs = 'hev1.2.4.L123.B0';

const stream3 = makeVideoStream(1080);
stream3.originalId = '3';
stream3.bandwidth = 120000;
stream3.codecs = 'dvhe.05.01';

const stream4 = makeVideoStream(1080);
stream4.originalId = '4';
stream4.bandwidth = 120000;
stream4.codecs = 'dvh1.05.01';

const stream5 = makeVideoStream(1080);
stream5.originalId = '5';
stream5.bandwidth = 120000;
stream5.codecs = 'avc1.42001f';

const stream6 = makeVideoStream(1080);
stream6.originalId = '6';
stream6.bandwidth = 120000;
stream6.codecs = 'avc3.42001f';

const stream7 = makeVideoStream(1080);
stream7.originalId = '7';
stream7.bandwidth = 120000;
stream7.codecs = 'vp09.00.10.08';

const stream8 = makeVideoStream(1080);
stream8.originalId = '8';
stream8.bandwidth = 120000;
stream8.codecs = 'vp09.01.20.08.01';

/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '0',
videoStreams: [
stream1, stream3, stream5, stream7,
],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '1',
videoStreams: [
stream2, stream4, stream6, stream8,
],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
];

await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants.length).toBe(4);
// We can use the originalId field to see what each track is composed of.
const video1 = variants[0].video;
expect(video1.originalId).toBe('1,2');

const video2 = variants[1].video;
expect(video2.originalId).toBe('3,4');

const video3 = variants[2].video;
expect(video3.originalId).toBe('5,6');

const video4 = variants[3].video;
expect(video4.originalId).toBe('7,8');
});

it('Matches streams with most roles in common', async () => {
const makeAudioStreamWithRoles = (roles) => {
Expand Down