From 04ff0fcc8b1e1606ecb6dccda96cc7da368d10e6 Mon Sep 17 00:00:00 2001 From: Gaetan Hervouet <4327141+ghouet@users.noreply.github.com> Date: Tue, 17 May 2022 12:46:43 -0400 Subject: [PATCH] fix: Fix PERIOD_FLATTENING_FAILED error when periods have different base sample types (#4206) Closes #4202 --- lib/dash/dash_parser.js | 4 +- lib/util/mime_utils.js | 49 +++++++++++++++++++++- lib/util/periods.js | 9 ++-- lib/util/stream_utils.js | 6 ++- test/util/mime_utils_unit.js | 54 ++++++++++++++++++++++++ test/util/periods_unit.js | 79 ++++++++++++++++++++++++++++++++++++ 6 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 test/util/mime_utils_unit.js diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 323868f122..af4132f792 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -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)); } } } diff --git a/lib/util/mime_utils.js b/lib/util/mime_utils.js index a9d3523ff5..547d76aa32 100644 --- a/lib/util/mime_utils.js +++ b/lib/util/mime_utils.js @@ -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. * @@ -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". diff --git a/lib/util/periods.js b/lib/util/periods.js index 9f41b98162..6b8e580b7b 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -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; @@ -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; } diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 9faaf3d673..a5c7d64a57 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -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; diff --git a/test/util/mime_utils_unit.js b/test/util/mime_utils_unit.js new file mode 100644 index 0000000000..0364314ac1 --- /dev/null +++ b/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'); + }); +}); diff --git a/test/util/periods_unit.js b/test/util/periods_unit.js index 87115a70e4..ad655ab941 100644 --- a/test/util/periods_unit.js +++ b/test/util/periods_unit.js @@ -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.} */ + 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) => {