From 9202678ec821078b433ff387ba2c926bfd8d7958 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 cb1e5c3473..197975cb6f 100644
--- a/lib/dash/dash_parser.js
+++ b/lib/dash/dash_parser.js
@@ -755,8 +755,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 43a207aa87..0d535bd2d2 100644
--- a/lib/util/periods.js
+++ b/lib/util/periods.js
@@ -574,8 +574,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;
@@ -1016,10 +1016,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.<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) => {