/* -*- 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/. */
usingnamespace mozilla::layers; using mozilla::net::nsMediaFragmentURIParser; usingnamespace mozilla::dom::HTMLMediaElement_Binding;
namespace mozilla::dom {
using AudibleState = AudioChannelService::AudibleState; using SinkInfoPromise = MediaDevices::SinkInfoPromise;
// Number of milliseconds between progress events as defined by spec staticconst uint32_t PROGRESS_MS = 350;
// Number of milliseconds of no data before a stall event is fired as defined by // spec staticconst uint32_t STALL_MS = 3000;
// Used by AudioChannel for suppresssing the volume to this ratio. #define FADED_VOLUME_RATIO 0.25
// These constants are arbitrary // Minimum playbackRate for a media staticconstdouble MIN_PLAYBACKRATE = 1.0 / 16; // Maximum playbackRate for a media staticconstdouble MAX_PLAYBACKRATE = 16.0;
if (aPlaybackRate == 0.0) { return aPlaybackRate;
} if (aPlaybackRate < MIN_PLAYBACKRATE) { return MIN_PLAYBACKRATE;
} if (aPlaybackRate > MAX_PLAYBACKRATE) { return MAX_PLAYBACKRATE;
} return aPlaybackRate;
}
// Media error values. These need to match the ones in MediaError.webidl. staticconstunsignedshort MEDIA_ERR_ABORTED = 1; staticconstunsignedshort MEDIA_ERR_NETWORK = 2; staticconstunsignedshort MEDIA_ERR_DECODE = 3; staticconstunsignedshort MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
/** * EventBlocker helps media element to postpone the event delivery by storing * the event runner, and execute them once media element decides not to postpone * the event delivery. If media element never resumes the event delivery, then * those runner would be cancelled. * For example, we postpone the event delivery when media element entering to * the bf-cache.
*/ class HTMLMediaElement::EventBlocker final : public nsISupports { public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL
NS_DECL_CYCLE_COLLECTION_CLASS(EventBlocker)
void PostponeEvent(nsMediaEventRunner* aRunner) {
MOZ_ASSERT(NS_IsMainThread()); // Element has been CCed, which would break the weak pointer. if (!mElement) { return;
}
MOZ_ASSERT(mShouldBlockEventDelivery);
MOZ_ASSERT(mElement);
LOG_EVENT(LogLevel::Debug,
("%p postpone runner %s for %s", mElement.get(),
NS_ConvertUTF16toUTF8(aRunner->Name()).get(),
NS_ConvertUTF16toUTF8(aRunner->EventName()).get()));
mPendingEventRunners.AppendElement(aRunner);
}
WeakPtr<HTMLMediaElement> mElement; bool mShouldBlockEventDelivery = false; // Contains event runners which should not be run for now because we want // to block all events delivery. They would be dispatched once media element // decides unblocking them.
nsTArray<RefPtr<nsMediaEventRunner>> mPendingEventRunners;
};
/** * We use MediaControlKeyListener to listen to media control key in order to * play and pause media element when user press media control keys and update * media's playback and audible state to the media controller. * * Use `Start()` to start listening event and use `Stop()` to stop listening * event. In addition, notifying any change to media controller MUST be done * after successfully calling `Start()`.
*/ class HTMLMediaElement::MediaControlKeyListener final
: public ContentMediaControlKeyReceiver { public:
NS_INLINE_DECL_REFCOUNTING(MediaControlKeyListener, override)
/** * Start listening to the media control keys which would make media being able * to be controlled via pressing media control keys.
*/ void Start() {
MOZ_ASSERT(NS_IsMainThread()); if (IsStarted()) { // We have already been started, do not notify start twice. return;
}
// Fail to init media agent, we are not able to notify the media controller // any update and also are not able to receive media control key events. if (!InitMediaAgent()) {
MEDIACONTROL_LOG("Failed to start due to not able to init media agent!"); return;
}
NotifyPlaybackStateChanged(MediaPlaybackState::eStarted); // If owner has started playing before the listener starts, we should update // the playing state as well. Eg. media starts inaudily and becomes audible // later. if (!Owner()->Paused()) {
NotifyMediaStartedPlaying();
} if (StaticPrefs::media_mediacontrol_testingevents_enabled()) { auto dispatcher = MakeRefPtr<AsyncEventDispatcher>(
Owner(), u"MozStartMediaControl"_ns, CanBubble::eYes,
ChromeOnlyDispatch::eYes);
dispatcher->PostDOMEvent();
}
}
/** * Stop listening to the media control keys which would make media not be able * to be controlled via pressing media control keys. If we haven't started * listening to the media control keys, then nothing would happen.
*/ void StopIfNeeded() {
MOZ_ASSERT(NS_IsMainThread()); if (!IsStarted()) { // We have already been stopped, do not notify stop twice. return;
}
NotifyMediaStoppedPlaying();
NotifyPlaybackStateChanged(MediaPlaybackState::eStopped);
// Remove ourselves from media agent, which would stop receiving event.
mControlAgent->RemoveReceiver(this);
mControlAgent = nullptr;
}
/** * Following methods should only be used after starting listener.
*/ void NotifyMediaStartedPlaying() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(IsStarted()); if (mState == MediaPlaybackState::eStarted ||
mState == MediaPlaybackState::ePaused) {
NotifyPlaybackStateChanged(MediaPlaybackState::ePlayed); // If media is `inaudible` in the beginning, then we don't need to notify // the state, because notifying `inaudible` should always come after // notifying `audible`. if (mIsOwnerAudible) {
NotifyAudibleStateChanged(MediaAudibleState::eAudible);
}
}
}
void NotifyMediaStoppedPlaying() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(IsStarted()); if (mState == MediaPlaybackState::ePlayed) {
NotifyPlaybackStateChanged(MediaPlaybackState::ePaused); // As media are going to be paused, so no sound is possible to be heard. if (mIsOwnerAudible) {
NotifyAudibleStateChanged(MediaAudibleState::eInaudible);
}
}
}
void NotifyMediaPositionState() { if (!IsStarted()) { return;
}
MOZ_ASSERT(mControlAgent); auto* owner = Owner();
PositionState state(owner->Duration(), owner->PlaybackRate(),
owner->CurrentTime(), TimeStamp::Now());
MEDIACONTROL_LOG( "Notify media position state (duration=%f, playbackRate=%f, " "position=%f)",
state.mDuration, state.mPlaybackRate,
state.mLastReportedPlaybackPosition);
mControlAgent->UpdateGuessedPositionState(mOwnerBrowsingContextId,
mElementId, Some(state));
}
// This method can be called before the listener starts, which would cache // the audible state and update after the listener starts. void UpdateMediaAudibleState(bool aIsOwnerAudible) {
MOZ_ASSERT(NS_IsMainThread()); if (mIsOwnerAudible == aIsOwnerAudible) { return;
}
mIsOwnerAudible = aIsOwnerAudible;
MEDIACONTROL_LOG("Media becomes %s",
mIsOwnerAudible ? "audible" : "inaudible"); // If media hasn't started playing, it doesn't make sense to update media // audible state. Therefore, in that case we would noitfy the audible state // when media starts playing. if (mState == MediaPlaybackState::ePlayed) {
NotifyAudibleStateChanged(mIsOwnerAudible
? MediaAudibleState::eAudible
: MediaAudibleState::eInaudible);
}
}
void SetPictureInPictureModeEnabled(bool aIsEnabled) {
MOZ_ASSERT(NS_IsMainThread()); if (mIsPictureInPictureEnabled == aIsEnabled) { return;
} // PIP state changes might happen before the listener starts or stops where // we haven't call `InitMediaAgent()` yet. Eg. Reset the PIP video's src, // then cancel the PIP. In addition, not like playback and audible state // which should be restricted to update via the same agent in order to keep // those states correct in each `ContextMediaInfo`, PIP state can be updated // through any browsing context, so we would use `ContentMediaAgent::Get()` // directly to update PIP state.
mIsPictureInPictureEnabled = aIsEnabled; if (RefPtr<IMediaInfoUpdater> updater =
ContentMediaAgent::Get(GetCurrentBrowsingContext())) {
updater->SetIsInPictureInPictureMode(mOwnerBrowsingContextId,
mIsPictureInPictureEnabled);
}
}
void HandleMediaKey(MediaControlKey aKey,
Maybe<SeekDetails> aDetails) override {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(IsStarted());
MEDIACONTROL_LOG("HandleEvent '%s'", GetEnumString(aKey).get()); switch (aKey) { case MediaControlKey::Play:
Owner()->Play(); break; case MediaControlKey::Pause:
Owner()->Pause(); break; case MediaControlKey::Stop:
Owner()->Pause();
StopIfNeeded(); break; case MediaControlKey::Seekto:
MOZ_ASSERT(aDetails->mAbsolute); if (aDetails->mAbsolute->mFastSeek) {
Owner()->FastSeek(aDetails->mAbsolute->mSeekTime, IgnoreErrors());
} else {
Owner()->SetCurrentTime(aDetails->mAbsolute->mSeekTime);
} break; case MediaControlKey::Seekforward:
MOZ_ASSERT(aDetails->mRelativeSeekOffset);
Owner()->SetCurrentTime(Owner()->CurrentTime() +
aDetails->mRelativeSeekOffset.value()); break; case MediaControlKey::Seekbackward:
MOZ_ASSERT(aDetails->mRelativeSeekOffset);
Owner()->SetCurrentTime(Owner()->CurrentTime() -
aDetails->mRelativeSeekOffset.value()); break; default:
MOZ_ASSERT_UNREACHABLE( "Unsupported media control key for media element!");
}
}
void UpdateOwnerBrowsingContextIfNeeded() { // Has not notified any information about the owner context yet. if (!IsStarted()) { return;
}
BrowsingContext* currentBC = GetCurrentBrowsingContext();
MOZ_ASSERT(currentBC); // Still in the same browsing context, no need to update. if (currentBC->Id() == mOwnerBrowsingContextId) { return;
}
MEDIACONTROL_LOG("Change browsing context from %" PRIu64 " to %" PRIu64,
mOwnerBrowsingContextId, currentBC->Id()); // This situation would happen when we start a media in an original browsing // context, then we move it to another browsing context, such as an iframe, // so its owner browsing context would be changed. Therefore, we should // reset the media status for the previous browsing context by calling // `Stop()`, in which the listener would notify `ePaused` (if it's playing) // and `eStop`. Then calls `Start()`, in which the listener would notify // `eStart` to the new browsing context. If the media was playing before, // we would also notify `ePlayed`. bool wasInPlayingState = mState == MediaPlaybackState::ePlayed;
StopIfNeeded();
Start(); if (wasInPlayingState) {
NotifyMediaStartedPlaying();
}
}
private:
~MediaControlKeyListener() = default;
// The media can be moved around different browsing contexts, so this context // might be different from the one that we used to initialize // `ContentMediaAgent`.
BrowsingContext* GetCurrentBrowsingContext() const { // Owner has been CCed, which would break the link of the weaker pointer. if (!Owner()) { return nullptr;
}
nsPIDOMWindowInner* window = Owner()->OwnerDoc()->GetInnerWindow(); return window ? window->GetBrowsingContext() : nullptr;
}
HTMLMediaElement* Owner() const { // `mElement` would be clear during CC unlinked, but it would only happen // after stopping the listener.
MOZ_ASSERT(mElement || !IsStarted()); return mElement.get();
}
void NotifyPlaybackStateChanged(MediaPlaybackState aState) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mControlAgent);
MEDIACONTROL_LOG("NotifyMediaState from state='%s' to state='%s'",
dom::EnumValueToString(mState),
dom::EnumValueToString(aState));
MOZ_ASSERT(mState != aState, "Should not notify same state again!");
mState = aState;
mControlAgent->NotifyMediaPlaybackChanged(mOwnerBrowsingContextId, mState);
if (aState == MediaPlaybackState::ePlayed) {
NotifyMediaPositionState();
}
}
// mediacapture-main says: // Note that once ended equals true the HTMLVideoElement will not play media // even if new MediaStreamTracks are added to the MediaStream (causing it to // return to the active state) unless autoplay is true or the web // application restarts the element, e.g., by calling play(). // // This is vague on exactly how to go from becoming active to playing, when // autoplaying. However, per the media element spec, to play an autoplaying // media element, we must load the source and reach readyState // HAVE_ENOUGH_DATA [1]. Hence, a MediaStream being assigned to a media // element and becoming active runs the load algorithm, so that it can // eventually be played. // // [1] // https://html.spec.whatwg.org/multipage/media.html#ready-states:event-media-play
LOG(LogLevel::Debug, ("%p, mSrcStream %p became active, checking if we " "need to run the load algorithm",
mElement.get(), mElement->mSrcStream.get())); if (!mElement->IsPlaybackEnded()) { return;
} if (!mElement->Autoplay()) { return;
}
LOG(LogLevel::Info, ("%p, mSrcStream %p became active on autoplaying, " "ended element. Reloading.",
mElement.get(), mElement->mSrcStream.get()));
mElement->DoLoad();
}
void NotifyActive() override { if (!mElement) { return;
}
if (!mElement->IsVideo()) { // Audio elements use NotifyAudible(). return;
}
OnActive();
}
void NotifyAudible() override { if (!mElement) { return;
}
if (mElement->IsVideo()) { // Video elements use NotifyActive(). return;
}
OnActive();
}
void OnInactive() {
MOZ_ASSERT(mElement);
if (mElement->IsPlaybackEnded()) { return;
}
LOG(LogLevel::Debug, ("%p, mSrcStream %p became inactive", mElement.get(),
mElement->mSrcStream.get()));
mElement->PlaybackEnded();
}
void NotifyInactive() override { if (!mElement) { return;
}
if (!mElement->IsVideo()) { // Audio elements use NotifyInaudible(). return;
}
OnInactive();
}
void NotifyInaudible() override { if (!mElement) { return;
}
if (mElement->IsVideo()) { // Video elements use NotifyInactive(). return;
}
/** * Helper class that manages audio and video outputs for all enabled tracks in a * media element. It also manages calculating the current time when playing a * MediaStream.
*/ class HTMLMediaElement::MediaStreamRenderer { public:
NS_INLINE_DECL_REFCOUNTING(MediaStreamRenderer)
for (constauto& t : mAudioTracks) { if (t) {
t->AsAudioStreamTrack()->AddAudioOutput(mAudioOutputKey,
mAudioOutputSink);
t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey,
mAudioOutputVolume);
}
}
if (mVideoTrack) {
mVideoTrack->AsVideoStreamTrack()->AddVideoOutput(mVideoContainer);
}
}
for (constauto& t : mAudioTracks) { if (t) {
t->AsAudioStreamTrack()->RemoveAudioOutput(mAudioOutputKey);
}
} // There is no longer an audio output that needs the device so the // device may not start. Ensure the promise is resolved.
ResolveAudioDevicePromiseIfExists(__func__);
if (mVideoTrack) {
mVideoTrack->AsVideoStreamTrack()->RemoveVideoOutput(mVideoContainer);
}
}
void SetAudioOutputVolume(float aVolume) { if (mAudioOutputVolume == aVolume) { return;
}
mAudioOutputVolume = aVolume; if (!mRendering) { return;
} for (constauto& t : mAudioTracks) { if (t) {
t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey,
mAudioOutputVolume);
}
}
}
if (!mRendering) {
MOZ_ASSERT(mSetAudioDevicePromise.IsEmpty()); return GenericPromise::CreateAndResolve(true, __func__);
}
nsTArray<RefPtr<GenericPromise>> promises; for (constauto& t : mAudioTracks) {
t->AsAudioStreamTrack()->RemoveAudioOutput(mAudioOutputKey);
promises.AppendElement(t->AsAudioStreamTrack()->AddAudioOutput(
mAudioOutputKey, mAudioOutputSink));
t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey,
mAudioOutputVolume);
} if (!promises.Length()) { // Not active track, save it for later
MOZ_ASSERT(mSetAudioDevicePromise.IsEmpty()); return GenericPromise::CreateAndResolve(true, __func__);
}
// Resolve any existing promise for a previous device so that promises // resolve in order of setSinkId() invocation.
ResolveAudioDevicePromiseIfExists(__func__);
RefPtr promise = mSetAudioDevicePromise.Ensure(__func__);
GenericPromise::AllSettled(GetCurrentSerialEventTarget(), promises)
->Then(GetMainThreadSerialEventTarget(), __func__,
[self = RefPtr{this}, this](const GenericPromise::AllSettledPromiseType::
ResolveOrRejectValue& aValue) { // This handler should have been disconnected if // mSetAudioDevicePromise has been settled.
MOZ_ASSERT(!mSetAudioDevicePromise.IsEmpty());
mDeviceStartedRequest.Complete(); // The AudioStreamTrack::AddAudioOutput() promise is rejected // either when the graph no longer needs the device, in which // case this handler would have already been disconnected, or // the graph is force shutdown. // mSetAudioDevicePromise is resolved regardless of whether // the AddAudioOutput() promises resolve or reject because // the underlying device has been changed.
LOG(LogLevel::Info,
("MediaStreamRenderer=%p SetAudioOutputDevice settled", this));
mSetAudioDevicePromise.Resolve(true, __func__);
})
->Track(mDeviceStartedRequest);
return promise;
}
void AddTrack(AudioStreamTrack* aTrack) {
MOZ_DIAGNOSTIC_ASSERT(!mAudioTracks.Contains(aTrack));
mAudioTracks.AppendElement(aTrack);
EnsureGraphTimeDummy(); if (mRendering) {
aTrack->AddAudioOutput(mAudioOutputKey, mAudioOutputSink);
aTrack->SetAudioOutputVolume(mAudioOutputKey, mAudioOutputVolume);
}
} void AddTrack(VideoStreamTrack* aTrack) {
MOZ_DIAGNOSTIC_ASSERT(!mVideoTrack); if (!mVideoContainer) { return;
}
mVideoTrack = aTrack;
EnsureGraphTimeDummy(); if (mFirstFrameVideoOutput) { // Add the first frame output even if we are rendering. It will only // accept one frame. If we are rendering, then the main output will // overwrite that with the same frame (and possibly more frames).
aTrack->AddVideoOutput(mFirstFrameVideoOutput);
} if (mRendering) {
aTrack->AddVideoOutput(mVideoContainer);
}
}
if (mAudioTracks.IsEmpty()) { // There is no longer an audio output that needs the device so the // device may not start. Ensure the promise is resolved.
ResolveAudioDevicePromiseIfExists(__func__);
}
} void RemoveTrack(VideoStreamTrack* aTrack) {
MOZ_DIAGNOSTIC_ASSERT(mVideoTrack == aTrack); if (!mVideoContainer) { return;
} if (mFirstFrameVideoOutput) {
aTrack->RemoveVideoOutput(mFirstFrameVideoOutput);
} if (mRendering) {
aTrack->RemoveVideoOutput(mVideoContainer);
}
mVideoTrack = nullptr;
}
double CurrentTime() const { if (!mGraphTimeDummy) { return 0.0;
}
// This dummy keeps `graph` alive and ensures access to it.
mGraphTimeDummy = MakeRefPtr<SharedDummyTrack>(
graph->CreateSourceTrack(MediaSegment::AUDIO));
}
// The audio output volume for all audio tracks. float mAudioOutputVolume = 1.0f;
// The sink device for all audio tracks.
RefPtr<AudioDeviceInfo> mAudioOutputSink; // The promise returned from SetAudioOutputDevice() when an output is // active.
MozPromiseHolder<GenericPromise> mSetAudioDevicePromise; // Request tracking the promise to indicate when the device passed to // SetAudioOutputDevice() is running.
MozPromiseRequestHolder<GenericPromise::AllSettledPromiseType>
mDeviceStartedRequest;
// WatchManager for mGraphTime.
WatchManager<MediaStreamRenderer> mWatchManager;
// A dummy MediaTrack to guarantee a MediaTrackGraph is kept alive while // we're actively rendering, so we can track the graph's current time. Set // when the first track is added, never unset.
RefPtr<SharedDummyTrack> mGraphTimeDummy;
// Watchable that relays the graph's currentTime updates to the media element // only while we're rendering. This is the current time of the rendering in // GraphTime units.
Watchable<GraphTime> mGraphTime = {0, "MediaStreamRenderer::mGraphTime"};
// Nothing until a track has been added. Then, the current GraphTime at the // time when we were last Start()ed.
Maybe<GraphTime> mGraphTimeOffset;
// Currently enabled (and rendered) audio tracks.
nsTArray<WeakPtr<MediaStreamTrack>> mAudioTracks;
// Currently selected (and rendered) video track.
WeakPtr<MediaStreamTrack> mVideoTrack;
// Holds a reference to the first-frame-getting video output attached to // mVideoTrack. Set by the constructor, unset when the media element tells us // it has rendered the first frame.
RefPtr<FirstFrameVideoOutput> mFirstFrameVideoOutput;
};
static uint32_t sDecoderCaptureSourceId = 0; static uint32_t sStreamCaptureSourceId = 0; class HTMLMediaElement::MediaElementTrackSource
: public MediaStreamTrackSource, public MediaStreamTrackSource::Sink, public MediaStreamTrackConsumer { public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaElementTrackSource,
MediaStreamTrackSource)
void Stop() override { // Do nothing. There may appear new output streams // that need tracks sourced from this source, so we // cannot destroy things yet.
}
/** * Do not keep the track source alive. The source lifetime is controlled by * its associated tracks.
*/ bool KeepsSourceAlive() const override { returnfalse; }
/** * Do not keep the track source on. It is controlled by its associated tracks.
*/ bool Enabled() const override { returnfalse; }
void Disable() override {}
void Enable() override {}
void PrincipalChanged() override { if (!mCapturedTrackSource) { // This could happen during shutdown. return;
}
RefPtr<MediaStreamTrack> mCapturedTrack;
RefPtr<MediaStreamTrackSource> mCapturedTrackSource; const RefPtr<ProcessedMediaTrack> mTrack;
RefPtr<MediaInputPort> mPort; // The mute state as intended by the media element.
OutputMuteState mIntendedElementMuteState; // The mute state as applied to this track source. It is applied async, so // needs to be tracked separately from the intended state.
OutputMuteState mElementMuteState; // Some<bool> if this is a MediaDecoder track source. const Maybe<bool> mMediaDecoderHasAlpha;
};
/** * There is a reference cycle involving this class: MediaLoadListener * holds a reference to the HTMLMediaElement, which holds a reference * to an nsIChannel, which holds a reference to this listener. * We break the reference cycle in OnStartRequest by clearing mElement.
*/ class HTMLMediaElement::MediaLoadListener final
: public nsIChannelEventSink, public nsIInterfaceRequestor, public nsIObserver, public nsIThreadRetargetableStreamListener {
~MediaLoadListener() = default;
if (!mElement) { // We've been notified by the shutdown observer, and are shutting down. return NS_BINDING_ABORTED;
}
// The element is only needed until we've had a chance to call // InitializeDecoderForChannel. So make sure mElement is cleared here.
RefPtr<HTMLMediaElement> element;
element.swap(mElement);
if (mLoadID != element->GetCurrentLoadID()) { // The channel has been cancelled before we had a chance to create // a decoder. Abort, don't dispatch an "error" event, as the new load // may not be in an error state. return NS_BINDING_ABORTED;
}
// Don't continue to load if the request failed or has been canceled.
nsresult status;
nsresult rv = aRequest->GetStatus(&status);
NS_ENSURE_SUCCESS(rv, rv); if (NS_FAILED(status)) { if (element) { // Handle media not loading error because source was a tracking URL (or // fingerprinting, cryptomining, etc). // We make a note of this media node by including it in a dedicated // array of blocked tracking nodes under its parent document. if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(
status)) {
element->OwnerDoc()->AddBlockedNodeByClassifier(element);
}
element->NotifyLoadError(
nsPrintfCString("%u: %s", uint32_t(status), "Request failed"));
} return status;
}
nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(aRequest); bool succeeded; if (hc && NS_SUCCEEDED(hc->GetRequestSucceeded(&succeeded)) && !succeeded) {
uint32_t responseStatus = 0;
Unused << hc->GetResponseStatus(&responseStatus);
nsAutoCString statusText;
Unused << hc->GetResponseStatusText(statusText); // we need status text for resist fingerprinting mode's message allowlist if (statusText.IsEmpty()) {
net_GetDefaultStatusTextForCode(responseStatus, statusText);
}
element->NotifyLoadError(
nsPrintfCString("%u: %s", responseStatus, statusText.get()));
nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); if (channel &&
NS_SUCCEEDED(rv = element->InitializeDecoderForChannel(
channel, getter_AddRefs(mNextListener))) &&
mNextListener) {
rv = mNextListener->OnStartRequest(aRequest);
} else { // If InitializeDecoderForChannel() returned an error, fire a network error. if (NS_FAILED(rv) && !mNextListener) { // Load failed, attempt to load the next candidate resource. If there // are none, this will trigger a MEDIA_ERR_SRC_NOT_SUPPORTED error.
element->NotifyLoadError("Failed to init decoder"_ns);
} // If InitializeDecoderForChannel did not return a listener (but may // have otherwise succeeded), we abort the connection since we aren't // interested in keeping the channel alive ourselves.
rv = NS_BINDING_ABORTED;
}
class HTMLMediaElement::AudioChannelAgentCallback final
: public nsIAudioChannelAgentCallback { public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(AudioChannelAgentCallback)
NS_IMETHODIMP WindowSuspendChanged(SuspendTypes aSuspend) override { // Currently this method is only be used for delaying autoplay, and we've // separated related codes to `MediaPlaybackDelayPolicy`. return NS_OK;
}
void StopAudioChanelAgent() {
MOZ_ASSERT(mAudioChannelAgent);
MOZ_ASSERT(mAudioChannelAgent->IsPlayingStarted());
mAudioChannelAgent->NotifyStoppedPlaying(); // If we have started audio capturing before, we have to tell media element // to clear the output capturing track.
mOwner->AudioCaptureTrackChange(false);
}
// The audio channel volume float mAudioChannelVolume; // Is this media element playing? bool mPlayingThroughTheAudioChannel; // Indicate whether media element is audible for users.
AudibleState mIsOwnerAudible; bool mIsShutDown;
};
class HTMLMediaElement::ChannelLoader final { public:
NS_INLINE_DECL_REFCOUNTING(ChannelLoader);
void LoadInternal(HTMLMediaElement* aElement) { if (mCancelled) { return;
}
// determine what security checks need to be performed in AsyncOpen().
nsSecurityFlags securityFlags =
aElement->ShouldCheckAllowOrigin()
? nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT
: nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
if (aElement->GetCORSMode() == CORS_USE_CREDENTIALS) {
securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
}
// If aElement has 'triggeringprincipal' attribute, we will use the value as // triggeringPrincipal for the channel, otherwise it will default to use // aElement->NodePrincipal(). // This function returns true when aElement has 'triggeringprincipal', so if // setAttrs is true we will override the origin attributes on the channel // later.
nsCOMPtr<nsIPrincipal> triggeringPrincipal; bool setAttrs = nsContentUtils::QueryTriggeringPrincipal(
aElement, aElement->mLoadingSrcTriggeringPrincipal,
getter_AddRefs(triggeringPrincipal));
if (NS_FAILED(rv)) { // Notify load error so the element will try next resource candidate.
aElement->NotifyLoadError("Fail to create channel"_ns); return;
}
nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); if (setAttrs) { // The function simply returns NS_OK, so we ignore the return value.
Unused << loadInfo->SetOriginAttributes(
triggeringPrincipal->OriginAttributesRef());
}
loadInfo->SetIsMediaRequest(true);
loadInfo->SetIsMediaInitialRequest(true);
nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel)); if (cos) { if (aElement->mUseUrgentStartForChannel) {
cos->AddClassFlags(nsIClassOfService::UrgentStart);
// Reset the flag to avoid loading again without initiated by user // interaction.
aElement->mUseUrgentStartForChannel = false;
}
// Unconditionally disable throttling since we want the media to fluently // play even when we switch the tab to background.
cos->AddClassFlags(nsIClassOfService::DontThrottle);
}
// The listener holds a strong reference to us. This creates a // reference cycle, once we've set mChannel, which is manually broken // in the listener's OnStartRequest method after it is finished with // the element. The cycle will also be broken if we get a shutdown // notification before OnStartRequest fires. Necko guarantees that // OnStartRequest will eventually fire if we don't shut down first.
RefPtr<MediaLoadListener> loadListener = new MediaLoadListener(aElement);
channel->SetNotificationCallbacks(loadListener);
nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(channel); if (hc) { // Use a byte range request from the start of the resource. // This enables us to detect if the stream supports byte range // requests, and therefore seeking, early.
rv = hc->SetRequestHeader("Range"_ns, "bytes=0-"_ns, false);
MOZ_ASSERT(NS_SUCCEEDED(rv));
aElement->SetRequestHeaders(hc);
}
rv = channel->AsyncOpen(loadListener); if (NS_FAILED(rv)) { // Notify load error so the element will try next resource candidate.
aElement->NotifyLoadError("Failed to open channel"_ns); return;
}
// Else the channel must be open and starting to download. If it encounters // a non-catastrophic failure, it will set a new task to continue loading // another candidate. It's safe to set it as mChannel now.
mChannel = channel;
// loadListener will be unregistered either on shutdown or when // OnStartRequest for the channel we just opened fires.
nsContentUtils::RegisterShutdownObserver(loadListener);
}
nsresult Load(HTMLMediaElement* aElement) {
MOZ_ASSERT(aElement); // Per bug 1235183 comment 8, we can't spin the event loop from stable // state. Defer NS_NewChannel() to a new regular runnable. return aElement->OwnerDoc()->Dispatch(NewRunnableMethod<HTMLMediaElement*>( "ChannelLoader::LoadInternal", this, &ChannelLoader::LoadInternal,
aElement));
}
void Done() {
MOZ_ASSERT(mChannel); // Decoder successfully created, the decoder now owns the MediaResource // which owns the channel.
mChannel = nullptr;
}
// Handle forwarding of Range header so that the intial detection // of seeking support (via result code 206) works across redirects.
nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel);
NS_ENSURE_STATE(http);
constexpr auto rangeHdr = "Range"_ns;
nsAutoCString rangeVal; if (NS_SUCCEEDED(http->GetRequestHeader(rangeHdr, rangeVal))) {
NS_ENSURE_STATE(!rangeVal.IsEmpty());
private:
~ChannelLoader() { MOZ_ASSERT(!mChannel); } // Holds a reference to the first channel we open to the media resource. // Once the decoder is created, control over the channel passes to the // decoder, and we null out this reference. We must store this in case // we need to cancel the channel before control of it passes to the decoder.
nsCOMPtr<nsIChannel> mChannel;
void SetError(uint16_t aErrorCode, const Maybe<MediaResult>& aResult) { // Since we have multiple paths calling into DecodeError, e.g. // MediaKeys::Terminated and EMEH264Decoder::Error. We should take the 1st // one only in order not to fire multiple 'error' events. if (mError) { return;
}
if (!IsValidErrorCode(aErrorCode)) {
NS_ASSERTION(false, "Undefined MediaError codes!"); return;
}
ReportErrorProbe(aErrorCode, aResult);
mError = new MediaError(mOwner, aErrorCode,
aResult ? aResult->Message() : nsCString());
mOwner->DispatchAsyncEvent(u"error"_ns); if (mOwner->ReadyState() == HAVE_NOTHING &&
aErrorCode == MEDIA_ERR_ABORTED) { // https://html.spec.whatwg.org/multipage/embedded-content.html#media-data-processing-steps-list // "If the media data fetching process is aborted by the user"
mOwner->DispatchAsyncEvent(u"abort"_ns);
mOwner->ChangeNetworkState(NETWORK_EMPTY);
mOwner->DispatchAsyncEvent(u"emptied"_ns); if (mOwner->mDecoder) {
mOwner->ShutdownDecoder();
}
} elseif (aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED) {
mOwner->ChangeNetworkState(NETWORK_NO_SOURCE);
} else {
mOwner->ChangeNetworkState(NETWORK_IDLE);
}
}
// Media elememt's life cycle would be longer than error sink, so we use the // raw pointer and this class would only be referenced by media element.
HTMLMediaElement* mOwner;
};
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement,
nsGenericHTMLElement)
tmp->RemoveMutationObserver(tmp); if (tmp->mSrcStream) { // Need to unhook everything that EndSrcMediaStreamPlayback would normally // do, without creating any new strong references. if (tmp->mSelectedVideoStreamTrack) {
tmp->mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(tmp);
} if (tmp->mMediaStreamRenderer) {
tmp->mMediaStreamRenderer->Shutdown(); // We null out mMediaStreamRenderer here since Shutdown() will shut down // its WatchManager, and UpdateSrcStreamPotentiallyPlaying() contains a // guard for this.
tmp->mMediaStreamRenderer = nullptr;
} if (tmp->mSecondaryMediaStreamRenderer) {
tmp->mSecondaryMediaStreamRenderer->Shutdown();
tmp->mSecondaryMediaStreamRenderer = nullptr;
} if (tmp->mMediaStreamTrackListener) {
tmp->mSrcStream->UnregisterTrackListener(
tmp->mMediaStreamTrackListener.get());
}
}
NS_IMPL_CYCLE_COLLECTION_UNLINK(mStreamWindowCapturer)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcStream)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcAttrStream)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcMediaSource)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourcePointer)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoadBlockedDoc)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceLoadCandidate) if (tmp->mAudioChannelWrapper) {
tmp->mAudioChannelWrapper->Shutdown();
}
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelWrapper)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mErrorSink->mError)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputTrackSources)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoTrackList)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaStreamTrackListener)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingMediaKeys)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPlayPromises)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSeekDOMPromise)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSetMediaKeysDOMPromise) if (tmp->mMediaControlKeyListener) {
tmp->mMediaControlKeyListener->Shutdown();
} if (tmp->mEventBlocker) {
tmp->mEventBlocker->Shutdown();
}
NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
// There are many other fields that might be worth reporting, but as seen in // bug 1595603, the event we postpone to dispatch can grow to be very large // sometimes, so at least report that. if (mEventBlocker) {
*aNodeSize +=
mEventBlocker->SizeOfExcludingThis(aSizes.mState.mMallocSizeOf);
}
}
void HTMLMediaElement::SetDecodeError(const nsAString& aError,
ErrorResult& aRv) { // The reason we use this map-ish structure is because we can't use // `CR.NS_ERROR.*` directly in test. In order to use them in test, we have to // add them into `xpc.msg`. As we won't use `CR.NS_ERROR.*` in the production // code, adding them to `xpc.msg` seems an overdesign and adding maintenance // effort (exposing them in CR also needs to add a description, which is // useless because we won't show them to users) staticstruct { constchar* mName;
nsresult mResult;
} kSupportedErrorList[] = {
{"NS_ERROR_DOM_MEDIA_ABORT_ERR", NS_ERROR_DOM_MEDIA_ABORT_ERR},
{"NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR",
NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR},
{"NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR",
NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR},
{"NS_ERROR_DOM_MEDIA_DECODE_ERR", NS_ERROR_DOM_MEDIA_DECODE_ERR},
{"NS_ERROR_DOM_MEDIA_FATAL_ERR", NS_ERROR_DOM_MEDIA_FATAL_ERR},
{"NS_ERROR_DOM_MEDIA_METADATA_ERR", NS_ERROR_DOM_MEDIA_METADATA_ERR},
{"NS_ERROR_DOM_MEDIA_OVERFLOW_ERR", NS_ERROR_DOM_MEDIA_OVERFLOW_ERR},
{"NS_ERROR_DOM_MEDIA_MEDIASINK_ERR", NS_ERROR_DOM_MEDIA_MEDIASINK_ERR},
{"NS_ERROR_DOM_MEDIA_DEMUXER_ERR", NS_ERROR_DOM_MEDIA_DEMUXER_ERR},
{"NS_ERROR_DOM_MEDIA_CDM_ERR", NS_ERROR_DOM_MEDIA_CDM_ERR},
{"NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR",
NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR}}; for (auto& error : kSupportedErrorList) { if (strcmp(error.mName, NS_ConvertUTF16toUTF8(aError).get()) == 0) {
DecoderDoctorDiagnostics diagnostics;
diagnostics.StoreDecodeError(OwnerDoc(), error.mResult, u""_ns, __func__); return;
}
}
aRv.Throw(NS_ERROR_FAILURE);
}
// TODO: In bug 1345404, handle case when video decoder is already suspended.
ImageContainer* container = GetImageContainer(); if (!container) { return nullptr;
}
void HTMLMediaElement::AbortExistingLoads() {
MOZ_ASSERT(NS_IsMainThread()); // Abort any already-running instance of the resource selection algorithm.
mLoadWaitStatus = NOT_WAITING;
// Set a new load ID. This will cause events which were enqueued // with a different load ID to silently be cancelled.
mCurrentLoadID++;
// Immediately reject or resolve the already-dispatched // nsResolveOrRejectPendingPlayPromisesRunners. These runners won't be // executed again later since the mCurrentLoadID had been changed. for (auto& runner : mPendingPlayPromisesRunners) {
runner->ResolveOrReject();
}
mPendingPlayPromisesRunners.Clear();
if (mChannelLoader) {
mChannelLoader->Cancel();
mChannelLoader = nullptr;
}
bool fireTimeUpdate = false;
if (mDecoder) {
fireTimeUpdate = mDecoder->GetCurrentTime() != 0.0;
ShutdownDecoder();
} if (mSrcStream) {
EndSrcMediaStreamPlayback();
}
if (mMediaSource) {
OwnerDoc()->RemoveMediaElementWithMSE();
}
if (mNetworkState != NETWORK_EMPTY) {
NS_ASSERTION(!mDecoder && !mSrcStream, "How did someone setup a new stream/decoder already?");
DispatchAsyncEvent(u"emptied"_ns);
// ChangeNetworkState() will call UpdateAudioChannelPlayingState() // indirectly which depends on mPaused. So we need to update mPaused first. if (!mPaused) {
mPaused = true;
PlayPromise::RejectPromises(TakePendingPlayPromises(),
NS_ERROR_DOM_MEDIA_ABORT_ERR);
}
ChangeNetworkState(NETWORK_EMPTY);
RemoveMediaTracks();
UpdateOutputTrackSources();
ChangeReadyState(HAVE_NOTHING);
// TODO: Apply the rules for text track cue rendering Bug 865407 if (mTextTrackManager) {
mTextTrackManager->GetTextTracks()->SetCuesInactive();
}
if (fireTimeUpdate) { // Since we destroyed the decoder above, the current playback position // will now be reported as 0. The playback position was non-zero when // we destroyed the decoder, so fire a timeupdate event so that the // change will be reflected in the controls.
FireTimeUpdate(TimeupdateType::eMandatory);
}
UpdateAudioChannelPlayingState();
}
if (IsVideo() && hadVideo) { // Ensure we render transparent black after resetting video resolution.
Maybe<nsIntSize> size = Some(nsIntSize(0, 0));
Invalidate(ImageSizeChanged::Yes, size, ForceInvalidate::No);
}
// As aborting current load would stop current playback, so we have no need to // resume a paused media element.
ClearResumeDelayedMediaPlaybackAgentIfNeeded();
mMediaControlKeyListener->StopIfNeeded();
// We may have changed mPaused, mCanAutoplayFlag, and other // things which can affect AddRemoveSelfReference
AddRemoveSelfReference();
if (NS_SUCCEEDED(rv) && !isSameOriginLoad) { // aErrorDetails can include sensitive details like MimeType or HTTP Status // Code. In case we're loading a 3rd party resource we should not leak this // and pass a Generic Error Message
mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED,
Some(MediaResult{NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR, "Failed to open media"_ns}));
} else {
mErrorSink->SetError(
MEDIA_ERR_SRC_NOT_SUPPORTED,
Some(MediaResult{NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR, aErrorDetails}));
}
void HTMLMediaElement::DoLoad() { // Check if media is allowed for the docshell.
nsCOMPtr<nsIDocShell> docShell = OwnerDoc()->GetDocShell(); if (docShell && !docShell->GetAllowMedia()) {
LOG(LogLevel::Debug, ("%p Media not allowed", this)); return;
}
if (mIsRunningLoadMethod) { return;
}
if (UserActivation::IsHandlingUserInput()) { // Detect if user has interacted with element so that play will not be // blocked when initiated by a script. This enables sites to capture user // intent to play by calling load() in the click handler of a "catalog // view" of a gallery of videos.
mIsBlessed = true; // Mark the channel as urgent-start when autoplay so that it will play the // media from src after loading enough resource. if (HasAttr(nsGkAtoms::autoplay)) {
mUseUrgentStartForChannel = true;
}
}
void HTMLMediaElement::ResetState() { // There might be a pending MediaDecoder::PlaybackPositionChanged() which // will overwrite |mMediaInfo.mVideo.mDisplay| in UpdateMediaSize() to give // staled videoWidth and videoHeight. We have to call ForgetElement() here // such that the staled callbacks won't reach us. if (mVideoFrameContainer) {
mVideoFrameContainer->ForgetElement();
mVideoFrameContainer = nullptr;
} if (mMediaStreamRenderer) { // mMediaStreamRenderer, has a strong reference to mVideoFrameContainer.
mMediaStreamRenderer->Shutdown();
mMediaStreamRenderer = nullptr;
} if (mSecondaryMediaStreamRenderer) { // mSecondaryMediaStreamRenderer, has a strong reference to // the secondary VideoFrameContainer.
mSecondaryMediaStreamRenderer->Shutdown();
mSecondaryMediaStreamRenderer = nullptr;
}
}
void HTMLMediaElement::SelectResource() { if (!mSrcAttrStream && !HasAttr(nsGkAtoms::src) && !HasSourceChildren(this)) { // The media element has neither a src attribute nor any source // element children, abort the load.
ChangeNetworkState(NETWORK_EMPTY);
ChangeDelayLoadStatus(false); return;
}
// Delay setting mIsRunningSeletResource until after UpdatePreloadAction // so that we don't lose our state change by bailing out of the preload // state update
UpdatePreloadAction();
mIsRunningSelectResource = true;
// If we have a 'src' attribute, use that exclusively.
nsAutoString src; if (mSrcAttrStream) {
SetupSrcMediaStreamPlayback(mSrcAttrStream);
} elseif (GetAttr(nsGkAtoms::src, src)) {
nsCOMPtr<nsIURI> uri;
MediaResult rv = NewURIFromString(src, getter_AddRefs(uri)); if (NS_SUCCEEDED(rv)) {
LOG(LogLevel::Debug, ("%p Trying load from src=%s", this,
NS_ConvertUTF16toUTF8(src).get())); if (profiler_is_collecting_markers()) {
nsPrintfCString markerName{"%p:mozloadresource", this};
profiler_add_marker(markerName, geckoprofiler::category::MEDIA_PLAYBACK,
{}, LoadSourceMarker{}, nsString{src}, nsString{},
nsString{});
}
NS_ASSERTION(
!mIsLoadingFromSourceChildren, "Should think we're not loading from source children by default");
RemoveMediaElementFromURITable(); if (!mSrcMediaSource) {
mLoadingSrc = uri;
} else {
mLoadingSrc = nullptr;
}
mLoadingSrcTriggeringPrincipal = mSrcAttrTriggeringPrincipal;
DDLOG(DDLogCategory::Property, "loading_src",
nsCString(NS_ConvertUTF16toUTF8(src))); bool hadMediaSource = !!mMediaSource;
mMediaSource = mSrcMediaSource; if (mMediaSource && !hadMediaSource) {
OwnerDoc()->AddMediaElementWithMSE();
}
DDLINKCHILD("mediasource", mMediaSource.get());
UpdatePreloadAction(); if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE && !mMediaSource) { // preload:none media, suspend the load here before we make any // network requests.
SuspendLoad(); return;
}
rv = LoadResource(); if (NS_SUCCEEDED(rv)) { return;
}
} else {
AutoTArray<nsString, 1> params = {src};
ReportLoadError("MediaLoadInvalidURI", params);
rv = MediaResult(rv.Code(), "MediaLoadInvalidURI");
} // The media element has neither a src attribute nor a source element child: // set the networkState to NETWORK_EMPTY, and abort these steps; the // synchronous section ends.
GetMainThreadSerialEventTarget()->Dispatch(NewRunnableMethod<nsCString>( "HTMLMediaElement::NoSupportedMediaSourceError", this,
&HTMLMediaElement::NoSupportedMediaSourceError, rv.Description()));
} else { // Otherwise, the source elements will be used.
mIsLoadingFromSourceChildren = true;
LoadFromSourceChildren();
}
}
void HTMLMediaElement::NotifyLoadError(const nsACString& aErrorDetails) { if (!mIsLoadingFromSourceChildren) {
LOG(LogLevel::Debug, ("NotifyLoadError(), no supported media error"));
NoSupportedMediaSourceError(aErrorDetails);
} elseif (mSourceLoadCandidate) {
DispatchAsyncSourceError(mSourceLoadCandidate, aErrorDetails);
QueueLoadFromSourceTask();
} else {
NS_WARNING("Should know the source we were loading from!");
} if (profiler_is_collecting_markers()) {
profiler_add_marker(nsPrintfCString("%p:mozloaderror", this),
geckoprofiler::category::MEDIA_PLAYBACK, {},
LoadErrorMarker{}, aErrorDetails);
}
}
void HTMLMediaElement::NotifyMediaTrackAdded(dom::MediaTrack* aTrack) { // The set of tracks changed.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
}
void HTMLMediaElement::NotifyMediaTrackRemoved(dom::MediaTrack* aTrack) { // The set of tracks changed.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
}
if (mSrcStream) { if (AudioTrack* t = aTrack->AsAudioTrack()) { if (mMediaStreamRenderer) {
mMediaStreamRenderer->AddTrack(t->GetAudioStreamTrack());
}
} elseif (VideoTrack* t = aTrack->AsVideoTrack()) {
MOZ_ASSERT(!mSelectedVideoStreamTrack);
mSelectedVideoStreamTrack = t->GetVideoStreamTrack();
mSelectedVideoStreamTrack->AddPrincipalChangeObserver(this); if (mMediaStreamRenderer) {
mMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack);
} if (mSecondaryMediaStreamRenderer) {
mSecondaryMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack);
} if (mMediaInfo.HasVideo()) {
mMediaInfo.mVideo.SetAlpha(mSelectedVideoStreamTrack->HasAlpha());
}
nsContentUtils::CombineResourcePrincipals(
&mSrcStreamVideoPrincipal, mSelectedVideoStreamTrack->GetPrincipal());
}
}
// The set of enabled/selected tracks changed.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
}
void HTMLMediaElement::NotifyMediaTrackDisabled(dom::MediaTrack* aTrack) {
MOZ_ASSERT(aTrack); if (!aTrack) { return;
}
nsString id;
aTrack->GetId(id);
LOG(LogLevel::Debug, ("MediaElement %p %sTrack with id %s disabled", this,
aTrack->AsAudioTrack() ? "Audio" : "Video",
NS_ConvertUTF16toUTF8(id).get()));
if (AudioTrack* t = aTrack->AsAudioTrack()) { if (mSrcStream) { if (mMediaStreamRenderer) {
mMediaStreamRenderer->RemoveTrack(t->GetAudioStreamTrack());
}
} // If we don't have any live tracks, we don't need to mute MediaElement.
MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked"); if (AudioTracks()->Length() > 0) { bool shouldMute = true; for (uint32_t i = 0; i < AudioTracks()->Length(); ++i) { if ((*AudioTracks())[i]->Enabled()) {
shouldMute = false; break;
}
}
if (shouldMute) {
SetMutedInternal(mMuted | MUTED_BY_AUDIO_TRACK);
}
}
} elseif (aTrack->AsVideoTrack()) { if (mSrcStream) {
MOZ_DIAGNOSTIC_ASSERT(mSelectedVideoStreamTrack ==
aTrack->AsVideoTrack()->GetVideoStreamTrack()); if (mMediaStreamRenderer) {
mMediaStreamRenderer->RemoveTrack(mSelectedVideoStreamTrack);
} if (mSecondaryMediaStreamRenderer) {
mSecondaryMediaStreamRenderer->RemoveTrack(mSelectedVideoStreamTrack);
}
mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(this);
mSelectedVideoStreamTrack = nullptr;
}
}
// The set of enabled/selected tracks changed.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
}
void HTMLMediaElement::DealWithFailedElement(nsIContent* aSourceElement) { if (mShuttingDown) { return;
}
void HTMLMediaElement::LoadFromSourceChildren() {
NS_ASSERTION(mDelayingLoadEvent, "Should delay load event (if in document) during load");
NS_ASSERTION(mIsLoadingFromSourceChildren, "Must remember we're loading from source children");
AddMutationObserverUnlessExists(this);
RemoveMediaTracks();
while (true) {
HTMLSourceElement* child = GetNextSource(); if (!child) { // Exhausted candidates, wait for more candidates to be appended to // the media element.
mLoadWaitStatus = WAITING_FOR_SOURCE;
ChangeNetworkState(NETWORK_NO_SOURCE);
ChangeDelayLoadStatus(false);
ReportLoadError("MediaLoadExhaustedCandidates"); return;
}
// Must have src attribute.
nsAutoString src; if (!child->GetAttr(nsGkAtoms::src, src)) {
ReportLoadError("MediaLoadSourceMissingSrc");
DealWithFailedElement(child); return;
}
// If we have a type attribute, it must be a supported type.
nsAutoString type; if (child->GetAttr(nsGkAtoms::type, type) && !type.IsEmpty()) {
DecoderDoctorDiagnostics diagnostics;
CanPlayStatus canPlay = GetCanPlay(type, &diagnostics);
diagnostics.StoreFormatDiagnostics(OwnerDoc(), type,
canPlay != CANPLAY_NO, __func__); if (canPlay == CANPLAY_NO) { // Check that at least one other source child exists and report that // we will try to load that one next.
nsIContent* nextChild = mSourcePointer->GetNextSibling();
AutoTArray<nsString, 2> params = {type, src};
while (nextChild) { if (nextChild && nextChild->IsHTMLElement(nsGkAtoms::source)) {
ReportLoadError("MediaLoadUnsupportedTypeAttributeLoadingNextChild",
params); break;
}
nextChild = nextChild->GetNextSibling();
};
if (!nextChild) {
ReportLoadError("MediaLoadUnsupportedTypeAttribute", params);
}
DealWithFailedElement(child); return;
}
}
nsAutoString media;
child->GetAttr(nsGkAtoms::media, media);
HTMLSourceElement* childSrc = HTMLSourceElement::FromNode(child);
MOZ_ASSERT(childSrc, "Expect child to be HTMLSourceElement"); if (childSrc && !childSrc->MatchesCurrentMedia()) {
AutoTArray<nsString, 2> params = {media, src};
ReportLoadError("MediaLoadSourceMediaNotMatched", params);
DealWithFailedElement(child);
LOG(LogLevel::Debug,
("%p Media did not match from <source>=%s type=%s media=%s", this,
NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(),
NS_ConvertUTF16toUTF8(media).get())); return;
}
LOG(LogLevel::Debug,
("%p Trying load from <source>=%s type=%s media=%s", this,
NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(),
NS_ConvertUTF16toUTF8(media).get()));
RemoveMediaElementFromURITable();
mLoadingSrc = uri;
mLoadingSrcTriggeringPrincipal = child->GetSrcTriggeringPrincipal();
DDLOG(DDLogCategory::Property, "loading_src",
nsCString(NS_ConvertUTF16toUTF8(src))); bool hadMediaSource = !!mMediaSource;
mMediaSource = child->GetSrcMediaSource(); if (mMediaSource && !hadMediaSource) {
OwnerDoc()->AddMediaElementWithMSE();
}
DDLINKCHILD("mediasource", mMediaSource.get());
NS_ASSERTION(mNetworkState == NETWORK_LOADING, "Network state should be loading");
if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE && !mMediaSource) { // preload:none media, suspend the load here before we make any // network requests.
SuspendLoad(); return;
}
if (NS_SUCCEEDED(LoadResource())) { return;
}
// If we fail to load, loop back and try loading the next resource.
DispatchAsyncSourceError(child, "Failed load on resource"_ns);
}
MOZ_ASSERT_UNREACHABLE("Execution should not reach here!");
}
void HTMLMediaElement::ResumeLoad(PreloadAction aAction) {
NS_ASSERTION(mSuspendedForPreloadNone, "Must be halted for preload:none to resume from preload:none " "suspended load.");
mSuspendedForPreloadNone = false;
mPreloadAction = aAction;
ChangeDelayLoadStatus(true);
ChangeNetworkState(NETWORK_LOADING); if (!mIsLoadingFromSourceChildren) { // We were loading from the element's src attribute.
MediaResult rv = LoadResource(); if (NS_FAILED(rv)) {
NoSupportedMediaSourceError(rv.Description());
}
} else { // We were loading from a child <source> element. Try to resume the // load of that child, and if that fails, try the next child. if (NS_FAILED(LoadResource())) {
LoadFromSourceChildren();
}
}
}
void HTMLMediaElement::UpdatePreloadAction() {
PreloadAction nextAction = PRELOAD_UNDEFINED; // If autoplay is set, or we're playing, we should always preload data, // as we'll need it to play. if ((AllowedToPlay() && HasAttr(nsGkAtoms::autoplay)) || !mPaused) {
nextAction = HTMLMediaElement::PRELOAD_ENOUGH;
} else { // Find the appropriate preload action by looking at the attribute. const nsAttrValue* val =
mAttrs.GetAttr(nsGkAtoms::preload, kNameSpaceID_None); // MSE doesn't work if preload is none, so it ignores the pref when src is // from MSE.
uint32_t preloadDefault = GetPreloadDefault();
uint32_t preloadAuto = GetPreloadDefaultAuto(); if (!val) { // Attribute is not set. Use the preload action specified by the // media.preload.default pref, or just preload metadata if not present.
nextAction = static_cast<PreloadAction>(preloadDefault);
} elseif (val->Type() == nsAttrValue::eEnum) {
PreloadAttrValue attr = static_cast<PreloadAttrValue>(val->GetEnumValue()); if (attr == HTMLMediaElement::PRELOAD_ATTR_EMPTY ||
attr == HTMLMediaElement::PRELOAD_ATTR_AUTO) {
nextAction = static_cast<PreloadAction>(preloadAuto);
} elseif (attr == HTMLMediaElement::PRELOAD_ATTR_METADATA) {
nextAction = HTMLMediaElement::PRELOAD_METADATA;
} elseif (attr == HTMLMediaElement::PRELOAD_ATTR_NONE) {
nextAction = HTMLMediaElement::PRELOAD_NONE;
}
} else { // Use the suggested "missing value default" of "metadata", or the value // specified by the media.preload.default, if present.
nextAction = static_cast<PreloadAction>(preloadDefault);
}
}
if (nextAction == HTMLMediaElement::PRELOAD_ENOUGH) { if (mSuspendedForPreloadNone) { // Our load was previouly suspended due to the media having preload // value "none". The preload value has changed to preload:auto, so // resume the load.
ResumeLoad(PRELOAD_ENOUGH);
} else { // Preload as much of the video as we can, i.e. don't suspend after // the first frame.
StopSuspendingAfterFirstFrame();
}
} elseif (nextAction == HTMLMediaElement::PRELOAD_METADATA) { // Ensure that the video can be suspended after first frame.
mAllowSuspendAfterFirstFrame = true; if (mSuspendedForPreloadNone) { // Our load was previouly suspended due to the media having preload // value "none". The preload value has changed to preload:metadata, so // resume the load. We'll pause the load again after we've read the // metadata.
ResumeLoad(PRELOAD_METADATA);
}
}
}
MediaResult HTMLMediaElement::LoadResource() {
NS_ASSERTION(mDelayingLoadEvent, "Should delay load event (if in document) during load");
if (mChannelLoader) {
mChannelLoader->Cancel();
mChannelLoader = nullptr;
}
// Set the media element's CORS mode only when loading a resource
mCORSMode = AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin));
HTMLMediaElement* other = LookupMediaElementURITable(mLoadingSrc); if (other && other->mDecoder) { // Clone it. // TODO: remove the cast by storing ChannelMediaDecoder in the URI table.
nsresult rv = InitializeDecoderAsClone( static_cast<ChannelMediaDecoder*>(other->mDecoder.get())); if (NS_SUCCEEDED(rv)) return rv;
}
RefPtr<MediaSourceDecoder> decoder = new MediaSourceDecoder(decoderInit); if (!mMediaSource->Attach(decoder)) { // TODO: Handle failure: run "If the media data cannot be fetched at // all, due to network errors, causing the user agent to give up // trying to fetch the resource" section of resource fetch algorithm.
decoder->Shutdown(); return MediaResult(NS_ERROR_FAILURE, "Failed to attach MediaSource");
}
ChangeDelayLoadStatus(false);
nsresult rv = decoder->Load(mMediaSource->GetPrincipal()); if (NS_FAILED(rv)) {
decoder->Shutdown();
LOG(LogLevel::Debug,
("%p Failed to load for decoder %p", this, decoder.get())); return MediaResult(rv, "Fail to load decoder");
}
rv = FinishDecoderSetup(decoder); return MediaResult(rv, "Failed to set up decoder");
}
AssertReadyStateIsNothing();
RefPtr<ChannelLoader> loader = new ChannelLoader;
nsresult rv = loader->Load(this); if (NS_SUCCEEDED(rv)) {
mChannelLoader = std::move(loader);
} return MediaResult(rv, "Failed to load channel");
}
void HTMLMediaElement::FastSeek(double aTime, ErrorResult& aRv) {
LOG(LogLevel::Debug, ("%p FastSeek(%f) called by JS", this, aTime));
Seek(aTime, SeekTarget::PrevSyncPoint, IgnoreErrors());
}
already_AddRefed<Promise> HTMLMediaElement::SeekToNextFrame(ErrorResult& aRv) { /* This will cause JIT code to be kept around longer, to help performance * when using SeekToNextFrame to iterate through every frame of a video.
*/
nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
if (win) { if (JSObject* obj = win->AsGlobal()->GetGlobalJSObject()) {
js::NotifyAnimationActivity(obj);
}
}
Seek(CurrentTime(), SeekTarget::NextFrame, aRv); if (aRv.Failed()) { return nullptr;
}
mSeekDOMPromise = CreateDOMPromise(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr;
}
return do_AddRef(mSeekDOMPromise);
}
void HTMLMediaElement::SetCurrentTime(double aCurrentTime, ErrorResult& aRv) {
LOG(LogLevel::Debug,
("%p SetCurrentTime(%lf) called by JS", this, aCurrentTime));
Seek(aCurrentTime, SeekTarget::Accurate, IgnoreErrors());
}
/** * Check if aValue is inside a range of aRanges, and if so returns true * and puts the range index in aIntervalIndex. If aValue is not * inside a range, returns false, and aIntervalIndex * is set to the index of the range which starts immediately after aValue * (and can be aRanges.Length() if aValue is after the last range).
*/ staticbool IsInRanges(TimeRanges& aRanges, double aValue,
uint32_t& aIntervalIndex) {
uint32_t length = aRanges.Length();
for (uint32_t i = 0; i < length; i++) { double start = aRanges.Start(i); if (start > aValue) {
aIntervalIndex = i; returnfalse;
} double end = aRanges.End(i); if (aValue <= end) {
aIntervalIndex = i; returntrue;
}
}
aIntervalIndex = length; returnfalse;
}
void HTMLMediaElement::Seek(double aTime, SeekTarget::Type aSeekType,
ErrorResult& aRv) { // Note: Seek is called both by synchronous code that expects errors thrown in // aRv, as well as asynchronous code that expects a promise. Make sure all // synchronous errors are returned using aRv, not promise rejections.
// aTime should be non-NaN.
MOZ_ASSERT(!std::isnan(aTime));
// Detect if user has interacted with element by seeking so that // play will not be blocked when initiated by a script. if (UserActivation::IsHandlingUserInput()) {
mIsBlessed = true;
}
StopSuspendingAfterFirstFrame();
if (mSrcAttrStream) { // do nothing since media streams have an empty Seekable range.
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return;
}
if (mPlayed && mCurrentPlayRangeStart != -1.0) { double rangeEndTime = CurrentTime();
LOG(LogLevel::Debug, ("%p Adding \'played\' a range : [%f, %f]", this,
mCurrentPlayRangeStart, rangeEndTime)); // Multiple seek without playing, or seek while playing. if (mCurrentPlayRangeStart != rangeEndTime) { // Don't round the left of the interval: it comes from script and needs // to be exact.
mPlayed->Add(mCurrentPlayRangeStart, rangeEndTime);
} // Reset the current played range start time. We'll re-set it once // the seek completes.
mCurrentPlayRangeStart = -1.0;
}
if (!mDecoder) { // mDecoder must always be set in order to reach this point.
NS_ASSERTION(mDecoder, "SetCurrentTime failed: no decoder");
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return;
}
// Clamp the seek target to inside the seekable ranges.
media::TimeRanges seekableRanges = mDecoder->GetSeekableTimeRanges(); if (seekableRanges.IsInvalid()) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return;
}
RefPtr<TimeRanges> seekable = new TimeRanges(ToSupports(OwnerDoc()), seekableRanges);
uint32_t length = seekable->Length(); if (length == 0) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return;
}
// If the position we want to seek to is not in a seekable range, we seek // to the closest position in the seekable ranges instead. If two positions // are equally close, we seek to the closest position from the currentTime. // See seeking spec, point 7 : // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#seeking
uint32_t range = 0; bool isInRange = IsInRanges(*seekable, aTime, range); if (!isInRange) { if (range == 0) { // aTime is before the first range in |seekable|, the closest point we can // seek to is the start of the first range.
aTime = seekable->Start(0);
} elseif (range == length) { // Seek target is after the end last range in seekable data. // Clamp the seek target to the end of the last seekable range.
aTime = seekable->End(length - 1);
} else { double leftBound = seekable->End(range - 1); double rightBound = seekable->Start(range); double distanceLeft = Abs(leftBound - aTime); double distanceRight = Abs(rightBound - aTime); if (distanceLeft == distanceRight) { double currentTime = CurrentTime();
distanceLeft = Abs(leftBound - currentTime);
distanceRight = Abs(rightBound - currentTime);
}
aTime = (distanceLeft < distanceRight) ? leftBound : rightBound;
}
}
// TODO: The spec requires us to update the current time to reflect the // actual seek target before beginning the synchronous section, but // that requires changing all MediaDecoderReaders to support telling // us the fastSeek target, and it's currently not possible to get // this information as we don't yet control the demuxer for all // MediaDecoderReaders.
mPlayingBeforeSeek = IsPotentiallyPlaying();
// The media backend is responsible for dispatching the timeupdate // event if it changes the playback position as a result of the seek.
LOG(LogLevel::Debug, ("%p SetCurrentTime(%f) starting seek", this, aTime));
mDecoder->Seek(aTime, aSeekType);
// We changed whether we're seeking so we need to AddRemoveSelfReference.
AddRemoveSelfReference();
already_AddRefed<TimeRanges> HTMLMediaElement::Played() {
RefPtr<TimeRanges> ranges = new TimeRanges(ToSupports(OwnerDoc()));
uint32_t timeRangeCount = 0; if (mPlayed) {
timeRangeCount = mPlayed->Length();
} for (uint32_t i = 0; i < timeRangeCount; i++) { double begin = mPlayed->Start(i); double end = mPlayed->End(i);
ranges->Add(begin, end);
}
if (mCurrentPlayRangeStart != -1.0) { double now = CurrentTime(); if (mCurrentPlayRangeStart != now) { // Don't round the left of the interval: it comes from script and needs // to be exact.
ranges->Add(mCurrentPlayRangeStart, now);
}
}
ranges->Normalize(); return ranges.forget();
}
void HTMLMediaElement::Pause(ErrorResult& aRv) {
LOG(LogLevel::Debug, ("%p Pause() called by JS", this)); if (mNetworkState == NETWORK_EMPTY) {
LOG(LogLevel::Debug, ("Loading due to Pause()"));
DoLoad();
}
PauseInternal();
}
void HTMLMediaElement::PauseInternal() { if (mDecoder && mNetworkState != NETWORK_EMPTY) {
mDecoder->Pause();
} bool oldPaused = mPaused;
mPaused = true; // Step 1, // https://html.spec.whatwg.org/multipage/media.html#internal-pause-steps
mCanAutoplayFlag = false; // We changed mPaused and mCanAutoplayFlag which can affect // AddRemoveSelfReference
AddRemoveSelfReference();
UpdateSrcMediaStreamPlaying(); if (mAudioChannelWrapper) {
mAudioChannelWrapper->NotifyPlayStateChanged();
}
// We don't need to resume media which is paused explicitly by user.
ClearResumeDelayedMediaPlaybackAgentIfNeeded();
if (!oldPaused) {
FireTimeUpdate(TimeupdateType::eMandatory);
DispatchAsyncEvent(u"pause"_ns);
AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_ABORT_ERR);
}
}
void HTMLMediaElement::SetVolume(double aVolume, ErrorResult& aRv) {
LOG(LogLevel::Debug, ("%p SetVolume(%f) called by JS", this, aVolume));
// Here we want just to update the volume.
SetVolumeInternal();
DispatchAsyncEvent(u"volumechange"_ns);
// We allow inaudible autoplay. But changing our volume may make this // media audible. So pause if we are no longer supposed to be autoplaying.
PauseIfShouldNotBePlaying();
}
void HTMLMediaElement::PauseIfShouldNotBePlaying() { if (GetPaused()) { return;
} if (!AllowedToPlay()) {
AUTOPLAY_LOG("pause because not allowed to play, element=%p", this);
ErrorResult rv;
Pause(rv);
}
}
// We allow inaudible autoplay. But changing our mute status may make this // media audible. So pause if we are no longer supposed to be autoplaying.
PauseIfShouldNotBePlaying();
}
void HTMLMediaElement::GetAllEnabledMediaTracks(
nsTArray<RefPtr<MediaTrack>>& aTracks) { if (AudioTrackList* tracks = AudioTracks()) { for (size_t i = 0; i < tracks->Length(); ++i) {
AudioTrack* track = (*tracks)[i]; if (track->Enabled()) {
aTracks.AppendElement(track);
}
}
} if (IsVideo()) { if (VideoTrackList* tracks = VideoTracks()) { for (size_t i = 0; i < tracks->Length(); ++i) {
VideoTrack* track = (*tracks)[i]; if (track->Selected()) {
aTracks.AppendElement(track);
}
}
}
}
}
void HTMLMediaElement::AddOutputTrackSourceToOutputStream(
MediaElementTrackSource* aSource, OutputMediaStream& aOutputStream,
AddTrackMode aMode) { if (aOutputStream.mStream == mSrcStream) { // Cycle detected. This can happen since tracks are added async. // We avoid forwarding it to the output here or we'd get into an infloop.
LOG(LogLevel::Warning,
("NOT adding output track source %p to output stream " "%p -- cycle detected",
aSource, aOutputStream.mStream.get())); return;
}
void HTMLMediaElement::UpdateOutputTrackSources() { // This updates the track sources in mOutputTrackSources so they're in sync // with the tracks being currently played, and state saying whether we should // be capturing tracks. This method is long so here is a breakdown: // - Figure out the tracks that should be captured // - Diff those against currently captured tracks (mOutputTrackSources), into // tracks-to-add, and tracks-to-remove // - Remove the tracks in tracks-to-remove and dispatch "removetrack" and // "ended" events for them // - If playback has ended, or there is no longer a media provider object, // remove any OutputMediaStreams that have the finish-when-ended flag set // - Create track sources for, and add to OutputMediaStreams, the tracks in // tracks-to-add
// Add track sources for all enabled/selected MediaTracks.
nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); if (!window) { return;
}
if (mDecoder) { if (!mTracksCaptured.Ref()) {
mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::None);
} elseif (!AudioTracks() || !VideoTracks() || !shouldHaveTrackSources) { // We've been unlinked, or tracks are not yet known.
mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::Halt);
} else {
mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::Capture,
mTracksCaptured.Ref().get());
}
}
// Start with all MediaTracks
AutoTArray<RefPtr<MediaTrack>, 4> mediaTracksToAdd; if (shouldHaveTrackSources) {
GetAllEnabledMediaTracks(mediaTracksToAdd);
}
// ...and all MediaElementTrackSources. auto trackSourcesToRemove =
ToTArray<AutoTArray<nsString, 4>>(mOutputTrackSources.Keys());
// Then work out the differences.
mediaTracksToAdd.RemoveLastElements(
mediaTracksToAdd.end() -
std::remove_if(mediaTracksToAdd.begin(), mediaTracksToAdd.end(),
[this, &trackSourcesToRemove](constauto& track) { constbool remove =
mOutputTrackSources.GetWeak(track->GetId()); if (remove) {
trackSourcesToRemove.RemoveElement(track->GetId());
} return remove;
}));
// First remove stale track sources. for (constauto& id : trackSourcesToRemove) {
RefPtr<MediaElementTrackSource> source = mOutputTrackSources.GetWeak(id);
if (mDecoder) {
mDecoder->RemoveOutputTrack(source->Track());
}
// The source of this track just ended. Force-notify that it ended. // If we bounce it to the MediaTrackGraph it might not be picked up, // for instance if the MediaInputPort was destroyed in the same // iteration as it was added.
GetMainThreadSerialEventTarget()->Dispatch(
NewRunnableMethod("MediaElementTrackSource::OverrideEnded", source,
&MediaElementTrackSource::OverrideEnded));
// Remove the track from the MediaStream after it ended. for (OutputMediaStream& ms : mOutputStreams) { if (source->Track()->mType == MediaSegment::VIDEO &&
ms.mCapturingAudioOnly) { continue;
}
DebugOnly<size_t> length = ms.mLiveTracks.Length();
ms.mLiveTracks.RemoveElementsBy(
[&](const RefPtr<MediaStreamTrack>& aTrack) { if (&aTrack->GetSource() != source) { returnfalse;
}
GetMainThreadSerialEventTarget()->Dispatch(
NewRunnableMethod<RefPtr<MediaStreamTrack>>( "DOMMediaStream::RemoveTrackInternal", ms.mStream,
&DOMMediaStream::RemoveTrackInternal, aTrack)); returntrue;
});
MOZ_ASSERT(ms.mLiveTracks.Length() == length - 1);
}
mOutputTrackSources.Remove(id);
}
// Then update finish-when-ended output streams as needed. for (size_t i = mOutputStreams.Length(); i-- > 0;) { if (!mOutputStreams[i].mFinishWhenEnded) { continue;
}
if (!mOutputStreams[i].mFinishWhenEndedLoadingSrc &&
!mOutputStreams[i].mFinishWhenEndedAttrStream &&
!mOutputStreams[i].mFinishWhenEndedMediaSource) { // This finish-when-ended stream has not seen any source loaded yet. // Update the loading src if it's time. if (!IsPlaybackEnded()) { if (mLoadingSrc) {
mOutputStreams[i].mFinishWhenEndedLoadingSrc = mLoadingSrc;
} elseif (mSrcAttrStream) {
mOutputStreams[i].mFinishWhenEndedAttrStream = mSrcAttrStream;
} elseif (mSrcMediaSource) {
mOutputStreams[i].mFinishWhenEndedMediaSource = mSrcMediaSource;
}
} continue;
}
// Discard finish-when-ended output streams with a loading src set as // needed. if (!IsPlaybackEnded() &&
mLoadingSrc == mOutputStreams[i].mFinishWhenEndedLoadingSrc) { continue;
} if (!IsPlaybackEnded() &&
mSrcAttrStream == mOutputStreams[i].mFinishWhenEndedAttrStream) { continue;
} if (!IsPlaybackEnded() &&
mSrcMediaSource == mOutputStreams[i].mFinishWhenEndedMediaSource) { continue;
}
LOG(LogLevel::Debug,
("Playback ended or source changed. Discarding stream %p",
mOutputStreams[i].mStream.get()));
mOutputStreams.RemoveElementAt(i); if (mOutputStreams.IsEmpty()) {
mTracksCaptured = nullptr; // mTracksCaptured is one of the Watchables triggering this method. // Unsetting it here means we'll run through this method again very soon. return;
}
}
// Finally add new MediaTracks. for (constauto& mediaTrack : mediaTracksToAdd) {
nsAutoString id;
mediaTrack->GetId(id);
MediaSegment::Type type; if (mediaTrack->AsAudioTrack()) {
type = MediaSegment::AUDIO;
} elseif (mediaTrack->AsVideoTrack()) {
type = MediaSegment::VIDEO;
} else {
MOZ_CRASH("Unknown track type");
}
RefPtr<ProcessedMediaTrack> track;
RefPtr<MediaElementTrackSource> source; if (mDecoder) {
track = mTracksCaptured.Ref()->mTrack->Graph()->CreateForwardedInputTrack(
type);
RefPtr<nsIPrincipal> principal = GetCurrentPrincipal(); if (!principal || IsCORSSameOrigin()) {
principal = NodePrincipal();
}
source = MakeAndAddRef<MediaElementTrackSource>(
track, principal, OutputTracksMuted(),
type == MediaSegment::VIDEO
? HTMLVideoElement::FromNode(this)->HasAlpha()
: false);
mDecoder->AddOutputTrack(track);
} elseif (mSrcStream) {
MediaStreamTrack* inputTrack; if (AudioTrack* t = mediaTrack->AsAudioTrack()) {
inputTrack = t->GetAudioStreamTrack();
} elseif (VideoTrack* t = mediaTrack->AsVideoTrack()) {
inputTrack = t->GetVideoStreamTrack();
} else {
MOZ_CRASH("Unknown track type");
}
MOZ_ASSERT(inputTrack); if (!inputTrack) {
NS_ERROR("Input track not found in source stream"); return;
}
MOZ_DIAGNOSTIC_ASSERT(!inputTrack->Ended());
// Track is muted initially, so we don't leak data if it's added while // paused and an MTG iteration passes before the mute comes into effect.
source->SetEnabled(mSrcStreamIsPlaying);
} else {
MOZ_CRASH("Unknown source");
}
// Add the new track source to any existing output streams for (OutputMediaStream& ms : mOutputStreams) { if (source->Track()->mType == MediaSegment::VIDEO &&
ms.mCapturingAudioOnly) { // If the output stream is for audio only we ignore video sources. continue;
}
AddOutputTrackSourceToOutputStream(source, ms);
}
}
}
bool HTMLMediaElement::CanBeCaptured(StreamCaptureType aCaptureType) { // Don't bother capturing when the document has gone away
nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); if (!window) { returnfalse;
}
// Prevent capturing restricted video if (aCaptureType == StreamCaptureType::CAPTURE_ALL_TRACKS &&
ContainsRestrictedContent()) { returnfalse;
} returntrue;
}
if (mTracksCaptured.Ref()) { // Already have an output stream. Check whether the graph rate matches if // specified. if (aGraph && aGraph != mTracksCaptured.Ref()->mTrack->Graph()) { return nullptr;
}
} else { // This is the first output stream, or there are no tracks. If the former, // start capturing all tracks. If the latter, they will be added later.
MediaTrackGraph* graph = aGraph; if (!graph) {
nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); if (!window) { return nullptr;
}
if (aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED &&
!mOutputTrackSources.IsEmpty()) { // This output stream won't receive any more tracks when playback of the // current src of this media element ends, or when the src of this media // element changes. If we're currently playing something (i.e., if there are // tracks currently captured), set the current src on the output stream so // this can be tracked. If we're not playing anything, // UpdateOutputTrackSources will set the current src when it becomes // available later. if (mLoadingSrc) {
out->mFinishWhenEndedLoadingSrc = mLoadingSrc;
} if (mSrcAttrStream) {
out->mFinishWhenEndedAttrStream = mSrcAttrStream;
} if (mSrcMediaSource) {
out->mFinishWhenEndedMediaSource = mSrcMediaSource;
}
MOZ_ASSERT(out->mFinishWhenEndedLoadingSrc ||
out->mFinishWhenEndedAttrStream ||
out->mFinishWhenEndedMediaSource);
}
if (aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO) { if (mSrcStream) { // We don't support applying volume and mute to the captured stream, when // capturing a MediaStream.
ReportToConsole(nsIScriptError::errorFlag, "MediaElementAudioCaptureOfMediaStreamError");
}
// mAudioCaptured tells the user that the audio played by this media element // is being routed to the captureStreams *instead* of being played to // speakers.
mAudioCaptured = true;
}
for (const RefPtr<MediaElementTrackSource>& source :
mOutputTrackSources.Values()) { if (source->Track()->mType == MediaSegment::VIDEO) { // Only add video tracks if we're a video element and the output stream // wants video. if (!IsVideo()) { continue;
} if (out->mCapturingAudioOnly) { continue;
}
}
AddOutputTrackSourceToOutputStream(source, *out, AddTrackMode::SYNC);
}
RefPtr<GenericNonExclusivePromise> HTMLMediaElement::GetAllowedToPlayPromise() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!mOutputStreams.IsEmpty(), "This method should only be called during stream capturing!"); if (AllowedToPlay()) {
AUTOPLAY_LOG("MediaElement %p has allowed to play, resolve promise", this); return GenericNonExclusivePromise::CreateAndResolve(true, __func__);
}
AUTOPLAY_LOG("create allow-to-play promise for MediaElement %p", this); return mAllowedToPlayPromise.Ensure(__func__);
}
using MediaElementURITable = nsTHashtable<MediaElementSetForURI>; // Elements in this table must have non-null mDecoder and mLoadingSrc, and those // can't change while the element is in the table. The table is keyed by // the element's mLoadingSrc. Each entry has a list of all elements with the // same mLoadingSrc. static MediaElementURITable* gElementTable;
#ifdef DEBUG staticbool URISafeEquals(nsIURI* a1, nsIURI* a2) { if (!a1 || !a2) { // Consider two empty URIs *not* equal! returnfalse;
} bool equal = false;
nsresult rv = a1->Equals(a2, &equal); return NS_SUCCEEDED(rv) && equal;
} // Returns the number of times aElement appears in the media element table // for aURI. If this returns other than 0 or 1, there's a bug somewhere! staticunsigned MediaElementTableCount(HTMLMediaElement* aElement,
nsIURI* aURI) { if (!gElementTable || !aElement) { return 0;
}
uint32_t uriCount = 0;
uint32_t otherCount = 0; for (constauto& entry : *gElementTable) {
uint32_t count = 0; for (constauto& elem : entry.mElements) { if (elem == aElement) {
count++;
}
} if (URISafeEquals(aURI, entry.GetKey())) {
uriCount = count;
} else {
otherCount += count;
}
}
NS_ASSERTION(otherCount == 0, "Should not have entries for unknown URIs"); return uriCount;
} #endif
void HTMLMediaElement::AddMediaElementToURITable() {
NS_ASSERTION(mDecoder, "Call this only with decoder Load called");
NS_ASSERTION(
MediaElementTableCount(this, mLoadingSrc) == 0, "Should not have entry for element in element table before addition"); if (!gElementTable) {
gElementTable = new MediaElementURITable();
}
MediaElementSetForURI* entry = gElementTable->PutEntry(mLoadingSrc);
entry->mElements.AppendElement(this);
NS_ASSERTION(
MediaElementTableCount(this, mLoadingSrc) == 1, "Should have a single entry for element in element table after addition");
}
void HTMLMediaElement::RemoveMediaElementFromURITable() { if (!mDecoder || !mLoadingSrc || !gElementTable) { return;
}
MediaElementSetForURI* entry = gElementTable->GetEntry(mLoadingSrc); if (!entry) { return;
}
entry->mElements.RemoveElement(this); if (entry->mElements.IsEmpty()) {
gElementTable->RemoveEntry(entry); if (gElementTable->Count() == 0) { delete gElementTable;
gElementTable = nullptr;
}
}
NS_ASSERTION(MediaElementTableCount(this, mLoadingSrc) == 0, "After remove, should no longer have an entry in element table");
}
HTMLMediaElement* HTMLMediaElement::LookupMediaElementURITable(nsIURI* aURI) { if (!gElementTable) { return nullptr;
}
MediaElementSetForURI* entry = gElementTable->GetEntry(aURI); if (!entry) { return nullptr;
} for (uint32_t i = 0; i < entry->mElements.Length(); ++i) {
HTMLMediaElement* elem = entry->mElements[i]; bool equal; // Look for elements that have the same principal and CORS mode. // Ditto for anything else that could cause us to send different headers. if (NS_SUCCEEDED(elem->NodePrincipal()->Equals(NodePrincipal(), &equal)) &&
equal && elem->mCORSMode == mCORSMode) { // See SetupDecoder() below. We only add a element to the table when // mDecoder is a ChannelMediaDecoder. auto* decoder = static_cast<ChannelMediaDecoder*>(elem->mDecoder.get());
NS_ASSERTION(decoder, "Decoder gone"); if (decoder->CanClone()) { return elem;
}
}
} return nullptr;
}
class HTMLMediaElement::ShutdownObserver : public nsIObserver { enumclass Phase : int8_t { Init, Subscribed, Unsubscribed };
public:
NS_DECL_ISUPPORTS
NS_IMETHOD Observe(nsISupports*, constchar* aTopic, const char16_t*) override { if (mPhase != Phase::Subscribed) { // Bail out if we are not subscribed for this might be called even after // |nsContentUtils::UnregisterShutdownObserver(this)|. return NS_OK;
}
MOZ_DIAGNOSTIC_ASSERT(mWeak); if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
mWeak->NotifyShutdownEvent();
} return NS_OK;
} void Subscribe(HTMLMediaElement* aPtr) {
MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Init);
MOZ_DIAGNOSTIC_ASSERT(!mWeak);
mWeak = aPtr;
nsContentUtils::RegisterShutdownObserver(this);
mPhase = Phase::Subscribed;
} void Unsubscribe() {
MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Subscribed);
MOZ_DIAGNOSTIC_ASSERT(mWeak);
MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, "ReleaseMediaElement should have been called first");
mWeak = nullptr;
nsContentUtils::UnregisterShutdownObserver(this);
mPhase = Phase::Unsubscribed;
} void AddRefMediaElement() {
MOZ_DIAGNOSTIC_ASSERT(mWeak);
MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, "Should only ever AddRef once");
mWeak->AddRef();
mAddRefed = true;
} void ReleaseMediaElement() {
MOZ_DIAGNOSTIC_ASSERT(mWeak);
MOZ_DIAGNOSTIC_ASSERT(mAddRefed, "Should only release after AddRef");
mWeak->Release();
mAddRefed = false;
}
private: virtual ~ShutdownObserver() {
MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Unsubscribed);
MOZ_DIAGNOSTIC_ASSERT(!mWeak);
MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, "ReleaseMediaElement should have been called first");
} // Guaranteed to be valid by HTMLMediaElement.
HTMLMediaElement* mWeak = nullptr;
Phase mPhase = Phase::Init; bool mAddRefed = false;
};
HTMLMediaElement::HTMLMediaElement(
already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
: nsGenericHTMLElement(std::move(aNodeInfo)),
mWatchManager(this, AbstractThread::MainThread()),
mShutdownObserver(new ShutdownObserver),
mTitleChangeObserver(new TitleChangeObserver(this)),
mEventBlocker(new EventBlocker(this)),
mPlayed(new TimeRanges(ToSupports(OwnerDoc()))),
mTracksCaptured(nullptr, "HTMLMediaElement::mTracksCaptured"),
mErrorSink(new ErrorSink(this)),
mAudioChannelWrapper(new AudioChannelAgentCallback(this)),
mSink(std::pair(nsString(), RefPtr<AudioDeviceInfo>())),
mShowPoster(IsVideo()),
mMediaControlKeyListener(new MediaControlKeyListener(this)) {
MOZ_ASSERT(GetMainThreadSerialEventTarget()); // Please don't add anything to this constructor or the initialization // list that can cause AddRef to be called. This prevents subclasses // from overriding AddRef in a way that works with our refcount // logging mechanisms. Put these things inside of the ::Init method // instead.
}
void HTMLMediaElement::Init() {
MOZ_ASSERT(mRefCnt == 0 && !mRefCnt.IsPurple(), "HTMLMediaElement::Init called when AddRef has been called " "at least once already, probably in the constructor. Please " "see the documentation in the HTMLMediaElement constructor.");
MOZ_ASSERT(!mRefCnt.IsPurple());
mAudioTrackList = new AudioTrackList(OwnerDoc()->GetParentObject(), this);
mVideoTrackList = new VideoTrackList(OwnerDoc()->GetParentObject(), this);
// We initialize the MediaShutdownManager as the HTMLMediaElement is always // constructed on the main thread, and not during stable state. // (MediaShutdownManager make use of nsIAsyncShutdownClient which is written // in JS)
MediaShutdownManager::InitStatics();
HTMLMediaElement::~HTMLMediaElement() {
MOZ_ASSERT(mInitialized, "HTMLMediaElement must be initialized before it is destroyed.");
NS_ASSERTION(
!mHasSelfReference, "How can we be destroyed if we're still holding a self reference?");
mWatchManager.Shutdown();
mShutdownObserver->Unsubscribe();
mTitleChangeObserver->Unsubscribe();
if (mVideoFrameContainer) {
mVideoFrameContainer->ForgetElement();
}
UnregisterActivityObserver();
// Force a reflow so that the poster frame hides or shows immediately.
nsIFrame* frame = GetPrimaryFrame(); if (!frame) { return;
}
frame->PresShell()->FrameNeedsReflow(frame, IntrinsicDirty::FrameAndAncestors,
NS_FRAME_IS_DIRTY);
}
already_AddRefed<Promise> HTMLMediaElement::Play(ErrorResult& aRv) {
LOG(LogLevel::Debug,
("%p Play() called by JS readyState=%d", this, mReadyState.Ref()));
// 4.8.12.8 // When the play() method on a media element is invoked, the user agent must // run the following steps.
RefPtr<PlayPromise> promise = CreatePlayPromise(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr;
}
// 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: we may require requesting permission from the user, so we do the // "not allowed" check below.
// 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));
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. // Note: Promise appended to list of pending promises as needed below.
if (ShouldBeSuspendedByInactiveDocShell()) {
LOG(LogLevel::Debug, ("%p no allow to play by the docShell for now", this));
mPendingPlayPromises.AppendElement(promise); return promise.forget();
}
// We may delay starting playback of a media resource for an unvisited tab // until it's going to foreground or being resumed by the play tab icon. if (MediaPlaybackDelayPolicy::ShouldDelayPlayback(this)) {
CreateResumeDelayedMediaPlaybackAgentIfNeeded();
LOG(LogLevel::Debug, ("%p delay Play() call", this));
MaybeDoLoad(); // When play is delayed, save a reference to the promise, and return it. // The promise will be resolved when we resume play by either the tab is // brought to the foreground, or the audio tab indicator is clicked.
mPendingPlayPromises.AppendElement(promise); return promise.forget();
}
if (AllowedToPlay()) {
AUTOPLAY_LOG("allow MediaElement %p to play", this);
mAllowedToPlayPromise.ResolveIfExists(true, __func__);
PlayInternal(handlingUserInput);
UpdateCustomPolicyAfterPlayed();
} else {
AUTOPLAY_LOG("reject MediaElement %p to play", this);
AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
} return promise.forget();
}
void HTMLMediaElement::DispatchEventsWhenPlayWasNotAllowed() { if (StaticPrefs::media_autoplay_block_event_enabled()) {
DispatchAsyncEvent(u"blocked"_ns);
}
DispatchBlockEventForVideoControl(); if (!mHasEverBeenBlockedForAutoplay) {
MaybeNotifyAutoplayBlocked();
ReportToConsole(nsIScriptError::warningFlag, "BlockAutoplayError");
mHasEverBeenBlockedForAutoplay = true;
}
}
void HTMLMediaElement::MaybeNotifyAutoplayBlocked() { // This event is used to notify front-end side that we've blocked autoplay, // so front-end side should show blocking icon as well.
RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(OwnerDoc(), u"GloballyAutoplayBlocked"_ns,
CanBubble::eYes, ChromeOnlyDispatch::eYes);
asyncDispatcher->PostDOMEvent();
}
void HTMLMediaElement::PlayInternal(bool aHandlingUserInput) { 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;
}
// 4.8.12.8 - Step 4: // If the media element's networkState attribute has the value NETWORK_EMPTY, // invoke the media element's resource selection algorithm.
MaybeDoLoad(); if (mSuspendedForPreloadNone) {
ResumeLoad(PRELOAD_ENOUGH);
}
// 4.8.12.8 - Step 5: // If the playback has ended and the direction of playback is forwards, // seek to the earliest possible position of the media resource.
// Even if we just did Load() or ResumeLoad(), we could already have a decoder // here if we managed to clone an existing decoder. if (mDecoder) { if (mDecoder->IsEnded()) {
SetCurrentTime(0);
} if (!mSuspendedByInactiveDocOrDocshell) {
mDecoder->Play();
}
}
if (mCurrentPlayRangeStart == -1.0) {
mCurrentPlayRangeStart = CurrentTime();
}
// We changed mPaused and mCanAutoplayFlag which can affect // AddRemoveSelfReference and our preload status.
AddRemoveSelfReference();
UpdatePreloadAction();
UpdateSrcMediaStreamPlaying();
StartMediaControlKeyListenerIfNeeded();
// Once play() has been called in a user generated event handler, // it is allowed to autoplay. Note: we can reach here when not in // a user generated event handler if our readyState has not yet // reached HAVE_METADATA.
mIsBlessed |= aHandlingUserInput;
// TODO: If the playback has ended, then the user agent must set // seek to the effective start.
// 4.8.12.8 - Step 6: // If the media element's paused attribute is true, run the following steps: if (oldPaused) { // 6.1. Change the value of paused to false. (Already done.) // This step is uplifted because the "block-media-playback" feature needs // the mPaused to be false before UpdateAudioChannelPlayingState() being // called.
// 6.2. If the show poster flag is true, set the element's show poster flag // to false and run the time marches on steps. if (mShowPoster) {
mShowPoster = false; if (mTextTrackManager) {
mTextTrackManager->TimeMarchesOn();
}
}
// 6.3. Queue a task to fire a simple event named play at the element.
DispatchAsyncEvent(u"play"_ns);
// 6.4. If the media element's readyState attribute has the value // HAVE_NOTHING, HAVE_METADATA, or HAVE_CURRENT_DATA, queue a task to // fire a simple event named waiting at the element. // Otherwise, the media element's readyState attribute has the value // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA: notify about playing for the // element. switch (mReadyState) { case HAVE_NOTHING:
DispatchAsyncEvent(u"waiting"_ns); break; case HAVE_METADATA: case HAVE_CURRENT_DATA:
DispatchAsyncEvent(u"waiting"_ns); break; case HAVE_FUTURE_DATA: case HAVE_ENOUGH_DATA:
NotifyAboutPlaying(); break;
}
} elseif (mReadyState >= HAVE_FUTURE_DATA) { // 7. Otherwise, if the media element's readyState attribute has the value // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take pending play promises and // queue a task to resolve pending play promises with the result.
AsyncResolvePendingPlayPromises();
}
// 8. Set the media element's autoplaying flag to false. (Already done.)
void HTMLMediaElement::UpdateWakeLock() {
MOZ_ASSERT(NS_IsMainThread()); // Ensure we have a wake lock if we're playing audibly. This ensures the // device doesn't sleep while playing. bool playing = !mPaused; bool isAudible = Volume() > 0.0 && !mMuted && mIsAudioTrackAudible; // WakeLock when playing audible media. if (playing && isAudible) {
CreateAudioWakeLockIfNeeded();
} else {
ReleaseAudioWakeLockIfExists();
}
}
// We will need to trap pointer, touch, and mouse events within the media // element, allowing media control exclusive consumption on these events, // and preventing the content from handling them. switch (aVisitor.mEvent->mMessage) { case ePointerDown: case ePointerUp: case eTouchEnd: // Always prevent touchmove captured in video element from being handled by // content, since we always do that for touchstart. case eTouchMove: case eTouchStart: case ePointerClick: case eMouseDoubleClick: case eMouseDown: case eMouseUp:
aVisitor.mCanHandle = false; return;
// The *move events however are only comsumed when the range input is being // dragged. case ePointerMove: case eMouseMove: {
nsINode* node =
nsINode::FromEventTargetOrNull(aVisitor.mEvent->mOriginalTarget); if (MOZ_UNLIKELY(!node)) { return;
}
HTMLInputElement* el = nullptr; if (node->ChromeOnlyAccess()) { if (node->IsHTMLElement(nsGkAtoms::input)) { // The node is a <input type="range">
el = static_cast<HTMLInputElement*>(node);
} elseif (node->GetParentNode() &&
node->GetParentNode()->IsHTMLElement(nsGkAtoms::input)) { // The node is a child of <input type="range">
el = static_cast<HTMLInputElement*>(node->GetParentNode());
}
} if (el && el->IsDraggingRange()) {
aVisitor.mCanHandle = false; return;
}
nsGenericHTMLElement::GetEventTargetParent(aVisitor); return;
} default:
nsGenericHTMLElement::GetEventTargetParent(aVisitor); return;
}
}
// Since AfterMaybeChangeAttr may call DoLoad, make sure that it is called // *after* any possible changes to mSrcMediaSource. if (aValue) {
AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify);
}
// https://html.spec.whatwg.org/#playing-the-media-resource:remove-an-element-from-a-document // // Dispatch a task to run once we're in a stable state which ensures we're // paused if we're no longer in a document. Note that we need to dispatch this // even if there are other tasks in flight for this because these can be // cancelled if there's a new load. // // FIXME(emilio): Per that spec section, we should only do this if we used to // be connected, though other browsers match our current behavior... // // Also, https://github.com/whatwg/html/issues/4928
nsCOMPtr<nsIRunnable> task =
NS_NewRunnableFunction("dom::HTMLMediaElement::UnbindFromTree",
[self = RefPtr<HTMLMediaElement>(this)]() { if (!self->IsInComposedDoc()) {
self->PauseInternal();
self->mMediaControlKeyListener->StopIfNeeded();
}
});
RunInStableState(task);
}
/* static */
CanPlayStatus HTMLMediaElement::GetCanPlay( const nsAString& aType, DecoderDoctorDiagnostics* aDiagnostics) {
Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType); if (!containerType) { return CANPLAY_NO;
}
CanPlayStatus status =
DecoderTraits::CanHandleContainerType(*containerType, aDiagnostics); if (status == CANPLAY_YES &&
(*containerType).ExtendedType().Codecs().IsEmpty()) { // Per spec: 'Generally, a user agent should never return "probably" for a // type that allows the `codecs` parameter if that parameter is not // present.' As all our currently-supported types allow for `codecs`, we can // do this check here. // TODO: Instead, missing `codecs` should be checked in each decoder's // `IsSupportedType` call from `CanHandleCodecsType()`. // See bug 1399023. return CANPLAY_MAYBE;
} return status;
}
nsresult HTMLMediaElement::InitializeDecoderAsClone(
ChannelMediaDecoder* aOriginal) {
NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
NS_ASSERTION(mDecoder == nullptr, "Shouldn't have a decoder");
AssertReadyStateIsNothing();
RefPtr<ChannelMediaDecoder> decoder = aOriginal->Clone(decoderInit); if (!decoder) return NS_ERROR_FAILURE;
LOG(LogLevel::Debug,
("%p Cloned decoder %p from %p", this, decoder.get(), aOriginal));
return FinishDecoderSetup(decoder);
}
template <typename DecoderType, typename... LoadArgs>
nsresult HTMLMediaElement::SetupDecoder(DecoderType* aDecoder,
LoadArgs&&... aArgs) {
LOG(LogLevel::Debug, ("%p Created decoder %p for type %s", this, aDecoder,
aDecoder->ContainerType().OriginalString().Data()));
nsresult rv = aDecoder->Load(std::forward<LoadArgs>(aArgs)...); if (NS_FAILED(rv)) {
aDecoder->Shutdown();
LOG(LogLevel::Debug, ("%p Failed to load for decoder %p", this, aDecoder)); return rv;
}
rv = FinishDecoderSetup(aDecoder); // Only ChannelMediaDecoder supports resource cloning. if (std::is_same_v<DecoderType, ChannelMediaDecoder> && NS_SUCCEEDED(rv)) {
AddMediaElementToURITable();
NS_ASSERTION(
MediaElementTableCount(this, mLoadingSrc) == 1, "Media element should have single table entry if decode initialized");
}
return rv;
}
nsresult HTMLMediaElement::InitializeDecoderForChannel(
nsIChannel* aChannel, nsIStreamListener** aListener) {
NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
AssertReadyStateIsNothing();
DecoderDoctorDiagnostics diagnostics;
nsAutoCString mimeType;
aChannel->GetContentType(mimeType);
NS_ASSERTION(!mimeType.IsEmpty(), "We should have the Content-Type.");
NS_ConvertUTF8toUTF16 mimeUTF16(mimeType);
// Set mDecoder now so if methods like GetCurrentSrc get called between // here and Load(), they work.
SetDecoder(aDecoder);
// 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);
}
if (mMediaKeys) { if (mMediaKeys->GetCDMProxy()) {
mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy());
} else { // CDM must have crashed.
ShutdownDecoder(); return NS_ERROR_FAILURE;
}
}
if (mChannelLoader) {
mChannelLoader->Done();
mChannelLoader = nullptr;
}
// We may want to suspend the new stream now. // This will also do an AddRemoveSelfReference.
NotifyOwnerDocumentActivityChanged();
if (!mDecoder) { // NotifyOwnerDocumentActivityChanged may shutdown the decoder if the // owning document is inactive and we're in the EME case. We could try and // handle this, but at the time of writing it's a pretty niche case, so just // bail. return NS_ERROR_FAILURE;
}
if (mSuspendedByInactiveDocOrDocshell) {
mDecoder->Suspend();
}
if (!mPaused) {
SetPlayedOrSeeked(true); if (!mSuspendedByInactiveDocOrDocshell) {
mDecoder->Play();
}
}
MaybeBeginCloningVisually();
return NS_OK;
}
void HTMLMediaElement::UpdateSrcMediaStreamPlaying(uint32_t aFlags) { if (!mSrcStream) { return;
}
if (shouldPlay) {
mSrcStreamPlaybackEnded = false;
mSrcStreamReportPlaybackEnded = false;
if (mMediaStreamRenderer) {
mMediaStreamRenderer->Start();
} if (mSecondaryMediaStreamRenderer) {
mSecondaryMediaStreamRenderer->Start();
}
SetCapturedOutputStreamsEnabled(true); // Unmute // If the input is a media stream, we don't check its data and always regard // it as audible when it's playing.
SetAudibleState(true);
} else { if (mMediaStreamRenderer) {
mMediaStreamRenderer->Stop();
} if (mSecondaryMediaStreamRenderer) {
mSecondaryMediaStreamRenderer->Stop();
}
SetCapturedOutputStreamsEnabled(false); // Mute
}
}
void HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying() { if (!mMediaStreamRenderer) { // Notifications are async, the renderer could have been cleared. return;
}
// If we pause this media element, track changes in the underlying stream // will continue to fire events at this element and alter its track list. // That's simpler than delaying the events, but probably confusing...
nsTArray<RefPtr<MediaStreamTrack>> tracks;
mSrcStream->GetTracks(tracks); for (const RefPtr<MediaStreamTrack>& track : tracks) {
NotifyMediaStreamTrackAdded(track);
}
mMediaStreamTrackListener = new MediaStreamTrackListener(this);
mSrcStream->RegisterTrackListener(mMediaStreamTrackListener.get());
LOG(LogLevel::Debug, ("%p, Adding %sTrack with id %s", this,
aTrack->AsAudioStreamTrack() ? "Audio" : "Video",
NS_ConvertUTF16toUTF8(id).get())); #endif
if (AudioStreamTrack* t = aTrack->AsAudioStreamTrack()) {
MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked");
RefPtr<AudioTrack> audioTrack =
CreateAudioTrack(t, AudioTracks()->GetOwnerGlobal());
AudioTracks()->AddTrack(audioTrack);
} elseif (VideoStreamTrack* t = aTrack->AsVideoStreamTrack()) { // TODO: Fix this per the spec on bug 1273443. if (!IsVideo()) { return;
}
MOZ_DIAGNOSTIC_ASSERT(VideoTracks(), "Element can't have been unlinked");
RefPtr<VideoTrack> videoTrack =
CreateVideoTrack(t, VideoTracks()->GetOwnerGlobal());
VideoTracks()->AddTrack(videoTrack); // New MediaStreamTrack added, set the new added video track as selected // video track when there is no selected track. if (VideoTracks()->SelectedIndex() == -1) {
MOZ_ASSERT(!mSelectedVideoStreamTrack);
videoTrack->SetEnabledInternal(true, dom::MediaTrack::FIRE_NO_EVENTS);
}
}
// The set of enabled AudioTracks and selected video track might have changed.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
AbstractThread::DispatchDirectTask(
NewRunnableMethod("HTMLMediaElement::FirstFrameLoaded", this,
&HTMLMediaElement::FirstFrameLoaded));
}
LOG(LogLevel::Debug, ("%p, Removing %sTrack with id %s", this,
aTrack->AsAudioStreamTrack() ? "Audio" : "Video",
NS_ConvertUTF16toUTF8(id).get()));
MOZ_DIAGNOSTIC_ASSERT(AudioTracks() && VideoTracks(), "Element can't have been unlinked"); if (dom::MediaTrack* t = AudioTracks()->GetTrackById(id)) {
AudioTracks()->RemoveTrack(t);
} elseif (dom::MediaTrack* t = VideoTracks()->GetTrackById(id)) {
VideoTracks()->RemoveTrack(t);
} else {
NS_ASSERTION(aTrack->AsVideoStreamTrack() && !IsVideo(), "MediaStreamTrack ended but did not exist in track lists. " "This is only allowed if a video element ends and we are an " "audio element."); return;
}
}
// Add output tracks synchronously now to be sure they're available in // "loadedmetadata" event handlers.
UpdateOutputTrackSources();
DispatchAsyncEvent(u"durationchange"_ns); if (IsVideo() && HasVideo()) {
DispatchAsyncEvent(u"resize"_ns);
Invalidate(ImageSizeChanged::No, Some(mMediaInfo.mVideo.mDisplay),
ForceInvalidate::No);
}
NS_ASSERTION(!HasVideo() || (mMediaInfo.mVideo.mDisplay.width > 0 &&
mMediaInfo.mVideo.mDisplay.height > 0), "Video resolution must be known on 'loadedmetadata'");
DispatchAsyncEvent(u"loadedmetadata"_ns);
if (mDecoder && mDecoder->IsTransportSeekable() &&
mDecoder->IsMediaSeekable()) {
ProcessMediaFragmentURI();
mDecoder->SetFragmentEndTime(mFragmentEnd);
} if (mIsEncrypted) { // We only support playback of encrypted content via MSE by default. if (!mMediaSource && Preferences::GetBool("media.eme.mse-only", true)) {
DecodeError(
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Encrypted content not supported outside of MSE")); return;
}
// Dispatch a distinct 'encrypted' event for each initData we have. for (constauto& initData : mPendingEncryptedInitData.mInitDatas) {
DispatchEncrypted(initData.mInitData, initData.mType);
}
mPendingEncryptedInitData.Reset();
}
if (IsVideo() && aInfo->HasVideo()) { // We are a video element playing video so update the screen wakelock
NotifyOwnerDocumentActivityChanged();
}
if (mDefaultPlaybackStartPosition != 0.0) {
SetCurrentTime(mDefaultPlaybackStartPosition);
mDefaultPlaybackStartPosition = 0.0;
}
void HTMLMediaElement::PlaybackEnded() { // We changed state which can affect AddRemoveSelfReference
AddRemoveSelfReference();
NS_ASSERTION(!mDecoder || mDecoder->IsEnded(), "Decoder fired ended, but not in ended state");
// IsPlaybackEnded() became true.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
if (mSrcStream) {
LOG(LogLevel::Debug,
("%p, got duration by reaching the end of the resource", this));
mSrcStreamPlaybackEnded = true;
DispatchAsyncEvent(u"durationchange"_ns);
} else { // mediacapture-main: // Setting the loop attribute has no effect since a MediaStream has no // defined end and therefore cannot be looped. if (HasAttr(nsGkAtoms::loop)) {
SetCurrentTime(0); return;
}
}
FireTimeUpdate(TimeupdateType::eMandatory);
if (!mPaused) {
Pause();
}
if (mSrcStream) { // A MediaStream that goes from inactive to active shall be eligible for // autoplay again according to the mediacapture-main spec.
mCanAutoplayFlag = true;
}
if (StaticPrefs::media_mediacontrol_stopcontrol_aftermediaends()) {
mMediaControlKeyListener->StopIfNeeded();
}
DispatchAsyncEvent(u"ended"_ns);
}
void HTMLMediaElement::SeekCompleted() {
mPlayingBeforeSeek = false;
SetPlayedOrSeeked(true); if (mTextTrackManager) {
mTextTrackManager->DidSeek();
} // https://html.spec.whatwg.org/multipage/media.html#seeking:dom-media-seek // (Step 16) // TODO (bug 1688131): run these steps in a stable state.
FireTimeUpdate(TimeupdateType::eMandatory);
DispatchAsyncEvent(u"seeked"_ns); // We changed whether we're seeking so we need to AddRemoveSelfReference
AddRemoveSelfReference(); if (mCurrentPlayRangeStart == -1.0) {
mCurrentPlayRangeStart = CurrentTime();
}
// If this is the first progress, or PROGRESS_MS has passed since the last // progress event fired and more data has arrived since then, fire a // progress event.
NS_ASSERTION(
(mProgressTime.IsNull() && !aHaveNewProgress) || !mDataTime.IsNull(), "null TimeStamp mDataTime should not be used in comparison"); if (mProgressTime.IsNull()
? aHaveNewProgress
: (now - mProgressTime >=
TimeDuration::FromMilliseconds(PROGRESS_MS) &&
mDataTime > mProgressTime)) {
DispatchAsyncEvent(u"progress"_ns); // Resolution() ensures that future data will have now > mProgressTime, // and so will trigger another event. mDataTime is not reset because it // is still required to detect stalled; it is similarly offset by // resolution to indicate the new data has not yet arrived.
mProgressTime = now - TimeDuration::Resolution(); if (mDataTime > mProgressTime) {
mDataTime = mProgressTime;
} if (!mProgressTimer) {
NS_ASSERTION(aHaveNewProgress, "timer dispatched when there was no timer"); // Were stalled. Restart timer.
StartProgressTimer(); if (!mLoadedDataFired) {
ChangeDelayLoadStatus(true);
}
} // Download statistics may have been updated, force a recheck of the // readyState.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
}
if (now - mDataTime >= TimeDuration::FromMilliseconds(STALL_MS)) { if (!mMediaSource) {
DispatchAsyncEvent(u"stalled"_ns);
} else {
ChangeDelayLoadStatus(false);
}
NS_ASSERTION(mProgressTimer, "detected stalled without timer"); // Stop timer events, which prevents repeated stalled events until there // is more progress.
StopProgress();
}
void HTMLMediaElement::StartProgress() { // Record the time now for detecting stalled.
mDataTime = TimeStamp::NowLoRes(); // Reset mProgressTime so that mDataTime is not indicating bytes received // after the last progress event.
mProgressTime = TimeStamp();
StartProgressTimer();
}
void HTMLMediaElement::StopProgress() {
MOZ_ASSERT(NS_IsMainThread()); if (!mProgressTimer) { return;
}
if (mDecoder && mReadyState < HAVE_METADATA) { // aNextFrame might have a next frame because the decoder can advance // on its own thread before MetadataLoaded gets a chance to run. // The arrival of more data can't change us out of this readyState.
LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " "Decoder ready state < HAVE_METADATA", this)); return;
}
if (mDecoder) { // IsPlaybackEnded() might have become false.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
}
if (mSrcStream && mReadyState < HAVE_METADATA) { bool hasAudioTracks = AudioTracks() && !AudioTracks()->IsEmpty(); bool hasVideoTracks = VideoTracks() && !VideoTracks()->IsEmpty(); if (!hasAudioTracks && !hasVideoTracks) {
LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " "Stream with no tracks", this)); // Give it one last chance to remove the self reference if needed.
AddRemoveSelfReference(); return;
}
if (IsVideo() && hasVideoTracks && !HasVideo()) {
LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " "Stream waiting for video", this)); return;
}
// We are playing a stream that has video and a video frame is now set. // This means we have all metadata needed to change ready state.
MediaInfo mediaInfo = mMediaInfo; if (hasAudioTracks) {
mediaInfo.EnableAudio();
} if (hasVideoTracks) {
mediaInfo.EnableVideo(); if (mSelectedVideoStreamTrack) {
mediaInfo.mVideo.SetAlpha(mSelectedVideoStreamTrack->HasAlpha());
}
}
MetadataLoaded(&mediaInfo, nullptr);
}
if (mMediaSource) { // readyState has changed, assuming it's following the pending mediasource // operations. Notify the Mediasource that the operations have completed.
mMediaSource->CompletePendingTransactions();
}
enum NextFrameStatus nextFrameStatus = NextFrameStatus(); if (mWaitingForKey == NOT_WAITING_FOR_KEY) { if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE && mDecoder &&
!mDecoder->IsEnded()) {
nextFrameStatus = mDecoder->NextFrameBufferedStatus();
}
} elseif (mWaitingForKey == WAITING_FOR_KEY) { if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE ||
nextFrameStatus == NEXT_FRAME_UNAVAILABLE_BUFFERING) { // http://w3c.github.io/encrypted-media/#wait-for-key // Continuing 7.3.4 Queue a "waitingforkey" Event // 4. Queue a task to fire a simple event named waitingforkey // at the media element. // 5. Set the readyState of media element to HAVE_METADATA. // NOTE: We'll change to HAVE_CURRENT_DATA or HAVE_METADATA // depending on whether we've loaded the first frame or not // below. // 6. Suspend playback. // Note: Playback will already be stalled, as the next frame is // unavailable.
mWaitingForKey = WAITING_FOR_KEY_DISPATCHED;
DispatchAsyncEvent(u"waitingforkey"_ns);
}
} else {
MOZ_ASSERT(mWaitingForKey == WAITING_FOR_KEY_DISPATCHED); if (nextFrameStatus == NEXT_FRAME_AVAILABLE) { // We have new frames after dispatching "waitingforkey". // This means we've got the key and can reset mWaitingForKey now.
mWaitingForKey = NOT_WAITING_FOR_KEY;
}
}
if (IsVideo() && VideoTracks() && !VideoTracks()->IsEmpty() &&
!IsPlaybackEnded() && GetImageContainer() &&
!GetImageContainer()->HasCurrentImage()) { // Don't advance if we are playing video, but don't have a video frame. // Also, if video became available after advancing to HAVE_CURRENT_DATA // while we are still playing, we need to revert to HAVE_METADATA until // a video frame is available.
LOG(LogLevel::Debug,
("MediaElement %p UpdateReadyStateInternal() " "Playing video but no video frame; Forcing HAVE_METADATA", this));
ChangeReadyState(HAVE_METADATA); return;
}
if (!mFirstFrameLoaded) { // We haven't yet loaded the first frame, making us unable to determine // if we have enough valid data at the present stage. return;
}
if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE_BUFFERING) { // Force HAVE_CURRENT_DATA when buffering.
ChangeReadyState(HAVE_CURRENT_DATA); return;
}
// TextTracks must be loaded for the HAVE_ENOUGH_DATA and // HAVE_FUTURE_DATA. // So force HAVE_CURRENT_DATA if text tracks not loaded. if (mTextTrackManager && !mTextTrackManager->IsLoaded()) {
ChangeReadyState(HAVE_CURRENT_DATA); return;
}
if (mDownloadSuspendedByCache && mDecoder && !mDecoder->IsEnded()) { // The decoder has signaled that the download has been suspended by the // media cache. So move readyState into HAVE_ENOUGH_DATA, in case there's // script waiting for a "canplaythrough" event; without this forced // transition, we will never fire the "canplaythrough" event if the // media cache is too small, and scripts are bound to fail. Don't force // this transition if the decoder is in ended state; the readyState // should remain at HAVE_CURRENT_DATA in this case. // Note that this state transition includes the case where we finished // downloaded the whole data stream.
LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " "Decoder download suspended by cache", this));
ChangeReadyState(HAVE_ENOUGH_DATA); return;
}
if (nextFrameStatus != MediaDecoderOwner::NEXT_FRAME_AVAILABLE) {
LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " "Next frame not available", this));
ChangeReadyState(HAVE_CURRENT_DATA); return;
}
// Now see if we should set HAVE_ENOUGH_DATA. // If it's something we don't know the size of, then we can't // make a real estimate, so we go straight to HAVE_ENOUGH_DATA once // we've downloaded enough data that our download rate is considered // reliable. We have to move to HAVE_ENOUGH_DATA at some point or // autoplay elements for live streams will never play. Otherwise we // move to HAVE_ENOUGH_DATA if we can play through the entire media // without stopping to buffer. if (mDecoder->CanPlayThrough()) {
LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " "Decoder can play through", this));
ChangeReadyState(HAVE_ENOUGH_DATA); return;
}
LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " "Default; Decoder has future data", this));
ChangeReadyState(HAVE_FUTURE_DATA);
}
// https://html.spec.whatwg.org/multipage/media.html#text-track-cue-active-flag // The user agent must synchronously unset cues' active flag whenever the // media element's readyState is changed back to HAVE_NOTHING. if (mReadyState == HAVE_NOTHING && mTextTrackManager) {
mTextTrackManager->NotifyReset();
}
if (mNetworkState == NETWORK_EMPTY) { return;
}
UpdateAudioChannelPlayingState();
// Handle raising of "waiting" event during seek (see 4.8.10.9) // or // 4.8.12.7 Ready states: // "If the previous ready state was HAVE_FUTURE_DATA or more, and the new // ready state is HAVE_CURRENT_DATA or less // If the media element was potentially playing before its readyState // attribute changed to a value lower than HAVE_FUTURE_DATA, and the element // has not ended playback, and playback has not stopped due to errors, // paused for user interaction, or paused for in-band content, the user agent // must queue a task to fire a simple event named timeupdate at the element, // and queue a task to fire a simple event named waiting at the element." if (mPlayingBeforeSeek && mReadyState < HAVE_FUTURE_DATA) {
DispatchAsyncEvent(u"waiting"_ns);
} elseif (oldState >= HAVE_FUTURE_DATA && mReadyState < HAVE_FUTURE_DATA &&
!Paused() && !Ended() && !mErrorSink->mError) {
FireTimeUpdate(TimeupdateType::eMandatory);
DispatchAsyncEvent(u"waiting"_ns);
}
nsMediaNetworkState oldState = mNetworkState;
mNetworkState = aState;
LOG(LogLevel::Debug,
("%p Network state changed to %s", this, gNetworkStateToString[aState]));
DDLOG(DDLogCategory::Property, "network_state",
gNetworkStateToString[aState]);
if (oldState == NETWORK_LOADING) { // Stop progress notification when exiting NETWORK_LOADING.
StopProgress();
}
if (mNetworkState == NETWORK_LOADING) { // Start progress notification when entering NETWORK_LOADING.
StartProgress();
} elseif (mNetworkState == NETWORK_IDLE && !mErrorSink->mError) { // Fire 'suspend' event when entering NETWORK_IDLE and no error presented.
DispatchAsyncEvent(u"suspend"_ns);
}
// According to the resource selection (step2, step9-18), dedicated media // source failure step (step4) and aborting existing load (step4), set show // poster flag to true. https://html.spec.whatwg.org/multipage/media.html if (mNetworkState == NETWORK_NO_SOURCE || mNetworkState == NETWORK_EMPTY) {
mShowPoster = true;
}
bool HTMLMediaElement::IsEligibleForAutoplay() { // We also activate autoplay when playing a media source since the data // download is controlled by the script and there is no way to evaluate // MediaDecoder::CanPlayThrough().
if (!HasAttr(nsGkAtoms::autoplay)) { returnfalse;
}
if (!mCanAutoplayFlag) { returnfalse;
}
if (IsEditable()) { returnfalse;
}
if (!mPaused) { returnfalse;
}
if (mSuspendedByInactiveDocOrDocshell) { returnfalse;
}
// Static document is used for print preview and printing, should not be // autoplay if (OwnerDoc()->IsStaticDocument()) { returnfalse;
}
if (ShouldBeSuspendedByInactiveDocShell()) {
LOG(LogLevel::Debug, ("%p prohibiting autoplay by the docShell", this)); returnfalse;
}
if (MediaPlaybackDelayPolicy::ShouldDelayPlayback(this)) {
CreateResumeDelayedMediaPlaybackAgentIfNeeded();
LOG(LogLevel::Debug, ("%p delay playing from autoplay", this)); returnfalse;
}
return mReadyState >= HAVE_ENOUGH_DATA;
}
void HTMLMediaElement::CheckAutoplayDataReady() { if (!IsEligibleForAutoplay()) { return;
} if (!AllowedToPlay()) {
DispatchEventsWhenPlayWasNotAllowed(); return;
}
RunAutoplay();
}
void HTMLMediaElement::RunAutoplay() {
mAllowedToPlayPromise.ResolveIfExists(true, __func__);
mPaused = false; // We changed mPaused which can affect AddRemoveSelfReference
AddRemoveSelfReference();
UpdateSrcMediaStreamPlaying();
UpdateAudioChannelPlayingState();
StartMediaControlKeyListenerIfNeeded();
// For blocked media, the event would be pending until it is resumed.
DispatchAsyncEvent(u"play"_ns);
DispatchAsyncEvent(u"playing"_ns);
}
bool HTMLMediaElement::IsActuallyInvisible() const { // That means an element is not connected. It probably hasn't connected to a // document tree, or connects to a disconnected DOM tree. if (!IsInComposedDoc()) { returntrue;
}
// An element is not in user's view port, which means it's either existing in // somewhere in the page where user hasn't seen yet, or is being set // `display:none`. if (!IsInViewPort()) { returntrue;
}
// Element being used in picture-in-picture mode would be always visible. if (IsBeingUsedInPictureInPictureMode()) { returnfalse;
}
// That check is the page is in the background. return OwnerDoc()->Hidden();
}
LOG(LogLevel::Debug,
("HTMLMediaElement %p video track principal changed to %p (combined " "into %p). Waiting for it to reach VideoFrameContainer before setting.", this, aTrack->GetPrincipal(), mSrcStreamVideoPrincipal.get()));
if (mVideoFrameContainer) {
UpdateSrcStreamVideoPrincipal(
mVideoFrameContainer->GetLastPrincipalHandle());
}
}
for (const RefPtr<VideoStreamTrack>& track : videoTracks) { if (PrincipalHandleMatches(aPrincipalHandle, track->GetPrincipal()) &&
!track->Ended()) { // When the PrincipalHandle for the VideoFrameContainer changes to that of // a live track in mSrcStream we know that a removed track was displayed // but is no longer so.
LOG(LogLevel::Debug, ("HTMLMediaElement %p VideoFrameContainer's " "PrincipalHandle matches track %p. That's all we " "need.", this, track.get()));
mSrcStreamVideoPrincipal = track->GetPrincipal(); break;
}
}
}
bool HTMLMediaElement::IsPotentiallyPlaying() const { // TODO: // playback has not stopped due to errors, // and the element has not paused for user interaction return !mPaused &&
(mReadyState == HAVE_ENOUGH_DATA || mReadyState == HAVE_FUTURE_DATA) &&
!IsPlaybackEnded();
}
bool HTMLMediaElement::IsPlaybackEnded() const { // TODO: // the current playback position is equal to the effective end of the media // resource. See bug 449157. if (mDecoder) { return mReadyState >= HAVE_METADATA && mDecoder->IsEnded();
} if (mSrcStream) { return mReadyState >= HAVE_METADATA && mSrcStreamPlaybackEnded;
} returnfalse;
}
already_AddRefed<nsIPrincipal> HTMLMediaElement::GetCurrentPrincipal() { if (mDecoder) { return mDecoder->GetCurrentPrincipal();
} if (mSrcStream) {
nsTArray<RefPtr<MediaStreamTrack>> tracks;
mSrcStream->GetTracks(tracks);
nsCOMPtr<nsIPrincipal> principal = mSrcStream->GetPrincipal(); return principal.forget();
} return nullptr;
}
if (aSuspendElement) { if (mDecoder) {
mDecoder->Pause();
mDecoder->Suspend();
mDecoder->SetDelaySeekMode(true);
}
mEventBlocker->SetBlockEventDelivery(true); // We won't want to resume media element from the bfcache.
ClearResumeDelayedMediaPlaybackAgentIfNeeded();
mMediaControlKeyListener->StopIfNeeded();
} else { if (mDecoder) {
mDecoder->Resume(); if (!mPaused && !mDecoder->IsEnded()) {
mDecoder->Play();
}
mDecoder->SetDelaySeekMode(false);
}
mEventBlocker->SetBlockEventDelivery(false); // If the media element has been blocked and isn't still allowed to play // when it comes back from the bfcache, we would notify front end to show // the blocking icon in order to inform user that the site is still being // blocked. if (mHasEverBeenBlockedForAutoplay && !AllowedToPlay()) {
MaybeNotifyAutoplayBlocked();
}
StartMediaControlKeyListenerIfNeeded();
} if (StaticPrefs::media_testing_only_events()) { auto dispatcher = MakeRefPtr<AsyncEventDispatcher>( this, u"MozMediaSuspendChanged"_ns, CanBubble::eYes,
ChromeOnlyDispatch::eYes);
dispatcher->PostDOMEvent();
}
}
bool HTMLMediaElement::ShouldBeSuspendedByInactiveDocShell() const {
BrowsingContext* bc = OwnerDoc()->GetBrowsingContext(); return bc && !bc->IsActive() && bc->Top()->GetSuspendMediaWhenInactive();
}
void HTMLMediaElement::NotifyOwnerDocumentActivityChanged() { if (mDecoder && !IsBeingDestroyed()) {
NotifyDecoderActivityChanges();
}
// We would suspend media when the document is inactive, or its docshell has // been set to hidden and explicitly wants to suspend media. In those cases, // the media would be not visible and we don't want them to continue playing. bool shouldSuspend =
!OwnerDoc()->IsActive() || ShouldBeSuspendedByInactiveDocShell();
SuspendOrResumeElement(shouldSuspend);
// If the owning document has become inactive we should shutdown the CDM. if (!OwnerDoc()->IsCurrentActiveDocument() && mMediaKeys) { // We don't shutdown MediaKeys here because it also listens for document // activity and will take care of shutting down itself.
DDUNLINKCHILD(mMediaKeys.get());
mMediaKeys = nullptr; if (mDecoder) {
ShutdownDecoder();
}
}
AddRemoveSelfReference();
}
void HTMLMediaElement::NotifyFullScreenChanged() { constbool isInFullScreen = IsInFullScreen(); if (isInFullScreen) {
StartMediaControlKeyListenerIfNeeded(); if (!mMediaControlKeyListener->IsStarted()) {
MEDIACONTROL_LOG("Failed to start the listener when entering fullscreen");
}
} // Updating controller fullscreen state no matter the listener starts or not.
BrowsingContext* bc = OwnerDoc()->GetBrowsingContext(); if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(bc)) {
updater->NotifyMediaFullScreenState(bc->Id(), isInFullScreen);
}
}
void HTMLMediaElement::AddRemoveSelfReference() { // XXX we could release earlier here in many situations if we examined // which event listeners are attached. Right now we assume there is a // potential listener for every event. We would also have to keep the // element alive if it was playing and producing audio output --- right now // that's covered by the !mPaused check.
Document* ownerDoc = OwnerDoc();
// See the comment at the top of this file for the explanation of this // boolean expression. bool needSelfReference =
!mShuttingDown && ownerDoc->IsActive() &&
(mDelayingLoadEvent || (!mPaused && !Ended()) ||
(mDecoder && mDecoder->IsSeeking()) || IsEligibleForAutoplay() ||
(mMediaSource ? mProgressTimer : mNetworkState == NETWORK_LOADING));
if (needSelfReference != mHasSelfReference) {
mHasSelfReference = needSelfReference;
RefPtr<HTMLMediaElement> self = this; if (needSelfReference) { // The shutdown observer will hold a strong reference to us. This // will do to keep us alive. We need to know about shutdown so that // we can release our self-reference.
GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( "dom::HTMLMediaElement::AddSelfReference",
[self]() { self->mShutdownObserver->AddRefMediaElement(); }));
} else { // Dispatch Release asynchronously so that we don't destroy this object // inside a call stack of method calls on this object
GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( "dom::HTMLMediaElement::AddSelfReference",
[self]() { self->mShutdownObserver->ReleaseMediaElement(); }));
}
}
}
nsCOMPtr<nsIRunnable> event = new nsSourceErrorEventRunner(this, aSourceElement, aErrorDetails);
GetMainThreadSerialEventTarget()->Dispatch(event.forget());
}
void HTMLMediaElement::NotifyAddedSource() { // If a source element is inserted as a child of a media element // that has no src attribute and whose networkState has the value // NETWORK_EMPTY, the user agent must invoke the media element's // resource selection algorithm. if (!HasAttr(nsGkAtoms::src) && mNetworkState == NETWORK_EMPTY) {
AssertReadyStateIsNothing();
QueueSelectResourceTask();
}
// A load was paused in the resource selection algorithm, waiting for // a new source child to be added, resume the resource selection algorithm. if (mLoadWaitStatus == WAITING_FOR_SOURCE) { // Rest the flag so we don't queue multiple LoadFromSourceTask() when // multiple <source> are attached in an event loop.
mLoadWaitStatus = NOT_WAITING;
QueueLoadFromSourceTask();
}
}
// If child is a <source> element, it is the next candidate. if (auto* source = HTMLSourceElement::FromNodeOrNull(child)) {
mSourceLoadCandidate = source; return source;
}
}
MOZ_ASSERT_UNREACHABLE("Execution should not reach here!"); return nullptr;
}
void HTMLMediaElement::ChangeDelayLoadStatus(bool aDelay) { if (mDelayingLoadEvent == aDelay) return;
mDelayingLoadEvent = aDelay;
LOG(LogLevel::Debug, ("%p ChangeDelayLoadStatus(%d) doc=0x%p", this, aDelay,
mLoadBlockedDoc.get())); if (mDecoder) {
mDecoder->SetLoadInBackground(!aDelay);
} if (aDelay) {
mLoadBlockedDoc = OwnerDoc();
mLoadBlockedDoc->BlockOnload();
} else { // mLoadBlockedDoc might be null due to GC unlinking if (mLoadBlockedDoc) {
mLoadBlockedDoc->UnblockOnload(false);
mLoadBlockedDoc = nullptr;
}
}
// We changed mDelayingLoadEvent which can affect AddRemoveSelfReference
AddRemoveSelfReference();
}
already_AddRefed<nsILoadGroup> HTMLMediaElement::GetDocumentLoadGroup() { if (!OwnerDoc()->IsActive()) {
NS_WARNING("Load group requested for media element in inactive document.");
} return OwnerDoc()->GetDocumentLoadGroup();
}
void HTMLMediaElement::SetRequestHeaders(nsIHttpChannel* aChannel) { // Send Accept header for video and audio types only (Bug 489071)
SetAcceptHeader(aChannel);
// Apache doesn't send Content-Length when gzip transfer encoding is used, // which prevents us from estimating the video length (if explicit // Content-Duration and a length spec in the container are not present either) // and from seeking. So, disable the standard "Accept-Encoding: gzip,deflate" // that we usually send. See bug 614760.
DebugOnly<nsresult> rv =
aChannel->SetRequestHeader("Accept-Encoding"_ns, ""_ns, false);
MOZ_ASSERT(NS_SUCCEEDED(rv));
// Set the Referrer header // // FIXME: Shouldn't this use the Element constructor? Though I guess it // doesn't matter as no HTMLMediaElement supports the referrerinfo attribute. auto referrerInfo = MakeRefPtr<ReferrerInfo>(*OwnerDoc());
rv = aChannel->SetReferrerInfoWithoutClone(referrerInfo);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
bool HTMLMediaElement::ShouldQueueTimeupdateAsyncTask(
TimeupdateType aType) const {
NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); // That means dispatching `timeupdate` is mandatorily required in the spec. if (aType == TimeupdateType::eMandatory) { returntrue;
}
// Number of milliseconds between timeupdate events as defined by spec. if (!mQueueTimeUpdateRunnerTime.IsNull() &&
TimeStamp::Now() - mQueueTimeUpdateRunnerTime <
TimeDuration::FromMilliseconds(TIMEUPDATE_MS)) { returnfalse;
} returntrue;
}
void HTMLMediaElement::FireTimeUpdate(TimeupdateType aType) {
NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
// Update the cues displaying on the video. // Here mTextTrackManager can be null if the cycle collector has unlinked // us before our parent. In that case UnbindFromTree will call us // when our parent is unlinked. if (mTextTrackManager) {
mTextTrackManager->TimeMarchesOn();
}
}
void HTMLMediaElement::GetCurrentSpec(nsCString& aString) { // If playing a regular URL, an ObjectURL of a Blob/File, return that. if (mLoadingSrc) {
mLoadingSrc->GetSpec(aString);
} elseif (mSrcMediaSource) { // If playing an ObjectURL, and it's a MediaSource, return the value of the // `src` attribute.
nsAutoString src;
GetSrc(src);
CopyUTF16toUTF8(src, aString);
} else { // Playing e.g. a MediaStream via an object URL - return an empty string
aString.Truncate();
}
}
// If there is no end fragment, or the fragment end is greater than the // duration, return the duration. return (mFragmentEnd < 0.0 || mFragmentEnd > duration) ? duration
: mFragmentEnd;
}
// Changing the playback rate of a media that has more than two channels is // not supported. if (aPlaybackRate < 0) {
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return;
}
void HTMLMediaElement::RemoveMediaKeys() {
LOG(LogLevel::Debug, ("%s", __func__)); // 5.2.3 Stop using the CDM instance represented by the mediaKeys attribute // to decrypt media data and remove the association with the media element. if (mMediaKeys) {
mMediaKeys->Unbind();
}
mMediaKeys = nullptr;
}
bool HTMLMediaElement::TryRemoveMediaKeysAssociation() {
MOZ_ASSERT(mMediaKeys);
LOG(LogLevel::Debug, ("%s", __func__)); // 5.2.1 If the user agent or CDM do not support removing the association, // let this object's attaching media keys value be false and reject promise // with a new DOMException whose name is NotSupportedError. // 5.2.2 If the association cannot currently be removed, let this object's // attaching media keys value be false and reject promise with a new // DOMException whose name is InvalidStateError. if (mDecoder) {
RefPtr<HTMLMediaElement> self = this;
mDecoder->SetCDMProxy(nullptr)
->Then(
AbstractMainThread(), __func__,
[self]() {
self->mSetCDMRequest.Complete();
self->RemoveMediaKeys(); if (self->AttachNewMediaKeys()) { // No incoming MediaKeys object or MediaDecoder is not // created yet.
self->MakeAssociationWithCDMResolved();
}
},
[self](const MediaResult& aResult) {
self->mSetCDMRequest.Complete(); // 5.2.4 If the preceding step failed, let this object's // attaching media keys value be false and reject promise with // a new DOMException whose name is the appropriate error name.
self->SetCDMProxyFailure(aResult);
})
->Track(mSetCDMRequest); returnfalse;
}
RemoveMediaKeys(); returntrue;
}
bool HTMLMediaElement::DetachExistingMediaKeys() {
LOG(LogLevel::Debug, ("%s", __func__));
MOZ_ASSERT(mSetMediaKeysDOMPromise); // 5.1 If mediaKeys is not null, CDM instance represented by mediaKeys is // already in use by another media element, and the user agent is unable // to use it with this element, let this object's attaching media keys // value be false and reject promise with a new DOMException whose name // is QuotaExceededError. if (mIncomingMediaKeys && mIncomingMediaKeys->IsBoundToMediaElement()) {
SetCDMProxyFailure(MediaResult(
NS_ERROR_DOM_MEDIA_KEY_QUOTA_EXCEEDED_ERR, "MediaKeys object is already bound to another HTMLMediaElement")); returnfalse;
}
// 5.2 If the mediaKeys attribute is not null, run the following steps: if (mMediaKeys) { return TryRemoveMediaKeysAssociation();
} returntrue;
}
// 5.4 Set the mediaKeys attribute to mediaKeys.
mMediaKeys = mIncomingMediaKeys; #ifdef MOZ_WMF_CDM if (mMediaKeys && mMediaKeys->GetCDMProxy()) {
mIsUsingWMFCDM = !!mMediaKeys->GetCDMProxy()->AsWMFCDMProxy();
} #endif // 5.5 Let this object's attaching media keys value be false.
ResetSetMediaKeysTempVariables(); // 5.6 Resolve promise.
mSetMediaKeysDOMPromise->MaybeResolveWithUndefined();
mSetMediaKeysDOMPromise = nullptr;
// 5.3.3 Queue a task to run the "Attempt to Resume Playback If Necessary" // algorithm on the media element. // Note: Setting the CDMProxy on the MediaDecoder will unblock playback. if (mDecoder) { // CDMProxy is set asynchronously in MediaFormatReader, once it's done, // HTMLMediaElement should resolve or reject the DOM promise.
RefPtr<HTMLMediaElement> self = this;
mDecoder->SetCDMProxy(aProxy)
->Then(
AbstractMainThread(), __func__,
[self]() {
self->mSetCDMRequest.Complete();
self->MakeAssociationWithCDMResolved();
},
[self](const MediaResult& aResult) {
self->mSetCDMRequest.Complete();
self->SetCDMProxyFailure(aResult);
})
->Track(mSetCDMRequest); returnfalse;
} returntrue;
}
// 5.3. If mediaKeys is not null, run the following steps: if (mIncomingMediaKeys) { auto* cdmProxy = mIncomingMediaKeys->GetCDMProxy(); if (!cdmProxy) {
SetCDMProxyFailure(MediaResult(
NS_ERROR_DOM_INVALID_STATE_ERR, "CDM crashed before binding MediaKeys object to HTMLMediaElement")); returnfalse;
}
// 5.3.1 Associate the CDM instance represented by mediaKeys with the // media element for decrypting media data. if (NS_FAILED(mIncomingMediaKeys->Bind(this))) { // 5.3.2 If the preceding step failed, run the following steps:
// 5.3.2.1 Set the mediaKeys attribute to null.
mMediaKeys = nullptr; // 5.3.2.2 Let this object's attaching media keys value be false. // 5.3.2.3 Reject promise with a new DOMException whose name is // the appropriate error name.
SetCDMProxyFailure(
MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR, "Failed to bind MediaKeys object to HTMLMediaElement")); returnfalse;
} return TryMakeAssociationWithCDM(cdmProxy);
} returntrue;
}
// 1. If mediaKeys and the mediaKeys attribute are the same object, // return a resolved promise. if (mMediaKeys == aMediaKeys) {
promise->MaybeResolveWithUndefined(); return promise.forget();
}
// 2. If this object's attaching media keys value is true, return a // promise rejected with a new DOMException whose name is InvalidStateError. if (mAttachingMediaKey) {
promise->MaybeRejectWithInvalidStateError( "A MediaKeys object is in attaching operation."); return promise.forget();
}
// 3. Let this object's attaching media keys value be true.
mAttachingMediaKey = true;
mIncomingMediaKeys = aMediaKeys;
// 4. Let promise be a new promise.
mSetMediaKeysDOMPromise = promise;
if (mReadyState == HAVE_NOTHING) { // Ready state not HAVE_METADATA (yet), don't dispatch encrypted now. // Queueing for later dispatch in MetadataLoaded.
mPendingEncryptedInitData.AddInitData(aInitDataType, aInitData); return;
}
// http://w3c.github.io/encrypted-media/#wait-for-key // 7.3.4 Queue a "waitingforkey" Event // 1. Let the media element be the specified HTMLMediaElement object. // 2. If the media element's waiting for key value is true, abort these steps. if (mWaitingForKey == NOT_WAITING_FOR_KEY) { // 3. Set the media element's waiting for key value to true. // Note: algorithm continues in UpdateReadyStateInternal() when all decoded // data enqueued in the MDSM is consumed.
mWaitingForKey = WAITING_FOR_KEY; // mWaitingForKey changed outside of UpdateReadyStateInternal. This may // affect mReadyState.
mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
}
}
bool HTMLMediaElement::IsCurrentlyPlaying() const { // We have playable data, but we still need to check whether data is "real" // current data. return mReadyState >= HAVE_CURRENT_DATA && !IsPlaybackEnded();
}
void HTMLMediaElement::NotifyAudioPlaybackChanged(
AudibleChangedReasons aReason) { if (mAudioChannelWrapper) {
mAudioChannelWrapper->NotifyAudioPlaybackChanged(aReason);
} // We would start the listener after media becomes audible. constbool isAudible = IsAudible(); if (isAudible && !mMediaControlKeyListener->IsStarted()) {
StartMediaControlKeyListenerIfNeeded();
}
mMediaControlKeyListener->UpdateMediaAudibleState(isAudible); // only request wake lock for audible media.
UpdateWakeLock();
}
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(u"playing"_ns);
}
void HTMLMediaElement::GetEMEInfo(dom::EMEDebugInfo& aInfo) {
MOZ_ASSERT(NS_IsMainThread(), "MediaKeys expects to be interacted with on main thread!"); if (!mMediaKeys) { return;
}
mMediaKeys->GetKeySystem(aInfo.mKeySystem);
mMediaKeys->GetSessionsInfo(aInfo.mSessionsInfo);
}
if (!FeaturePolicyUtils::IsFeatureAllowed(win->GetExtantDoc(),
u"speaker-selection"_ns)) {
promise->MaybeRejectWithNotAllowedError( "Document's Permissions Policy does not allow setSinkId()");
}
if (mSink.first.Equals(aSinkId)) {
promise->MaybeResolveWithUndefined(); return promise.forget();
}
RefPtr<MediaDevices> mediaDevices = win->Navigator()->GetMediaDevices(aRv); if (aRv.Failed()) { return nullptr;
}
nsString sinkId(aSinkId);
mediaDevices->GetSinkDevice(sinkId)
->Then(
AbstractMainThread(), __func__,
[self = RefPtr<HTMLMediaElement>(this), this](RefPtr<AudioDeviceInfo>&& aInfo) { // Sink found switch output device.
MOZ_ASSERT(aInfo); if (mDecoder) {
RefPtr<SinkInfoPromise> p = mDecoder->SetSink(aInfo)->Then(
AbstractMainThread(), __func__,
[aInfo](const GenericPromise::ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) { return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
} return SinkInfoPromise::CreateAndReject(
aValue.RejectValue(), __func__);
}); return p;
} if (mSrcStream) {
MOZ_ASSERT(mMediaStreamRenderer);
RefPtr<SinkInfoPromise> p =
mMediaStreamRenderer->SetAudioOutputDevice(aInfo)->Then(
AbstractMainThread(), __func__,
[aInfo]( const GenericPromise::ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) { return SinkInfoPromise::CreateAndResolve(aInfo,
__func__);
} return SinkInfoPromise::CreateAndReject(
aValue.RejectValue(), __func__);
}); return p;
} // 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(AbstractMainThread(), __func__,
[promise, self = RefPtr<HTMLMediaElement>(this), this,
sinkId](const SinkInfoPromise::ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) {
LOG(LogLevel::Info, ("%p, set sinkid=%s", this,
NS_ConvertUTF16toUTF8(sinkId).get()));
mSink = std::pair(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: {
promise->MaybeRejectWithNotFoundError( "The object can not be found here."); break;
} default:
MOZ_ASSERT_UNREACHABLE("Invalid error.");
}
}
});
bool HTMLMediaElement::ShouldStartMediaControlKeyListener() const { if (!IsPlayable()) {
MEDIACONTROL_LOG("Not start listener because media is not playable"); returnfalse;
}
if (mSrcStream) {
MEDIACONTROL_LOG("Not listening because media is real-time"); returnfalse;
}
if (IsBeingUsedInPictureInPictureMode()) {
MEDIACONTROL_LOG("Start listener because of being used in PiP mode"); returntrue;
}
if (IsInFullScreen()) {
MEDIACONTROL_LOG("Start listener because of being used in fullscreen"); returntrue;
}
// In order to filter out notification-ish sound, we use this pref to set the // eligible media duration to prevent showing media control for those short // sound. if (Duration() <
StaticPrefs::media_mediacontrol_eligible_media_duration_s()) {
MEDIACONTROL_LOG("Not listening because media's duration %f is too short.",
Duration()); returnfalse;
}
// This includes cases such like `video is muted`, `video has zero volume`, // `video's audio track is still inaudible` and `tab is muted by audio channel // (tab sound indicator)`, all these cases would make media inaudible. // `ComputedVolume()` would return the final volume applied the affection made // by audio channel, which is used to detect if the tab is muted by audio // channel. if (!IsAudible() || ComputedVolume() == 0.0f) {
MEDIACONTROL_LOG("Not listening because media is inaudible"); returnfalse;
} returntrue;
}
void HTMLMediaElement::StartMediaControlKeyListenerIfNeeded() { if (!ShouldStartMediaControlKeyListener()) { return;
}
mMediaControlKeyListener->Start();
}
void HTMLMediaElement::UpdateMediaControlAfterPictureInPictureModeChanged() { if (IsBeingUsedInPictureInPictureMode()) { // When media enters PIP mode, we should ensure that the listener has been // started because we always want to control PIP video.
StartMediaControlKeyListenerIfNeeded(); if (!mMediaControlKeyListener->IsStarted()) {
MEDIACONTROL_LOG("Failed to start listener when entering PIP mode");
} // Updating controller PIP state no matter the listener starts or not.
mMediaControlKeyListener->SetPictureInPictureModeEnabled(true);
} else {
mMediaControlKeyListener->SetPictureInPictureModeEnabled(false);
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.