tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 371c2babef9b736604032073be5861b706d14bcf
parent 46886fd6617d79a46d2f8273753e170a898b5f88
Author: Chun-Min Chang <chun.m.chang@gmail.com>
Date:   Tue, 30 Sep 2025 23:32:01 +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:
Mdom/media/MediaRecorder.cpp | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdom/media/metrics.yaml | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdom/media/test/mochitest_media_recorder.toml | 2++
Adom/media/test/test_mediarecorder_glean.html | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 408 insertions(+), 0 deletions(-)

diff --git a/dom/media/MediaRecorder.cpp b/dom/media/MediaRecorder.cpp @@ -25,6 +25,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 +428,234 @@ 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(CodecType::Unknown) + 1>, + UnderlyingValue(ParsedMIMEType::Container::Unknown) + 1>, + UnderlyingValue(ParsedMIMEType::MediaType::Unknown) + 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", + ParsedMIMEType::EnumValueToString(aType.mMediaType), + ParsedMIMEType::EnumValueToString(aType.mContainer), + EnumValueToString(codec), 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>