1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/dom/HTMLVideoElement.h"
9 #include "mozilla/AsyncEventDispatcher.h"
10 #include "mozilla/dom/HTMLVideoElementBinding.h"
11 #include "nsGenericHTMLElement.h"
12 #include "nsGkAtoms.h"
15 #include "nsIHttpChannel.h"
16 #include "nsNodeInfoManager.h"
19 #include "nsRFPService.h"
20 #include "nsThreadUtils.h"
21 #include "ImageContainer.h"
22 #include "VideoFrameContainer.h"
23 #include "VideoOutput.h"
25 #include "FrameStatistics.h"
26 #include "MediaError.h"
27 #include "MediaDecoder.h"
28 #include "MediaDecoderStateMachine.h"
29 #include "mozilla/Preferences.h"
30 #include "mozilla/dom/WakeLock.h"
31 #include "mozilla/dom/power/PowerManagerService.h"
32 #include "mozilla/dom/Performance.h"
33 #include "mozilla/dom/TimeRanges.h"
34 #include "mozilla/dom/VideoPlaybackQuality.h"
35 #include "mozilla/dom/VideoStreamTrack.h"
36 #include "mozilla/StaticPrefs_media.h"
37 #include "mozilla/Unused.h"
42 nsGenericHTMLElement
* NS_NewHTMLVideoElement(
43 already_AddRefed
<mozilla::dom::NodeInfo
>&& aNodeInfo
,
44 mozilla::dom::FromParser aFromParser
) {
45 RefPtr
<mozilla::dom::NodeInfo
> nodeInfo(aNodeInfo
);
46 auto* nim
= nodeInfo
->NodeInfoManager();
47 mozilla::dom::HTMLVideoElement
* element
=
48 new (nim
) mozilla::dom::HTMLVideoElement(nodeInfo
.forget());
53 namespace mozilla::dom
{
55 nsresult
HTMLVideoElement::Clone(mozilla::dom::NodeInfo
* aNodeInfo
,
56 nsINode
** aResult
) const {
58 RefPtr
<mozilla::dom::NodeInfo
> ni(aNodeInfo
);
59 auto* nim
= ni
->NodeInfoManager();
60 HTMLVideoElement
* it
= new (nim
) HTMLVideoElement(ni
.forget());
62 nsCOMPtr
<nsINode
> kungFuDeathGrip
= it
;
63 nsresult rv
= const_cast<HTMLVideoElement
*>(this)->CopyInnerTo(it
);
64 if (NS_SUCCEEDED(rv
)) {
65 kungFuDeathGrip
.swap(*aResult
);
70 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLVideoElement
,
73 NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLVideoElement
)
75 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HTMLVideoElement
)
76 NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneTarget
)
77 NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneTargetPromise
)
78 NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneSource
)
79 tmp
->mSecondaryVideoOutput
= nullptr;
80 NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(HTMLMediaElement
)
82 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLVideoElement
,
84 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneTarget
)
85 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneTargetPromise
)
86 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneSource
)
87 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
89 HTMLVideoElement::HTMLVideoElement(already_AddRefed
<NodeInfo
>&& aNodeInfo
)
90 : HTMLMediaElement(std::move(aNodeInfo
)),
91 mIsOrientationLocked(false),
92 mVideoWatchManager(this, mAbstractMainThread
) {
93 DecoderDoctorLogger::LogConstruction(this);
96 HTMLVideoElement::~HTMLVideoElement() {
97 mVideoWatchManager
.Shutdown();
98 DecoderDoctorLogger::LogDestruction(this);
101 void HTMLVideoElement::UpdateMediaSize(const nsIntSize
& aSize
) {
102 HTMLMediaElement::UpdateMediaSize(aSize
);
103 // If we have a clone target, we should update its size as well.
104 if (mVisualCloneTarget
) {
105 Maybe
<nsIntSize
> newSize
= Some(aSize
);
106 mVisualCloneTarget
->Invalidate(true, newSize
, true);
110 Maybe
<CSSIntSize
> HTMLVideoElement::GetVideoSize() const {
111 if (!mMediaInfo
.HasVideo()) {
120 switch (mMediaInfo
.mVideo
.mRotation
) {
121 case VideoInfo::Rotation::kDegree_90
:
122 case VideoInfo::Rotation::kDegree_270
: {
123 size
.width
= mMediaInfo
.mVideo
.mDisplay
.height
;
124 size
.height
= mMediaInfo
.mVideo
.mDisplay
.width
;
127 case VideoInfo::Rotation::kDegree_0
:
128 case VideoInfo::Rotation::kDegree_180
:
130 size
.height
= mMediaInfo
.mVideo
.mDisplay
.height
;
131 size
.width
= mMediaInfo
.mVideo
.mDisplay
.width
;
138 void HTMLVideoElement::Invalidate(bool aImageSizeChanged
,
139 Maybe
<nsIntSize
>& aNewIntrinsicSize
,
140 bool aForceInvalidate
) {
141 HTMLMediaElement::Invalidate(aImageSizeChanged
, aNewIntrinsicSize
,
143 if (mVisualCloneTarget
) {
144 VideoFrameContainer
* container
=
145 mVisualCloneTarget
->GetVideoFrameContainer();
147 container
->Invalidate();
152 bool HTMLVideoElement::ParseAttribute(int32_t aNamespaceID
, nsAtom
* aAttribute
,
153 const nsAString
& aValue
,
154 nsIPrincipal
* aMaybeScriptedPrincipal
,
155 nsAttrValue
& aResult
) {
156 if (aAttribute
== nsGkAtoms::width
|| aAttribute
== nsGkAtoms::height
) {
157 return aResult
.ParseHTMLDimension(aValue
);
160 return HTMLMediaElement::ParseAttribute(aNamespaceID
, aAttribute
, aValue
,
161 aMaybeScriptedPrincipal
, aResult
);
164 void HTMLVideoElement::MapAttributesIntoRule(
165 const nsMappedAttributes
* aAttributes
, MappedDeclarations
& aDecls
) {
166 nsGenericHTMLElement::MapImageSizeAttributesInto(aAttributes
, aDecls
,
167 MapAspectRatio::Yes
);
168 nsGenericHTMLElement::MapCommonAttributesInto(aAttributes
, aDecls
);
172 HTMLVideoElement::IsAttributeMapped(const nsAtom
* aAttribute
) const {
173 static const MappedAttributeEntry attributes
[] = {
174 {nsGkAtoms::width
}, {nsGkAtoms::height
}, {nullptr}};
176 static const MappedAttributeEntry
* const map
[] = {attributes
,
177 sCommonAttributeMap
};
179 return FindAttributeDependence(aAttribute
, map
);
182 nsMapRuleToAttributesFunc
HTMLVideoElement::GetAttributeMappingFunction()
184 return &MapAttributesIntoRule
;
187 void HTMLVideoElement::UnbindFromTree(bool aNullParent
) {
188 if (mVisualCloneSource
) {
189 mVisualCloneSource
->EndCloningVisually();
190 } else if (mVisualCloneTarget
) {
191 RefPtr
<AsyncEventDispatcher
> asyncDispatcher
=
192 new AsyncEventDispatcher(this, u
"MozStopPictureInPicture"_ns
,
193 CanBubble::eNo
, ChromeOnlyDispatch::eYes
);
194 asyncDispatcher
->RunDOMEventWhenSafe();
196 EndCloningVisually();
199 HTMLMediaElement::UnbindFromTree(aNullParent
);
202 nsresult
HTMLVideoElement::SetAcceptHeader(nsIHttpChannel
* aChannel
) {
207 "application/ogg;q=0.7,"
208 "audio/*;q=0.6,*/*;q=0.5");
210 return aChannel
->SetRequestHeader("Accept"_ns
, value
, false);
213 bool HTMLVideoElement::IsInteractiveHTMLContent() const {
214 return HasAttr(kNameSpaceID_None
, nsGkAtoms::controls
) ||
215 HTMLMediaElement::IsInteractiveHTMLContent();
218 gfx::IntSize
HTMLVideoElement::GetVideoIntrinsicDimensions() {
219 layers::ImageContainer
* container
= GetImageContainer();
220 // Prefer the size of the container as it's more up to date.
221 if (container
&& container
->GetCurrentSize().width
!= 0) {
222 // But adjust to the aspect ratio of the container.
223 float dar
= static_cast<float>(mMediaInfo
.mVideo
.mDisplay
.width
) /
224 mMediaInfo
.mVideo
.mDisplay
.height
;
225 gfx::IntSize size
= container
->GetCurrentSize();
226 float imageDar
= static_cast<float>(size
.width
) / size
.height
;
227 return gfx::IntSize(int(size
.width
* (dar
/ imageDar
)), size
.height
);
229 return mMediaInfo
.mVideo
.mDisplay
;
232 uint32_t HTMLVideoElement::VideoWidth() {
233 if (!mMediaInfo
.HasVideo()) {
236 gfx::IntSize size
= GetVideoIntrinsicDimensions();
237 if (mMediaInfo
.mVideo
.mRotation
== VideoInfo::Rotation::kDegree_90
||
238 mMediaInfo
.mVideo
.mRotation
== VideoInfo::Rotation::kDegree_270
) {
244 uint32_t HTMLVideoElement::VideoHeight() {
245 if (!mMediaInfo
.HasVideo()) {
248 gfx::IntSize size
= GetVideoIntrinsicDimensions();
249 if (mMediaInfo
.mVideo
.mRotation
== VideoInfo::Rotation::kDegree_90
||
250 mMediaInfo
.mVideo
.mRotation
== VideoInfo::Rotation::kDegree_270
) {
256 uint32_t HTMLVideoElement::MozParsedFrames() const {
257 MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
258 if (!IsVideoStatsEnabled()) {
262 if (OwnerDoc()->ShouldResistFingerprinting()) {
263 return nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
266 return mDecoder
? mDecoder
->GetFrameStatistics().GetParsedFrames() : 0;
269 uint32_t HTMLVideoElement::MozDecodedFrames() const {
270 MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
271 if (!IsVideoStatsEnabled()) {
275 if (OwnerDoc()->ShouldResistFingerprinting()) {
276 return nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
279 return mDecoder
? mDecoder
->GetFrameStatistics().GetDecodedFrames() : 0;
282 uint32_t HTMLVideoElement::MozPresentedFrames() {
283 MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
284 if (!IsVideoStatsEnabled()) {
288 if (OwnerDoc()->ShouldResistFingerprinting()) {
289 return nsRFPService::GetSpoofedPresentedFrames(TotalPlayTime(),
290 VideoWidth(), VideoHeight());
293 return mDecoder
? mDecoder
->GetFrameStatistics().GetPresentedFrames() : 0;
296 uint32_t HTMLVideoElement::MozPaintedFrames() {
297 MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
298 if (!IsVideoStatsEnabled()) {
302 if (OwnerDoc()->ShouldResistFingerprinting()) {
303 return nsRFPService::GetSpoofedPresentedFrames(TotalPlayTime(),
304 VideoWidth(), VideoHeight());
307 layers::ImageContainer
* container
= GetImageContainer();
308 return container
? container
->GetPaintCount() : 0;
311 double HTMLVideoElement::MozFrameDelay() {
312 MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
314 if (!IsVideoStatsEnabled() || OwnerDoc()->ShouldResistFingerprinting()) {
318 VideoFrameContainer
* container
= GetVideoFrameContainer();
319 // Hide negative delays. Frame timing tweaks in the compositor (e.g.
320 // adding a bias value to prevent multiple dropped/duped frames when
321 // frame times are aligned with composition times) may produce apparent
322 // negative delay, but we shouldn't report that.
323 return container
? std::max(0.0, container
->GetFrameDelay()) : 0.0;
326 bool HTMLVideoElement::MozHasAudio() const {
327 MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
331 JSObject
* HTMLVideoElement::WrapNode(JSContext
* aCx
,
332 JS::Handle
<JSObject
*> aGivenProto
) {
333 return HTMLVideoElement_Binding::Wrap(aCx
, this, aGivenProto
);
336 already_AddRefed
<VideoPlaybackQuality
>
337 HTMLVideoElement::GetVideoPlaybackQuality() {
338 DOMHighResTimeStamp creationTime
= 0;
339 uint32_t totalFrames
= 0;
340 uint32_t droppedFrames
= 0;
342 if (IsVideoStatsEnabled()) {
343 if (nsPIDOMWindowInner
* window
= OwnerDoc()->GetInnerWindow()) {
344 Performance
* perf
= window
->GetPerformance();
346 creationTime
= perf
->Now();
351 if (OwnerDoc()->ShouldResistFingerprinting()) {
352 totalFrames
= nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
353 droppedFrames
= nsRFPService::GetSpoofedDroppedFrames(
354 TotalPlayTime(), VideoWidth(), VideoHeight());
356 FrameStatistics
* stats
= &mDecoder
->GetFrameStatistics();
357 if (sizeof(totalFrames
) >= sizeof(stats
->GetParsedFrames())) {
358 totalFrames
= stats
->GetTotalFrames();
359 droppedFrames
= stats
->GetDroppedFrames();
361 uint64_t total
= stats
->GetTotalFrames();
362 const auto maxNumber
= std::numeric_limits
<uint32_t>::max();
363 if (total
<= maxNumber
) {
364 totalFrames
= uint32_t(total
);
365 droppedFrames
= uint32_t(stats
->GetDroppedFrames());
367 // Too big number(s) -> Resize everything to fit in 32 bits.
368 double ratio
= double(maxNumber
) / double(total
);
369 totalFrames
= maxNumber
; // === total * ratio
370 droppedFrames
= uint32_t(double(stats
->GetDroppedFrames()) * ratio
);
374 if (!StaticPrefs::media_video_dropped_frame_stats_enabled()) {
380 RefPtr
<VideoPlaybackQuality
> playbackQuality
=
381 new VideoPlaybackQuality(this, creationTime
, totalFrames
, droppedFrames
);
382 return playbackQuality
.forget();
385 void HTMLVideoElement::WakeLockRelease() {
386 HTMLMediaElement::WakeLockRelease();
387 ReleaseVideoWakeLockIfExists();
390 void HTMLVideoElement::UpdateWakeLock() {
391 HTMLMediaElement::UpdateWakeLock();
393 CreateVideoWakeLockIfNeeded();
395 ReleaseVideoWakeLockIfExists();
399 bool HTMLVideoElement::ShouldCreateVideoWakeLock() const {
400 // Only request wake lock for video with audio or video from media stream,
401 // because non-stream video without audio is often used as a background image.
403 // Some web conferencing sites route audio outside the video element, and
404 // would not be detected unless we check for media stream, so do that below.
406 // Media streams generally aren't used as background images, though if they
407 // were we'd get false positives. If this is an issue, we could check for
408 // media stream AND document has audio playing (but that was tricky to do).
409 return HasVideo() && (mSrcStream
|| HasAudio());
412 void HTMLVideoElement::CreateVideoWakeLockIfNeeded() {
413 if (!mScreenWakeLock
&& ShouldCreateVideoWakeLock()) {
414 RefPtr
<power::PowerManagerService
> pmService
=
415 power::PowerManagerService::GetInstance();
416 NS_ENSURE_TRUE_VOID(pmService
);
419 mScreenWakeLock
= pmService
->NewWakeLock(u
"video-playing"_ns
,
420 OwnerDoc()->GetInnerWindow(), rv
);
424 void HTMLVideoElement::ReleaseVideoWakeLockIfExists() {
425 if (mScreenWakeLock
) {
427 mScreenWakeLock
->Unlock(rv
);
428 rv
.SuppressException();
429 mScreenWakeLock
= nullptr;
434 bool HTMLVideoElement::SetVisualCloneTarget(
435 RefPtr
<HTMLVideoElement
> aVisualCloneTarget
,
436 RefPtr
<Promise
> aVisualCloneTargetPromise
) {
437 MOZ_DIAGNOSTIC_ASSERT(
438 !aVisualCloneTarget
|| aVisualCloneTarget
->IsInComposedDoc(),
439 "Can't set the clone target to a disconnected video "
441 MOZ_DIAGNOSTIC_ASSERT(!mVisualCloneSource
,
442 "Can't clone a video element that is already a clone.");
443 if (!aVisualCloneTarget
||
444 (aVisualCloneTarget
->IsInComposedDoc() && !mVisualCloneSource
)) {
445 mVisualCloneTarget
= std::move(aVisualCloneTarget
);
446 mVisualCloneTargetPromise
= std::move(aVisualCloneTargetPromise
);
452 bool HTMLVideoElement::SetVisualCloneSource(
453 RefPtr
<HTMLVideoElement
> aVisualCloneSource
) {
454 MOZ_DIAGNOSTIC_ASSERT(
455 !aVisualCloneSource
|| aVisualCloneSource
->IsInComposedDoc(),
456 "Can't set the clone source to a disconnected video "
458 MOZ_DIAGNOSTIC_ASSERT(!mVisualCloneTarget
,
459 "Can't clone a video element that is already a "
461 if (!aVisualCloneSource
||
462 (aVisualCloneSource
->IsInComposedDoc() && !mVisualCloneTarget
)) {
463 mVisualCloneSource
= std::move(aVisualCloneSource
);
470 bool HTMLVideoElement::IsVideoStatsEnabled() {
471 return StaticPrefs::media_video_stats_enabled();
474 double HTMLVideoElement::TotalPlayTime() const {
478 uint32_t timeRangeCount
= mPlayed
->Length();
480 for (uint32_t i
= 0; i
< timeRangeCount
; i
++) {
481 double begin
= mPlayed
->Start(i
);
482 double end
= mPlayed
->End(i
);
483 total
+= end
- begin
;
486 if (mCurrentPlayRangeStart
!= -1.0) {
487 double now
= CurrentTime();
488 if (mCurrentPlayRangeStart
!= now
) {
489 total
+= now
- mCurrentPlayRangeStart
;
497 already_AddRefed
<Promise
> HTMLVideoElement::CloneElementVisually(
498 HTMLVideoElement
& aTargetVideo
, ErrorResult
& aRv
) {
499 MOZ_ASSERT(IsInComposedDoc(),
500 "Can't clone a video that's not bound to a DOM tree.");
501 MOZ_ASSERT(aTargetVideo
.IsInComposedDoc(),
502 "Can't clone to a video that's not bound to a DOM tree.");
503 if (!IsInComposedDoc() || !aTargetVideo
.IsInComposedDoc()) {
504 aRv
.Throw(NS_ERROR_UNEXPECTED
);
508 nsPIDOMWindowInner
* win
= OwnerDoc()->GetInnerWindow();
510 aRv
.Throw(NS_ERROR_UNEXPECTED
);
514 RefPtr
<Promise
> promise
= Promise::Create(win
->AsGlobal(), aRv
);
519 // Do we already have a visual clone target? If so, shut it down.
520 if (mVisualCloneTarget
) {
521 EndCloningVisually();
524 // If there's a poster set on the target video, clear it, otherwise
525 // it'll display over top of the cloned frames.
526 aTargetVideo
.UnsetHTMLAttr(nsGkAtoms::poster
, aRv
);
531 if (!SetVisualCloneTarget(&aTargetVideo
, promise
)) {
532 aRv
.Throw(NS_ERROR_FAILURE
);
536 if (!aTargetVideo
.SetVisualCloneSource(this)) {
537 mVisualCloneTarget
= nullptr;
538 aRv
.Throw(NS_ERROR_FAILURE
);
542 aTargetVideo
.SetMediaInfo(mMediaInfo
);
544 if (IsInComposedDoc() && !StaticPrefs::media_cloneElementVisually_testing()) {
545 NotifyUAWidgetSetupOrChange();
548 MaybeBeginCloningVisually();
550 return promise
.forget();
553 void HTMLVideoElement::StopCloningElementVisually() {
554 if (mVisualCloneTarget
) {
555 EndCloningVisually();
559 void HTMLVideoElement::MaybeBeginCloningVisually() {
560 if (!mVisualCloneTarget
) {
565 mDecoder
->SetSecondaryVideoContainer(
566 mVisualCloneTarget
->GetVideoFrameContainer());
567 NotifyDecoderActivityChanges();
568 UpdateMediaControlAfterPictureInPictureModeChanged();
569 OwnerDoc()->EnableChildElementInPictureInPictureMode();
570 } else if (mSrcStream
) {
571 VideoFrameContainer
* container
=
572 mVisualCloneTarget
->GetVideoFrameContainer();
574 mSecondaryVideoOutput
=
575 MakeRefPtr
<FirstFrameVideoOutput
>(container
, mAbstractMainThread
);
576 mVideoWatchManager
.Watch(
577 mSecondaryVideoOutput
->mFirstFrameRendered
,
578 &HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered
);
579 SetSecondaryMediaStreamRenderer(container
, mSecondaryVideoOutput
);
581 UpdateMediaControlAfterPictureInPictureModeChanged();
582 OwnerDoc()->EnableChildElementInPictureInPictureMode();
586 void HTMLVideoElement::EndCloningVisually() {
587 MOZ_ASSERT(mVisualCloneTarget
);
590 mDecoder
->SetSecondaryVideoContainer(nullptr);
591 NotifyDecoderActivityChanges();
592 OwnerDoc()->DisableChildElementInPictureInPictureMode();
593 } else if (mSrcStream
) {
594 if (mSecondaryVideoOutput
) {
595 mVideoWatchManager
.Unwatch(
596 mSecondaryVideoOutput
->mFirstFrameRendered
,
597 &HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered
);
598 mSecondaryVideoOutput
= nullptr;
600 SetSecondaryMediaStreamRenderer(nullptr);
601 OwnerDoc()->DisableChildElementInPictureInPictureMode();
604 Unused
<< mVisualCloneTarget
->SetVisualCloneSource(nullptr);
605 Unused
<< SetVisualCloneTarget(nullptr);
607 UpdateMediaControlAfterPictureInPictureModeChanged();
609 if (IsInComposedDoc() && !StaticPrefs::media_cloneElementVisually_testing()) {
610 NotifyUAWidgetSetupOrChange();
614 void HTMLVideoElement::OnSecondaryVideoContainerInstalled(
615 const RefPtr
<VideoFrameContainer
>& aSecondaryContainer
) {
616 MOZ_ASSERT(NS_IsMainThread());
617 MOZ_DIAGNOSTIC_ASSERT_IF(mVisualCloneTargetPromise
, mVisualCloneTarget
);
618 if (!mVisualCloneTargetPromise
) {
619 // Clone target was unset.
623 VideoFrameContainer
* container
= mVisualCloneTarget
->GetVideoFrameContainer();
624 if (NS_WARN_IF(container
!= aSecondaryContainer
)) {
625 // Not the right container.
629 mMainThreadEventTarget
->Dispatch(NewRunnableMethod(
630 "Promise::MaybeResolveWithUndefined", mVisualCloneTargetPromise
,
631 &Promise::MaybeResolveWithUndefined
));
632 mVisualCloneTargetPromise
= nullptr;
635 void HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered() {
636 OnSecondaryVideoContainerInstalled(
637 mVisualCloneTarget
->GetVideoFrameContainer());
640 } // namespace mozilla::dom