Bug 1453176 - Add telemetry to report fulfilment of HTMLMediaElement.play(). r=bryce draft
authorChris Pearce <cpearce@mozilla.com>
Fri, 13 Apr 2018 20:28:39 +1200
changeset 783403 4a164cb0b4fb7fb6944cd371c6e90dde021a4dc0
parent 779997 0528a414c2a86dad0623779abde5301d37337934
push id106699
push userbmo:cpearce@mozilla.com
push dateTue, 17 Apr 2018 09:15:36 +0000
reviewersbryce
bugs1453176
milestone61.0a1
Bug 1453176 - Add telemetry to report fulfilment of HTMLMediaElement.play(). r=bryce We'd like to know the proportion of HTMLMediaElement.play() calls that are rejected due to autoplay being blocked. There are also other conditions that cause us to reject the promise returned by HTMLMediaElement.play(), so add telemetry to report all the identifyable conditions under which play() succeeds or fails. MozReview-Commit-ID: AZ67WWXaowN
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
dom/html/PlayPromise.cpp
dom/html/PlayPromise.h
dom/html/moz.build
toolkit/components/telemetry/Histograms.json
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "mozilla/dom/HTMLMediaElementBinding.h"
 #include "mozilla/dom/HTMLSourceElement.h"
 #include "mozilla/dom/HTMLAudioElement.h"
 #include "mozilla/dom/HTMLVideoElement.h"
 #include "mozilla/dom/ElementInlines.h"
+#include "mozilla/dom/PlayPromise.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/dom/MediaEncryptedEvent.h"
 #include "mozilla/EMEUtils.h"
 #include "mozilla/EventDispatcher.h"
@@ -119,17 +120,17 @@
 #include <cmath>
 #ifdef XP_WIN
 #include "objbase.h"
 // Some Windows header defines this, so undef it as it conflicts with our
 // function of the same name.
 #undef GetCurrentTime
 #endif
 
-static mozilla::LazyLogModule gMediaElementLog("nsMediaElement");
+mozilla::LazyLogModule gMediaElementLog("nsMediaElement");
 static mozilla::LazyLogModule gMediaElementEventsLog("nsMediaElementEvents");
 
 #define LOG(type, msg) MOZ_LOG(gMediaElementLog, type, msg)
 #define LOG_EVENT(type, msg) MOZ_LOG(gMediaElementEventsLog, type, msg)
 
 #include "nsIContentSecurityPolicy.h"
 
 #include "mozilla/Preferences.h"
@@ -177,25 +178,25 @@ static const double THRESHOLD_LOW_PLAYBA
 
 // Media error values.  These need to match the ones in MediaError.webidl.
 static const unsigned short MEDIA_ERR_ABORTED = 1;
 static const unsigned short MEDIA_ERR_NETWORK = 2;
 static const unsigned short MEDIA_ERR_DECODE = 3;
 static const unsigned short MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
 
 static void
-ResolvePromisesWithUndefined(const nsTArray<RefPtr<Promise>>& aPromises)
+ResolvePromisesWithUndefined(const nsTArray<RefPtr<PlayPromise>>& aPromises)
 {
   for (auto& promise : aPromises) {
     promise->MaybeResolveWithUndefined();
   }
 }
 
 static void
-RejectPromises(const nsTArray<RefPtr<Promise>>& aPromises, nsresult aError)
+RejectPromises(const nsTArray<RefPtr<PlayPromise>>& aPromises, nsresult aError)
 {
   for (auto& promise : aPromises) {
     promise->MaybeReject(aError);
   }
 }
 
 // Under certain conditions there may be no-one holding references to
 // a media element from script, DOM parent, etc, but the element may still
@@ -296,26 +297,29 @@ public:
  *
  * The constructor appends the constructed instance into the passed media
  * element's mPendingPlayPromisesRunners member and once the the runner is run
  * (whether fulfilled or canceled), it removes itself from
  * mPendingPlayPromisesRunners.
  */
 class HTMLMediaElement::nsResolveOrRejectPendingPlayPromisesRunner : public nsMediaEvent
 {
-  nsTArray<RefPtr<Promise>> mPromises;
+  nsTArray<RefPtr<PlayPromise>> mPromises;
   nsresult mError;
 
 public:
-  nsResolveOrRejectPendingPlayPromisesRunner(HTMLMediaElement* aElement,
-                                             nsTArray<RefPtr<Promise>>&& aPromises,
-                                             nsresult aError = NS_OK)
-  : nsMediaEvent("HTMLMediaElement::nsResolveOrRejectPendingPlayPromisesRunner", aElement)
-  , mPromises(Move(aPromises))
-  , mError(aError)
+  nsResolveOrRejectPendingPlayPromisesRunner(
+    HTMLMediaElement* aElement,
+    nsTArray<RefPtr<PlayPromise>>&& aPromises,
+    nsresult aError = NS_OK)
+    : nsMediaEvent(
+        "HTMLMediaElement::nsResolveOrRejectPendingPlayPromisesRunner",
+        aElement)
+    , mPromises(Move(aPromises))
+    , mError(aError)
   {
     mElement->mPendingPlayPromisesRunners.AppendElement(this);
   }
 
   void ResolveOrReject()
   {
     if (NS_SUCCEEDED(mError)) {
       ResolvePromisesWithUndefined(mPromises);
@@ -333,20 +337,21 @@ public:
     mElement->mPendingPlayPromisesRunners.RemoveElement(this);
     return NS_OK;
   }
 };
 
 class HTMLMediaElement::nsNotifyAboutPlayingRunner : public nsResolveOrRejectPendingPlayPromisesRunner
 {
 public:
-  nsNotifyAboutPlayingRunner(HTMLMediaElement* aElement,
-                             nsTArray<RefPtr<Promise>>&& aPendingPlayPromises)
-  : nsResolveOrRejectPendingPlayPromisesRunner(aElement,
-                                               Move(aPendingPlayPromises))
+  nsNotifyAboutPlayingRunner(
+    HTMLMediaElement* aElement,
+    nsTArray<RefPtr<PlayPromise>>&& aPendingPlayPromises)
+    : nsResolveOrRejectPendingPlayPromisesRunner(aElement,
+                                                 Move(aPendingPlayPromises))
   {
   }
 
   NS_IMETHOD Run() override
   {
     if (IsCancelled()) {
       mElement->mPendingPlayPromisesRunners.RemoveElement(this);
       return NS_OK;
@@ -3960,17 +3965,17 @@ HTMLMediaElement::Play(ErrorResult& aRv)
   LOG(LogLevel::Debug, ("%p Play() called by JS readyState=%d", this, mReadyState));
 
   if (mAudioChannelWrapper && mAudioChannelWrapper->IsPlaybackBlocked()) {
     MaybeDoLoad();
 
     // A blocked media element will be resumed later, so we return a pending
     // promise which might be resolved/rejected depends on the result of
     // resuming the blocked media element.
-    RefPtr<Promise> promise = CreateDOMPromise(aRv);
+    RefPtr<PlayPromise> promise = CreatePlayPromise(aRv);
 
     if (NS_WARN_IF(aRv.Failed())) {
       return nullptr;
     }
 
     LOG(LogLevel::Debug, ("%p Play() call delayed by AudioChannelAgent", this));
 
     mPendingPlayPromises.AppendElement(promise);
@@ -3984,54 +3989,56 @@ HTMLMediaElement::Play(ErrorResult& aRv)
   return promise.forget();
 }
 
 already_AddRefed<Promise>
 HTMLMediaElement::PlayInternal(ErrorResult& aRv)
 {
   MOZ_ASSERT(!aRv.Failed());
 
+  RefPtr<PlayPromise> promise = CreatePlayPromise(aRv);
+
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+
   // 4.8.12.8
   // When the play() method on a media element is invoked, the user agent must
   // run the following steps.
 
   // 4.8.12.8 - Step 1:
   // If the media element is not allowed to play, return a promise rejected
   // with a "NotAllowedError" DOMException and abort these steps.
   // Note: IsAllowedToPlay() needs to know whether there is an audio track
   // in the resource, and for that we need to be at readyState HAVE_METADATA
   // or above. So only reject here if we're at readyState HAVE_METADATA. If
   // we're below that, we'll we delay fulfilling the play promise until we've
   // reached readyState >= HAVE_METADATA below.
   if (mReadyState >= HAVE_METADATA && !IsAllowedToPlay()) {
     // NOTE: for promise-based-play, will return a rejected promise here.
     LOG(LogLevel::Debug,
         ("%p Play() promise rejected because not allowed to play.", this));
-    aRv.Throw(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
-    return nullptr;
+    promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
+    return promise.forget();
   }
 
   // 4.8.12.8 - Step 2:
   // If the media element's error attribute is not null and its code
   // attribute has the value MEDIA_ERR_SRC_NOT_SUPPORTED, return a promise
   // rejected with a "NotSupportedError" DOMException and abort these steps.
   if (GetError() && GetError()->Code() == MEDIA_ERR_SRC_NOT_SUPPORTED) {
     LOG(LogLevel::Debug,
         ("%p Play() promise rejected because source not supported.", this));
-    aRv.Throw(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR);
-    return nullptr;
+    promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR);
+    return promise.forget();
   }
 
   // 4.8.12.8 - Step 3:
   // Let promise be a new promise and append promise to the list of pending
   // play promises.
-  RefPtr<Promise> promise = CreateDOMPromise(aRv);
-  if (NS_WARN_IF(aRv.Failed())) {
-    return nullptr;
-  }
   mPendingPlayPromises.AppendElement(promise);
 
   if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE) {
     // The media load algorithm will be initiated by a user interaction.
     // We want to boost the channel priority for better responsiveness.
     // Note this must be done before UpdatePreloadAction() which will
     // update |mPreloadAction|.
     mUseUrgentStartForChannel = true;
@@ -4071,18 +4078,18 @@ HTMLMediaElement::PlayInternal(ErrorResu
           // If something wrong between |mPendingPlayPromises.AppendElement(promise);|
           // and here, the _promise_ should already have been rejected. Otherwise,
           // the _promise_ won't be returned to JS at all, so just leave it in the
           // _mPendingPlayPromises_ and let it be resolved/rejected with the
           // following actions and the promise-resolution won't be observed at all.
           LOG(LogLevel::Debug,
               ("%p Play() promise rejected because failed to play MediaDecoder.",
               this));
-          aRv.Throw(rv);
-          return nullptr;
+          promise->MaybeReject(rv);
+          return promise.forget();
         }
       }
     }
   } else if (mReadyState < HAVE_METADATA) {
     mAttemptPlayUponLoadedMetadata = true;
   }
 
   if (mCurrentPlayRangeStart == -1.0) {
@@ -7582,30 +7589,46 @@ HTMLMediaElement::UpdateCustomPolicyAfte
 AbstractThread*
 HTMLMediaElement::AbstractMainThread() const
 {
   MOZ_ASSERT(mAbstractMainThread);
 
   return mAbstractMainThread;
 }
 
-nsTArray<RefPtr<Promise>>
+nsTArray<RefPtr<PlayPromise>>
 HTMLMediaElement::TakePendingPlayPromises()
 {
   return Move(mPendingPlayPromises);
 }
 
 void
 HTMLMediaElement::NotifyAboutPlaying()
 {
   // Stick to the DispatchAsyncEvent() call path for now because we want to
   // trigger some telemetry-related codes in the DispatchAsyncEvent() method.
   DispatchAsyncEvent(NS_LITERAL_STRING("playing"));
 }
 
+already_AddRefed<PlayPromise>
+HTMLMediaElement::CreatePlayPromise(ErrorResult& aRv) const
+{
+  nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
+
+  if (!win) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+
+  RefPtr<PlayPromise> promise = PlayPromise::Create(win->AsGlobal(), aRv);
+  LOG(LogLevel::Debug, ("%p created PlayPromise %p", this, promise.get()));
+
+  return promise.forget();
+}
+
 already_AddRefed<Promise>
 HTMLMediaElement::CreateDOMPromise(ErrorResult& aRv) const
 {
   nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
 
   if (!win) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -77,16 +77,17 @@ class nsRange;
 namespace mozilla {
 namespace dom {
 
 // Number of milliseconds between timeupdate events as defined by spec
 #define TIMEUPDATE_MS 250
 
 class MediaError;
 class MediaSource;
+class PlayPromise;
 class Promise;
 class TextTrackList;
 class AudioTrackList;
 class VideoTrackList;
 
 enum class StreamCaptureType : uint8_t
 {
   CAPTURE_ALL_TRACKS,
@@ -1321,17 +1322,17 @@ protected:
   nsresult DispatchEvent(const nsAString& aName);
 
   // Open unsupported types media with the external app when the media element
   // triggers play() after loaded fail. eg. preload the data before start play.
   void OpenUnsupportedMediaWithExternalAppIfNeeded() const;
 
   // This method moves the mPendingPlayPromises into a temperate object. So the
   // mPendingPlayPromises is cleared after this method call.
-  nsTArray<RefPtr<Promise>> TakePendingPlayPromises();
+  nsTArray<RefPtr<PlayPromise>> TakePendingPlayPromises();
 
   // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises()
   // and queues a task to resolve them.
   void AsyncResolvePendingPlayPromises();
 
   // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises()
   // and queues a task to reject them.
   void AsyncRejectPendingPlayPromises(nsresult aError);
@@ -1775,16 +1776,19 @@ public:
       return mCount + 1;
     }
   private:
     TimeStamp mStartTime;
     TimeDuration mSum;
     uint32_t mCount;
   };
 private:
+
+  already_AddRefed<PlayPromise> CreatePlayPromise(ErrorResult& aRv) const;
+
   /**
    * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
    * It will not be called if the value is being unset.
    *
    * @param aNamespaceID the namespace of the attr being set
    * @param aName the localname of the attribute being set
    * @param aNotify Whether we plan to notify document observers.
    */
@@ -1836,17 +1840,17 @@ private:
 
   // This wrapper will handle all audio channel related stuffs, eg. the operations
   // of tab audio indicator, Fennec's media control.
   // Note: mAudioChannelWrapper might be null after GC happened.
   RefPtr<AudioChannelAgentCallback> mAudioChannelWrapper;
 
   // A list of pending play promises. The elements are pushed during the play()
   // method call and are resolved/rejected during further playback steps.
-  nsTArray<RefPtr<Promise>> mPendingPlayPromises;
+  nsTArray<RefPtr<PlayPromise>> mPendingPlayPromises;
 
   // A list of already-dispatched but not yet run
   // nsResolveOrRejectPendingPlayPromisesRunners.
   // Runners whose Run() method is called remove themselves from this list.
   // We keep track of these because the load algorithm resolves/rejects all
   // already-dispatched pending play promises.
   nsTArray<nsResolveOrRejectPendingPlayPromisesRunner*> mPendingPlayPromisesRunners;
 
new file mode 100644
--- /dev/null
+++ b/dom/html/PlayPromise.cpp
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/dom/PlayPromise.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Telemetry.h"
+
+extern mozilla::LazyLogModule gMediaElementLog;
+
+#define PLAY_PROMISE_LOG(msg, ...)                                             \
+  MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+namespace mozilla {
+namespace dom {
+
+PlayPromise::PlayPromise(nsIGlobalObject* aGlobal)
+  : Promise(aGlobal)
+{
+}
+
+PlayPromise::~PlayPromise()
+{
+  if (!mFulfilled && PromiseObj()) {
+    MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+  }
+}
+
+/* static */
+already_AddRefed<PlayPromise>
+PlayPromise::Create(nsIGlobalObject* aGlobal, ErrorResult& aRv)
+{
+  RefPtr<PlayPromise> promise = new PlayPromise(aGlobal);
+  promise->CreateWrapper(nullptr, aRv);
+  return aRv.Failed() ? nullptr : promise.forget();
+}
+
+void
+PlayPromise::MaybeResolveWithUndefined()
+{
+  if (mFulfilled) {
+    return;
+  }
+  mFulfilled = true;
+  PLAY_PROMISE_LOG("PlayPromise %p resolved with undefined", this);
+  auto reason = Telemetry::LABELS_MEDIA_PLAY_PROMISE_RESOLUTION::Resolved;
+  Telemetry::AccumulateCategorical(reason);
+  Promise::MaybeResolveWithUndefined();
+}
+
+using PlayLabel = Telemetry::LABELS_MEDIA_PLAY_PROMISE_RESOLUTION;
+
+struct PlayPromiseTelemetryResult
+{
+  nsresult mValue;
+  PlayLabel mLabel;
+  const char* mName;
+};
+
+static const PlayPromiseTelemetryResult sPlayPromiseTelemetryResults[] = {
+  {
+    NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR,
+    PlayLabel::NotAllowedErr,
+    "NotAllowedErr",
+  },
+  {
+    NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR,
+    PlayLabel::SrcNotSupportedErr,
+    "SrcNotSupportedErr",
+  },
+  {
+    NS_ERROR_DOM_MEDIA_ABORT_ERR,
+    PlayLabel::PauseAbortErr,
+    "PauseAbortErr",
+  },
+  {
+    NS_ERROR_DOM_ABORT_ERR,
+    PlayLabel::AbortErr,
+    "AbortErr",
+  },
+};
+
+static const PlayPromiseTelemetryResult*
+FindPlayPromiseTelemetryResult(nsresult aReason)
+{
+  for (const auto& p : sPlayPromiseTelemetryResults) {
+    if (p.mValue == aReason) {
+      return &p;
+    }
+  }
+  return nullptr;
+}
+
+static PlayLabel
+ToPlayResultLabel(nsresult aReason)
+{
+  auto p = FindPlayPromiseTelemetryResult(aReason);
+  return p ? p->mLabel : PlayLabel::UnknownErr;
+}
+
+static const char*
+ToPlayResultStr(nsresult aReason)
+{
+  auto p = FindPlayPromiseTelemetryResult(aReason);
+  return p ? p->mName : "UnknownErr";
+}
+
+void
+PlayPromise::MaybeReject(nsresult aReason)
+{
+  if (mFulfilled) {
+    return;
+  }
+  mFulfilled = true;
+  PLAY_PROMISE_LOG("PlayPromise %p rejected with 0x%x (%s)",
+                   this,
+                   static_cast<uint32_t>(aReason),
+                   ToPlayResultStr(aReason));
+  Telemetry::AccumulateCategorical(ToPlayResultLabel(aReason));
+  Promise::MaybeReject(aReason);
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/html/PlayPromise.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef __PlayPromise_h__
+#define __PlayPromise_h__
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Telemetry.h"
+
+namespace mozilla {
+namespace dom {
+
+// Decorates a DOM Promise to report telemetry as to whether it was resolved
+// or rejected and why.
+class PlayPromise : public Promise
+{
+public:
+  static already_AddRefed<PlayPromise> Create(nsIGlobalObject* aGlobal,
+                                              ErrorResult& aRv);
+  ~PlayPromise();
+  void MaybeResolveWithUndefined();
+  void MaybeReject(nsresult aReason);
+
+private:
+  explicit PlayPromise(nsIGlobalObject* aGlobal);
+  bool mFulfilled = false;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // __PlayPromise_h__
--- a/dom/html/moz.build
+++ b/dom/html/moz.build
@@ -117,16 +117,17 @@ EXPORTS.mozilla.dom += [
     'HTMLTimeElement.h',
     'HTMLTitleElement.h',
     'HTMLTrackElement.h',
     'HTMLUnknownElement.h',
     'HTMLVideoElement.h',
     'ImageDocument.h',
     'MediaError.h',
     'nsBrowserElement.h',
+    'PlayPromise.h',
     'RadioNodeList.h',
     'TextTrackManager.h',
     'TimeRanges.h',
     'ValidityState.h',
 ]
 
 UNIFIED_SOURCES += [
     'HTMLAllCollection.cpp',
@@ -206,16 +207,17 @@ UNIFIED_SOURCES += [
     'nsGenericHTMLElement.cpp',
     'nsGenericHTMLFrameElement.cpp',
     'nsHTMLContentSink.cpp',
     'nsHTMLDNSPrefetch.cpp',
     'nsHTMLDocument.cpp',
     'nsIConstraintValidation.cpp',
     'nsRadioVisitor.cpp',
     'nsTextEditorState.cpp',
+    'PlayPromise.cpp',
     'RadioNodeList.cpp',
     'TextTrackManager.cpp',
     'TimeRanges.cpp',
     'ValidityState.cpp',
     'VideoDocument.cpp',
 ]
 
 SOURCES += [
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -11291,16 +11291,25 @@
     "expires_in_version": "60",
     "bug_numbers": [1230265],
     "kind": "linear",
     "high": 1000,
     "n_buckets": 100,
     "description": "720p VP9 decode benchmark measurement in frames per second",
     "releaseChannelCollection": "opt-out"
   },
+  "MEDIA_PLAY_PROMISE_RESOLUTION": {
+    "record_in_processes": ["main", "content"],
+    "bug_numbers": [1453176],
+    "alert_emails": ["cpearce@mozilla.com", "drno@ohlmeier.org"],
+    "expires_in_version": "72",
+    "kind": "categorical",
+    "labels": ["Resolved", "NotAllowedErr", "SrcNotSupportedErr", "PauseAbortErr", "AbortErr", "UnknownErr"],
+    "description": "Records whether promise returned by HTMLMediaElement.play() successfully resolved, or the error code which it was rejected with."
+  },
   "MEDIA_CODEC_USED": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["cpearce@mozilla.com"],
     "expires_in_version": "never",
     "keyed": true,
     "kind": "count",
     "description": "Count of use of audio/video codecs in HTMLMediaElements and WebAudio. Those with 'resource' prefix are approximate; report based on HTTP ContentType or sniffing. Those with 'webaudio' prefix are for WebAudio."
   },