summaryrefslogtreecommitdiffstats
path: root/dom/media/VideoUtils.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/media/VideoUtils.cpp
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/VideoUtils.cpp')
-rw-r--r--dom/media/VideoUtils.cpp1252
1 files changed, 1252 insertions, 0 deletions
diff --git a/dom/media/VideoUtils.cpp b/dom/media/VideoUtils.cpp
new file mode 100644
index 0000000000..279ce9b912
--- /dev/null
+++ b/dom/media/VideoUtils.cpp
@@ -0,0 +1,1252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoUtils.h"
+
+#include <functional>
+#include <stdint.h>
+
+#include "CubebUtils.h"
+#include "ImageContainer.h"
+#include "MediaContainerType.h"
+#include "MediaResource.h"
+#include "TimeUnits.h"
+#include "mozilla/Base64.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/Telemetry.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsContentTypeParser.h"
+#include "nsIConsoleService.h"
+#include "nsINetworkLinkService.h"
+#include "nsIRandomGenerator.h"
+#include "nsMathUtils.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "AudioStream.h"
+
+namespace mozilla {
+
+using gfx::ColorRange;
+using gfx::CICP::ColourPrimaries;
+using gfx::CICP::MatrixCoefficients;
+using gfx::CICP::TransferCharacteristics;
+using layers::PlanarYCbCrImage;
+using media::TimeUnit;
+
+double ToMicrosecondResolution(double aSeconds) {
+ double integer;
+ modf(aSeconds * USECS_PER_S, &integer);
+ return integer / USECS_PER_S;
+}
+
+CheckedInt64 SaferMultDiv(int64_t aValue, uint64_t aMul, uint64_t aDiv) {
+ if (aMul > INT64_MAX || aDiv > INT64_MAX) {
+ return CheckedInt64(INT64_MAX) + 1; // Return an invalid checked int.
+ }
+ int64_t mul = AssertedCast<int64_t>(aMul);
+ int64_t div = AssertedCast<int64_t>(aDiv);
+ int64_t major = aValue / div;
+ int64_t remainder = aValue % div;
+ return CheckedInt64(remainder) * mul / div + CheckedInt64(major) * mul;
+}
+
+// Converts from number of audio frames to microseconds, given the specified
+// audio rate.
+CheckedInt64 FramesToUsecs(int64_t aFrames, uint32_t aRate) {
+ return SaferMultDiv(aFrames, USECS_PER_S, aRate);
+}
+
+// Converts from microseconds to number of audio frames, given the specified
+// audio rate.
+CheckedInt64 UsecsToFrames(int64_t aUsecs, uint32_t aRate) {
+ return SaferMultDiv(aUsecs, aRate, USECS_PER_S);
+}
+
+// Format TimeUnit as number of frames at given rate.
+CheckedInt64 TimeUnitToFrames(const TimeUnit& aTime, uint32_t aRate) {
+ return aTime.IsValid() ? UsecsToFrames(aTime.ToMicroseconds(), aRate)
+ : CheckedInt64(INT64_MAX) + 1;
+}
+
+nsresult SecondsToUsecs(double aSeconds, int64_t& aOutUsecs) {
+ if (aSeconds * double(USECS_PER_S) > double(INT64_MAX)) {
+ return NS_ERROR_FAILURE;
+ }
+ aOutUsecs = int64_t(aSeconds * double(USECS_PER_S));
+ return NS_OK;
+}
+
+static int32_t ConditionDimension(float aValue) {
+ // This will exclude NaNs and too-big values.
+ if (aValue > 1.0 && aValue <= float(INT32_MAX)) {
+ return int32_t(NS_round(aValue));
+ }
+ return 0;
+}
+
+void ScaleDisplayByAspectRatio(gfx::IntSize& aDisplay, float aAspectRatio) {
+ if (aAspectRatio > 1.0) {
+ // Increase the intrinsic width
+ aDisplay.width =
+ ConditionDimension(aAspectRatio * AssertedCast<float>(aDisplay.width));
+ } else {
+ // Increase the intrinsic height
+ aDisplay.height =
+ ConditionDimension(AssertedCast<float>(aDisplay.height) / aAspectRatio);
+ }
+}
+
+static int64_t BytesToTime(int64_t offset, int64_t length, int64_t durationUs) {
+ NS_ASSERTION(length > 0, "Must have positive length");
+ double r = double(offset) / double(length);
+ if (r > 1.0) {
+ r = 1.0;
+ }
+ return int64_t(double(durationUs) * r);
+}
+
+media::TimeIntervals GetEstimatedBufferedTimeRanges(
+ mozilla::MediaResource* aStream, int64_t aDurationUsecs) {
+ media::TimeIntervals buffered;
+ // Nothing to cache if the media takes 0us to play.
+ if (aDurationUsecs <= 0 || !aStream) {
+ return buffered;
+ }
+
+ // Special case completely cached files. This also handles local files.
+ if (aStream->IsDataCachedToEndOfResource(0)) {
+ buffered += media::TimeInterval(TimeUnit::Zero(),
+ TimeUnit::FromMicroseconds(aDurationUsecs));
+ return buffered;
+ }
+
+ int64_t totalBytes = aStream->GetLength();
+
+ // If we can't determine the total size, pretend that we have nothing
+ // buffered. This will put us in a state of eternally-low-on-undecoded-data
+ // which is not great, but about the best we can do.
+ if (totalBytes <= 0) {
+ return buffered;
+ }
+
+ int64_t startOffset = aStream->GetNextCachedData(0);
+ while (startOffset >= 0) {
+ int64_t endOffset = aStream->GetCachedDataEnd(startOffset);
+ // Bytes [startOffset..endOffset] are cached.
+ NS_ASSERTION(startOffset >= 0, "Integer underflow in GetBuffered");
+ NS_ASSERTION(endOffset >= 0, "Integer underflow in GetBuffered");
+
+ int64_t startUs = BytesToTime(startOffset, totalBytes, aDurationUsecs);
+ int64_t endUs = BytesToTime(endOffset, totalBytes, aDurationUsecs);
+ if (startUs != endUs) {
+ buffered += media::TimeInterval(TimeUnit::FromMicroseconds(startUs),
+ TimeUnit::FromMicroseconds(endUs));
+ }
+ startOffset = aStream->GetNextCachedData(endOffset);
+ }
+ return buffered;
+}
+
+void DownmixStereoToMono(mozilla::AudioDataValue* aBuffer, uint32_t aFrames) {
+ MOZ_ASSERT(aBuffer);
+ const int channels = 2;
+ for (uint32_t fIdx = 0; fIdx < aFrames; ++fIdx) {
+#ifdef MOZ_SAMPLE_TYPE_FLOAT32
+ float sample = 0.0;
+#else
+ int sample = 0;
+#endif
+ // The sample of the buffer would be interleaved.
+ sample = (aBuffer[fIdx * channels] + aBuffer[fIdx * channels + 1]) * 0.5f;
+ aBuffer[fIdx * channels] = aBuffer[fIdx * channels + 1] = sample;
+ }
+}
+
+uint32_t DecideAudioPlaybackChannels(const AudioInfo& info) {
+ if (StaticPrefs::accessibility_monoaudio_enable()) {
+ return 1;
+ }
+
+ if (StaticPrefs::media_forcestereo_enabled()) {
+ return 2;
+ }
+
+ return info.mChannels;
+}
+
+uint32_t DecideAudioPlaybackSampleRate(const AudioInfo& aInfo,
+ bool aShouldResistFingerprinting) {
+ bool resampling = StaticPrefs::media_resampling_enabled();
+
+ uint32_t rate = 0;
+
+ if (resampling) {
+ rate = 48000;
+ } else if (aInfo.mRate >= 44100) {
+ // The original rate is of good quality and we want to minimize unecessary
+ // resampling, so we let cubeb decide how to resample (if needed).
+ rate = aInfo.mRate;
+ } else {
+ // We will resample all data to match cubeb's preferred sampling rate.
+ rate = CubebUtils::PreferredSampleRate(aShouldResistFingerprinting);
+ if (rate > 384000) {
+ // bogus rate, fall back to something else;
+ rate = 48000;
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(rate, "output rate can't be 0.");
+
+ return rate;
+}
+
+bool IsDefaultPlaybackDeviceMono() {
+ return CubebUtils::MaxNumberOfChannels() == 1;
+}
+
+bool IsVideoContentType(const nsCString& aContentType) {
+ constexpr auto video = "video"_ns;
+ return FindInReadable(video, aContentType);
+}
+
+bool IsValidVideoRegion(const gfx::IntSize& aFrame,
+ const gfx::IntRect& aPicture,
+ const gfx::IntSize& aDisplay) {
+ return aFrame.width > 0 && aFrame.width <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aFrame.height > 0 &&
+ aFrame.height <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aFrame.width * aFrame.height <= MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT &&
+ aPicture.width > 0 &&
+ aPicture.width <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.x < PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.x + aPicture.width < PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.height > 0 &&
+ aPicture.height <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.y < PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.y + aPicture.height < PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.width * aPicture.height <=
+ MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT &&
+ aDisplay.width > 0 &&
+ aDisplay.width <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aDisplay.height > 0 &&
+ aDisplay.height <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aDisplay.width * aDisplay.height <= MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT;
+}
+
+already_AddRefed<SharedThreadPool> GetMediaThreadPool(MediaThreadType aType) {
+ const char* name;
+ uint32_t threads = 4;
+ switch (aType) {
+ case MediaThreadType::PLATFORM_DECODER:
+ name = "MediaPDecoder";
+ break;
+ case MediaThreadType::WEBRTC_CALL_THREAD:
+ name = "WebrtcCallThread";
+ threads = 1;
+ break;
+ case MediaThreadType::WEBRTC_WORKER:
+ name = "WebrtcWorker";
+ break;
+ case MediaThreadType::MDSM:
+ name = "MediaDecoderStateMachine";
+ threads = 1;
+ break;
+ case MediaThreadType::PLATFORM_ENCODER:
+ name = "MediaPEncoder";
+ break;
+ default:
+ MOZ_FALLTHROUGH_ASSERT("Unexpected MediaThreadType");
+ case MediaThreadType::SUPERVISOR:
+ name = "MediaSupervisor";
+ break;
+ }
+
+ RefPtr<SharedThreadPool> pool =
+ SharedThreadPool::Get(nsDependentCString(name), threads);
+
+ // Ensure a larger stack for platform decoder threads
+ if (aType == MediaThreadType::PLATFORM_DECODER) {
+ const uint32_t minStackSize = 512 * 1024;
+ uint32_t stackSize;
+ MOZ_ALWAYS_SUCCEEDS(pool->GetThreadStackSize(&stackSize));
+ if (stackSize < minStackSize) {
+ MOZ_ALWAYS_SUCCEEDS(pool->SetThreadStackSize(minStackSize));
+ }
+ }
+
+ return pool.forget();
+}
+
+bool ExtractVPXCodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aBitDepth) {
+ uint8_t dummyChromaSubsampling = 1;
+ VideoColorSpace dummyColorspace;
+ return ExtractVPXCodecDetails(aCodec, aProfile, aLevel, aBitDepth,
+ dummyChromaSubsampling, dummyColorspace);
+}
+
+bool ExtractVPXCodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aBitDepth,
+ uint8_t& aChromaSubsampling,
+ VideoColorSpace& aColorSpace) {
+ // Assign default value.
+ aChromaSubsampling = 1;
+ auto splitter = aCodec.Split(u'.');
+ auto fieldsItr = splitter.begin();
+ auto fourCC = *fieldsItr;
+
+ if (!fourCC.EqualsLiteral("vp09") && !fourCC.EqualsLiteral("vp08")) {
+ // Invalid 4CC
+ return false;
+ }
+ ++fieldsItr;
+ uint8_t primary, transfer, matrix, range;
+ uint8_t* fields[] = {&aProfile, &aLevel, &aBitDepth, &aChromaSubsampling,
+ &primary, &transfer, &matrix, &range};
+ int fieldsCount = 0;
+ nsresult rv;
+ for (; fieldsItr != splitter.end(); ++fieldsItr, ++fieldsCount) {
+ if (fieldsCount > 7) {
+ // No more than 8 fields are expected.
+ return false;
+ }
+ *(fields[fieldsCount]) =
+ static_cast<uint8_t>((*fieldsItr).ToInteger(&rv, 10));
+ // We got invalid field value, parsing error.
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+ // Mandatory Fields
+ // <sample entry 4CC>.<profile>.<level>.<bitDepth>.
+ // Optional Fields
+ // <chromaSubsampling>.<colourPrimaries>.<transferCharacteristics>.
+ // <matrixCoefficients>.<videoFullRangeFlag>
+ // First three fields are mandatory(we have parsed 4CC).
+ if (fieldsCount < 3) {
+ // Invalid number of fields.
+ return false;
+ }
+ // Start to validate the parsing value.
+
+ // profile should be 0,1,2 or 3.
+ // See https://www.webmproject.org/vp9/profiles/
+ if (aProfile > 3) {
+ // Invalid profile.
+ return false;
+ }
+
+ // level, See https://www.webmproject.org/vp9/mp4/#semantics_1
+ switch (aLevel) {
+ case 10:
+ case 11:
+ case 20:
+ case 21:
+ case 30:
+ case 31:
+ case 40:
+ case 41:
+ case 50:
+ case 51:
+ case 52:
+ case 60:
+ case 61:
+ case 62:
+ break;
+ default:
+ // Invalid level.
+ return false;
+ }
+
+ if (aBitDepth != 8 && aBitDepth != 10 && aBitDepth != 12) {
+ // Invalid bitDepth:
+ return false;
+ }
+
+ if (fieldsCount == 3) {
+ // No more options.
+ return true;
+ }
+
+ // chromaSubsampling should be 0,1,2,3...4~7 are reserved.
+ if (aChromaSubsampling > 3) {
+ return false;
+ }
+
+ if (fieldsCount == 4) {
+ // No more options.
+ return true;
+ }
+
+ // It is an integer that is defined by the "Colour primaries"
+ // section of ISO/IEC 23001-8:2016 Table 2.
+ // We treat reserved value as false case.
+ if (primary == 0 || primary == 3 || primary > 22) {
+ // reserved value.
+ return false;
+ }
+ if (primary > 12 && primary < 22) {
+ // 13~21 are reserved values.
+ return false;
+ }
+ aColorSpace.mPrimaries = static_cast<ColourPrimaries>(primary);
+
+ if (fieldsCount == 5) {
+ // No more options.
+ return true;
+ }
+
+ // It is an integer that is defined by the
+ // "Transfer characteristics" section of ISO/IEC 23001-8:2016 Table 3.
+ // We treat reserved value as false case.
+ if (transfer == 0 || transfer == 3 || transfer > 18) {
+ // reserved value.
+ return false;
+ }
+ aColorSpace.mTransfer = static_cast<TransferCharacteristics>(transfer);
+
+ if (fieldsCount == 6) {
+ // No more options.
+ return true;
+ }
+
+ // It is an integer that is defined by the
+ // "Matrix coefficients" section of ISO/IEC 23001-8:2016 Table 4.
+ // We treat reserved value as false case.
+ if (matrix == 3 || matrix > 11) {
+ return false;
+ }
+ aColorSpace.mMatrix = static_cast<MatrixCoefficients>(matrix);
+
+ // If matrixCoefficients is 0 (RGB), then chroma subsampling MUST be 3
+ // (4:4:4).
+ if (aColorSpace.mMatrix == MatrixCoefficients::MC_IDENTITY &&
+ aChromaSubsampling != 3) {
+ return false;
+ }
+
+ if (fieldsCount == 7) {
+ // No more options.
+ return true;
+ }
+
+ // videoFullRangeFlag indicates the black level and range of the luma and
+ // chroma signals. 0 = legal range (e.g. 16-235 for 8 bit sample depth);
+ // 1 = full range (e.g. 0-255 for 8-bit sample depth).
+ aColorSpace.mRange = static_cast<ColorRange>(range);
+ return range <= 1;
+}
+
+bool ExtractH264CodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aConstraint, uint8_t& aLevel) {
+ // H.264 codecs parameters have a type defined as avcN.PPCCLL, where
+ // N = avc type. avc3 is avcc with SPS & PPS implicit (within stream)
+ // PP = profile_idc, CC = constraint_set flags, LL = level_idc.
+ // We ignore the constraint_set flags, as it's not clear from any
+ // documentation what constraints the platform decoders support.
+ // See
+ // http://blog.pearce.org.nz/2013/11/what-does-h264avc1-codecs-parameters.html
+ // for more details.
+ if (aCodec.Length() != strlen("avc1.PPCCLL")) {
+ return false;
+ }
+
+ // Verify the codec starts with "avc1." or "avc3.".
+ const nsAString& sample = Substring(aCodec, 0, 5);
+ if (!sample.EqualsASCII("avc1.") && !sample.EqualsASCII("avc3.")) {
+ return false;
+ }
+
+ // Extract the profile_idc, constraint_flags and level_idc.
+ nsresult rv = NS_OK;
+ aProfile = Substring(aCodec, 5, 2).ToInteger(&rv, 16);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Constraint flags are stored on the 6 most significant bits, first two bits
+ // are reserved_zero_2bits.
+ aConstraint = Substring(aCodec, 7, 2).ToInteger(&rv, 16);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ aLevel = Substring(aCodec, 9, 2).ToInteger(&rv, 16);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (aLevel == 9) {
+ aLevel = H264_LEVEL_1_b;
+ } else if (aLevel <= 5) {
+ aLevel *= 10;
+ }
+
+ return true;
+}
+
+bool IsH265ProfileRecognizable(uint8_t aProfile,
+ int32_t aProfileCompabilityFlags) {
+ enum Profile {
+ eUnknown,
+ eHighThroughputScreenExtended,
+ eScalableRangeExtension,
+ eScreenExtended,
+ e3DMain,
+ eScalableMain,
+ eMultiviewMain,
+ eHighThroughput,
+ eRangeExtension,
+ eMain10,
+ eMain,
+ eMainStillPicture
+ };
+ Profile p = eUnknown;
+
+ // Spec A.3.8
+ if (aProfile == 11 || (aProfileCompabilityFlags & 0x800)) {
+ p = eHighThroughputScreenExtended;
+ }
+ // Spec H.11.1.2
+ if (aProfile == 10 || (aProfileCompabilityFlags & 0x400)) {
+ p = eScalableRangeExtension;
+ }
+ // Spec A.3.7
+ if (aProfile == 9 || (aProfileCompabilityFlags & 0x200)) {
+ p = eScreenExtended;
+ }
+ // Spec I.11.1.1
+ if (aProfile == 8 || (aProfileCompabilityFlags & 0x100)) {
+ p = e3DMain;
+ }
+ // Spec H.11.1.1
+ if (aProfile == 7 || (aProfileCompabilityFlags & 0x80)) {
+ p = eScalableMain;
+ }
+ // Spec G.11.1.1
+ if (aProfile == 6 || (aProfileCompabilityFlags & 0x40)) {
+ p = eMultiviewMain;
+ }
+ // Spec A.3.6
+ if (aProfile == 5 || (aProfileCompabilityFlags & 0x20)) {
+ p = eHighThroughput;
+ }
+ // Spec A.3.5
+ if (aProfile == 4 || (aProfileCompabilityFlags & 0x10)) {
+ p = eRangeExtension;
+ }
+ // Spec A.3.3
+ // NOTICE: Do not change the order of below sections
+ if (aProfile == 2 || (aProfileCompabilityFlags & 0x4)) {
+ p = eMain10;
+ }
+ // Spec A.3.2
+ // When aProfileCompabilityFlags[1] is equal to 1,
+ // aProfileCompabilityFlags[2] should be equal to 1 as well.
+ if (aProfile == 1 || (aProfileCompabilityFlags & 0x2)) {
+ p = eMain;
+ }
+ // Spec A.3.4
+ // When aProfileCompabilityFlags[3] is equal to 1,
+ // aProfileCompabilityFlags[1] and
+ // aProfileCompabilityFlags[2] should be equal to 1 as well.
+ if (aProfile == 3 || (aProfileCompabilityFlags & 0x8)) {
+ p = eMainStillPicture;
+ }
+
+ return p != eUnknown;
+}
+
+bool ExtractH265CodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, nsTArray<uint8_t>& aConstraints) {
+ // HEVC codec id consists of:
+ const size_t maxHevcCodecIdLength =
+ 5 + // 'hev1.' or 'hvc1.' prefix (5 chars)
+ 4 + // profile, e.g. '.A12' (max 4 chars)
+ 9 + // profile_compatibility, dot + 32-bit hex number (max 9 chars)
+ 5 + // tier and level, e.g. '.H120' (max 5 chars)
+ 18; // up to 6 constraint bytes, bytes are dot-separated and hex-encoded.
+
+ if (aCodec.Length() > maxHevcCodecIdLength) {
+ return false;
+ }
+
+ // Verify the codec starts with "hev1." or "hvc1.".
+ const nsAString& sample = Substring(aCodec, 0, 5);
+ if (!sample.EqualsASCII("hev1.") && !sample.EqualsASCII("hvc1.")) {
+ return false;
+ }
+
+ nsresult rv;
+ CheckedUint8 profile;
+ int32_t compabilityFlags = 0;
+ CheckedUint8 level = 0;
+ nsTArray<uint8_t> constraints;
+
+ auto splitter = aCodec.Split(u'.');
+ size_t count = 0;
+ for (auto iter = splitter.begin(); iter != splitter.end(); ++iter, ++count) {
+ const auto& fieldStr = *iter;
+ if (fieldStr.IsEmpty()) {
+ return false;
+ }
+
+ if (count == 0) {
+ MOZ_RELEASE_ASSERT(fieldStr.EqualsASCII("hev1") ||
+ fieldStr.EqualsASCII("hvc1"));
+ continue;
+ }
+
+ if (count == 1) { // profile
+ Maybe<uint8_t> validProfileSpace;
+ if (fieldStr.First() == u'A' || fieldStr.First() == u'B' ||
+ fieldStr.First() == u'C') {
+ validProfileSpace.emplace(1 + (fieldStr.First() - 'A'));
+ }
+ // If fieldStr.First() is not A, B, C or a digit, ToInteger() should fail.
+ profile = validProfileSpace ? Substring(fieldStr, 1).ToInteger(&rv)
+ : fieldStr.ToInteger(&rv);
+ if (NS_FAILED(rv) || !profile.isValid() || profile.value() > 0x1F) {
+ return false;
+ }
+ continue;
+ }
+
+ if (count == 2) { // profile compatibility flags
+ compabilityFlags = fieldStr.ToInteger(&rv, 16);
+ NS_ENSURE_SUCCESS(rv, false);
+ continue;
+ }
+
+ if (count == 3) { // tier and level
+ Maybe<uint8_t> validProfileTier;
+ if (fieldStr.First() == u'L' || fieldStr.First() == u'H') {
+ validProfileTier.emplace(fieldStr.First() == u'L' ? 0 : 1);
+ }
+ // If fieldStr.First() is not L, H, or a digit, ToInteger() should fail.
+ level = validProfileTier ? Substring(fieldStr, 1).ToInteger(&rv)
+ : fieldStr.ToInteger(&rv);
+ if (NS_FAILED(rv) || !level.isValid()) {
+ return false;
+ }
+ continue;
+ }
+
+ // The rest is constraint bytes.
+ if (count > 10) {
+ return false;
+ }
+
+ CheckedUint8 byte(fieldStr.ToInteger(&rv, 16));
+ if (NS_FAILED(rv) || !byte.isValid()) {
+ return false;
+ }
+ constraints.AppendElement(byte.value());
+ }
+
+ if (count < 4 /* Parse til level at least */ || constraints.Length() > 6 ||
+ !IsH265ProfileRecognizable(profile.value(), compabilityFlags)) {
+ return false;
+ }
+
+ aProfile = profile.value();
+ aLevel = level.value();
+ aConstraints = std::move(constraints);
+ return true;
+}
+
+bool ExtractAV1CodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aTier, uint8_t& aBitDepth,
+ bool& aMonochrome, bool& aSubsamplingX,
+ bool& aSubsamplingY, uint8_t& aChromaSamplePosition,
+ VideoColorSpace& aColorSpace) {
+ auto fourCC = Substring(aCodec, 0, 4);
+
+ if (!fourCC.EqualsLiteral("av01")) {
+ // Invalid 4CC
+ return false;
+ }
+
+ // Format is:
+ // av01.N.NN[MH].NN.B.BBN.NN.NN.NN.B
+ // where
+ // N = decimal digit
+ // [] = single character
+ // B = binary digit
+ // Field order:
+ // <sample entry 4CC>.<profile>.<level><tier>.<bitDepth>
+ // [.<monochrome>.<chromaSubsampling>
+ // .<colorPrimaries>.<transferCharacteristics>.<matrixCoefficients>
+ // .<videoFullRangeFlag>]
+ //
+ // If any optional field is found, all the rest must be included.
+ //
+ // Parsing stops but does not fail upon encountering unexpected characters
+ // at the end of an otherwise well-formed string.
+ //
+ // See https://aomediacodec.github.io/av1-isobmff/#codecsparam
+
+ struct AV1Field {
+ uint8_t* field;
+ size_t length;
+ };
+ uint8_t monochrome;
+ uint8_t subsampling;
+ uint8_t primary;
+ uint8_t transfer;
+ uint8_t matrix;
+ uint8_t range;
+ AV1Field fields[] = {{&aProfile, 1},
+ {&aLevel, 2},
+ // parsing loop skips tier
+ {&aBitDepth, 2},
+ {&monochrome, 1},
+ {&subsampling, 3},
+ {&primary, 2},
+ {&transfer, 2},
+ {&matrix, 2},
+ {&range, 1}};
+
+ auto splitter = aCodec.Split(u'.');
+ auto iter = splitter.begin();
+ ++iter;
+ size_t fieldCount = 0;
+ while (iter != splitter.end()) {
+ // Exit if there are too many fields.
+ if (fieldCount >= 9) {
+ return false;
+ }
+
+ AV1Field& field = fields[fieldCount];
+ auto fieldStr = *iter;
+
+ if (field.field == &aLevel) {
+ // Parse tier and remove it from the level field.
+ if (fieldStr.Length() < 3) {
+ return false;
+ }
+ auto tier = fieldStr[2];
+ switch (tier) {
+ case 'M':
+ aTier = 0;
+ break;
+ case 'H':
+ aTier = 1;
+ break;
+ default:
+ return false;
+ }
+ fieldStr.SetLength(2);
+ }
+
+ if (fieldStr.Length() < field.length) {
+ return false;
+ }
+
+ // Manually parse values since nsString.ToInteger silently stops parsing
+ // upon encountering unknown characters.
+ uint8_t value = 0;
+ for (size_t i = 0; i < field.length; i++) {
+ uint8_t oldValue = value;
+ char16_t character = fieldStr[i];
+ if ('0' <= character && character <= '9') {
+ value = (value * 10) + (character - '0');
+ } else {
+ return false;
+ }
+ if (value < oldValue) {
+ // Overflow is possible on the 3-digit subsampling field.
+ return false;
+ }
+ }
+
+ *field.field = value;
+
+ ++fieldCount;
+ ++iter;
+
+ // Field had extra characters, exit early.
+ if (fieldStr.Length() > field.length) {
+ // Disallow numbers as unexpected characters.
+ char16_t character = fieldStr[field.length];
+ if ('0' <= character && character <= '9') {
+ return false;
+ }
+ break;
+ }
+ }
+
+ // Spec requires profile, level/tier, bitdepth, or for all possible fields to
+ // be present.
+ if (fieldCount != 3 && fieldCount != 9) {
+ return false;
+ }
+
+ // Valid profiles are: Main (0), High (1), Professional (2).
+ // Levels range from 0 to 23, or 31 to remove level restrictions.
+ if (aProfile > 2 || (aLevel > 23 && aLevel != 31)) {
+ return false;
+ }
+
+ if (fieldCount == 3) {
+ // If only required fields are included, set to the spec defaults for the
+ // rest and continue validating.
+ aMonochrome = false;
+ aSubsamplingX = true;
+ aSubsamplingY = true;
+ aChromaSamplePosition = 0;
+ aColorSpace.mPrimaries = ColourPrimaries::CP_BT709;
+ aColorSpace.mTransfer = TransferCharacteristics::TC_BT709;
+ aColorSpace.mMatrix = MatrixCoefficients::MC_BT709;
+ aColorSpace.mRange = ColorRange::LIMITED;
+ } else {
+ // Extract the individual values for the remaining fields, and check for
+ // valid values for each.
+
+ // Monochrome is a boolean.
+ if (monochrome > 1) {
+ return false;
+ }
+ aMonochrome = !!monochrome;
+
+ // Extract individual digits of the subsampling field.
+ // Subsampling is two binary digits for x and y
+ // and one enumerated sample position field of
+ // Unknown (0), Vertical (1), Colocated (2).
+ uint8_t subsamplingX = (subsampling / 100) % 10;
+ uint8_t subsamplingY = (subsampling / 10) % 10;
+ if (subsamplingX > 1 || subsamplingY > 1) {
+ return false;
+ }
+ aSubsamplingX = !!subsamplingX;
+ aSubsamplingY = !!subsamplingY;
+ aChromaSamplePosition = subsampling % 10;
+ if (aChromaSamplePosition > 2) {
+ return false;
+ }
+
+ // We can validate the color space values using CICP enums, as the values
+ // are standardized in Rec. ITU-T H.273.
+ aColorSpace.mPrimaries = static_cast<ColourPrimaries>(primary);
+ aColorSpace.mTransfer = static_cast<TransferCharacteristics>(transfer);
+ aColorSpace.mMatrix = static_cast<MatrixCoefficients>(matrix);
+ if (gfx::CICP::IsReserved(aColorSpace.mPrimaries) ||
+ gfx::CICP::IsReserved(aColorSpace.mTransfer) ||
+ gfx::CICP::IsReserved(aColorSpace.mMatrix)) {
+ return false;
+ }
+ // Range is a boolean, true meaning full and false meaning limited range.
+ if (range > 1) {
+ return false;
+ }
+ aColorSpace.mRange = static_cast<ColorRange>(range);
+ }
+
+ // Begin validating all parameter values:
+
+ // Only Levels 8 and above (4.0 and greater) can specify Tier.
+ // See: 5.5.1. General sequence header OBU syntax,
+ // if ( seq_level_idx[ i ] > 7 ) seq_tier[ i ] = f(1)
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=42
+ // Also: Annex A, A.3. Levels, columns MainMbps and HighMbps
+ // at https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=652
+ if (aLevel < 8 && aTier > 0) {
+ return false;
+ }
+
+ // Supported bit depths are 8, 10 and 12.
+ if (aBitDepth != 8 && aBitDepth != 10 && aBitDepth != 12) {
+ return false;
+ }
+ // Profiles 0 and 1 only support 8-bit and 10-bit.
+ if (aProfile < 2 && aBitDepth == 12) {
+ return false;
+ }
+
+ // x && y subsampling is used to specify monochrome 4:0:0 as well
+ bool is420or400 = aSubsamplingX && aSubsamplingY;
+ bool is422 = aSubsamplingX && !aSubsamplingY;
+ bool is444 = !aSubsamplingX && !aSubsamplingY;
+
+ // Profile 0 only supports 4:2:0.
+ if (aProfile == 0 && !is420or400) {
+ return false;
+ }
+ // Profile 1 only supports 4:4:4.
+ if (aProfile == 1 && !is444) {
+ return false;
+ }
+ // Profile 2 only allows 4:2:2 at 10 bits and below.
+ if (aProfile == 2 && aBitDepth < 12 && !is422) {
+ return false;
+ }
+ // Chroma sample position can only be specified with 4:2:0.
+ if (aChromaSamplePosition != 0 && !is420or400) {
+ return false;
+ }
+
+ // When video is monochrome, subsampling must be 4:0:0.
+ if (aMonochrome && (aChromaSamplePosition != 0 || !is420or400)) {
+ return false;
+ }
+ // Monochrome can only be signaled when profile is 0 or 2.
+ // Note: This check is redundant with the above subsampling check,
+ // as profile 1 only supports 4:4:4.
+ if (aMonochrome && aProfile != 0 && aProfile != 2) {
+ return false;
+ }
+
+ // Identity matrix requires 4:4:4 subsampling.
+ if (aColorSpace.mMatrix == MatrixCoefficients::MC_IDENTITY &&
+ (aSubsamplingX || aSubsamplingY ||
+ aColorSpace.mRange != gfx::ColorRange::FULL)) {
+ return false;
+ }
+
+ return true;
+}
+
+nsresult GenerateRandomName(nsCString& aOutSalt, uint32_t aLength) {
+ nsresult rv;
+ nsCOMPtr<nsIRandomGenerator> rg =
+ do_GetService("@mozilla.org/security/random-generator;1", &rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // For each three bytes of random data we will get four bytes of ASCII.
+ const uint32_t requiredBytesLength =
+ static_cast<uint32_t>((aLength + 3) / 4 * 3);
+
+ uint8_t* buffer;
+ rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCString temp;
+ nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer),
+ requiredBytesLength);
+ rv = Base64Encode(randomData, temp);
+ free(buffer);
+ buffer = nullptr;
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ aOutSalt = std::move(temp);
+ return NS_OK;
+}
+
+nsresult GenerateRandomPathName(nsCString& aOutSalt, uint32_t aLength) {
+ nsresult rv = GenerateRandomName(aOutSalt, aLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need
+ // to replace illegal characters -- notably '/'
+ aOutSalt.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_');
+ return NS_OK;
+}
+
+already_AddRefed<TaskQueue> CreateMediaDecodeTaskQueue(const char* aName) {
+ RefPtr<TaskQueue> queue = TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER), aName);
+ return queue.forget();
+}
+
+void SimpleTimer::Cancel() {
+ if (mTimer) {
+#ifdef DEBUG
+ nsCOMPtr<nsIEventTarget> target;
+ mTimer->GetTarget(getter_AddRefs(target));
+ bool onCurrent;
+ nsresult rv = target->IsOnCurrentThread(&onCurrent);
+ MOZ_ASSERT(NS_SUCCEEDED(rv) && onCurrent);
+#endif
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ mTask = nullptr;
+}
+
+NS_IMETHODIMP
+SimpleTimer::Notify(nsITimer* timer) {
+ RefPtr<SimpleTimer> deathGrip(this);
+ if (mTask) {
+ mTask->Run();
+ mTask = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SimpleTimer::GetName(nsACString& aName) {
+ aName.AssignLiteral("SimpleTimer");
+ return NS_OK;
+}
+
+nsresult SimpleTimer::Init(nsIRunnable* aTask, uint32_t aTimeoutMs,
+ nsIEventTarget* aTarget) {
+ nsresult rv;
+
+ // Get target thread first, so we don't have to cancel the timer if it fails.
+ nsCOMPtr<nsIEventTarget> target;
+ if (aTarget) {
+ target = aTarget;
+ } else {
+ target = GetMainThreadSerialEventTarget();
+ if (!target) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+
+ rv = NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, aTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT, target);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mTask = aTask;
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(SimpleTimer, nsITimerCallback, nsINamed)
+
+already_AddRefed<SimpleTimer> SimpleTimer::Create(nsIRunnable* aTask,
+ uint32_t aTimeoutMs,
+ nsIEventTarget* aTarget) {
+ RefPtr<SimpleTimer> t(new SimpleTimer());
+ if (NS_FAILED(t->Init(aTask, aTimeoutMs, aTarget))) {
+ return nullptr;
+ }
+ return t.forget();
+}
+
+void LogToBrowserConsole(const nsAString& aMsg) {
+ if (!NS_IsMainThread()) {
+ nsString msg(aMsg);
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "LogToBrowserConsole", [msg]() { LogToBrowserConsole(msg); });
+ SchedulerGroup::Dispatch(task.forget());
+ return;
+ }
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService("@mozilla.org/consoleservice;1"));
+ if (!console) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+ nsAutoString msg(aMsg);
+ console->LogStringMessage(msg.get());
+}
+
+bool ParseCodecsString(const nsAString& aCodecs,
+ nsTArray<nsString>& aOutCodecs) {
+ aOutCodecs.Clear();
+ bool expectMoreTokens = false;
+ nsCharSeparatedTokenizer tokenizer(aCodecs, ',');
+ while (tokenizer.hasMoreTokens()) {
+ const nsAString& token = tokenizer.nextToken();
+ expectMoreTokens = tokenizer.separatorAfterCurrentToken();
+ aOutCodecs.AppendElement(token);
+ }
+ return !expectMoreTokens;
+}
+
+bool ParseMIMETypeString(const nsAString& aMIMEType,
+ nsString& aOutContainerType,
+ nsTArray<nsString>& aOutCodecs) {
+ nsContentTypeParser parser(aMIMEType);
+ nsresult rv = parser.GetType(aOutContainerType);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ nsString codecsStr;
+ parser.GetParameter("codecs", codecsStr);
+ return ParseCodecsString(codecsStr, aOutCodecs);
+}
+
+template <int N>
+static bool StartsWith(const nsACString& string, const char (&prefix)[N]) {
+ if (N - 1 > string.Length()) {
+ return false;
+ }
+ return memcmp(string.Data(), prefix, N - 1) == 0;
+}
+
+bool IsH264CodecString(const nsAString& aCodec) {
+ uint8_t profile = 0;
+ uint8_t constraint = 0;
+ uint8_t level = 0;
+ return ExtractH264CodecDetails(aCodec, profile, constraint, level);
+}
+
+bool IsH265CodecString(const nsAString& aCodec) {
+ uint8_t profile = 0;
+ uint8_t level = 0;
+ nsTArray<uint8_t> constraints;
+ return ExtractH265CodecDetails(aCodec, profile, level, constraints);
+}
+
+bool IsAACCodecString(const nsAString& aCodec) {
+ return aCodec.EqualsLiteral("mp4a.40.2") || // MPEG4 AAC-LC
+ aCodec.EqualsLiteral(
+ "mp4a.40.02") || // MPEG4 AAC-LC(for compatibility)
+ aCodec.EqualsLiteral("mp4a.40.5") || // MPEG4 HE-AAC
+ aCodec.EqualsLiteral(
+ "mp4a.40.05") || // MPEG4 HE-AAC(for compatibility)
+ aCodec.EqualsLiteral("mp4a.67") || // MPEG2 AAC-LC
+ aCodec.EqualsLiteral("mp4a.40.29"); // MPEG4 HE-AACv2
+}
+
+bool IsVP8CodecString(const nsAString& aCodec) {
+ uint8_t profile = 0;
+ uint8_t level = 0;
+ uint8_t bitDepth = 0;
+ return aCodec.EqualsLiteral("vp8") || aCodec.EqualsLiteral("vp8.0") ||
+ (StartsWith(NS_ConvertUTF16toUTF8(aCodec), "vp08") &&
+ ExtractVPXCodecDetails(aCodec, profile, level, bitDepth));
+}
+
+bool IsVP9CodecString(const nsAString& aCodec) {
+ uint8_t profile = 0;
+ uint8_t level = 0;
+ uint8_t bitDepth = 0;
+ return aCodec.EqualsLiteral("vp9") || aCodec.EqualsLiteral("vp9.0") ||
+ (StartsWith(NS_ConvertUTF16toUTF8(aCodec), "vp09") &&
+ ExtractVPXCodecDetails(aCodec, profile, level, bitDepth));
+}
+
+bool IsAV1CodecString(const nsAString& aCodec) {
+ uint8_t profile, level, tier, bitDepth, chromaPosition;
+ bool monochrome, subsamplingX, subsamplingY;
+ VideoColorSpace colorSpace;
+ return aCodec.EqualsLiteral("av1") ||
+ (StartsWith(NS_ConvertUTF16toUTF8(aCodec), "av01") &&
+ ExtractAV1CodecDetails(aCodec, profile, level, tier, bitDepth,
+ monochrome, subsamplingX, subsamplingY,
+ chromaPosition, colorSpace));
+}
+
+UniquePtr<TrackInfo> CreateTrackInfoWithMIMEType(
+ const nsACString& aCodecMIMEType) {
+ UniquePtr<TrackInfo> trackInfo;
+ if (StartsWith(aCodecMIMEType, "audio/")) {
+ trackInfo.reset(new AudioInfo());
+ trackInfo->mMimeType = aCodecMIMEType;
+ } else if (StartsWith(aCodecMIMEType, "video/")) {
+ trackInfo.reset(new VideoInfo());
+ trackInfo->mMimeType = aCodecMIMEType;
+ }
+ return trackInfo;
+}
+
+UniquePtr<TrackInfo> CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ const nsACString& aCodecMIMEType,
+ const MediaContainerType& aContainerType) {
+ UniquePtr<TrackInfo> trackInfo = CreateTrackInfoWithMIMEType(aCodecMIMEType);
+ if (trackInfo) {
+ VideoInfo* videoInfo = trackInfo->GetAsVideoInfo();
+ if (videoInfo) {
+ Maybe<int32_t> maybeWidth = aContainerType.ExtendedType().GetWidth();
+ if (maybeWidth && *maybeWidth > 0) {
+ videoInfo->mImage.width = *maybeWidth;
+ videoInfo->mDisplay.width = *maybeWidth;
+ }
+ Maybe<int32_t> maybeHeight = aContainerType.ExtendedType().GetHeight();
+ if (maybeHeight && *maybeHeight > 0) {
+ videoInfo->mImage.height = *maybeHeight;
+ videoInfo->mDisplay.height = *maybeHeight;
+ }
+ } else if (trackInfo->GetAsAudioInfo()) {
+ AudioInfo* audioInfo = trackInfo->GetAsAudioInfo();
+ Maybe<int32_t> maybeChannels =
+ aContainerType.ExtendedType().GetChannels();
+ if (maybeChannels && *maybeChannels > 0) {
+ audioInfo->mChannels = *maybeChannels;
+ }
+ Maybe<int32_t> maybeSamplerate =
+ aContainerType.ExtendedType().GetSamplerate();
+ if (maybeSamplerate && *maybeSamplerate > 0) {
+ audioInfo->mRate = *maybeSamplerate;
+ }
+ }
+ }
+ return trackInfo;
+}
+
+bool OnCellularConnection() {
+ uint32_t linkType = nsINetworkLinkService::LINK_TYPE_UNKNOWN;
+ if (XRE_IsContentProcess()) {
+ mozilla::dom::ContentChild* cpc =
+ mozilla::dom::ContentChild::GetSingleton();
+ if (!cpc) {
+ NS_WARNING("Can't get ContentChild singleton in content process!");
+ return false;
+ }
+ linkType = cpc->NetworkLinkType();
+ } else {
+ nsresult rv;
+ nsCOMPtr<nsINetworkLinkService> nls =
+ do_GetService(NS_NETWORK_LINK_SERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Can't get nsINetworkLinkService.");
+ return false;
+ }
+
+ rv = nls->GetLinkType(&linkType);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Can't get network link type.");
+ return false;
+ }
+ }
+
+ switch (linkType) {
+ case nsINetworkLinkService::LINK_TYPE_UNKNOWN:
+ case nsINetworkLinkService::LINK_TYPE_ETHERNET:
+ case nsINetworkLinkService::LINK_TYPE_USB:
+ case nsINetworkLinkService::LINK_TYPE_WIFI:
+ return false;
+ case nsINetworkLinkService::LINK_TYPE_WIMAX:
+ case nsINetworkLinkService::LINK_TYPE_MOBILE:
+ return true;
+ }
+
+ return false;
+}
+
+bool IsWaveMimetype(const nsACString& aMimeType) {
+ return aMimeType.EqualsLiteral("audio/x-wav") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=1") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=3") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=6") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=7") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=65534");
+}
+
+void DetermineResolutionForTelemetry(const MediaInfo& aInfo,
+ nsCString& aResolutionOut) {
+ if (aInfo.HasAudio()) {
+ aResolutionOut.AppendASCII("AV,");
+ } else {
+ aResolutionOut.AppendASCII("V,");
+ }
+ static const struct {
+ int32_t mH;
+ const char* mRes;
+ } sResolutions[] = {{240, "0<h<=240"}, {480, "240<h<=480"},
+ {576, "480<h<=576"}, {720, "576<h<=720"},
+ {1080, "720<h<=1080"}, {2160, "1080<h<=2160"}};
+ const char* resolution = "h>2160";
+ int32_t height = aInfo.mVideo.mDisplay.height;
+ for (const auto& res : sResolutions) {
+ if (height <= res.mH) {
+ resolution = res.mRes;
+ break;
+ }
+ }
+ aResolutionOut.AppendASCII(resolution);
+}
+
+} // end namespace mozilla