diff --git a/dom/chrome-webidl/MediaController.webidl b/dom/chrome-webidl/MediaController.webidl index b16282877ae8..66467ec19954 100644 --- a/dom/chrome-webidl/MediaController.webidl +++ b/dom/chrome-webidl/MediaController.webidl @@ -54,8 +54,8 @@ interface MediaController : EventTarget { undefined stop(); undefined prevTrack(); undefined nextTrack(); - undefined seekBackward(); - undefined seekForward(); + undefined seekBackward(double seekOffset); + undefined seekForward(double seekOffset); undefined skipAd(); undefined seekTo(double seekTime, optional boolean fastSeek = false); }; diff --git a/dom/media/mediacontrol/ContentPlaybackController.cpp b/dom/media/mediacontrol/ContentPlaybackController.cpp index ba06ea1cdbec..a48d5eaedc27 100644 --- a/dom/media/mediacontrol/ContentPlaybackController.cpp +++ b/dom/media/mediacontrol/ContentPlaybackController.cpp @@ -120,12 +120,22 @@ void ContentPlaybackController::Pause() { } } -void ContentPlaybackController::SeekBackward() { - NotifyMediaSessionWhenActionIsSupported(MediaSessionAction::Seekbackward); +void ContentPlaybackController::SeekBackward(double aSeekOffset) { + MediaSessionActionDetails details; + details.mAction = MediaSessionAction::Seekbackward; + details.mSeekOffset.Construct(aSeekOffset); + if (IsMediaSessionActionSupported(details.mAction)) { + NotifyMediaSession(details); + } } -void ContentPlaybackController::SeekForward() { - NotifyMediaSessionWhenActionIsSupported(MediaSessionAction::Seekforward); +void ContentPlaybackController::SeekForward(double aSeekOffset) { + MediaSessionActionDetails details; + details.mAction = MediaSessionAction::Seekforward; + details.mSeekOffset.Construct(aSeekOffset); + if (IsMediaSessionActionSupported(details.mAction)) { + NotifyMediaSession(details); + } } void ContentPlaybackController::PreviousTrack() { @@ -195,18 +205,26 @@ void ContentMediaControlKeyHandler::HandleMediaControlAction( case MediaControlKey::Nexttrack: controller.NextTrack(); return; - case MediaControlKey::Seekbackward: - controller.SeekBackward(); + case MediaControlKey::Seekbackward: { + const SeekDetails& details = *aAction.mDetails; + MOZ_ASSERT(details.mRelativeSeekOffset); + controller.SeekBackward(details.mRelativeSeekOffset.value()); return; - case MediaControlKey::Seekforward: - controller.SeekForward(); + } + case MediaControlKey::Seekforward: { + const SeekDetails& details = *aAction.mDetails; + MOZ_ASSERT(details.mRelativeSeekOffset); + controller.SeekForward(details.mRelativeSeekOffset.value()); return; + } case MediaControlKey::Skipad: controller.SkipAd(); return; case MediaControlKey::Seekto: { const SeekDetails& details = *aAction.mDetails; - controller.SeekTo(details.mSeekTime, details.mFastSeek); + MOZ_ASSERT(details.mAbsolute); + controller.SeekTo(details.mAbsolute->mSeekTime, + details.mAbsolute->mFastSeek); return; } default: diff --git a/dom/media/mediacontrol/ContentPlaybackController.h b/dom/media/mediacontrol/ContentPlaybackController.h index a692dedde049..8b8e66e234f9 100644 --- a/dom/media/mediacontrol/ContentPlaybackController.h +++ b/dom/media/mediacontrol/ContentPlaybackController.h @@ -42,8 +42,8 @@ class MOZ_STACK_CLASS ContentPlaybackController { MOZ_CAN_RUN_SCRIPT_BOUNDARY void Focus(); void Play(); void Pause(); - void SeekBackward(); - void SeekForward(); + void SeekBackward(double aSeekOffset); + void SeekForward(double aSeekOffset); void PreviousTrack(); void NextTrack(); void SkipAd(); diff --git a/dom/media/mediacontrol/MediaControlIPC.h b/dom/media/mediacontrol/MediaControlIPC.h index 100e5832c222..71082dc1385e 100644 --- a/dom/media/mediacontrol/MediaControlIPC.h +++ b/dom/media/mediacontrol/MediaControlIPC.h @@ -35,8 +35,8 @@ struct ParamTraits mozilla::dom::MediaAudibleState::eAudible> {}; template <> -struct ParamTraits { - typedef mozilla::dom::SeekDetails paramType; +struct ParamTraits { + typedef mozilla::dom::AbsoluteSeek paramType; static void Write(MessageWriter* aWriter, const paramType& aParam) { WriteParam(aWriter, aParam.mSeekTime); @@ -52,6 +52,24 @@ struct ParamTraits { } }; +template <> +struct ParamTraits { + typedef mozilla::dom::SeekDetails paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mAbsolute); + WriteParam(aWriter, aParam.mRelativeSeekOffset); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mAbsolute) || + !ReadParam(aReader, &aResult->mRelativeSeekOffset)) { + return false; + } + return true; + } +}; + template <> struct ParamTraits { typedef mozilla::dom::MediaControlAction paramType; diff --git a/dom/media/mediacontrol/MediaControlKeySource.cpp b/dom/media/mediacontrol/MediaControlKeySource.cpp index db372fc57b56..d57d6853ba96 100644 --- a/dom/media/mediacontrol/MediaControlKeySource.cpp +++ b/dom/media/mediacontrol/MediaControlKeySource.cpp @@ -64,18 +64,26 @@ void MediaControlKeyHandler::OnActionPerformed( case MediaControlKey::Nexttrack: controller->NextTrack(); return; - case MediaControlKey::Seekbackward: - controller->SeekBackward(); + case MediaControlKey::Seekbackward: { + const SeekDetails& details = *aAction.mDetails; + MOZ_ASSERT(details.mRelativeSeekOffset); + controller->SeekBackward(fmin(details.mRelativeSeekOffset.value(), 10.0)); return; - case MediaControlKey::Seekforward: - controller->SeekForward(); + } + case MediaControlKey::Seekforward: { + const SeekDetails& details = *aAction.mDetails; + MOZ_ASSERT(details.mRelativeSeekOffset); + controller->SeekForward(fmin(details.mRelativeSeekOffset.value(), 10.0)); return; + } case MediaControlKey::Skipad: controller->SkipAd(); return; case MediaControlKey::Seekto: { const SeekDetails& details = *aAction.mDetails; - controller->SeekTo(details.mSeekTime, details.mFastSeek); + MOZ_ASSERT(details.mAbsolute); + controller->SeekTo(details.mAbsolute->mSeekTime, + details.mAbsolute->mFastSeek); return; } case MediaControlKey::Stop: diff --git a/dom/media/mediacontrol/MediaControlKeySource.h b/dom/media/mediacontrol/MediaControlKeySource.h index a3261b31f9fe..f05448ca898b 100644 --- a/dom/media/mediacontrol/MediaControlKeySource.h +++ b/dom/media/mediacontrol/MediaControlKeySource.h @@ -5,6 +5,7 @@ #ifndef DOM_MEDIA_MEDIACONTROL_MEDIACONTROLKEYSOURCE_H_ #define DOM_MEDIA_MEDIACONTROL_MEDIACONTROLKEYSOURCE_H_ +#include "mozilla/Maybe.h" #include "mozilla/dom/MediaControllerBinding.h" #include "mozilla/dom/MediaMetadata.h" #include "mozilla/dom/MediaSession.h" @@ -15,15 +16,20 @@ namespace mozilla::dom { // This is used to store seek related properties from MediaSessionActionDetails. -// However, currently we have no plan to support `seekOffset`. // https://w3c.github.io/mediasession/#the-mediasessionactiondetails-dictionary +struct AbsoluteSeek { + double mSeekTime; + bool mFastSeek; +}; struct SeekDetails { + Maybe mAbsolute; + Maybe mRelativeSeekOffset; + SeekDetails() = default; - explicit SeekDetails(double aSeekTime) : mSeekTime(aSeekTime) {} SeekDetails(double aSeekTime, bool aFastSeek) - : mSeekTime(aSeekTime), mFastSeek(aFastSeek) {} - double mSeekTime = 0.0; - bool mFastSeek = false; + : mAbsolute(Some(AbsoluteSeek{aSeekTime, aFastSeek})) {} + explicit SeekDetails(double aRelativeSeekOffset) + : mRelativeSeekOffset(Some(aRelativeSeekOffset)) {} }; struct MediaControlAction { diff --git a/dom/media/mediacontrol/MediaControlService.cpp b/dom/media/mediacontrol/MediaControlService.cpp index c64749f55640..3fca926175af 100644 --- a/dom/media/mediacontrol/MediaControlService.cpp +++ b/dom/media/mediacontrol/MediaControlService.cpp @@ -307,12 +307,19 @@ void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey) { if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) { return; } - // Generate a seek details for `seekto` - if (aKey == MediaControlKey::Seekto) { - mMediaKeysHandler->OnActionPerformed( - MediaControlAction(aKey, SeekDetails())); - } else { - mMediaKeysHandler->OnActionPerformed(MediaControlAction(aKey)); + // Generate seek details when necessary + switch (aKey) { + case MediaControlKey::Seekto: + mMediaKeysHandler->OnActionPerformed( + MediaControlAction(aKey, SeekDetails(0.0, false /* fast seek */))); + break; + case MediaControlKey::Seekbackward: + case MediaControlKey::Seekforward: + mMediaKeysHandler->OnActionPerformed( + MediaControlAction(aKey, SeekDetails(0.0))); + break; + default: + mMediaKeysHandler->OnActionPerformed(MediaControlAction(aKey)); } } diff --git a/dom/media/mediacontrol/MediaController.cpp b/dom/media/mediacontrol/MediaController.cpp index 4290e952b01c..a0443423807f 100644 --- a/dom/media/mediacontrol/MediaController.cpp +++ b/dom/media/mediacontrol/MediaController.cpp @@ -142,16 +142,16 @@ void MediaController::NextTrack() { MediaControlAction(MediaControlKey::Nexttrack)); } -void MediaController::SeekBackward() { +void MediaController::SeekBackward(double aSeekOffset) { LOG("Seek Backward"); - UpdateMediaControlActionToContentMediaIfNeeded( - MediaControlAction(MediaControlKey::Seekbackward)); + UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction( + MediaControlKey::Seekbackward, SeekDetails(aSeekOffset))); } -void MediaController::SeekForward() { +void MediaController::SeekForward(double aSeekOffset) { LOG("Seek Forward"); - UpdateMediaControlActionToContentMediaIfNeeded( - MediaControlAction(MediaControlKey::Seekforward)); + UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction( + MediaControlKey::Seekforward, SeekDetails(aSeekOffset))); } void MediaController::SkipAd() { diff --git a/dom/media/mediacontrol/MediaController.h b/dom/media/mediacontrol/MediaController.h index f95ff6368e23..8fec9c59e38b 100644 --- a/dom/media/mediacontrol/MediaController.h +++ b/dom/media/mediacontrol/MediaController.h @@ -36,8 +36,8 @@ class IMediaController { virtual void Stop() = 0; virtual void PrevTrack() = 0; virtual void NextTrack() = 0; - virtual void SeekBackward() = 0; - virtual void SeekForward() = 0; + virtual void SeekBackward(double aSeekOffset) = 0; + virtual void SeekForward(double aSeekOffset) = 0; virtual void SkipAd() = 0; virtual void SeekTo(double aSeekTime, bool aFastSeek) = 0; @@ -104,8 +104,8 @@ class MediaController final : public DOMEventTargetHelper, void Stop() override; void PrevTrack() override; void NextTrack() override; - void SeekBackward() override; - void SeekForward() override; + void SeekBackward(double aSeekOffset) override; + void SeekForward(double aSeekOffset) override; void SkipAd() override; void SeekTo(double aSeekTime, bool aFastSeek) override; diff --git a/dom/media/mediacontrol/tests/browser/browser.toml b/dom/media/mediacontrol/tests/browser/browser.toml index 2e7b642737b6..7fc5e6ebad3b 100644 --- a/dom/media/mediacontrol/tests/browser/browser.toml +++ b/dom/media/mediacontrol/tests/browser/browser.toml @@ -50,7 +50,7 @@ skip-if = ["verify && os == 'mac'"] # bug 1673509 ["browser_media_control_position_state.js"] skip-if = ["apple_catalina && !debug"] # Bug 1775892 -["browser_media_control_seekto.js"] +["browser_media_control_seek.js"] ["browser_media_control_stop_timer.js"] diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js b/dom/media/mediacontrol/tests/browser/browser_media_control_seek.js similarity index 59% rename from dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js rename to dom/media/mediacontrol/tests/browser/browser_media_control_seek.js index 7502c6d1b1db..aa2fe1eba936 100644 --- a/dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js +++ b/dom/media/mediacontrol/tests/browser/browser_media_control_seek.js @@ -28,7 +28,7 @@ add_task(async function testSetPositionState() { seekTime: seektime, }); - info(`seek to ${seektime} seconds and set fastseek to boolean`); + info(`seek to ${seektime} seconds and set fastseek to true`); await PerformSeekTo(tab, { seekTime: seektime, fastSeek: true, @@ -44,6 +44,28 @@ add_task(async function testSetPositionState() { await tab.close(); }); +add_task(async function testSeekRelative() { + info(`open media page`); + const tab = await createLoadedTabWrapper(PAGE_URL); + + info(`start media`); + await playMedia(tab, testVideoId); + + const seekoffset = 5; + info(`seek forward ${seekoffset} seconds`); + await PerformSeekRelative(tab, "seekforward", { + seekOffset: seekoffset, + }); + + info(`seek backward ${seekoffset} seconds`); + await PerformSeekRelative(tab, "seekbackward", { + seekOffset: seekoffset, + }); + + info(`remove tab`); + await tab.close(); +}); + /** * The following is helper function. */ @@ -82,10 +104,46 @@ async function PerformSeekTo(tab, seekDetails) { [testVideoId], Id => { const video = content.document.getElementById(Id); - return new Promise(r => (video.onseeking = r())); + return new Promise(r => (video.onseeked = r())); } ); const { seekTime, fastSeek } = seekDetails; tab.linkedBrowser.browsingContext.mediaController.seekTo(seekTime, fastSeek); await seekPromise; } + +async function PerformSeekRelative(tab, mode, seekDetails) { + await SpecialPowers.spawn( + tab.linkedBrowser, + [seekDetails, mode, testVideoId], + (seekDetails, mode, Id) => { + const { seekOffset } = seekDetails; + content.navigator.mediaSession.setActionHandler(mode, details => { + Assert.notEqual( + details.seekOffset, + undefined, + "Seekoffset must be presented" + ); + is(seekOffset, details.seekOffset, "Get correct seekoffset"); + + content.document.getElementById(Id).currentTime += + mode == "seekForward" ? seekOffset : -seekOffset; + }); + } + ); + const seekPromise = SpecialPowers.spawn( + tab.linkedBrowser, + [testVideoId], + Id => { + const video = content.document.getElementById(Id); + return new Promise(r => (video.onseeked = r())); + } + ); + const { seekOffset } = seekDetails; + if (mode === "seekforward") { + tab.linkedBrowser.browsingContext.mediaController.seekForward(seekOffset); + } else { + tab.linkedBrowser.browsingContext.mediaController.seekBackward(seekOffset); + } + await seekPromise; +} diff --git a/widget/gtk/MPRISServiceHandler.cpp b/widget/gtk/MPRISServiceHandler.cpp index f8265be79135..a100b747f2fd 100644 --- a/widget/gtk/MPRISServiceHandler.cpp +++ b/widget/gtk/MPRISServiceHandler.cpp @@ -49,7 +49,9 @@ static inline Maybe GetMediaControlKey( {"Pause", dom::MediaControlKey::Pause}, {"PlayPause", dom::MediaControlKey::Playpause}, {"Stop", dom::MediaControlKey::Stop}, - {"Play", dom::MediaControlKey::Play}}; + {"Play", dom::MediaControlKey::Play}, + {"SetPosition", dom::MediaControlKey::Seekto}, + {"Seek", dom::MediaControlKey::Seekforward}}; auto it = map.find(aMethodName); return it == map.end() ? Nothing() : Some(it->second); @@ -73,8 +75,33 @@ static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender, return; } + dom::SeekDetails seekDetails{}; + if (key.value() == dom::MediaControlKey::Seekto || + key.value() == dom::MediaControlKey::Seekforward) { + RefPtr child = dont_AddRef(g_variant_get_child_value( + aParameters, key.value() == dom::MediaControlKey::Seekto)); + double seekValue; + if (g_variant_is_of_type(child, G_VARIANT_TYPE_INT64)) { + seekValue = (double)g_variant_get_int64(child) / 1000000.0; + } else { + g_dbus_method_invocation_return_error( + aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "Invalid arguments for %s.%s.%s", aObjectPath, aInterfaceName, + aMethodName); + return; + } + if (key.value() == dom::MediaControlKey::Seekto) { + seekDetails = dom::SeekDetails(seekValue, false /* fast seek */); + } else if (seekValue > 0.0) { + seekDetails = dom::SeekDetails(seekValue); + } else { + key = Some(dom::MediaControlKey::Seekbackward); + seekDetails = dom::SeekDetails(-1 * seekValue); + } + } + MPRISServiceHandler* handler = static_cast(aUserData); - if (handler->PressKey(key.value())) { + if (handler->PressKey(dom::MediaControlAction(key.value(), seekDetails))) { g_dbus_method_invocation_return_value(aInvocation, nullptr); } else { g_dbus_method_invocation_return_error( @@ -100,6 +127,7 @@ enum class Property : uint8_t { eCanControl, eGetPlaybackStatus, eGetMetadata, + eGetPosition, }; static inline Maybe GetPairedKey(Property aProperty) { @@ -137,7 +165,8 @@ static inline Maybe GetProperty(const gchar* aPropertyName) { {"CanSeek", Property::eCanSeek}, {"CanControl", Property::eCanControl}, {"PlaybackStatus", Property::eGetPlaybackStatus}, - {"Metadata", Property::eGetMetadata}}; + {"Metadata", Property::eGetMetadata}, + {"Position", Property::eGetPosition}}; auto it = map.find(aPropertyName); return (it == map.end() ? Nothing() : Some(it->second)); @@ -170,16 +199,21 @@ static GVariant* HandleGetProperty(GDBusConnection* aConnection, return handler->GetPlaybackStatus(); case Property::eGetMetadata: return handler->GetMetadataAsGVariant(); + case Property::eGetPosition: { + CheckedInt64 position = + CheckedInt64((int64_t)handler->GetPositionSeconds()) * 1000000; + return g_variant_new_int64(position.isValid() ? position.value() : 0); + } case Property::eIdentity: return g_variant_new_string(handler->Identity()); case Property::eDesktopEntry: return g_variant_new_string(handler->DesktopEntry()); case Property::eHasTrackList: case Property::eCanQuit: - case Property::eCanSeek: return g_variant_new_boolean(false); // Play/Pause would be blocked if CanControl is false case Property::eCanControl: + case Property::eCanSeek: return g_variant_new_boolean(true); case Property::eCanRaise: case Property::eCanGoNext: @@ -443,14 +477,16 @@ const char* MPRISServiceHandler::DesktopEntry() const { return mDesktopEntry.get(); } -bool MPRISServiceHandler::PressKey(dom::MediaControlKey aKey) const { +bool MPRISServiceHandler::PressKey( + const dom::MediaControlAction& aAction) const { MOZ_ASSERT(mInitialized); - if (!IsMediaKeySupported(aKey)) { - LOGMPRIS("%s is not supported", dom::GetEnumString(aKey).get()); + if (!IsMediaKeySupported(aAction.mKey.value())) { + LOGMPRIS("%s is not supported", + dom::GetEnumString(aAction.mKey.value()).get()); return false; } - LOGMPRIS("Press %s", dom::GetEnumString(aKey).get()); - EmitEvent(aKey); + LOGMPRIS("Press %s", dom::GetEnumString(aAction.mKey.value()).get()); + EmitEvent(aAction); return true; } @@ -812,9 +848,10 @@ GVariant* MPRISServiceHandler::GetMetadataAsGVariant() const { return g_variant_builder_end(&builder); } -void MPRISServiceHandler::EmitEvent(dom::MediaControlKey aKey) const { +void MPRISServiceHandler::EmitEvent( + const dom::MediaControlAction& aAction) const { for (const auto& listener : mListeners) { - listener->OnActionPerformed(dom::MediaControlAction(aKey)); + listener->OnActionPerformed(aAction); } } @@ -860,6 +897,18 @@ void MPRISServiceHandler::SetSupportedMediaKeys( } } +void MPRISServiceHandler::SetPositionState( + const Maybe& aState) { + mPositionState = aState; +} + +double MPRISServiceHandler::GetPositionSeconds() const { + if (mPositionState.isSome()) { + return mPositionState.value().CurrentPlaybackPosition(); + } + return 0.0; +} + bool MPRISServiceHandler::IsMediaKeySupported(dom::MediaControlKey aKey) const { return mSupportedKeys & GetMediaKeyMask(aKey); } diff --git a/widget/gtk/MPRISServiceHandler.h b/widget/gtk/MPRISServiceHandler.h index 469090efc028..924693d4cfd5 100644 --- a/widget/gtk/MPRISServiceHandler.h +++ b/widget/gtk/MPRISServiceHandler.h @@ -72,13 +72,16 @@ class MPRISServiceHandler final : public dom::MediaControlKeySource { const char* Identity() const; const char* DesktopEntry() const; - bool PressKey(dom::MediaControlKey aKey) const; + bool PressKey(const dom::MediaControlAction& aAction) const; void SetMediaMetadata(const dom::MediaMetadataBase& aMetadata) override; GVariant* GetMetadataAsGVariant() const; void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override; + void SetPositionState(const Maybe& aState) override; + double GetPositionSeconds() const; + bool IsMediaKeySupported(dom::MediaControlKey aKey) const; void OwnName(GDBusConnection* aConnection); @@ -110,6 +113,8 @@ class MPRISServiceHandler final : public dom::MediaControlKeySource { // A bitmask indicating what keys are enabled uint32_t mSupportedKeys = 0; + Maybe mPositionState; + class MPRISMetadata : public dom::MediaMetadataBase { public: MPRISMetadata() = default; @@ -168,7 +173,7 @@ class MPRISServiceHandler final : public dom::MediaControlKeySource { static void OnBusAcquiredStatic(GDBusConnection* aConnection, const gchar* aName, gpointer aUserData); - void EmitEvent(dom::MediaControlKey aKey) const; + void EmitEvent(const dom::MediaControlAction& aAction) const; bool EmitMetadataChanged() const; diff --git a/widget/windows/WindowsSMTCProvider.cpp b/widget/windows/WindowsSMTCProvider.cpp index 3ddd26b6ccae..8cce5612def6 100644 --- a/widget/windows/WindowsSMTCProvider.cpp +++ b/widget/windows/WindowsSMTCProvider.cpp @@ -426,9 +426,9 @@ void WindowsSMTCProvider::OnPositionChangeRequested(double aPosition) const { } for (const auto& listener : mListeners) { - listener->OnActionPerformed( - mozilla::dom::MediaControlAction(mozilla::dom::MediaControlKey::Seekto, - mozilla::dom::SeekDetails(aPosition))); + listener->OnActionPerformed(mozilla::dom::MediaControlAction( + mozilla::dom::MediaControlKey::Seekto, + mozilla::dom::SeekDetails(aPosition, false /* fast seek */))); } }