Bug 934425 - Implement setSinkId in HTMLMediaElement. r=pehrsons
authorAlex Chronopoulos <achronop@gmail.com>
Fri, 12 Oct 2018 09:39:30 +0000
changeset 440843 55c832b1f2fecec90fb4afef27669ab971fc2fab
parent 440842 8aef0f43b2d891668022474f0af701980de6e39d
child 440844 94caf76cf9a3d32fc46780f6144b027fd4b11536
push id34837
push userncsoregi@mozilla.com
push dateFri, 12 Oct 2018 16:56:54 +0000
treeherdermozilla-central@7fd59dc00149 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspehrsons
bugs934425
milestone64.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 934425 - Implement setSinkId in HTMLMediaElement. r=pehrsons Differential Revision: https://phabricator.services.mozilla.com/D5874
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -8,16 +8,17 @@
 #include "objbase.h"
 // Some Windows header defines this, so undef it as it conflicts with our
 // function of the same name.
 #undef GetCurrentTime
 #endif
 
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "AudioChannelService.h"
+#include "AudioDeviceInfo.h"
 #include "AudioStreamTrack.h"
 #include "AutoplayPolicy.h"
 #include "ChannelMediaDecoder.h"
 #include "DOMMediaStream.h"
 #include "DecoderDoctorDiagnostics.h"
 #include "DecoderDoctorLogger.h"
 #include "DecoderTraits.h"
 #include "FrameStatistics.h"
@@ -26,16 +27,17 @@
 #include "HLSDecoder.h"
 #endif
 #include "HTMLMediaElement.h"
 #include "ImageContainer.h"
 #include "Layers.h"
 #include "MP4Decoder.h"
 #include "MediaContainerType.h"
 #include "MediaError.h"
+#include "MediaManager.h"
 #include "MediaMetadataManager.h"
 #include "MediaResource.h"
 #include "MediaSourceDecoder.h"
 #include "MediaStreamError.h"
 #include "MediaStreamGraph.h"
 #include "MediaStreamListener.h"
 #include "MediaTrackList.h"
 #include "SVGObserverUtils.h"
@@ -3880,16 +3882,17 @@ HTMLMediaElement::HTMLMediaElement(
   , mWatchManager(this, OwnerDoc()->AbstractMainThreadFor(TaskCategory::Other))
   , mMainThreadEventTarget(OwnerDoc()->EventTargetFor(TaskCategory::Other))
   , mAbstractMainThread(OwnerDoc()->AbstractMainThreadFor(TaskCategory::Other))
   , mShutdownObserver(new ShutdownObserver)
   , mPlayed(new TimeRanges(ToSupports(OwnerDoc())))
   , mPaused(true, "HTMLMediaElement::mPaused")
   , mErrorSink(new ErrorSink(this))
   , mAudioChannelWrapper(new AudioChannelAgentCallback(this))
+  , mSink(MakePair(nsString(), RefPtr<AudioDeviceInfo>()))
 {
   MOZ_ASSERT(mMainThreadEventTarget);
   MOZ_ASSERT(mAbstractMainThread);
 
   DecoderDoctorLogger::LogConstruction(this);
 
   mWatchManager.Watch(mPaused, &HTMLMediaElement::UpdateWakeLock);
 
@@ -5080,16 +5083,29 @@ HTMLMediaElement::FinishDecoderSetup(Med
 
   // Notify the decoder of the initial activity status.
   NotifyDecoderActivityChanges();
 
   // Update decoder principal before we start decoding, since it
   // can affect how we feed data to MediaStreams
   NotifyDecoderPrincipalChanged();
 
+  // Set sink device if we have one. Otherwise the default is used.
+  if (mSink.second()) {
+    mDecoder->SetSink(mSink.second())
+#ifdef DEBUG
+      ->Then(mAbstractMainThread, __func__,
+      [](const GenericPromise::ResolveOrRejectValue& aValue) {
+        MOZ_ASSERT(aValue.IsResolve() && !aValue.ResolveValue());
+      });
+#else
+    ;
+#endif
+  }
+
   for (OutputMediaStream& ms : mOutputStreams) {
     if (ms.mCapturingMediaStream) {
       MOZ_ASSERT(!ms.mCapturingDecoder);
       continue;
     }
 
     ms.mCapturingDecoder = true;
     aDecoder->AddOutputStream(ms.mStream->GetInputStream()->AsProcessedStream(),
@@ -5311,16 +5327,19 @@ HTMLMediaElement::UpdateSrcMediaStreamPl
     mSrcStreamPausedCurrentTime = -1;
 
     mMediaStreamListener =
       new StreamListener(this, "HTMLMediaElement::mMediaStreamListener");
     stream->AddListener(mMediaStreamListener);
 
     stream->AddAudioOutput(this);
     SetVolumeInternal();
+    if (mSink.second()) {
+      NS_WARNING("setSinkId() when playing a MediaStream is not supported yet and will be ignored");
+    }
 
     VideoFrameContainer* container = GetVideoFrameContainer();
     if (mSelectedVideoStreamTrack && container) {
       mSelectedVideoStreamTrack->AddVideoOutput(container);
     }
 
     SetCapturedOutputStreamsEnabled(true); // Unmute
     // If the input is a media stream, we don't check its data and always regard
@@ -8260,13 +8279,93 @@ HTMLMediaElement::ReportCanPlayTelemetry
             Telemetry::Accumulate(
               Telemetry::HistogramID::VIDEO_CAN_CREATE_H264_DECODER, h264);
             thread->AsyncShutdown();
           }));
       }),
     NS_DISPATCH_NORMAL);
 }
 
+already_AddRefed<Promise>
+HTMLMediaElement::SetSinkId(const nsAString& aSinkId, ErrorResult& aRv)
+{
+  nsCOMPtr<nsPIDOMWindowInner> win = OwnerDoc()->GetInnerWindow();
+  if (!win) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+
+  RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  if (mSink.first().Equals(aSinkId)) {
+    promise->MaybeResolveWithUndefined();
+    return promise.forget();
+  }
+
+  nsString sinkId(aSinkId);
+  MediaManager::Get()->GetSinkDevice(win, sinkId)
+    ->Then(mAbstractMainThread, __func__,
+           [self = RefPtr<HTMLMediaElement>(this)](RefPtr<AudioDeviceInfo>&& aInfo) {
+             // Sink found switch output device.
+             MOZ_ASSERT(aInfo);
+             if (self->mDecoder) {
+               RefPtr<SinkInfoPromise> p = self->mDecoder->SetSink(aInfo)
+                 ->Then(self->mAbstractMainThread, __func__,
+                       [aInfo] (const GenericPromise::ResolveOrRejectValue& aValue) {
+                         if (aValue.IsResolve()) {
+                           return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
+                         }
+                         return SinkInfoPromise::CreateAndReject(aValue.RejectValue(), __func__);
+                       });
+               return p;
+             }
+             if (self->GetSrcMediaStream()) {
+               // Set Sink Id through MSG is not supported yet.
+               return SinkInfoPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+             }
+             // No media attached to the element save it for later.
+             return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
+           },
+           [](nsresult res){
+             // Promise is rejected, sink not found.
+             return SinkInfoPromise::CreateAndReject(res, __func__);
+           })
+    ->Then(mAbstractMainThread, __func__,
+           [promise, self = RefPtr<HTMLMediaElement>(this),
+           sinkId = std::move(sinkId)] (const SinkInfoPromise::ResolveOrRejectValue& aValue) {
+             if (aValue.IsResolve()) {
+               self->mSink = MakePair(sinkId, aValue.ResolveValue());
+               promise->MaybeResolveWithUndefined();
+             } else {
+               switch (aValue.RejectValue()) {
+                 case NS_ERROR_ABORT:
+                   promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+                   break;
+                 case NS_ERROR_NOT_AVAILABLE:
+                 {
+                   ErrorResult notFoundError;
+                   notFoundError.ThrowDOMException(
+                         NS_ERROR_DOM_NOT_FOUND_ERR,
+                         NS_LITERAL_CSTRING("The object can not be found here."));
+                   promise->MaybeReject(notFoundError);
+                   break;
+                 }
+                 case NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR:
+                   promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+                   break;
+                 default:
+                   MOZ_ASSERT_UNREACHABLE("Invalid error.");
+               }
+             }
+           });
+
+  aRv = NS_OK;
+  return promise.forget();
+}
+
 } // namespace dom
 } // namespace mozilla
 
 #undef LOG
 #undef LOG_EVENT
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -61,16 +61,17 @@ class TextTrack;
 class TimeRanges;
 class WakeLock;
 class MediaTrack;
 class MediaStreamTrack;
 class VideoStreamTrack;
 } // namespace dom
 } // namespace mozilla
 
+class AudioDeviceInfo;
 class nsIChannel;
 class nsIHttpChannel;
 class nsILoadGroup;
 class nsIRunnable;
 class nsISerialEventTarget;
 class nsITimer;
 class nsRange;
 
@@ -822,16 +823,27 @@ public:
   void AsyncResolveSeekDOMPromiseIfExists() override;
   void AsyncRejectSeekDOMPromiseIfExists() override;
 
   nsISerialEventTarget* MainThreadEventTarget()
   {
     return mMainThreadEventTarget;
   }
 
+  // Set the sink id (of the output device) that the audio will play. If aSinkId
+  // is empty the default device will be set.
+  already_AddRefed<Promise> SetSinkId(const nsAString& aSinkId, ErrorResult& aRv);
+  // Get the sink id of the device that audio is being played. Initial value is
+  // empty and the default device is being used.
+  void GetSinkId(nsString& aSinkId)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    aSinkId = mSink.first();
+  }
+
 protected:
   virtual ~HTMLMediaElement();
 
   class AudioChannelAgentCallback;
   class ChannelLoader;
   class ErrorSink;
   class MediaLoadListener;
   class MediaStreamTracksAvailableCallback;
@@ -1888,16 +1900,24 @@ private:
   // AsyncRejectSeekDOMPromiseIfExists() methods.
   RefPtr<dom::Promise> mSeekDOMPromise;
 
   // For debugging bug 1407148.
   void AssertReadyStateIsNothing();
 
   // Attach UA Shadow Root if it is not attached.
   void AttachAndSetUAShadowRoot();
+
+  // Contains the unique id of the sink device and the device info.
+  // The initial value is ("", nullptr) and the default output device is used.
+  // It can contain an invalid id and info if the device has been
+  // unplugged. It can be set to ("", nullptr). It follows the spec attribute:
+  // https://w3c.github.io/mediacapture-output/#htmlmediaelement-extensions
+  // Read/Write from the main thread only.
+  Pair<nsString, RefPtr<AudioDeviceInfo>> mSink;
 };
 
 // Check if the context is chrome or has the debugger or tabs permission
 bool
 HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj);
 
 } // namespace dom
 } // namespace mozilla