commit 61f4fb3dadd7d5c24ecf8101426f927e5832d5a0
parent 1c3ddd9fab5beda602b70d4afdd212f05377c438
Author: Chun-Min Chang <chun.m.chang@gmail.com>
Date: Thu, 2 Oct 2025 01:18:59 +0000
Bug 1986763 - Add telemetry for MediaRecorder container–codec queries r=media-playback-reviewers,pehrsons,alwu,padenot
This patch adds telemetry to capture the container-codec pairs queried
in MediaRecorder, whether through `IsTypeSupported` or the
`MediaRecorder` constructor.
The primary goal is to measure how frequently the MP4 container is
requested, while also collecting data on other container/codec
combinations as a secondery benefit. This telemetry will help us better
understand usage patterns and prioritize future work on specific
containers or codecs.
Differential Revision: https://phabricator.services.mozilla.com/D263643
Diffstat:
4 files changed, 409 insertions(+), 0 deletions(-)
diff --git a/dom/media/MediaRecorder.cpp b/dom/media/MediaRecorder.cpp
@@ -14,10 +14,12 @@
#include "MediaTrackGraph.h"
#include "VideoUtils.h"
#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/DefineEnum.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/TaskQueue.h"
+#include "mozilla/ToString.h"
#include "mozilla/dom/AudioStreamTrack.h"
#include "mozilla/dom/BlobEvent.h"
#include "mozilla/dom/Document.h"
@@ -25,6 +27,7 @@
#include "mozilla/dom/File.h"
#include "mozilla/dom/MediaRecorderErrorEvent.h"
#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/glean/DomMediaMetrics.h"
#include "mozilla/media/MediaUtils.h"
#include "nsContentTypeParser.h"
#include "nsContentUtils.h"
@@ -427,13 +430,233 @@ TypeSupport CanRecordWith(MediaStreamTrack* aTrack,
MOZ_CRASH("Unexpected track type");
}
+struct ParsedMIMEType {
+ MOZ_DEFINE_ENUM_CLASS_WITH_TOSTRING_AT_CLASS_SCOPE(MediaType,
+ (Audio, Video, Unknown));
+ MediaType mMediaType = MediaType::Unknown;
+ MOZ_DEFINE_ENUM_CLASS_WITH_TOSTRING_AT_CLASS_SCOPE(Container, (MP4, MKV, WebM,
+ Ogg, Unknown));
+ Container mContainer = Container::Unknown;
+ nsTArray<CodecType> mCodecs;
+};
+
+constexpr std::array<std::array<CodecType, 5>, 5> kValidAudioCodecs = {{
+ // MP4
+ {{CodecType::AAC, CodecType::Flac, CodecType::Opus}},
+ // MKV
+ {{CodecType::AAC, CodecType::Flac, CodecType::Opus, CodecType::PCM,
+ CodecType::Vorbis}},
+ // WebM
+ {{CodecType::Opus, CodecType::Vorbis}},
+ // Ogg
+ {{CodecType::Flac, CodecType::Opus, CodecType::Vorbis}},
+ // Unknown
+ {{}},
+}};
+
+constexpr std::array<std::array<CodecType, 5>, 5> kValidVideoOnlyCodecs = {{
+ // MP4
+ {{CodecType::AV1, CodecType::H264, CodecType::H265, CodecType::VP9}},
+ // MKV
+ {{CodecType::AV1, CodecType::H264, CodecType::H265, CodecType::VP8,
+ CodecType::VP9}},
+ // WebM
+ {{CodecType::AV1, CodecType::VP8, CodecType::VP9}},
+ // Ogg
+ {{CodecType::VP8, CodecType::VP9}},
+ // Unknown
+ {{}},
+}};
+
+constexpr auto kValidContainerCodecPairs = []() constexpr {
+ std::array<
+ std::array<std::array<CodecType, UnderlyingValue(kHighestCodecType) + 1>,
+ UnderlyingValue(ParsedMIMEType::sHighestContainer) + 1>,
+ UnderlyingValue(ParsedMIMEType::sHighestMediaType) + 1>
+ result{};
+
+ // Generate valid audio container-codec pairs
+ for (size_t c = 0; c < kValidAudioCodecs.size(); ++c) {
+ for (size_t i = 0;
+ i < kValidAudioCodecs[c].size() && IsAudio(kValidAudioCodecs[c][i]);
+ ++i) {
+ result[UnderlyingValue(ParsedMIMEType::MediaType::Audio)][c][i] =
+ kValidAudioCodecs[c][i];
+ }
+ }
+
+ // Generate valid video container-codec pairs
+ for (size_t c = 0; c < kValidVideoOnlyCodecs.size(); ++c) {
+ size_t k = 0;
+ // Add video-only codecs
+ for (size_t i = 0; i < kValidVideoOnlyCodecs[c].size() &&
+ IsVideo(kValidVideoOnlyCodecs[c][i]);
+ ++i) {
+ result[UnderlyingValue(ParsedMIMEType::MediaType::Video)][c][k++] =
+ kValidVideoOnlyCodecs[c][i];
+ }
+ // Add audio-only codecs
+ for (size_t i = 0;
+ i < kValidAudioCodecs[c].size() && IsAudio(kValidAudioCodecs[c][i]);
+ ++i) {
+ result[UnderlyingValue(ParsedMIMEType::MediaType::Video)][c][k++] =
+ kValidAudioCodecs[c][i];
+ }
+ }
+
+ return result;
+}();
+
+static ParsedMIMEType::Container GetContainerFromMimeType(
+ const MediaMIMEType& aType) {
+ if (aType == MEDIAMIMETYPE(VIDEO_MP4) || aType == MEDIAMIMETYPE(AUDIO_MP4)) {
+ return ParsedMIMEType::Container::MP4;
+ }
+ if (aType == MEDIAMIMETYPE(VIDEO_MATROSKA) ||
+ aType == MEDIAMIMETYPE(VIDEO_MATROSKA_LEGACY) ||
+ aType == MEDIAMIMETYPE(AUDIO_MATROSKA) ||
+ aType == MEDIAMIMETYPE(AUDIO_MATROSKA_LEGACY)) {
+ return ParsedMIMEType::Container::MKV;
+ }
+ if (aType == MEDIAMIMETYPE(VIDEO_WEBM) ||
+ aType == MEDIAMIMETYPE(AUDIO_WEBM)) {
+ return ParsedMIMEType::Container::WebM;
+ }
+ if (aType == MEDIAMIMETYPE(VIDEO_OGG) || aType == MEDIAMIMETYPE(AUDIO_OGG)) {
+ return ParsedMIMEType::Container::Ogg;
+ }
+ return ParsedMIMEType::Container::Unknown;
+}
+
+static CodecType GetCodecTypeFromString(const nsAString& aCodec) {
+ if (IsVP8CodecString(aCodec)) {
+ return CodecType::VP8;
+ }
+ if (IsVP9CodecString(aCodec)) {
+ return CodecType::VP9;
+ }
+ if (IsAV1CodecString(aCodec)) {
+ return CodecType::AV1;
+ }
+ if (IsH264CodecString(aCodec)) {
+ return CodecType::H264;
+ }
+ if (IsH265CodecString(aCodec)) {
+ return CodecType::H265;
+ }
+ if (IsAACCodecString(aCodec)) {
+ return CodecType::AAC;
+ }
+ if (aCodec.EqualsLiteral("flac")) {
+ return CodecType::Flac;
+ }
+ if (aCodec.EqualsLiteral("pcm")) {
+ return CodecType::PCM;
+ }
+ if (aCodec.EqualsLiteral("opus")) {
+ return CodecType::Opus;
+ }
+ if (aCodec.EqualsLiteral("vorbis")) {
+ return CodecType::Vorbis;
+ }
+ return CodecType::Unknown;
+}
+
+static ParsedMIMEType ParseMimeType(const Maybe<MediaContainerType>& aType) {
+ ParsedMIMEType result;
+
+ if (!aType) {
+ return result;
+ }
+
+ result.mMediaType = [&] {
+ if (aType->Type().HasAudioMajorType()) {
+ return ParsedMIMEType::MediaType::Audio;
+ }
+ if (aType->Type().HasVideoMajorType()) {
+ return ParsedMIMEType::MediaType::Video;
+ }
+ return ParsedMIMEType::MediaType::Unknown;
+ }();
+ result.mContainer = GetContainerFromMimeType(aType->Type());
+ for (const auto& codec : aType->ExtendedType().Codecs().Range()) {
+ result.mCodecs.AppendElement(GetCodecTypeFromString(codec));
+ }
+ return result;
+}
+
+static bool IsValidContainerCodecPair(ParsedMIMEType::MediaType aMediaType,
+ ParsedMIMEType::Container aContainer,
+ CodecType aCodec) {
+ const auto& validCodecs =
+ kValidContainerCodecPairs[UnderlyingValue(aMediaType)]
+ [UnderlyingValue(aContainer)];
+ return std::find(validCodecs.begin(), validCodecs.end(), aCodec) !=
+ validCodecs.end();
+}
+
+static nsTArray<nsCString> GetMIMELabelStrings(const ParsedMIMEType& aType) {
+ nsTArray<nsCString> labels;
+ if (aType.mContainer == ParsedMIMEType::Container::Unknown ||
+ aType.mMediaType == ParsedMIMEType::MediaType::Unknown) {
+ labels.AppendElement("others"_ns);
+ return labels;
+ }
+ nsCString baseLabel(ParsedMIMEType::EnumValueToString(aType.mContainer));
+ ToLowerCase(baseLabel);
+ if (aType.mCodecs.IsEmpty()) {
+ nsCString label = baseLabel;
+ label.AppendLiteral("_unspecified");
+ labels.AppendElement(label);
+ return labels;
+ }
+ for (const auto& codec : aType.mCodecs) {
+ nsCString label = baseLabel;
+ if (IsValidContainerCodecPair(aType.mMediaType, aType.mContainer, codec)) {
+ label.AppendLiteral("_");
+ label.Append(EnumValueToString(codec));
+ ToLowerCase(label);
+ } else {
+ label.AppendLiteral("_others");
+ }
+ LOG(LogLevel::Verbose,
+ ("GetMIMELabelStrings: type: %s, container: %s, codec: %s => label: %s",
+ ToString(aType.mMediaType).c_str(), ToString(aType.mContainer).c_str(),
+ ToString(codec).c_str(), label.get()));
+ labels.AppendElement(label);
+ }
+ return labels;
+}
+
+// The primary goal is to measure how frequently the MP4 container is requested,
+// while also collecting data on other container/codec combinations as a
+// secondary benefit.
+static void RecordQueriedMIMEType(const Maybe<MediaContainerType>& aMimeType,
+ const nsAString& aMimeTypeString) {
+ LOG(LogLevel::Verbose, ("RecordQueriedMIMEType: %s",
+ NS_ConvertUTF16toUTF8(aMimeTypeString).get()));
+ if (aMimeTypeString.IsEmpty()) {
+ LOG(LogLevel::Verbose, ("MIME queried is empty"));
+ glean::media_recorder::mime_type_query.Get("empty"_ns).Add(1);
+ return;
+ }
+ ParsedMIMEType aType = ParseMimeType(aMimeType);
+ nsTArray<nsCString> labels = GetMIMELabelStrings(aType);
+ for (const auto& label : labels) {
+ LOG(LogLevel::Verbose, ("MIME queried: %s", label.get()));
+ glean::media_recorder::mime_type_query.Get(label).Add(1);
+ }
+}
+
TypeSupport IsTypeSupportedImpl(const nsAString& aMIMEType) {
if (aMIMEType.IsEmpty()) {
+ RecordQueriedMIMEType(Nothing(), aMIMEType);
// Lie and return true even if no container/codec support is enabled,
// because the spec mandates it.
return TypeSupport::Supported;
}
Maybe<MediaContainerType> mime = MakeMediaContainerType(aMIMEType);
+ RecordQueriedMIMEType(mime, aMIMEType);
TypeSupport audioSupport = CanRecordAudioTrackWith(mime, aMIMEType);
TypeSupport videoSupport = CanRecordVideoTrackWith(mime, aMIMEType);
return std::max(audioSupport, videoSupport);
diff --git a/dom/media/metrics.yaml b/dom/media/metrics.yaml
@@ -244,6 +244,59 @@ media.playback:
type: boolean
expires: never
+media.recorder:
+ mime_type_query:
+ type: labeled_counter
+ description: >
+ Count the amount of times where a mime type is queried via
+ MediaRecorder.isTypeSupported() or passed to the MediaRecorder constructor.
+ The result is accumulated per mime type.
+ labels:
+ - mp4_av1
+ - mp4_h264
+ - mp4_h265
+ - mp4_vp9
+ - mp4_aac
+ - mp4_flac
+ - mp4_opus
+ - mp4_unspecified # no codec specified (codecs string is empty but valid)
+ - mp4_others # invalid/unrecognized codec
+ - webm_av1
+ - webm_vp8
+ - webm_vp9
+ - webm_opus
+ - webm_vorbis
+ - webm_unspecified # no codec specified (codecs string is empty but valid)
+ - webm_others # invalid codec
+ - mkv_av1
+ - mkv_h264
+ - mkv_h265
+ - mkv_vp8
+ - mkv_vp9
+ - mkv_aac
+ - mkv_flac
+ - mkv_opus
+ - mkv_pcm
+ - mkv_vorbis
+ - mkv_unspecified # no codec specified (codecs string is empty but valid)
+ - mkv_others # invalid/unrecognized codec
+ - ogg_vp8
+ - ogg_vp9
+ - ogg_flac
+ - ogg_opus
+ - ogg_vorbis
+ - ogg_unspecified # no codec specified (codecs string is empty but valid)
+ - ogg_others # invalid/unrecognized codec
+ - empty # The whole MIME string is empty but valid
+ - others # invalid container
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1986763
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1986763
+ notification_emails:
+ - media-alerts@mozilla.com
+ expires: never
+
media:
element_in_page_count:
type: counter
diff --git a/dom/media/test/mochitest_media_recorder.toml b/dom/media/test/mochitest_media_recorder.toml
@@ -794,3 +794,5 @@ tags = "mtg capturestream"
tags = "mtg capturestream"
["test_mediarecorder_webm_support.html"]
+
+["test_mediarecorder_glean.html"]
diff --git a/dom/media/test/test_mediarecorder_glean.html b/dom/media/test/test_mediarecorder_glean.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Glean Test for MediaRecorder</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ <script src="/tests/SimpleTest/GleanTest.js"></script>
+</head>
+<body>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+const testConfigs = [
+ {
+ type: "video",
+ containers: [
+ { name: "mp4" },
+ { name: "webm" },
+ { name: "x-matroska", label: "mkv" },
+ ],
+ codecs: [
+ { name: "vp8" },
+ { name: "vp9" },
+ { name: "av01.0.19M.08", label: "av1" },
+ { name: "avc1.64003E", label: "h264" },
+ { name: "hvc1.1.6.L186.B0", label: "h265" },
+ { name: "" },
+ { name: "blah" }, // invalid codec
+ ],
+ },
+ {
+ type: "audio",
+ containers: [{ name: "mp4" }, { name: "webm" }, { name: "ogg" }],
+ codecs: [
+ { name: "mp4a.40.2", label: "aac" },
+ { name: "flac" },
+ { name: "opus" },
+ { name: "vorbis" },
+ { name: "" },
+ { name: "blah" }, // invalid codec
+ { name: "av01.0.19M.08", label: "av1" }, // invalid codec
+ ],
+ },
+];
+
+const audioLabels = {
+ mp4: ["aac", "flac", "opus", "unspecified"],
+ webm: ["opus", "vorbis", "unspecified"],
+ mkv: ["aac", "flac", "opus", "pcm", "vorbis", "unspecified"],
+ ogg: ["flac", "opus", "vorbis", "unspecified"],
+};
+
+const validContainerCodecLabels = {
+ audio: audioLabels,
+ video: {
+ mp4: [...audioLabels.mp4, "av1", "h264", "h265", "vp9"],
+ webm: [...audioLabels.webm, "vp8", "vp9", "av1"],
+ mkv: [...audioLabels.mkv, "av1", "h264", "h265", "vp8", "vp9"],
+ ogg: [...audioLabels.ogg, "vp8", "vp9"],
+ },
+};
+
+add_task(async function testGleanIsTypeSupportedLabels() {
+ await GleanTest.testResetFOG();
+
+ function getLabel(type, container, codec, emptyCodecReplacement) {
+ const containerLabel = container.label || container.name;
+ const codecLabel =
+ codec.name == "" && emptyCodecReplacement
+ ? emptyCodecReplacement
+ : codec.label || codec.name;
+ const isValid = validContainerCodecLabels[type][containerLabel]?.includes(codecLabel);
+ return `${containerLabel}_${isValid ? codecLabel : "others"}`;
+ }
+
+ const singleCodecTests = testConfigs.flatMap(({ type, containers, codecs }) =>
+ containers.flatMap(container =>
+ codecs.map(codec => ({
+ mimeType: `${type}/${container.name};codecs=${codec.name}`,
+ labels: [getLabel(type, container, codec, "unspecified")], // empty codecs string is valid.
+ }))
+ )
+ );
+
+ const videoConfig = testConfigs.find(c => c.type === "video");
+ const audioConfig = testConfigs.find(c => c.type === "audio");
+ const pairedCodecTests = videoConfig.containers.flatMap(container =>
+ videoConfig.codecs.flatMap(videoCodec =>
+ audioConfig.codecs.map(audioCodec => ({
+ mimeType: `video/${container.name};codecs=${videoCodec.name},${audioCodec.name}`,
+ labels: [
+ getLabel("video", container, videoCodec),
+ getLabel("video", container, audioCodec),
+ ],
+ }))
+ )
+ );
+
+ const allTests = [...singleCodecTests, ...pairedCodecTests];
+ const counters = {};
+ for (const { mimeType, labels } of allTests) {
+ dump(`Testing MediaRecorder.isTypeSupported with mimeType: ${mimeType}, labels: [${labels.join(", ")}]\n`);
+ MediaRecorder.isTypeSupported(mimeType);
+ // A single call can update multiple counters, so we check each expected label.
+ const LabeledCounters = {};
+ for (const label of labels) {
+ LabeledCounters[label] = (LabeledCounters[label] || 0) + 1;
+ }
+ dump(` Expected increments: ${JSON.stringify(LabeledCounters)}\n`);
+ for (const label of Object.keys(LabeledCounters)) {
+ counters[label] = (counters[label] || 0) + LabeledCounters[label];
+ const value = await GleanTest.mediaRecorder.mimeTypeQuery[label].testGetValue();
+ is(value, counters[label], `count for label '${label}' from mimeType '${mimeType}' should be ${counters[label]}`);
+ }
+ }
+
+ MediaRecorder.isTypeSupported("");
+ const emptyValue = await GleanTest.mediaRecorder.mimeTypeQuery.empty.testGetValue();
+ is(emptyValue, 1, `count for empty mime type in MediaRecorder.isTypeSupported should be 1`);
+
+ MediaRecorder.isTypeSupported("blah");
+ const othersValue = await GleanTest.mediaRecorder.mimeTypeQuery.others.testGetValue();
+ is(othersValue, 1, `count for invalid mime type in MediaRecorder.isTypeSupported should be 1`);
+
+ // testGetValue returns value of the stored metric, or null if there is no value.
+ const unusedValue = await GleanTest.mediaRecorder.mimeTypeQuery.mkv_pcm.testGetValue();
+ is(unusedValue, null, `count for unused mime type in MediaRecorder.isTypeSupported should be null`);
+});
+</script>
+</body>
+</html>