Bug 1894522 - Implement default seek handlers r=alwu,media-playback-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D219016
This commit is contained in:
Alex T
2024-10-03 18:31:50 +00:00
parent 3475e35fe7
commit 639466b6a7
12 changed files with 150 additions and 49 deletions

View File

@@ -63,7 +63,7 @@ interface MediaController : EventTarget {
[ChromeOnly,Exposed=Window,HeaderFile="mozilla/dom/MediaControlService.h"] [ChromeOnly,Exposed=Window,HeaderFile="mozilla/dom/MediaControlService.h"]
namespace MediaControlService { namespace MediaControlService {
// This is used to generate fake media control keys event in testing. // This is used to generate fake media control keys event in testing.
undefined generateMediaControlKey(MediaControlKey aKey); undefined generateMediaControlKey(MediaControlKey aKey, optional double aSeekValue = 0.0);
// This is used to get the media metadata from the current main controller in // This is used to get the media metadata from the current main controller in
// testing. // testing.

View File

@@ -480,19 +480,43 @@ class HTMLMediaElement::MediaControlKeyListener final
} }
} }
void HandleMediaKey(MediaControlKey aKey) override { void HandleMediaKey(MediaControlKey aKey,
Maybe<SeekDetails> aDetails) override {
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(IsStarted()); MOZ_ASSERT(IsStarted());
MEDIACONTROL_LOG("HandleEvent '%s'", GetEnumString(aKey).get()); MEDIACONTROL_LOG("HandleEvent '%s'", GetEnumString(aKey).get());
if (aKey == MediaControlKey::Play) { switch (aKey) {
case MediaControlKey::Play:
Owner()->Play(); Owner()->Play();
} else if (aKey == MediaControlKey::Pause) { break;
case MediaControlKey::Pause:
Owner()->Pause(); Owner()->Pause();
} else { break;
MOZ_ASSERT(aKey == MediaControlKey::Stop, case MediaControlKey::Stop:
"Not supported key for media element!");
Owner()->Pause(); Owner()->Pause();
StopIfNeeded(); 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!");
} }
} }

View File

@@ -351,27 +351,30 @@ void ContentMediaController::RemoveReceiver(
mReceivers.RemoveElement(aListener); mReceivers.RemoveElement(aListener);
} }
void ContentMediaController::HandleMediaKey(MediaControlKey aKey) { void ContentMediaController::HandleMediaKey(MediaControlKey aKey,
Maybe<SeekDetails> aDetails) {
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
if (mReceivers.IsEmpty()) { if (mReceivers.IsEmpty()) {
return; return;
} }
LOG("Handle '%s' event, receiver num=%zu", GetEnumString(aKey).get(), LOG("Handle '%s' event, receiver num=%zu", GetEnumString(aKey).get(),
mReceivers.Length()); mReceivers.Length());
// We have default handlers for play, pause and stop. // We have default handlers for these actions
// https://w3c.github.io/mediasession/#ref-for-dom-mediasessionaction-play%E2%91%A3 // https://w3c.github.io/mediasession/#ref-for-dom-mediasessionaction-play%E2%91%A3
switch (aKey) { switch (aKey) {
case MediaControlKey::Pause: case MediaControlKey::Pause:
PauseOrStopMedia(); PauseOrStopMedia();
return; return;
case MediaControlKey::Play: case MediaControlKey::Play:
[[fallthrough]];
case MediaControlKey::Stop: case MediaControlKey::Stop:
case MediaControlKey::Seekto:
case MediaControlKey::Seekforward:
case MediaControlKey::Seekbackward:
// When receiving `Stop`, the amount of receiver would vary during the // When receiving `Stop`, the amount of receiver would vary during the
// iteration, so we use the backward iteration to avoid accessing the // iteration, so we use the backward iteration to avoid accessing the
// index which is over the array length. // index which is over the array length.
for (auto& receiver : Reversed(mReceivers)) { for (auto& receiver : Reversed(mReceivers)) {
receiver->HandleMediaKey(aKey); receiver->HandleMediaKey(aKey, aDetails);
} }
return; return;
default: default:

View File

@@ -24,7 +24,8 @@ class ContentMediaControlKeyReceiver {
static ContentMediaControlKeyReceiver* Get(BrowsingContext* aBC); static ContentMediaControlKeyReceiver* Get(BrowsingContext* aBC);
// Use this method to handle the event from `ContentMediaAgent`. // Use this method to handle the event from `ContentMediaAgent`.
virtual void HandleMediaKey(MediaControlKey aKey) = 0; virtual void HandleMediaKey(MediaControlKey aKey,
Maybe<SeekDetails> aDetails = Nothing()) = 0;
virtual bool IsPlaying() const = 0; virtual bool IsPlaying() const = 0;
}; };
@@ -94,7 +95,8 @@ class ContentMediaController final : public ContentMediaAgent,
void RemoveReceiver(ContentMediaControlKeyReceiver* aListener) override; void RemoveReceiver(ContentMediaControlKeyReceiver* aListener) override;
// ContentMediaControlKeyReceiver method // ContentMediaControlKeyReceiver method
void HandleMediaKey(MediaControlKey aKey) override; void HandleMediaKey(MediaControlKey aKey,
Maybe<SeekDetails> aDetails = Nothing()) override;
private: private:
~ContentMediaController() = default; ~ContentMediaController() = default;

View File

@@ -42,12 +42,12 @@ MediaSession* ContentPlaybackController::GetMediaSession() const {
} }
void ContentPlaybackController::NotifyContentMediaControlKeyReceiver( void ContentPlaybackController::NotifyContentMediaControlKeyReceiver(
MediaControlKey aKey) { MediaControlKey aKey, Maybe<SeekDetails> aDetails) {
if (RefPtr<ContentMediaControlKeyReceiver> receiver = if (RefPtr<ContentMediaControlKeyReceiver> receiver =
ContentMediaControlKeyReceiver::Get(mBC)) { ContentMediaControlKeyReceiver::Get(mBC)) {
LOG("Handle '%s' in default behavior for BC %" PRIu64, LOG("Handle '%s' in default behavior for BC %" PRIu64,
GetEnumString(aKey).get(), mBC->Id()); GetEnumString(aKey).get(), mBC->Id());
receiver->HandleMediaKey(aKey); receiver->HandleMediaKey(aKey, aDetails);
} }
} }
@@ -124,8 +124,12 @@ void ContentPlaybackController::SeekBackward(double aSeekOffset) {
MediaSessionActionDetails details; MediaSessionActionDetails details;
details.mAction = MediaSessionAction::Seekbackward; details.mAction = MediaSessionAction::Seekbackward;
details.mSeekOffset.Construct(aSeekOffset); details.mSeekOffset.Construct(aSeekOffset);
RefPtr<MediaSession> session = GetMediaSession();
if (IsMediaSessionActionSupported(details.mAction)) { if (IsMediaSessionActionSupported(details.mAction)) {
NotifyMediaSession(details); NotifyMediaSession(details);
} else if (!GetActiveMediaSessionId() || (session && session->IsActive())) {
NotifyContentMediaControlKeyReceiver(MediaControlKey::Seekbackward,
Some(SeekDetails(aSeekOffset)));
} }
} }
@@ -133,8 +137,12 @@ void ContentPlaybackController::SeekForward(double aSeekOffset) {
MediaSessionActionDetails details; MediaSessionActionDetails details;
details.mAction = MediaSessionAction::Seekforward; details.mAction = MediaSessionAction::Seekforward;
details.mSeekOffset.Construct(aSeekOffset); details.mSeekOffset.Construct(aSeekOffset);
RefPtr<MediaSession> session = GetMediaSession();
if (IsMediaSessionActionSupported(details.mAction)) { if (IsMediaSessionActionSupported(details.mAction)) {
NotifyMediaSession(details); NotifyMediaSession(details);
} else if (!GetActiveMediaSessionId() || (session && session->IsActive())) {
NotifyContentMediaControlKeyReceiver(MediaControlKey::Seekforward,
Some(SeekDetails(aSeekOffset)));
} }
} }
@@ -163,11 +171,15 @@ void ContentPlaybackController::SeekTo(double aSeekTime, bool aFastSeek) {
MediaSessionActionDetails details; MediaSessionActionDetails details;
details.mAction = MediaSessionAction::Seekto; details.mAction = MediaSessionAction::Seekto;
details.mSeekTime.Construct(aSeekTime); details.mSeekTime.Construct(aSeekTime);
RefPtr<MediaSession> session = GetMediaSession();
if (aFastSeek) { if (aFastSeek) {
details.mFastSeek.Construct(aFastSeek); details.mFastSeek.Construct(aFastSeek);
} }
if (IsMediaSessionActionSupported(details.mAction)) { if (IsMediaSessionActionSupported(details.mAction)) {
NotifyMediaSession(details); NotifyMediaSession(details);
} else if (!GetActiveMediaSessionId() || (session && session->IsActive())) {
NotifyContentMediaControlKeyReceiver(
MediaControlKey::Seekto, Some(SeekDetails(aSeekTime, aFastSeek)));
} }
} }

View File

@@ -51,7 +51,8 @@ class MOZ_STACK_CLASS ContentPlaybackController {
void SeekTo(double aSeekTime, bool aFastSeek); void SeekTo(double aSeekTime, bool aFastSeek);
private: private:
void NotifyContentMediaControlKeyReceiver(MediaControlKey aKey); void NotifyContentMediaControlKeyReceiver(
MediaControlKey aKey, Maybe<SeekDetails> aDetails = Nothing());
void NotifyMediaSession(MediaSessionAction aAction); void NotifyMediaSession(MediaSessionAction aAction);
void NotifyMediaSession(const MediaSessionActionDetails& aDetails); void NotifyMediaSession(const MediaSessionActionDetails& aDetails);
void NotifyMediaSessionWhenActionIsSupported(MediaSessionAction aAction); void NotifyMediaSessionWhenActionIsSupported(MediaSessionAction aAction);

View File

@@ -53,10 +53,11 @@ RefPtr<MediaControlService> MediaControlService::GetService() {
/* static */ /* static */
void MediaControlService::GenerateMediaControlKey(const GlobalObject& global, void MediaControlService::GenerateMediaControlKey(const GlobalObject& global,
MediaControlKey aKey) { MediaControlKey aKey,
double aSeekTime) {
RefPtr<MediaControlService> service = MediaControlService::GetService(); RefPtr<MediaControlService> service = MediaControlService::GetService();
if (service) { if (service) {
service->GenerateTestMediaControlKey(aKey); service->GenerateTestMediaControlKey(aKey, aSeekTime);
} }
} }
@@ -245,20 +246,21 @@ MediaController* MediaControlService::GetMainController() const {
return mControllerManager->GetMainController(); return mControllerManager->GetMainController();
} }
void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey) { void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey,
double aSeekValue) {
if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) { if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
return; return;
} }
// Generate seek details when necessary // Generate seek details when necessary
switch (aKey) { switch (aKey) {
case MediaControlKey::Seekto: case MediaControlKey::Seekto:
mMediaKeysHandler->OnActionPerformed( mMediaKeysHandler->OnActionPerformed(MediaControlAction(
MediaControlAction(aKey, SeekDetails(0.0, false /* fast seek */))); aKey, SeekDetails(aSeekValue, false /* fast seek */)));
break; break;
case MediaControlKey::Seekbackward: case MediaControlKey::Seekbackward:
case MediaControlKey::Seekforward: case MediaControlKey::Seekforward:
mMediaKeysHandler->OnActionPerformed( mMediaKeysHandler->OnActionPerformed(
MediaControlAction(aKey, SeekDetails(0.0))); MediaControlAction(aKey, SeekDetails(aSeekValue)));
break; break;
default: default:
mMediaKeysHandler->OnActionPerformed(MediaControlAction(aKey)); mMediaKeysHandler->OnActionPerformed(MediaControlAction(aKey));

View File

@@ -36,7 +36,7 @@ class MediaControlService final : public nsIObserver {
// Currently these following static methods are only being used in testing. // Currently these following static methods are only being used in testing.
static void GenerateMediaControlKey(const GlobalObject& global, static void GenerateMediaControlKey(const GlobalObject& global,
MediaControlKey aKey); MediaControlKey aKey, double aSeekTime);
static void GetCurrentActiveMediaMetadata(const GlobalObject& aGlobal, static void GetCurrentActiveMediaMetadata(const GlobalObject& aGlobal,
MediaMetadataInit& aMetadata); MediaMetadataInit& aMetadata);
static MediaSessionPlaybackState GetCurrentMediaSessionPlaybackState( static MediaSessionPlaybackState GetCurrentMediaSessionPlaybackState(
@@ -70,7 +70,7 @@ class MediaControlService final : public nsIObserver {
* generate fake media control key events, get the media metadata and playback * generate fake media control key events, get the media metadata and playback
* state from the main controller. * state from the main controller.
*/ */
void GenerateTestMediaControlKey(MediaControlKey aKey); void GenerateTestMediaControlKey(MediaControlKey aKey, double aSeekValue);
MediaMetadataBase GetMainControllerMediaMetadata() const; MediaMetadataBase GetMainControllerMediaMetadata() const;
MediaSessionPlaybackState GetMainControllerPlaybackState() const; MediaSessionPlaybackState GetMainControllerPlaybackState() const;

View File

@@ -75,9 +75,10 @@ void MediaController::GetMetadata(MediaMetadataInit& aMetadata,
} }
static const MediaControlKey sDefaultSupportedKeys[] = { static const MediaControlKey sDefaultSupportedKeys[] = {
MediaControlKey::Focus, MediaControlKey::Play, MediaControlKey::Pause, MediaControlKey::Focus, MediaControlKey::Play,
MediaControlKey::Playpause, MediaControlKey::Stop, MediaControlKey::Pause, MediaControlKey::Playpause,
}; MediaControlKey::Stop, MediaControlKey::Seekto,
MediaControlKey::Seekforward, MediaControlKey::Seekbackward};
static void GetDefaultSupportedKeys(nsTArray<MediaControlKey>& aKeys) { static void GetDefaultSupportedKeys(nsTArray<MediaControlKey>& aKeys) {
for (const auto& key : sDefaultSupportedKeys) { for (const auto& key : sDefaultSupportedKeys) {
@@ -183,13 +184,23 @@ bool MediaController::IsActive() const { return mIsActive; };
bool MediaController::ShouldPropagateActionToAllContexts( bool MediaController::ShouldPropagateActionToAllContexts(
const MediaControlAction& aAction) const { const MediaControlAction& aAction) const {
// These three actions have default action handler for each frame, so we // These actions have default action handler for each frame, so we
// need to propagate to all contexts. We would handle default handlers in // need to propagate to all contexts. We would handle default handlers in
// `ContentMediaController::HandleMediaKey`. // `ContentMediaController::HandleMediaKey`.
return aAction.mKey.isSome() && if (aAction.mKey.isSome()) {
(aAction.mKey.value() == MediaControlKey::Play || switch (aAction.mKey.value()) {
aAction.mKey.value() == MediaControlKey::Pause || case MediaControlKey::Play:
aAction.mKey.value() == MediaControlKey::Stop); case MediaControlKey::Pause:
case MediaControlKey::Stop:
case MediaControlKey::Seekto:
case MediaControlKey::Seekforward:
case MediaControlKey::Seekbackward:
return true;
default:
return false;
}
}
return false;
} }
void MediaController::UpdateMediaControlActionToContentMediaIfNeeded( void MediaController::UpdateMediaControlActionToContentMediaIfNeeded(

View File

@@ -12,7 +12,7 @@ const videoId = "video";
/** /**
* This test is used to check the scenario when we should use the customized * This test is used to check the scenario when we should use the customized
* action handler and the the default action handler (play/pause/stop). * action handler and the the default action handler (play/pause/stop/seekXXX).
* If a frame (DOM Window, it could be main frame or an iframe) has active media * If a frame (DOM Window, it could be main frame or an iframe) has active media
* session, then it should use the customized action handler it it has one. * session, then it should use the customized action handler it it has one.
* Otherwise, the default action handler should be used. * Otherwise, the default action handler should be used.
@@ -47,6 +47,24 @@ add_task(async function triggerDefaultActionHandler() {
info(`default action handler should pause media`); info(`default action handler should pause media`);
await checkOrWaitUntilMediaPauses(tab, { videoId }); await checkOrWaitUntilMediaPauses(tab, { videoId });
info(`test 'seekto' action`);
await simulateMediaAction(tab, "seekto", 2.0);
info(`default action handler should set currentTime`);
await checkOrWaitUntilMediaSeek(tab, { videoId }, 2.0);
info(`test 'seekforward' action`);
await simulateMediaAction(tab, "seekforward", 1.0);
info(`default action handler should set currentTime`);
await checkOrWaitUntilMediaSeek(tab, { videoId }, 3.0);
info(`test 'seekbackward' action`);
await simulateMediaAction(tab, "seekbackward", 1.0);
info(`default action handler should set currentTime`);
await checkOrWaitUntilMediaSeek(tab, { videoId }, 2.0);
info(`test 'play' action`); info(`test 'play' action`);
await simulateMediaAction(tab, "play"); await simulateMediaAction(tab, "play");
@@ -136,7 +154,7 @@ add_task(
await pauseAllMedia(tab); await pauseAllMedia(tab);
info( info(
`press '${action}' would trigger default andler on main frame because it doesn't set action handler` `press '${action}' would trigger default handler on main frame because it doesn't set action handler`
); );
await simulateMediaAction(tab, action); await simulateMediaAction(tab, action);
await checkOrWaitUntilMediaPlays(tab, { videoId }); await checkOrWaitUntilMediaPlays(tab, { videoId });
@@ -147,7 +165,7 @@ add_task(
await checkOrWaitUntilMediaPlays(tab, { frameId }); await checkOrWaitUntilMediaPlays(tab, { frameId });
} else { } else {
info( info(
`press '${action}' would trigger default andler on main frame because it doesn't set action handler` `press '${action}' would trigger default handler on main frame because it doesn't set action handler`
); );
await simulateMediaAction(tab, action); await simulateMediaAction(tab, action);
await checkOrWaitUntilMediaPauses(tab, { videoId }); await checkOrWaitUntilMediaPauses(tab, { videoId });
@@ -313,6 +331,30 @@ function checkOrWaitUntilMediaPlays(tab, { videoId, frameId }) {
); );
} }
function checkOrWaitUntilMediaSeek(tab, { videoId }, expectedTime) {
return SpecialPowers.spawn(
tab.linkedBrowser,
[videoId, expectedTime],
(videoId, expectedTime) => {
return new Promise(r => {
const video = content.document.getElementById(videoId);
ok(video.paused, "video is paused");
if (video.currentTime == expectedTime) {
ok(true, `media has been seeked`);
r();
} else {
info(`wait until media seeked`);
video.ontimeupdate = () => {
video.ontimeupdate = null;
is(video.currentTime, expectedTime, `correct time set`);
r();
};
}
});
}
);
}
function setActionHandler(tab, action, frameId = null) { function setActionHandler(tab, action, frameId = null) {
return SpecialPowers.spawn( return SpecialPowers.spawn(
tab.linkedBrowser, tab.linkedBrowser,
@@ -366,12 +408,12 @@ async function waitUntilActionHandlerIsTriggered(tab, action, frameId = null) {
); );
} }
async function simulateMediaAction(tab, action) { async function simulateMediaAction(tab, action, seekValue = 0.0) {
const controller = tab.linkedBrowser.browsingContext.mediaController; const controller = tab.linkedBrowser.browsingContext.mediaController;
if (!controller.isActive) { if (!controller.isActive) {
await new Promise(r => (controller.onactivated = r)); await new Promise(r => (controller.onactivated = r));
} }
MediaControlService.generateMediaControlKey(action); MediaControlService.generateMediaControlKey(action, seekValue);
} }
function loadIframe(tab, iframeId, url) { function loadIframe(tab, iframeId, url) {

View File

@@ -2,7 +2,16 @@ const PAGE_NON_AUTOPLAY =
"https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html"; "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
const testVideoId = "video"; const testVideoId = "video";
const sDefaultSupportedKeys = ["focus", "play", "pause", "playpause", "stop"]; const sDefaultSupportedKeys = [
"focus",
"play",
"pause",
"playpause",
"stop",
"seekto",
"seekforward",
"seekbackward",
];
add_task(async function setupTestingPref() { add_task(async function setupTestingPref() {
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
@@ -80,12 +89,7 @@ add_task(async function testSettingActionsWhichAreNotDefaultKeys() {
await playMedia(tab, testVideoId); await playMedia(tab, testVideoId);
info(`create media session but not set any action handler`); info(`create media session but not set any action handler`);
let nonDefaultActions = [ let nonDefaultActions = ["previoustrack", "nexttrack"];
"seekbackward",
"seekforward",
"previoustrack",
"nexttrack",
];
await setMediaSessionSupportedAction(tab, nonDefaultActions); await setMediaSessionSupportedAction(tab, nonDefaultActions);
info( info(

View File

@@ -24275,7 +24275,7 @@ declare namespace L10nOverlays {
} }
declare namespace MediaControlService { declare namespace MediaControlService {
function generateMediaControlKey(aKey: MediaControlKey): void; function generateMediaControlKey(aKey: MediaControlKey, aSeekValue?: double): void;
function getCurrentActiveMediaMetadata(): MediaMetadataInit; function getCurrentActiveMediaMetadata(): MediaMetadataInit;
function getCurrentMediaSessionPlaybackState(): MediaSessionPlaybackState; function getCurrentMediaSessionPlaybackState(): MediaSessionPlaybackState;
} }