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"]
namespace MediaControlService {
// 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
// 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(IsStarted());
MEDIACONTROL_LOG("HandleEvent '%s'", GetEnumString(aKey).get());
if (aKey == MediaControlKey::Play) {
switch (aKey) {
case MediaControlKey::Play:
Owner()->Play();
} else if (aKey == MediaControlKey::Pause) {
break;
case MediaControlKey::Pause:
Owner()->Pause();
} else {
MOZ_ASSERT(aKey == MediaControlKey::Stop,
"Not supported key for media element!");
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!");
}
}

View File

@@ -351,27 +351,30 @@ void ContentMediaController::RemoveReceiver(
mReceivers.RemoveElement(aListener);
}
void ContentMediaController::HandleMediaKey(MediaControlKey aKey) {
void ContentMediaController::HandleMediaKey(MediaControlKey aKey,
Maybe<SeekDetails> aDetails) {
MOZ_ASSERT(NS_IsMainThread());
if (mReceivers.IsEmpty()) {
return;
}
LOG("Handle '%s' event, receiver num=%zu", GetEnumString(aKey).get(),
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
switch (aKey) {
case MediaControlKey::Pause:
PauseOrStopMedia();
return;
case MediaControlKey::Play:
[[fallthrough]];
case MediaControlKey::Stop:
case MediaControlKey::Seekto:
case MediaControlKey::Seekforward:
case MediaControlKey::Seekbackward:
// When receiving `Stop`, the amount of receiver would vary during the
// iteration, so we use the backward iteration to avoid accessing the
// index which is over the array length.
for (auto& receiver : Reversed(mReceivers)) {
receiver->HandleMediaKey(aKey);
receiver->HandleMediaKey(aKey, aDetails);
}
return;
default:

View File

@@ -24,7 +24,8 @@ class ContentMediaControlKeyReceiver {
static ContentMediaControlKeyReceiver* Get(BrowsingContext* aBC);
// 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;
};
@@ -94,7 +95,8 @@ class ContentMediaController final : public ContentMediaAgent,
void RemoveReceiver(ContentMediaControlKeyReceiver* aListener) override;
// ContentMediaControlKeyReceiver method
void HandleMediaKey(MediaControlKey aKey) override;
void HandleMediaKey(MediaControlKey aKey,
Maybe<SeekDetails> aDetails = Nothing()) override;
private:
~ContentMediaController() = default;

View File

@@ -42,12 +42,12 @@ MediaSession* ContentPlaybackController::GetMediaSession() const {
}
void ContentPlaybackController::NotifyContentMediaControlKeyReceiver(
MediaControlKey aKey) {
MediaControlKey aKey, Maybe<SeekDetails> aDetails) {
if (RefPtr<ContentMediaControlKeyReceiver> receiver =
ContentMediaControlKeyReceiver::Get(mBC)) {
LOG("Handle '%s' in default behavior for BC %" PRIu64,
GetEnumString(aKey).get(), mBC->Id());
receiver->HandleMediaKey(aKey);
receiver->HandleMediaKey(aKey, aDetails);
}
}
@@ -124,8 +124,12 @@ void ContentPlaybackController::SeekBackward(double aSeekOffset) {
MediaSessionActionDetails details;
details.mAction = MediaSessionAction::Seekbackward;
details.mSeekOffset.Construct(aSeekOffset);
RefPtr<MediaSession> session = GetMediaSession();
if (IsMediaSessionActionSupported(details.mAction)) {
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;
details.mAction = MediaSessionAction::Seekforward;
details.mSeekOffset.Construct(aSeekOffset);
RefPtr<MediaSession> session = GetMediaSession();
if (IsMediaSessionActionSupported(details.mAction)) {
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;
details.mAction = MediaSessionAction::Seekto;
details.mSeekTime.Construct(aSeekTime);
RefPtr<MediaSession> session = GetMediaSession();
if (aFastSeek) {
details.mFastSeek.Construct(aFastSeek);
}
if (IsMediaSessionActionSupported(details.mAction)) {
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);
private:
void NotifyContentMediaControlKeyReceiver(MediaControlKey aKey);
void NotifyContentMediaControlKeyReceiver(
MediaControlKey aKey, Maybe<SeekDetails> aDetails = Nothing());
void NotifyMediaSession(MediaSessionAction aAction);
void NotifyMediaSession(const MediaSessionActionDetails& aDetails);
void NotifyMediaSessionWhenActionIsSupported(MediaSessionAction aAction);

View File

@@ -53,10 +53,11 @@ RefPtr<MediaControlService> MediaControlService::GetService() {
/* static */
void MediaControlService::GenerateMediaControlKey(const GlobalObject& global,
MediaControlKey aKey) {
MediaControlKey aKey,
double aSeekTime) {
RefPtr<MediaControlService> service = MediaControlService::GetService();
if (service) {
service->GenerateTestMediaControlKey(aKey);
service->GenerateTestMediaControlKey(aKey, aSeekTime);
}
}
@@ -245,20 +246,21 @@ MediaController* MediaControlService::GetMainController() const {
return mControllerManager->GetMainController();
}
void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey) {
void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey,
double aSeekValue) {
if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
return;
}
// Generate seek details when necessary
switch (aKey) {
case MediaControlKey::Seekto:
mMediaKeysHandler->OnActionPerformed(
MediaControlAction(aKey, SeekDetails(0.0, false /* fast seek */)));
mMediaKeysHandler->OnActionPerformed(MediaControlAction(
aKey, SeekDetails(aSeekValue, false /* fast seek */)));
break;
case MediaControlKey::Seekbackward:
case MediaControlKey::Seekforward:
mMediaKeysHandler->OnActionPerformed(
MediaControlAction(aKey, SeekDetails(0.0)));
MediaControlAction(aKey, SeekDetails(aSeekValue)));
break;
default:
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.
static void GenerateMediaControlKey(const GlobalObject& global,
MediaControlKey aKey);
MediaControlKey aKey, double aSeekTime);
static void GetCurrentActiveMediaMetadata(const GlobalObject& aGlobal,
MediaMetadataInit& aMetadata);
static MediaSessionPlaybackState GetCurrentMediaSessionPlaybackState(
@@ -70,7 +70,7 @@ class MediaControlService final : public nsIObserver {
* generate fake media control key events, get the media metadata and playback
* state from the main controller.
*/
void GenerateTestMediaControlKey(MediaControlKey aKey);
void GenerateTestMediaControlKey(MediaControlKey aKey, double aSeekValue);
MediaMetadataBase GetMainControllerMediaMetadata() const;
MediaSessionPlaybackState GetMainControllerPlaybackState() const;

View File

@@ -75,9 +75,10 @@ void MediaController::GetMetadata(MediaMetadataInit& aMetadata,
}
static const MediaControlKey sDefaultSupportedKeys[] = {
MediaControlKey::Focus, MediaControlKey::Play, MediaControlKey::Pause,
MediaControlKey::Playpause, MediaControlKey::Stop,
};
MediaControlKey::Focus, MediaControlKey::Play,
MediaControlKey::Pause, MediaControlKey::Playpause,
MediaControlKey::Stop, MediaControlKey::Seekto,
MediaControlKey::Seekforward, MediaControlKey::Seekbackward};
static void GetDefaultSupportedKeys(nsTArray<MediaControlKey>& aKeys) {
for (const auto& key : sDefaultSupportedKeys) {
@@ -183,13 +184,23 @@ bool MediaController::IsActive() const { return mIsActive; };
bool MediaController::ShouldPropagateActionToAllContexts(
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
// `ContentMediaController::HandleMediaKey`.
return aAction.mKey.isSome() &&
(aAction.mKey.value() == MediaControlKey::Play ||
aAction.mKey.value() == MediaControlKey::Pause ||
aAction.mKey.value() == MediaControlKey::Stop);
if (aAction.mKey.isSome()) {
switch (aAction.mKey.value()) {
case MediaControlKey::Play:
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(

View File

@@ -12,7 +12,7 @@ const videoId = "video";
/**
* 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
* session, then it should use the customized action handler it it has one.
* Otherwise, the default action handler should be used.
@@ -47,6 +47,24 @@ add_task(async function triggerDefaultActionHandler() {
info(`default action handler should pause media`);
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`);
await simulateMediaAction(tab, "play");
@@ -136,7 +154,7 @@ add_task(
await pauseAllMedia(tab);
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 checkOrWaitUntilMediaPlays(tab, { videoId });
@@ -147,7 +165,7 @@ add_task(
await checkOrWaitUntilMediaPlays(tab, { frameId });
} else {
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 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) {
return SpecialPowers.spawn(
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;
if (!controller.isActive) {
await new Promise(r => (controller.onactivated = r));
}
MediaControlService.generateMediaControlKey(action);
MediaControlService.generateMediaControlKey(action, seekValue);
}
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";
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() {
await SpecialPowers.pushPrefEnv({
@@ -80,12 +89,7 @@ add_task(async function testSettingActionsWhichAreNotDefaultKeys() {
await playMedia(tab, testVideoId);
info(`create media session but not set any action handler`);
let nonDefaultActions = [
"seekbackward",
"seekforward",
"previoustrack",
"nexttrack",
];
let nonDefaultActions = ["previoustrack", "nexttrack"];
await setMediaSessionSupportedAction(tab, nonDefaultActions);
info(

View File

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