Bug 1533129 - Add extended MIME parameter support to MediaSource.isTypeSupported on YouTube only. r=jya
authorChris Pearce <cpearce@mozilla.com>
Thu, 11 Apr 2019 09:53:26 +0000
changeset 468971 43b78a85f3266f65c0ed4045e28c16dec90b480c
parent 468970 a7b675fcb8d31cbbdfcf9179303d49ec294647e0
child 468972 040df193736f2274ca87a23d0fb405b4a0f2e1f2
push id35856
push usercsabou@mozilla.com
push dateFri, 12 Apr 2019 03:19:48 +0000
treeherdermozilla-central@940684cd1065 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjya
bugs1533129
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1533129 - Add extended MIME parameter support to MediaSource.isTypeSupported on YouTube only. r=jya YouTube.com/tv uses YouTube specific extensions to MediaSource.isTypeSupported in order to determine whether it serves 4K. It checks with bogus values, and if we reject the bogus values, it assumes we're responding truthfully to the other queries. So add support to reject the bogus values on YouTube.com. With this patch, we can play 4K on YouTube.com/tv. Differential Revision: https://phabricator.services.mozilla.com/D26655
dom/media/MediaMIMETypes.cpp
dom/media/MediaMIMETypes.h
dom/media/mediasource/MediaSource.cpp
dom/media/mediasource/test/file_isTypeSupported.html
dom/media/mediasource/test/mochitest.ini
dom/media/mediasource/test/test_isTypeSupportedExtensions.html
--- a/dom/media/MediaMIMETypes.cpp
+++ b/dom/media/MediaMIMETypes.cpp
@@ -111,24 +111,26 @@ static int32_t GetParameterAsNumber(cons
     return aErrorReturn;
   }
   return number;
 }
 
 MediaExtendedMIMEType::MediaExtendedMIMEType(
     const nsACString& aOriginalString, const nsACString& aMIMEType,
     bool aHaveCodecs, const nsAString& aCodecs, int32_t aWidth, int32_t aHeight,
-    double aFramerate, int32_t aBitrate)
+    double aFramerate, int32_t aBitrate, EOTF aEOTF, int32_t aChannels)
     : mOriginalString(aOriginalString),
       mMIMEType(aMIMEType),
       mHaveCodecs(aHaveCodecs),
       mCodecs(aCodecs),
       mWidth(aWidth),
       mHeight(aHeight),
       mFramerate(aFramerate),
+      mEOTF(aEOTF),
+      mChannels(aChannels),
       mBitrate(aBitrate) {}
 
 MediaExtendedMIMEType::MediaExtendedMIMEType(
     const nsACString& aOriginalString, const nsACString& aMIMEType,
     bool aHaveCodecs, const nsAString& aCodecs, int32_t aChannels,
     int32_t aSamplerate, int32_t aBitrate)
     : mOriginalString(aOriginalString),
       mMIMEType(aMIMEType),
@@ -176,16 +178,28 @@ Maybe<double> MediaExtendedMIMEType::Com
   result = first / second;
   if (result <= 0) {
     return Nothing();
   }
 
   return Some(result);
 }
 
+static EOTF GetParameterAsEOTF(const nsContentTypeParser& aParser) {
+  nsAutoString eotf;
+  nsresult rv = aParser.GetParameter("eotf", eotf);
+  if (NS_FAILED_impl(rv)) {
+    return EOTF::UNSPECIFIED;
+  }
+  if (eotf.LowerCaseEqualsASCII("bt709")) {
+    return EOTF::BT709;
+  }
+  return EOTF::NOT_SUPPORTED;
+}
+
 Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(const nsAString& aType) {
   nsContentTypeParser parser(aType);
   nsAutoString mime;
   nsresult rv = parser.GetType(mime);
   if (!NS_SUCCEEDED(rv) || mime.IsEmpty()) {
     return Nothing();
   }
 
@@ -197,20 +211,22 @@ Maybe<MediaExtendedMIMEType> MakeMediaEx
   nsAutoString codecs;
   rv = parser.GetParameter("codecs", codecs);
   bool haveCodecs = NS_SUCCEEDED(rv);
 
   int32_t width = GetParameterAsNumber(parser, "width", -1);
   int32_t height = GetParameterAsNumber(parser, "height", -1);
   double framerate = GetParameterAsNumber(parser, "framerate", -1);
   int32_t bitrate = GetParameterAsNumber(parser, "bitrate", -1);
+  EOTF eotf = GetParameterAsEOTF(parser);
+  int32_t channels = GetParameterAsNumber(parser, "channels", -1);
 
   return Some(MediaExtendedMIMEType(NS_ConvertUTF16toUTF8(aType), mime8,
                                     haveCodecs, codecs, width, height,
-                                    framerate, bitrate));
+                                    framerate, bitrate, eotf, channels));
 }
 
 Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
     const dom::VideoConfiguration& aConfig) {
   if (aConfig.mContentType.IsEmpty()) {
     return Nothing();
   }
   nsContentTypeParser parser(aConfig.mContentType);
@@ -230,19 +246,20 @@ Maybe<MediaExtendedMIMEType> MakeMediaEx
   bool haveCodecs = NS_SUCCEEDED(rv);
 
   auto framerate =
       MediaExtendedMIMEType::ComputeFractionalString(aConfig.mFramerate);
   if (!framerate) {
     return Nothing();
   }
 
-  return Some(MediaExtendedMIMEType(
-      NS_ConvertUTF16toUTF8(aConfig.mContentType), mime8, haveCodecs, codecs,
-      aConfig.mWidth, aConfig.mHeight, framerate.ref(), aConfig.mBitrate));
+  return Some(MediaExtendedMIMEType(NS_ConvertUTF16toUTF8(aConfig.mContentType),
+                                    mime8, haveCodecs, codecs, aConfig.mWidth,
+                                    aConfig.mHeight, framerate.ref(),
+                                    aConfig.mBitrate, EOTF::UNSPECIFIED));
 }
 
 Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
     const dom::AudioConfiguration& aConfig) {
   if (aConfig.mContentType.IsEmpty()) {
     return Nothing();
   }
   nsContentTypeParser parser(aConfig.mContentType);
--- a/dom/media/MediaMIMETypes.h
+++ b/dom/media/MediaMIMETypes.h
@@ -138,16 +138,23 @@ class MediaCodecs {
 
  private:
   // UTF16 comma-separated list of codecs.
   // See http://www.rfc-editor.org/rfc/rfc4281.txt for the description
   // of the 'codecs' parameter.
   nsString mCodecs;
 };
 
+// Electro-Optical Transfer Functions
+enum class EOTF {
+  UNSPECIFIED = -1,
+  NOT_SUPPORTED = 0,
+  BT709 = 1,
+};
+
 // Class containing pre-parsed media MIME type parameters, e.g.:
 // MIME type/subtype, optional codecs, etc.
 class MediaExtendedMIMEType {
  public:
   explicit MediaExtendedMIMEType(const MediaMIMEType& aType);
   explicit MediaExtendedMIMEType(MediaMIMEType&& aType);
 
   // MIME "type/subtype".
@@ -160,16 +167,19 @@ class MediaExtendedMIMEType {
 
   // Sizes and rates.
   Maybe<int32_t> GetWidth() const { return GetMaybeNumber(mWidth); }
   Maybe<int32_t> GetHeight() const { return GetMaybeNumber(mHeight); }
   Maybe<double> GetFramerate() const { return GetMaybeNumber(mFramerate); }
   Maybe<int32_t> GetBitrate() const { return GetMaybeNumber(mBitrate); }
   Maybe<int32_t> GetChannels() const { return GetMaybeNumber(mChannels); }
   Maybe<int32_t> GetSamplerate() const { return GetMaybeNumber(mSamplerate); }
+  Maybe<EOTF> GetEOTF() const {
+    return (mEOTF == EOTF::UNSPECIFIED) ? Nothing() : Some(mEOTF);
+  }
 
   // Original string. Note that "type/subtype" may not be lowercase,
   // use Type().AsString() instead to get the normalized "type/subtype".
   const nsCString& OriginalString() const { return mOriginalString; }
 
   size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
 
   // aFrac is either a floating-point number or a fraction made of two
@@ -182,17 +192,18 @@ class MediaExtendedMIMEType {
   friend Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
       const dom::VideoConfiguration& aConfig);
   friend Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
       const dom::AudioConfiguration& aConfig);
 
   MediaExtendedMIMEType(const nsACString& aOriginalString,
                         const nsACString& aMIMEType, bool aHaveCodecs,
                         const nsAString& aCodecs, int32_t aWidth,
-                        int32_t aHeight, double aFramerate, int32_t aBitrate);
+                        int32_t aHeight, double aFramerate, int32_t aBitrate,
+                        EOTF aEOTF = EOTF::UNSPECIFIED, int32_t aChannels = -1);
   MediaExtendedMIMEType(const nsACString& aOriginalString,
                         const nsACString& aMIMEType, bool aHaveCodecs,
                         const nsAString& aCodecs, int32_t aChannels,
                         int32_t aSamplerate, int32_t aBitrate);
 
   template <typename T>
   Maybe<T> GetMaybeNumber(T aNumber) const {
     return (aNumber < 0) ? Maybe<T>(Nothing()) : Some(T(aNumber));
@@ -201,16 +212,17 @@ class MediaExtendedMIMEType {
   nsCString mOriginalString;  // Original full string.
   MediaMIMEType mMIMEType;    // MIME type/subtype.
   bool mHaveCodecs = false;   // If false, mCodecs must be empty.
   MediaCodecs mCodecs;
   // For video
   int32_t mWidth = -1;     // -1 if not provided.
   int32_t mHeight = -1;    // -1 if not provided.
   double mFramerate = -1;  // -1 if not provided.
+  EOTF mEOTF = EOTF::UNSPECIFIED;
   // For audio
   int32_t mChannels = -1;    // -1 if not provided.
   int32_t mSamplerate = -1;  // -1 if not provided.
   // For both audio and video.
   int32_t mBitrate = -1;  // -1 if not provided.
 };
 
 Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(const nsAString& aType);
--- a/dom/media/mediasource/MediaSource.cpp
+++ b/dom/media/mediasource/MediaSource.cpp
@@ -356,22 +356,72 @@ void MediaSource::EndOfStream(const Medi
   MOZ_ASSERT(NS_IsMainThread());
   MSE_API("EndOfStream(aError=%s)", aError.ErrorName().get());
 
   SetReadyState(MediaSourceReadyState::Ended);
   mSourceBuffers->Ended();
   mDecoder->DecodeError(aError);
 }
 
+static bool AreExtraParametersSane(const nsAString& aType) {
+  Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType);
+  if (!containerType) {
+    return false;
+  }
+  auto extendedType = containerType->ExtendedType();
+  auto bitrate = extendedType.GetBitrate();
+  if (bitrate && *bitrate > 10000000) {
+    return false;
+  }
+  if (containerType->Type().HasVideoMajorType()) {
+    auto width = extendedType.GetWidth();
+    if (width && *width > MAX_VIDEO_WIDTH) {
+      return false;
+    }
+    auto height = extendedType.GetHeight();
+    if (height && *height > MAX_VIDEO_HEIGHT) {
+      return false;
+    }
+    auto framerate = extendedType.GetFramerate();
+    if (framerate && *framerate > 1000) {
+      return false;
+    }
+    auto eotf = extendedType.GetEOTF();
+    if (eotf && *eotf == EOTF::NOT_SUPPORTED) {
+      return false;
+    }
+  } else if (containerType->Type().HasAudioMajorType()) {
+    auto channels = extendedType.GetChannels();
+    if (channels && *channels > 6) {
+      return false;
+    }
+    auto samplerate = extendedType.GetSamplerate();
+    if (samplerate && *samplerate > 192000) {
+      return false;
+    }
+  }
+  return true;
+}
+
+static bool IsYouTube(const GlobalObject& aOwner) {
+  nsCString domain;
+  return aOwner.GetSubjectPrincipal() &&
+         NS_SUCCEEDED(aOwner.GetSubjectPrincipal()->GetBaseDomain(domain)) &&
+         domain.EqualsLiteral("youtube.com");
+}
+
 /* static */
 bool MediaSource::IsTypeSupported(const GlobalObject& aOwner,
                                   const nsAString& aType) {
   MOZ_ASSERT(NS_IsMainThread());
   DecoderDoctorDiagnostics diagnostics;
   nsresult rv = IsTypeSupported(aType, &diagnostics);
+  if (NS_SUCCEEDED(rv) && IsYouTube(aOwner) && !AreExtraParametersSane(aType)) {
+    rv = NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+  }
   nsCOMPtr<nsPIDOMWindowInner> window =
       do_QueryInterface(aOwner.GetAsSupports());
   diagnostics.StoreFormatDiagnostics(window ? window->GetExtantDoc() : nullptr,
                                      aType, NS_SUCCEEDED(rv), __func__);
   MOZ_LOG(GetMediaSourceAPILog(), mozilla::LogLevel::Debug,
           ("MediaSource::%s: IsTypeSupported(aType=%s) %s", __func__,
            NS_ConvertUTF16toUTF8(aType).get(),
            rv == NS_OK ? "OK" : "[not supported]"));
new file mode 100644
--- /dev/null
+++ b/dom/media/mediasource/test/file_isTypeSupported.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>MSE: isTypeSupported extended mime extensions</title>
+  </head>
+  <body>
+    <script>
+      window.addEventListener("message", (e) => {
+        const result = MediaSource.isTypeSupported(e.data);
+        const w = window.opener || window.parent;
+        w.postMessage(result, "*");
+      });
+      const w = window.opener || window.parent;
+      w.postMessage("ready", "*");
+    </script>
+  </body>
+</html>
--- a/dom/media/mediasource/test/mochitest.ini
+++ b/dom/media/mediasource/test/mochitest.ini
@@ -44,16 +44,17 @@ support-files =
   bipbop/bipbop_480_624kbps-video1.m4s bipbop/bipbop_480_624kbps-video1.m4s^headers^
   bipbop/bipbop_480_624kbps-video2.m4s bipbop/bipbop_480_624kbps-video2.m4s^headers^
   flac/IS.mp4 flac/IS.mp4^headers^ flac/00001.m4s flac/00001.m4s^headers^
   flac/00002.m4s flac/00002.m4s^headers^ flac/00003.m4s flac/00003.m4s^headers^
   avc3/init.mp4 avc3/init.mp4^headers^ avc3/segment1.m4s avc3/segment1.m4s^headers^
   tags_before_cluster.webm
   tags_before_cluster.webm^header^
   1516754.webm 1516754.webm^headers^
+  file_isTypeSupported.html
 
 [test_AbortAfterPartialMediaSegment.html]
 [test_AppendPartialInitSegment.html]
 [test_AVC3_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_AudioChange_mp4.html]
 skip-if = toolkit == 'android' || (os == "win" && processor == "aarch64") # Not supported on android, aarch64 due to 1538331
 [test_AutoRevocation.html]
@@ -83,16 +84,17 @@ skip-if = android_version == '22' || too
 skip-if = toolkit == 'android' # Not supported on android
 [test_Eviction_mp4.html]
 [test_ExperimentalAsync.html]
 skip-if = android_version == '22' || toolkit == 'android' || (os == "win" && processor == "aarch64") # bug 1341519, bug 1401090, aarch64 due to 1538391
 [test_FrameSelection.html]
 skip-if = android_version == '22' || toolkit == 'android' # bug 1341519, bug 1401090
 [test_FrameSelection_mp4.html]
 skip-if = toolkit == 'android' || os == 'win' # Not supported on android, # bug 1487973
+[test_isTypeSupportedExtensions.html]
 [test_HaveMetadataUnbufferedSeek.html]
 skip-if = android_version == '22' || toolkit == 'android' # bug 1342247, bug 1401090
 [test_HaveMetadataUnbufferedSeek_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_LiveSeekable.html]
 [test_LoadedDataFired_mp4.html]
 skip-if = toolkit == 'android' # Not supported on android
 [test_LoadedMetadataFired.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/mediasource/test/test_isTypeSupportedExtensions.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>MSE: isTypeSupported extended mime extensions</title>
+    <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  </head>
+  <body>
+    <pre id="test">
+      <script class="testbody" type="text/javascript">
+
+        SimpleTest.waitForExplicitFinish();
+
+        // Returns a promise that resolves with the next message received on
+        // our window.
+        function nextMessage() {
+          return new Promise((resolve, reject) => {
+            const f = (event) => {
+              window.removeEventListener("message", f);
+              resolve(event);
+            };
+            window.addEventListener("message", f);
+          });
+        }
+
+        // Tests YouTube's MIME type extensions. Runs through the list of cases
+        // and checks that the MIME extensions are only supported on a YouTube
+        // origin, where supported means we check the invalid cases and reject
+        // them.
+        async function runTest() {
+          const supportedCases = [
+            'video/mp4; codecs="avc1.42001E"',
+            'audio/mp4; codecs="mp4a.40.2"',
+            'video/webm; codecs="vp09.02.51.10.01.09.16.09"',
+            'audio/webm; codecs="opus"',
+            'audio/webm; codecs="opus"; channels=2',
+            'video/webm; codecs="vp9"',
+            'video/webm; codecs="vp9"; width=640',
+            'video/webm; codecs="vp9"',
+            'video/webm; codecs="vp9"; height=360',
+            'video/webm; codecs="vp9"',
+            'video/webm; codecs="vp9"; framerate=30',
+            'video/webm; codecs="vp9"; width=3840; height=2160; bitrate=2000000',
+            'video/mp4; codecs="avc1.4d4015"; width=426; height=240; framerate=24',
+            'video/mp4; codecs="avc1.4d401e"; width=640; height=360; framerate=24',
+            'video/mp4; codecs="avc1.4d401e"; width=854; height=480; framerate=24',
+            'video/mp4; codecs="avc1.4d401f"; width=1280; height=720; framerate=24',
+            'video/mp4; codecs="avc1.640028"; width=1920; height=1080; framerate=24',
+            'audio/mp4; codecs="mp4a.40.2"; channels=2',
+            'video/mp4; codecs="avc1.4d400c"; width=256; height=144; framerate=24',
+            'audio/webm; codecs="vorbis"; channels=2',
+            'video/webm; codecs="vp9"',
+            'video/webm; codecs="vp9"; eotf=bt709',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=426; height=240; framerate=24',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=640; height=360; framerate=24',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=854; height=480; framerate=24',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=1280; height=720; framerate=24',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=1920; height=1080; framerate=24',
+            'audio/webm; codecs="opus"; channels=2',
+            'audio/webm; codecs="opus"; channels=2',
+            'audio/webm; codecs="opus"; channels=2',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=2560; height=1440; framerate=24',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=3840; height=2160; framerate=24',
+            'video/mp4; codecs="av01.0.05M.08"; width=256; height=144; framerate=24',
+            'video/mp4; codecs="av01.0.05M.08"; width=426; height=240; framerate=24',
+            'video/mp4; codecs="av01.0.05M.08"; width=640; height=360; framerate=24',
+            'video/mp4; codecs="av01.0.05M.08"; width=854; height=480; framerate=24',
+            'video/mp4; codecs="av01.0.05M.08"; width=1280; height=720; framerate=24',
+            'video/mp4; codecs="avc1.4d4015"; width=426; height=240; framerate=25',
+            'video/mp4; codecs="avc1.4d401e"; width=640; height=360; framerate=25',
+            'video/mp4; codecs="avc1.4d401e"; width=854; height=480; framerate=25',
+            'video/mp4; codecs="avc1.4d401f"; width=1280; height=720; framerate=25',
+            'video/mp4; codecs="avc1.640028"; width=1920; height=1080; framerate=25',
+            'audio/mp4; codecs="mp4a.40.2"; channels=2',
+            'video/mp4; codecs="avc1.4d400c"; width=256; height=144; framerate=25',
+            'audio/webm; codecs="vorbis"; channels=2',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=426; height=240; framerate=25',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=640; height=360; framerate=25',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=854; height=480; framerate=25',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=1280; height=720; framerate=25',
+            'video/webm; codecs="vp09.00.51.08.01.01.01.01"; width=1920; height=1080; framerate=25',
+            'audio/webm; codecs="opus"; channels=2',
+            'audio/webm; codecs="opus"; channels=2',
+            'audio/webm; codecs="opus"; channels=2',
+          ];
+
+          const unsupportedOnYTCases = [
+            'audio/webm; codecs="opus"; channels=99',
+            'video/webm; codecs="vp9"; width=99999',
+            'video/webm; codecs="vp9"; height=99999',
+            'video/webm; codecs="vp9"; framerate=9999',
+            'video/webm; codecs="vp9"; width=3840; height=2160; bitrate=20000000',
+            'video/webm; codecs="vp9"; eotf=catavision',
+          ];
+
+          const unsupportedCases = [
+            'video/webm; codecs="vp09.02.51.10.01.09.99.99"',
+          ];
+
+          const frame = document.getElementById("frame");
+
+          const supportedOnYT = async (t) => {
+            // Sends a message to the YouTube iframe, which runs the check there.
+            const m = nextMessage();
+            frame.contentWindow.postMessage(t, "*");
+            const result = await m;
+            return result.data;
+          };
+
+          for (const t of supportedCases) {
+            ok(MediaSource.isTypeSupported(t), "Case '" + t + "' supported in non-YouTube origin");
+            is(await supportedOnYT(t), true, "Case '" + t + "' supported in YouTube origin");
+          }
+
+          for (const t of unsupportedOnYTCases) {
+            ok(MediaSource.isTypeSupported(t), "Case '" + t + "' supported in non-YouTube origin");
+            is(await supportedOnYT(t), false, "Case '" + t + "' *not* supported in YouTube origin");
+          }
+
+          for (const t of unsupportedCases) {
+            ok(!MediaSource.isTypeSupported(t), "Case '" + t + "' *not* supported in non-YouTube origin");
+            is(await supportedOnYT(t), false, "Case '" + t + "' *not* supported in YouTube origin");
+          }
+
+          SimpleTest.finish();
+        }
+
+        // Start the test once the child fake YouTube origin frame has signaled
+        // it's ready to receive requests.
+        nextMessage().then(runTest);
+
+        const f = document.createElement("iframe");
+        f.id = "frame";
+        f.src = "http://mochitest.youtube.com:443/tests/dom/media/mediasource/test/file_isTypeSupported.html";
+        document.getElementById("test").appendChild(f);
+
+
+      </script>
+      </pre>
+  </body>
+</html>