Bug 1634192 - Implement Seek and SetPosition for MPRIS r=media-playback-reviewers,stransky,alwu,win-reviewers,gstoll

Differential Revision: https://phabricator.services.mozilla.com/D212939
This commit is contained in:
Alex T
2024-08-09 16:39:25 +00:00
parent a31177f356
commit 85e68ce06b
14 changed files with 229 additions and 60 deletions

View File

@@ -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);
};

View File

@@ -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:

View File

@@ -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();

View File

@@ -35,8 +35,8 @@ struct ParamTraits<mozilla::dom::MediaAudibleState>
mozilla::dom::MediaAudibleState::eAudible> {};
template <>
struct ParamTraits<mozilla::dom::SeekDetails> {
typedef mozilla::dom::SeekDetails paramType;
struct ParamTraits<mozilla::dom::AbsoluteSeek> {
typedef mozilla::dom::AbsoluteSeek paramType;
static void Write(MessageWriter* aWriter, const paramType& aParam) {
WriteParam(aWriter, aParam.mSeekTime);
@@ -52,6 +52,24 @@ struct ParamTraits<mozilla::dom::SeekDetails> {
}
};
template <>
struct ParamTraits<mozilla::dom::SeekDetails> {
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<mozilla::dom::MediaControlAction> {
typedef mozilla::dom::MediaControlAction paramType;

View File

@@ -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:

View File

@@ -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<AbsoluteSeek> mAbsolute;
Maybe<double> 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 {

View File

@@ -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));
}
}

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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"]

View File

@@ -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;
}

View File

@@ -49,7 +49,9 @@ static inline Maybe<dom::MediaControlKey> 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<GVariant> 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<MPRISServiceHandler*>(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<dom::MediaControlKey> GetPairedKey(Property aProperty) {
@@ -137,7 +165,8 @@ static inline Maybe<Property> 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<dom::PositionState>& 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);
}

View File

@@ -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<dom::PositionState>& 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<dom::PositionState> 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;

View File

@@ -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 */)));
}
}