Skip to content

Commit

Permalink
fix: Fix PERIOD_FLATTENING_FAILED error when periods have different b…
Browse files Browse the repository at this point in the history
…ase sample types (#4206)

Closes #4202
  • Loading branch information
ghouet authored and joeyparrish committed May 17, 2022
1 parent 1fe8df2 commit 04ff0fc
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 9 deletions.
4 changes: 2 additions & 2 deletions lib/dash/dash_parser.js
Expand Up @@ -764,8 +764,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
49 changes: 48 additions & 1 deletion lib/util/mime_utils.js
Expand Up @@ -94,6 +94,53 @@ shaka.util.MimeUtils = class {
return codecs.split(',');
}

/**
* Get the normalized codec from a codec string,
* independently of their container.
*
* @param {string} codecString
* @return {string}
*/
static getNormalizedCodec(codecString) {
const parts =
shaka.util.MimeUtils.getCodecParts_(codecString);
const base = parts[0];
const profile = parts[1].toLowerCase();
switch (true) {
case base === 'mp4a' && profile === '69':
case base === 'mp4a' && profile === '6b':
return 'mp3';
case base === 'mp4a' && profile === '66':
case base === 'mp4a' && profile === '67':
case base === 'mp4a' && profile === '68':
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':
case base === 'mp4a' && profile === '40.42': // Extended HE-AAC
return 'aac';
case base === 'mp4a' && profile === 'a5':
return 'ac-3'; // Dolby Digital
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 @@ -150,7 +197,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
54 changes: 54 additions & 0 deletions test/util/mime_utils_unit.js
@@ -0,0 +1,54 @@
/*! @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('aac');
expect(getNormalizedCodec('mp4a.67')).toBe('aac');
expect(getNormalizedCodec('mp4a.68')).toBe('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('aac');
expect(getNormalizedCodec('mp4a.40.02')).toBe('aac');
expect(getNormalizedCodec('mp4a.40.5')).toBe('aac');
expect(getNormalizedCodec('mp4a.40.05')).toBe('aac');
expect(getNormalizedCodec('mp4a.40.29')).toBe('aac');
expect(getNormalizedCodec('mp4a.40.42')).toBe('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('dtsc')).toBe('dtsc');
expect(getNormalizedCodec('mp4a.a9')).toBe('dtsc');

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

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

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

0 comments on commit 04ff0fc

Please sign in to comment.