Bug 1894522 - Implement default seek handlers r=alwu,media-playback-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D219016
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
2
tools/@types/lib.gecko.dom.d.ts
vendored
2
tools/@types/lib.gecko.dom.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user