Bug 1172394 - Make dom::MediaTrack lifetime spec compliant. r=bryce

This makes us forget tracks at the right times. The spec also says no
removetrack events should be fired because of this, yet it seems to be something
other user agents do:
https://wpt.fyi/results/media-source/mediasource-avtracks.html

This is of low importance however, since MediaTracks are prefed off by default.

Differential Revision: https://phabricator.services.mozilla.com/D52038

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Andreas Pehrson 2019-11-13 08:48:16 +00:00
Родитель 9112bdefc4
Коммит ed705988de
6 изменённых файлов: 98 добавлений и 93 удалений

Просмотреть файл

@ -1917,17 +1917,6 @@ void HTMLMediaElement::AbortExistingLoads() {
mFirstFrameListener = nullptr; mFirstFrameListener = nullptr;
} }
// When aborting the existing loads, empty the objects in audio track list and
// video track list, no events (in particular, no removetrack events) are
// fired as part of this. Ending MediaTrack sends track ended notifications,
// so we empty the track lists prior.
if (AudioTracks()) {
AudioTracks()->EmptyTracks();
}
if (VideoTracks()) {
VideoTracks()->EmptyTracks();
}
if (mDecoder) { if (mDecoder) {
fireTimeUpdate = mDecoder->GetCurrentTime() != 0.0; fireTimeUpdate = mDecoder->GetCurrentTime() != 0.0;
ShutdownDecoder(); ShutdownDecoder();
@ -1979,6 +1968,7 @@ void HTMLMediaElement::AbortExistingLoads() {
RejectPromises(TakePendingPlayPromises(), NS_ERROR_DOM_MEDIA_ABORT_ERR); RejectPromises(TakePendingPlayPromises(), NS_ERROR_DOM_MEDIA_ABORT_ERR);
} }
ChangeNetworkState(NETWORK_EMPTY); ChangeNetworkState(NETWORK_EMPTY);
RemoveMediaTracks();
ChangeReadyState(HAVE_NOTHING); ChangeReadyState(HAVE_NOTHING);
// TODO: Apply the rules for text track cue rendering Bug 865407 // TODO: Apply the rules for text track cue rendering Bug 865407
@ -2026,6 +2016,7 @@ void HTMLMediaElement::NoSupportedMediaSourceError(
ShutdownDecoder(); ShutdownDecoder();
} }
mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails); mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails);
RemoveMediaTracks();
ChangeDelayLoadStatus(false); ChangeDelayLoadStatus(false);
UpdateAudioChannelPlayingState(); UpdateAudioChannelPlayingState();
RejectPromises(TakePendingPlayPromises(), RejectPromises(TakePendingPlayPromises(),
@ -2471,6 +2462,8 @@ void HTMLMediaElement::LoadFromSourceChildren() {
AddMutationObserverUnlessExists(this); AddMutationObserverUnlessExists(this);
RemoveMediaTracks();
while (true) { while (true) {
Element* child = GetNextSource(); Element* child = GetNextSource();
if (!child) { if (!child) {
@ -5119,6 +5112,10 @@ void HTMLMediaElement::MetadataLoaded(const MediaInfo* aInfo,
UniquePtr<const MetadataTags> aTags) { UniquePtr<const MetadataTags> aTags) {
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
if (mDecoder) {
ConstructMediaTracks(aInfo);
}
SetMediaInfo(*aInfo); SetMediaInfo(*aInfo);
mIsEncrypted = mIsEncrypted =
@ -5234,12 +5231,6 @@ void HTMLMediaElement::DecodeError(const MediaResult& aError) {
DecoderDoctorDiagnostics diagnostics; DecoderDoctorDiagnostics diagnostics;
diagnostics.StoreDecodeError(OwnerDoc(), aError, src, __func__); diagnostics.StoreDecodeError(OwnerDoc(), aError, src, __func__);
if (AudioTracks()) {
AudioTracks()->EmptyTracks();
}
if (VideoTracks()) {
VideoTracks()->EmptyTracks();
}
if (mIsLoadingFromSourceChildren) { if (mIsLoadingFromSourceChildren) {
mErrorSink->ResetError(); mErrorSink->ResetError();
if (mSourceLoadCandidate) { if (mSourceLoadCandidate) {
@ -7241,12 +7232,10 @@ bool HTMLMediaElement::IsAudible() const {
} }
void HTMLMediaElement::ConstructMediaTracks(const MediaInfo* aInfo) { void HTMLMediaElement::ConstructMediaTracks(const MediaInfo* aInfo) {
if (mMediaTracksConstructed || !aInfo) { if (!aInfo) {
return; return;
} }
mMediaTracksConstructed = true;
AudioTrackList* audioList = AudioTracks(); AudioTrackList* audioList = AudioTracks();
if (audioList && aInfo->HasAudio()) { if (audioList && aInfo->HasAudio()) {
const TrackInfo& info = aInfo->mAudio; const TrackInfo& info = aInfo->mAudio;
@ -7273,12 +7262,9 @@ void HTMLMediaElement::RemoveMediaTracks() {
if (mAudioTrackList) { if (mAudioTrackList) {
mAudioTrackList->RemoveTracks(); mAudioTrackList->RemoveTracks();
} }
if (mVideoTrackList) { if (mVideoTrackList) {
mVideoTrackList->RemoveTracks(); mVideoTrackList->RemoveTracks();
} }
mMediaTracksConstructed = false;
} }
class MediaElementGMPCrashHelper : public GMPCrashHelper { class MediaElementGMPCrashHelper : public GMPCrashHelper {

Просмотреть файл

@ -693,10 +693,6 @@ class HTMLMediaElement : public nsGenericHTMLElement,
Document* GetDocument() const override; Document* GetDocument() const override;
void ConstructMediaTracks(const MediaInfo* aInfo) override;
void RemoveMediaTracks() override;
already_AddRefed<GMPCrashHelper> CreateGMPCrashHelper() override; already_AddRefed<GMPCrashHelper> CreateGMPCrashHelper() override;
nsISerialEventTarget* MainThreadEventTarget() { nsISerialEventTarget* MainThreadEventTarget() {
@ -1249,6 +1245,18 @@ class HTMLMediaElement : public nsGenericHTMLElement,
// Pass information for deciding the video decode mode to decoder. // Pass information for deciding the video decode mode to decoder.
void NotifyDecoderActivityChanges() const; void NotifyDecoderActivityChanges() const;
// Constructs an AudioTrack in mAudioTrackList if aInfo reports that audio is
// available, and a VideoTrack in mVideoTrackList if aInfo reports that video
// is available.
void ConstructMediaTracks(const MediaInfo* aInfo);
// Removes all MediaTracks from mAudioTrackList and mVideoTrackList and fires
// "removetrack" on the lists accordingly.
// Note that by spec, this should not fire "removetrack". However, it appears
// other user agents do, per
// https://wpt.fyi/results/media-source/mediasource-avtracks.html.
void RemoveMediaTracks();
// Mark the decoder owned by the element as tainted so that the // Mark the decoder owned by the element as tainted so that the
// suspend-video-decoder is disabled. // suspend-video-decoder is disabled.
void MarkAsTainted(); void MarkAsTainted();
@ -1807,10 +1815,6 @@ class HTMLMediaElement : public nsGenericHTMLElement,
// For use by mochitests. Enabling pref "media.test.video-suspend" // For use by mochitests. Enabling pref "media.test.video-suspend"
bool mForcedHidden = false; bool mForcedHidden = false;
// True if audio tracks and video tracks are constructed and added into the
// track list, false if all tracks are removed from the track list.
bool mMediaTracksConstructed = false;
Visibility mVisibilityState = Visibility::Untracked; Visibility mVisibilityState = Visibility::Untracked;
UniquePtr<ErrorSink> mErrorSink; UniquePtr<ErrorSink> mErrorSink;

Просмотреть файл

@ -380,9 +380,6 @@ void MediaDecoder::Shutdown() {
mAbstractMainThread->Dispatch(r.forget()); mAbstractMainThread->Dispatch(r.forget());
} }
// Ask the owner to remove its audio/video tracks.
GetOwner()->RemoveMediaTracks();
ChangeState(PLAY_STATE_SHUTDOWN); ChangeState(PLAY_STATE_SHUTDOWN);
mVideoDecodingOberver->UnregisterEvent(); mVideoDecodingOberver->UnregisterEvent();
mVideoDecodingOberver = nullptr; mVideoDecodingOberver = nullptr;
@ -664,7 +661,6 @@ void MediaDecoder::MetadataLoaded(
mMediaSeekableOnlyInBufferedRanges = mMediaSeekableOnlyInBufferedRanges =
aInfo->mMediaSeekableOnlyInBufferedRanges; aInfo->mMediaSeekableOnlyInBufferedRanges;
mInfo = aInfo.release(); mInfo = aInfo.release();
GetOwner()->ConstructMediaTracks(mInfo);
mDecoderStateMachine->EnsureOutputStreamManagerHasTracks(*mInfo); mDecoderStateMachine->EnsureOutputStreamManagerHasTracks(*mInfo);
// Make sure the element and the frame (if any) are told about // Make sure the element and the frame (if any) are told about
@ -856,12 +852,6 @@ void MediaDecoder::ChangeState(PlayState aState) {
DDLOG(DDLogCategory::Property, "play_state", ToPlayStateStr(aState)); DDLOG(DDLogCategory::Property, "play_state", ToPlayStateStr(aState));
} }
mPlayState = aState; mPlayState = aState;
if (mPlayState == PLAY_STATE_PLAYING) {
GetOwner()->ConstructMediaTracks(mInfo);
} else if (IsEnded()) {
GetOwner()->RemoveMediaTracks();
}
} }
bool MediaDecoder::IsLoopingBack(double aPrevPos, double aCurPos) const { bool MediaDecoder::IsLoopingBack(double aPrevPos, double aCurPos) const {

Просмотреть файл

@ -139,14 +139,6 @@ class MediaDecoderOwner {
virtual void DispatchEncrypted(const nsTArray<uint8_t>& aInitData, virtual void DispatchEncrypted(const nsTArray<uint8_t>& aInitData,
const nsAString& aInitDataType) = 0; const nsAString& aInitDataType) = 0;
// Called by the media decoder to create audio/video tracks and add to its
// owner's track list.
virtual void ConstructMediaTracks(const MediaInfo* aInfo) = 0;
// Called by the media decoder to removes all audio/video tracks from its
// owner's track list.
virtual void RemoveMediaTracks() = 0;
// Notified by the decoder that a decryption key is required before emitting // Notified by the decoder that a decryption key is required before emitting
// further output. // further output.
virtual void NotifyWaitingForKey() {} virtual void NotifyWaitingForKey() {}

Просмотреть файл

@ -10,19 +10,19 @@
<pre id="test"> <pre id="test">
<script class="testbody" type="text/javascript"> <script class="testbody" type="text/javascript">
var manager = new MediaTestManager; const manager = new MediaTestManager;
function startTest(test, token) { function startTest(test, token) {
var elemType = getMajorMimeType(test.type); const elemType = getMajorMimeType(test.type);
var element = document.createElement(elemType); const element = document.createElement(elemType);
var audioOnchange = 0; let audioOnchange = 0;
var audioOnaddtrack = 0; let audioOnaddtrack = 0;
var audioOnremovetrack = 0; let audioOnremovetrack = 0;
var videoOnchange = 0; let videoOnchange = 0;
var videoOnaddtrack = 0; let videoOnaddtrack = 0;
var videoOnremovetrack = 0; let videoOnremovetrack = 0;
var isPlaying = false; let isPlaying = false;
isnot(element.audioTracks, undefined, isnot(element.audioTracks, undefined,
'HTMLMediaElement::AudioTracks() property should be available.'); 'HTMLMediaElement::AudioTracks() property should be available.');
@ -53,26 +53,43 @@ function startTest(test, token) {
videoOnchange++; videoOnchange++;
} }
function checkTrackRemoved() { function checkTrackNotRemoved() {
is(audioOnremovetrack, 0, 'Should have no calls of onremovetrack on audioTracks.');
is(videoOnremovetrack, 0, 'Should have no calls of onremovetrack on videoTracks.');
if (isPlaying) { if (isPlaying) {
if (test.hasAudio) { is(element.audioTracks.length, test.hasAudio ? 1 : 0,
is(audioOnremovetrack, 1, 'Calls of onremovetrack on audioTracks should be 1.'); 'Expected length of audioTracks.');
is(element.audioTracks.length, 0, 'The length of audioTracks should be 0.'); is(element.videoTracks.length, test.hasVideo ? 1 : 0,
} 'Expected length of videoTracks.');
if (test.hasVideo) { }
is(videoOnremovetrack, 1, 'Calls of onremovetrack on videoTracks should be 1.'); }
is(element.videoTracks.length, 0, 'The length of videoTracks should be 0.');
} function checkTrackRemoved() {
is(element.audioTracks.length, 0, 'The length of audioTracks should be 0.');
is(element.videoTracks.length, 0, 'The length of videoTracks should be 0.');
if (isPlaying) {
is(audioOnremovetrack, test.hasAudio ? 1 : 0,
'Expected calls of onremovetrack on audioTracks.');
is(videoOnremovetrack, test.hasVideo ? 1 : 0,
'Expected calls of onremovetrack on videoTracks.');
} }
} }
function onended() { function onended() {
ok(true, 'Event ended is expected to be fired on element.'); ok(true, 'Event ended is expected to be fired on element.');
checkTrackRemoved(); checkTrackNotRemoved();
element.onended = null; element.onended = null;
element.onplaying = null; element.onplaying = null;
element.onpause = null; element.onpause = null;
manager.finished(element.token); element.src = "";
is(element.audioTracks.length, 0, 'audioTracks have been forgotten');
is(element.videoTracks.length, 0, 'videoTracks have been forgotten');
is(audioOnremovetrack, 0, 'No audio removetrack events yet');
is(videoOnremovetrack, 0, 'No video removetrack events yet');
setTimeout(() => {
checkTrackRemoved();
manager.finished(element.token);
}, 100);
} }
function checkTrackAdded() { function checkTrackAdded() {

Просмотреть файл

@ -10,28 +10,29 @@
<pre id="test"> <pre id="test">
<script class="testbody" type="text/javascript"> <script class="testbody" type="text/javascript">
var manager = new MediaTestManager; const manager = new MediaTestManager;
function startTest(test, token) { function startTest(test, token) {
// Scenario to test: // Scenario to test:
// 1. Audio tracks and video tracks should be added to the track list when // 1. Audio tracks and video tracks should be added to the track list when
// playing, and all tracks should be removed from the list after we seek // metadata has loaded, and all tracks should remain even after we seek to
// to the end. // the end.
// 2. All tracks should be added back to the list if we replay from the end, // 2. No tracks should be added back to the list if we replay from the end,
// and all tracks should be removed from the list after we seek to the end. // and no tracks should be removed from the list after we seek to the end.
// 3. After seek to the middle from end of playback, all tracks should be // 3. After seek to the middle from end of playback, all tracks should remain
// added back to the list if we play from here, and all tracks should be // in the list if we play from here, and no tracks should be removed from
// removed from the list after we seek to the end. // the list after we seek to the end.
// 4. Unsetting the media element's src attribute should remove all tracks.
var elemType = getMajorMimeType(test.type); const elemType = getMajorMimeType(test.type);
var element = document.createElement(elemType); const element = document.createElement(elemType);
var audioOnaddtrack = 0; let audioOnaddtrack = 0;
var audioOnremovetrack = 0; let audioOnremovetrack = 0;
var videoOnaddtrack = 0; let videoOnaddtrack = 0;
var videoOnremovetrack = 0; let videoOnremovetrack = 0;
var isPlaying = false; let isPlaying = false;
var steps = 0; let steps = 0;
element.audioTracks.onaddtrack = function(e) { element.audioTracks.onaddtrack = function(e) {
audioOnaddtrack++; audioOnaddtrack++;
@ -49,16 +50,23 @@ function startTest(test, token) {
videoOnremovetrack++; videoOnremovetrack++;
} }
function testTrackEventCalls(expectedCalls) { function testExpectedAddtrack(expectedCalls) {
if (test.hasAudio) { if (test.hasAudio) {
is(audioOnaddtrack, expectedCalls, is(audioOnaddtrack, expectedCalls,
'Calls of onaddtrack on audioTracks should be '+expectedCalls+' times.'); 'Calls of onaddtrack on audioTracks should be '+expectedCalls+' times.');
is(audioOnremovetrack, expectedCalls,
'Calls of onremovetrack on audioTracks should be '+expectedCalls+' times.');
} }
if (test.hasVideo) { if (test.hasVideo) {
is(videoOnaddtrack, expectedCalls, is(videoOnaddtrack, expectedCalls,
'Calls of onaddtrack on videoTracks should be '+expectedCalls+' times.'); 'Calls of onaddtrack on videoTracks should be '+expectedCalls+' times.');
}
}
function testExpectedRemovetrack(expectedCalls) {
if (test.hasAudio) {
is(audioOnremovetrack, expectedCalls,
'Calls of onremovetrack on audioTracks should be '+expectedCalls+' times.');
}
if (test.hasVideo) {
is(videoOnremovetrack, expectedCalls, is(videoOnremovetrack, expectedCalls,
'Calls of onremovetrack on videoTracks should be '+expectedCalls+' times.'); 'Calls of onremovetrack on videoTracks should be '+expectedCalls+' times.');
} }
@ -76,21 +84,29 @@ function startTest(test, token) {
if (isPlaying) { if (isPlaying) {
switch(steps) { switch(steps) {
case 1: case 1:
testTrackEventCalls(1); testExpectedAddtrack(1);
testExpectedRemovetrack(0);
element.onplaying = onplaying; element.onplaying = onplaying;
element.play(); element.play();
steps++; steps++;
break; break;
case 2: case 2:
testTrackEventCalls(2); testExpectedAddtrack(1);
testExpectedRemovetrack(0);
element.currentTime = element.duration * 0.5; element.currentTime = element.duration * 0.5;
element.onplaying = onplaying; element.onplaying = onplaying;
element.play(); element.play();
steps++; steps++;
break; break;
case 3: case 3:
testTrackEventCalls(3); testExpectedAddtrack(1);
finishTesting(); testExpectedRemovetrack(0);
element.src = "";
setTimeout(() => {
testExpectedAddtrack(1);
testExpectedRemovetrack(1);
finishTesting();
}, 0);
break; break;
} }
} else { } else {