summaryrefslogtreecommitdiffstats
path: root/dom/media/mediacontrol
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
commita90a5cba08fdf6c0ceb95101c275108a152a3aed (patch)
tree532507288f3defd7f4dcf1af49698bcb76034855 /dom/media/mediacontrol
parentAdding debian version 126.0.1-1. (diff)
downloadfirefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.tar.xz
firefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/mediacontrol')
-rw-r--r--dom/media/mediacontrol/ContentMediaController.cpp31
-rw-r--r--dom/media/mediacontrol/ContentMediaController.h3
-rw-r--r--dom/media/mediacontrol/MediaControlKeyManager.cpp7
-rw-r--r--dom/media/mediacontrol/MediaControlService.cpp1
-rw-r--r--dom/media/mediacontrol/MediaPlaybackStatus.cpp64
-rw-r--r--dom/media/mediacontrol/MediaPlaybackStatus.h16
-rw-r--r--dom/media/mediacontrol/MediaStatusManager.cpp29
-rw-r--r--dom/media/mediacontrol/MediaStatusManager.h17
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js243
-rw-r--r--dom/media/mediacontrol/tests/browser/head.js81
10 files changed, 446 insertions, 46 deletions
diff --git a/dom/media/mediacontrol/ContentMediaController.cpp b/dom/media/mediacontrol/ContentMediaController.cpp
index e1fe574d9b..0c3bbbecdc 100644
--- a/dom/media/mediacontrol/ContentMediaController.cpp
+++ b/dom/media/mediacontrol/ContentMediaController.cpp
@@ -304,6 +304,37 @@ void ContentMediaAgent::UpdatePositionState(
}
}
+void ContentMediaAgent::UpdateGuessedPositionState(
+ uint64_t aBrowsingContextId, const nsID& aMediaId,
+ const Maybe<PositionState>& aState) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ if (aState) {
+ LOG("Update guessed position state for BC %" PRId64
+ " media id %s (duration=%f, playbackRate=%f, position=%f)",
+ bc->Id(), aMediaId.ToString().get(), aState->mDuration,
+ aState->mPlaybackRate, aState->mLastReportedPlaybackPosition);
+ } else {
+ LOG("Clear guessed position state for BC %" PRId64 " media id %s", bc->Id(),
+ aMediaId.ToString().get());
+ }
+
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyGuessedPositionStateChanged(bc, aMediaId,
+ aState);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->UpdateGuessedPositionState(bc->Id(), aMediaId, aState);
+ }
+}
+
ContentMediaController::ContentMediaController(uint64_t aId) {
LOG("Create content media controller for BC %" PRId64, aId);
}
diff --git a/dom/media/mediacontrol/ContentMediaController.h b/dom/media/mediacontrol/ContentMediaController.h
index a58be24b9d..236b3b254d 100644
--- a/dom/media/mediacontrol/ContentMediaController.h
+++ b/dom/media/mediacontrol/ContentMediaController.h
@@ -67,6 +67,9 @@ class ContentMediaAgent : public IMediaInfoUpdater {
bool aIsInFullScreen) override;
void UpdatePositionState(uint64_t aBrowsingContextId,
const Maybe<PositionState>& aState) override;
+ void UpdateGuessedPositionState(uint64_t aBrowsingContextId,
+ const nsID& aMediaId,
+ const Maybe<PositionState>& aState) override;
// Use these methods to register/unregister `ContentMediaControlKeyReceiver`
// in order to listen to media control key events.
diff --git a/dom/media/mediacontrol/MediaControlKeyManager.cpp b/dom/media/mediacontrol/MediaControlKeyManager.cpp
index b40d3af91e..92e2679bdd 100644
--- a/dom/media/mediacontrol/MediaControlKeyManager.cpp
+++ b/dom/media/mediacontrol/MediaControlKeyManager.cpp
@@ -107,6 +107,7 @@ void MediaControlKeyManager::StopMonitoringControlKeys() {
nullptr);
obs->NotifyObservers(nullptr, "media-displayed-metadata-changed",
nullptr);
+ obs->NotifyObservers(nullptr, "media-position-state-changed", nullptr);
}
}
}
@@ -197,6 +198,12 @@ void MediaControlKeyManager::SetPositionState(
if (mEventSource && mEventSource->IsOpened()) {
mEventSource->SetPositionState(aState);
}
+
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-position-state-changed", nullptr);
+ }
+ }
}
void MediaControlKeyManager::OnPreferenceChange() {
diff --git a/dom/media/mediacontrol/MediaControlService.cpp b/dom/media/mediacontrol/MediaControlService.cpp
index f45ab4253d..c64749f556 100644
--- a/dom/media/mediacontrol/MediaControlService.cpp
+++ b/dom/media/mediacontrol/MediaControlService.cpp
@@ -482,6 +482,7 @@ void MediaControlService::ControllerManager::UpdateMainControllerInternal(
mSource->SetPlaybackState(mMainController->PlaybackState());
mSource->SetMediaMetadata(mMainController->GetCurrentMediaMetadata());
mSource->SetSupportedMediaKeys(mMainController->GetSupportedMediaKeys());
+ mSource->SetPositionState(mMainController->GetCurrentPositionState());
ConnectMainControllerEvents();
}
diff --git a/dom/media/mediacontrol/MediaPlaybackStatus.cpp b/dom/media/mediacontrol/MediaPlaybackStatus.cpp
index 80dedf8599..434d6dbd7e 100644
--- a/dom/media/mediacontrol/MediaPlaybackStatus.cpp
+++ b/dom/media/mediacontrol/MediaPlaybackStatus.cpp
@@ -71,6 +71,23 @@ void MediaPlaybackStatus::UpdateMediaAudibleState(uint64_t aContextId,
}
}
+void MediaPlaybackStatus::UpdateGuessedPositionState(
+ uint64_t aContextId, const nsID& aElementId,
+ const Maybe<PositionState>& aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aState) {
+ LOG("Update guessed position state for context %" PRIu64
+ " element %s (duration=%f, playbackRate=%f, position=%f)",
+ aContextId, aElementId.ToString().get(), aState->mDuration,
+ aState->mPlaybackRate, aState->mLastReportedPlaybackPosition);
+ } else {
+ LOG("Clear guessed position state for context %" PRIu64 " element %s",
+ aContextId, aElementId.ToString().get());
+ }
+ ContextMediaInfo& info = GetNotNullContextInfo(aContextId);
+ info.UpdateGuessedPositionState(aElementId, aState);
+}
+
bool MediaPlaybackStatus::IsPlaying() const {
MOZ_ASSERT(NS_IsMainThread());
return std::any_of(mContextInfoMap.Values().cbegin(),
@@ -92,6 +109,35 @@ bool MediaPlaybackStatus::IsAnyMediaBeingControlled() const {
[](const auto& info) { return info->IsAnyMediaBeingControlled(); });
}
+Maybe<PositionState> MediaPlaybackStatus::GuessedMediaPositionState(
+ Maybe<uint64_t> aPreferredContextId) const {
+ auto contextId = aPreferredContextId;
+ if (!contextId) {
+ contextId = mOwningAudioFocusContextId;
+ }
+
+ // either the preferred or focused context
+ if (contextId) {
+ auto entry = mContextInfoMap.Lookup(*contextId);
+ if (!entry) {
+ return Nothing();
+ }
+ LOG("Using guessed position state from preferred/focused BC %" PRId64,
+ *contextId);
+ return entry.Data()->GuessedPositionState();
+ }
+
+ // look for the first position state
+ for (const auto& context : mContextInfoMap.Values()) {
+ auto state = context->GuessedPositionState();
+ if (state) {
+ LOG("Using guessed position state from BC %" PRId64, context->Id());
+ return state;
+ }
+ }
+ return Nothing();
+}
+
MediaPlaybackStatus::ContextMediaInfo&
MediaPlaybackStatus::GetNotNullContextInfo(uint64_t aContextId) {
MOZ_ASSERT(NS_IsMainThread());
@@ -139,4 +185,22 @@ bool MediaPlaybackStatus::IsContextOwningAudioFocus(uint64_t aContextId) const {
: false;
}
+Maybe<PositionState>
+MediaPlaybackStatus::ContextMediaInfo::GuessedPositionState() const {
+ if (mGuessedPositionStateMap.Count() != 1) {
+ LOG("Count is %d", mGuessedPositionStateMap.Count());
+ return Nothing();
+ }
+ return Some(mGuessedPositionStateMap.begin()->GetData());
+}
+
+void MediaPlaybackStatus::ContextMediaInfo::UpdateGuessedPositionState(
+ const nsID& aElementId, const Maybe<PositionState>& aState) {
+ if (aState) {
+ mGuessedPositionStateMap.InsertOrUpdate(aElementId, *aState);
+ } else {
+ mGuessedPositionStateMap.Remove(aElementId);
+ }
+}
+
} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/MediaPlaybackStatus.h b/dom/media/mediacontrol/MediaPlaybackStatus.h
index da597e4dfa..f9ac25f73d 100644
--- a/dom/media/mediacontrol/MediaPlaybackStatus.h
+++ b/dom/media/mediacontrol/MediaPlaybackStatus.h
@@ -7,9 +7,11 @@
#include "mozilla/Maybe.h"
#include "mozilla/RefPtr.h"
+#include "mozilla/dom/MediaSession.h"
#include "nsISupportsImpl.h"
#include "nsTArray.h"
#include "nsTHashMap.h"
+#include "nsID.h"
namespace mozilla::dom {
@@ -63,10 +65,14 @@ class MediaPlaybackStatus final {
public:
void UpdateMediaPlaybackState(uint64_t aContextId, MediaPlaybackState aState);
void UpdateMediaAudibleState(uint64_t aContextId, MediaAudibleState aState);
+ void UpdateGuessedPositionState(uint64_t aContextId, const nsID& aElementId,
+ const Maybe<PositionState>& aState);
bool IsPlaying() const;
bool IsAudible() const;
bool IsAnyMediaBeingControlled() const;
+ Maybe<PositionState> GuessedMediaPositionState(
+ Maybe<uint64_t> aPreferredContextId) const;
Maybe<uint64_t> GetAudioFocusOwnerContextId() const;
@@ -121,6 +127,10 @@ class MediaPlaybackStatus final {
bool IsAnyMediaBeingControlled() const { return mControlledMediaNum > 0; }
uint64_t Id() const { return mContextId; }
+ Maybe<PositionState> GuessedPositionState() const;
+ void UpdateGuessedPositionState(const nsID& aElementId,
+ const Maybe<PositionState>& aState);
+
private:
/**
* The possible value for those three numbers should follow this rule,
@@ -130,6 +140,12 @@ class MediaPlaybackStatus final {
uint32_t mAudibleMediaNum = 0;
uint32_t mPlayingMediaNum = 0;
uint64_t mContextId = 0;
+
+ /**
+ * Contains the guessed position state of all media elements in this
+ * browsing context identified by their ID.
+ */
+ nsTHashMap<nsID, PositionState> mGuessedPositionStateMap;
};
ContextMediaInfo& GetNotNullContextInfo(uint64_t aContextId);
diff --git a/dom/media/mediacontrol/MediaStatusManager.cpp b/dom/media/mediacontrol/MediaStatusManager.cpp
index 633ae19a44..6e86dbf2eb 100644
--- a/dom/media/mediacontrol/MediaStatusManager.cpp
+++ b/dom/media/mediacontrol/MediaStatusManager.cpp
@@ -380,6 +380,29 @@ void MediaStatusManager::UpdatePositionState(
mPositionStateChangedEvent.Notify(aState);
}
+void MediaStatusManager::UpdateGuessedPositionState(
+ uint64_t aBrowsingContextId, const nsID& aMediaId,
+ const Maybe<PositionState>& aGuessedState) {
+ mPlaybackStatusDelegate.UpdateGuessedPositionState(aBrowsingContextId,
+ aMediaId, aGuessedState);
+
+ // The position state comes from a non-active media session and
+ // there is another one active (with some metadata).
+ if (mActiveMediaSessionContextId &&
+ *mActiveMediaSessionContextId != aBrowsingContextId) {
+ return;
+ }
+
+ // media session is declared for the updated session, but there's no active
+ // session - it will get emitted once the session becomes active
+ if (mMediaSessionInfoMap.Contains(aBrowsingContextId) &&
+ !mActiveMediaSessionContextId) {
+ return;
+ }
+
+ mPositionStateChangedEvent.Notify(GetCurrentPositionState());
+}
+
void MediaStatusManager::NotifySupportedKeysChangedIfNeeded(
uint64_t aBrowsingContextId) {
// Only the active media session's supported actions would be shown in virtual
@@ -431,11 +454,13 @@ MediaMetadataBase MediaStatusManager::GetCurrentMediaMetadata() const {
Maybe<PositionState> MediaStatusManager::GetCurrentPositionState() const {
if (mActiveMediaSessionContextId) {
auto info = mMediaSessionInfoMap.Lookup(*mActiveMediaSessionContextId);
- if (info) {
+ if (info && info->mPositionState) {
return info->mPositionState;
}
}
- return Nothing();
+
+ return mPlaybackStatusDelegate.GuessedMediaPositionState(
+ mActiveMediaSessionContextId);
}
void MediaStatusManager::FillMissingTitleAndArtworkIfNeeded(
diff --git a/dom/media/mediacontrol/MediaStatusManager.h b/dom/media/mediacontrol/MediaStatusManager.h
index a4216c8453..45f3ccccc5 100644
--- a/dom/media/mediacontrol/MediaStatusManager.h
+++ b/dom/media/mediacontrol/MediaStatusManager.h
@@ -120,6 +120,12 @@ class IMediaInfoUpdater {
// Use this method when media session update its position state.
virtual void UpdatePositionState(uint64_t aBrowsingContextId,
const Maybe<PositionState>& aState) = 0;
+
+ // Use this method to update controlled media's position state and the
+ // browsing context where controlled media exists.
+ virtual void UpdateGuessedPositionState(
+ uint64_t aBrowsingContextId, const nsID& aMediaId,
+ const Maybe<PositionState>& aGuessedState) = 0;
};
/**
@@ -165,12 +171,19 @@ class MediaStatusManager : public IMediaInfoUpdater {
MediaSessionAction aAction) override;
void UpdatePositionState(uint64_t aBrowsingContextId,
const Maybe<PositionState>& aState) override;
+ void UpdateGuessedPositionState(
+ uint64_t aBrowsingContextId, const nsID& aMediaId,
+ const Maybe<PositionState>& aGuessedState) override;
// Return active media session's metadata if active media session exists and
// it has already set its metadata. Otherwise, return default media metadata
// which is based on website's title and favicon.
MediaMetadataBase GetCurrentMediaMetadata() const;
+ // Return the active media session's position state. If the active media
+ // session doesn't exist or doesn't have any state, Nothing is returned.
+ Maybe<PositionState> GetCurrentPositionState() const;
+
bool IsMediaAudible() const;
bool IsMediaPlaying() const;
bool IsAnyMediaBeingControlled() const;
@@ -247,10 +260,6 @@ class MediaStatusManager : public IMediaInfoUpdater {
// media session doesn't exist, return 'None' instead.
MediaSessionPlaybackState GetCurrentDeclaredPlaybackState() const;
- // Return the active media session's position state. If the active media
- // session doesn't exist or doesn't have any state, Nothing is returned.
- Maybe<PositionState> GetCurrentPositionState() const;
-
// This state can match to the `guessed playback state` in the spec [1], it
// indicates if we have any media element playing within the tab which this
// controller belongs to. But currently we only take media elements into
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js b/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js
index 6074e2ee16..75f65eb34b 100644
--- a/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js
@@ -4,6 +4,7 @@ const IFRAME_URL =
"https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
const testVideoId = "video";
+const videoDuration = 5.589333;
add_task(async function setupTestingPref() {
await SpecialPowers.pushPrefEnv({
@@ -18,9 +19,15 @@ add_task(async function setupTestingPref() {
add_task(async function testSetPositionState() {
info(`open media page`);
const tab = await createLoadedTabWrapper(PAGE_URL);
+ logPositionStateChangeEvents(tab);
+
+ info(`apply initial position state`);
+ await applyPositionState(tab, { duration: 10 });
info(`start media`);
+ const initialPositionState = isNextPositionState(tab, { duration: 10 });
await playMedia(tab, testVideoId);
+ await initialPositionState;
info(`set duration only`);
await setPositionState(tab, {
@@ -47,9 +54,15 @@ add_task(async function testSetPositionState() {
add_task(async function testSetPositionStateFromInactiveMediaSession() {
info(`open media page`);
const tab = await createLoadedTabWrapper(PAGE_URL);
+ logPositionStateChangeEvents(tab);
+
+ info(`apply initial position state`);
+ await applyPositionState(tab, { duration: 10 });
info(`start media`);
+ const initialPositionState = isNextPositionState(tab, { duration: 10 });
await playMedia(tab, testVideoId);
+ await initialPositionState;
info(
`add an event listener to measure how many times the position state changes`
@@ -82,48 +95,193 @@ add_task(async function testSetPositionStateFromInactiveMediaSession() {
});
/**
- * The following are helper functions.
+ *
+ * @param {boolean} withMetadata
+ * Specifies if the tab should set metadata for the playing video
*/
-async function setPositionState(tab, positionState) {
+async function testGuessedPositionState(withMetadata) {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ logPositionStateChangeEvents(tab);
+
+ if (withMetadata) {
+ info(`set media metadata`);
+ await setMediaMetadata(tab, { title: "A Video" });
+ }
+
+ info(`start media`);
+ await emitsPositionState(() => playMedia(tab, testVideoId), tab, {
+ duration: videoDuration,
+ position: 0,
+ playbackRate: 1.0,
+ });
+
+ info(`set playback rate to 2x`);
+ await emitsPositionState(() => setPlaybackRate(tab, testVideoId, 2.0), tab, {
+ duration: videoDuration,
+ position: null, // ignored,
+ playbackRate: 2.0,
+ });
+
+ info(`seek to 1s`);
+ await emitsPositionState(() => setCurrentTime(tab, testVideoId, 1.0), tab, {
+ duration: videoDuration,
+ position: 1.0,
+ playbackRate: 2.0,
+ });
+
+ let positionChangedNum = 0;
const controller = tab.linkedBrowser.browsingContext.mediaController;
- const positionStateChanged = new Promise(r => {
- controller.addEventListener(
- "positionstatechange",
- event => {
- const { duration, playbackRate, position } = positionState;
- // duration is mandatory.
- is(
- event.duration,
- duration,
- `expected duration ${event.duration} is equal to ${duration}`
- );
-
- // Playback rate is optional, if it's not present, default should be 1.0
- if (playbackRate) {
- is(
- event.playbackRate,
- playbackRate,
- `expected playbackRate ${event.playbackRate} is equal to ${playbackRate}`
- );
- } else {
- is(event.playbackRate, 1.0, `expected default playbackRate is 1.0`);
- }
-
- // Position state is optional, if it's not present, default should be 0.0
- if (position) {
- is(
- event.position,
- position,
- `expected position ${event.position} is equal to ${position}`
- );
- } else {
- is(event.position, 0.0, `expected default position is 0.0`);
- }
- r();
- },
- { once: true }
- );
+ controller.onpositionstatechange = () => positionChangedNum++;
+
+ info(`pause media`);
+ // shouldn't generate an event
+ await pauseMedia(tab, testVideoId);
+
+ info(`seek to 2s`);
+ await emitsPositionState(() => setCurrentTime(tab, testVideoId, 2.0), tab, {
+ duration: videoDuration,
+ position: 2.0,
+ playbackRate: 2.0,
+ });
+
+ info(`start media`);
+ await emitsPositionState(() => playMedia(tab, testVideoId), tab, {
+ duration: videoDuration,
+ position: 2.0,
+ playbackRate: 2.0,
});
+
+ is(
+ positionChangedNum,
+ 2,
+ `We should only receive two of position changes, because pausing is effectless`
+ );
+
+ info(`remove tab`);
+ await tab.close();
+}
+
+add_task(async function testGuessedPositionStateWithMetadata() {
+ testGuessedPositionState(true);
+});
+
+add_task(async function testGuessedPositionStateWithoutMetadata() {
+ testGuessedPositionState(false);
+});
+
+/**
+ * @typedef {{
+ * duration: number,
+ * playbackRate?: number | null,
+ * position?: number | null,
+ * }} ExpectedPositionState
+ */
+
+/**
+ * Checks if the next received position state matches the expected one.
+ *
+ * @param {tab} tab
+ * The tab that contains the media
+ * @param {ExpectedPositionState} positionState
+ * The expected position state. `duration` is mandatory. `playbackRate`
+ * and `position` are optional. If they're `null`, they're ignored,
+ * otherwise if they're not present or undefined, they're expected to
+ * be the default value.
+ * @returns {Promise}
+ * Resolves when the event has been received
+ */
+async function isNextPositionState(tab, positionState) {
+ const got = await nextPositionState(tab);
+ isPositionState(got, positionState);
+}
+
+/**
+ * Waits for the next position state and returns it
+ *
+ * @param {tab} tab The tab to receive position state from
+ * @returns {Promise<MediaPositionState>} The emitted position state
+ */
+function nextPositionState(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ return new Promise(r => {
+ controller.addEventListener("positionstatechange", r, { once: true });
+ });
+}
+
+/**
+ * @param {MediaPositionState} got
+ * The received position state
+ * @param {ExpectedPositionState} expected
+ * The expected position state. `duration` is mandatory. `playbackRate`
+ * and `position` are optional. If they're `null`, they're ignored,
+ * otherwise if they're not present or undefined, they're expected to
+ * be the default value.
+ */
+function isPositionState(got, expected) {
+ const { duration, playbackRate, position } = expected;
+ // duration is mandatory.
+ isFuzzyEq(got.duration, duration, "duration");
+
+ // Playback rate is optional, if it's not present, default should be 1.0
+ if (typeof playbackRate === "number") {
+ isFuzzyEq(got.playbackRate, playbackRate, "playbackRate");
+ } else if (playbackRate !== null) {
+ is(got.playbackRate, 1.0, `expected default playbackRate is 1.0`);
+ }
+
+ // Position is optional, if it's not present, default should be 0.0
+ if (typeof position === "number") {
+ isFuzzyEq(got.position, position, "position");
+ } else if (position !== null) {
+ is(got.position, 0.0, `expected default position is 0.0`);
+ }
+}
+
+/**
+ * Checks if two numbers are equal within one significant digit
+ *
+ * @param {number} got
+ * The value received while testing
+ * @param {number} expected
+ * The expected value
+ * @param {string} role
+ * The role of the check (used for formatting)
+ */
+function isFuzzyEq(got, expected, role) {
+ expected = expected.toFixed(1);
+ got = got.toFixed(1);
+ is(got, expected, `expected ${role} ${got} to equal ${expected}`);
+}
+
+/**
+ * Test if `cb` emits a position state event.
+ *
+ * @param {() => (void | Promise<void>)} cb
+ * A callback that is expected to generate a position state event
+ * @param {tab} tab
+ * The tab that contains the media
+ * @param {ExpectedPositionState} positionState
+ * The expected position state to be generated.
+ */
+async function emitsPositionState(cb, tab, positionState) {
+ const positionStateChanged = isNextPositionState(tab, positionState);
+ await cb();
+ await positionStateChanged;
+}
+
+/**
+ * The following are helper functions.
+ */
+async function setPositionState(tab, positionState) {
+ await emitsPositionState(
+ () => applyPositionState(tab, positionState),
+ tab,
+ positionState
+ );
+}
+
+async function applyPositionState(tab, positionState) {
await SpecialPowers.spawn(
tab.linkedBrowser,
[positionState],
@@ -131,7 +289,12 @@ async function setPositionState(tab, positionState) {
content.navigator.mediaSession.setPositionState(positionState);
}
);
- await positionStateChanged;
+}
+
+async function setMediaMetadata(tab, metadata) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [metadata], data => {
+ content.navigator.mediaSession.metadata = new content.MediaMetadata(data);
+ });
}
async function setPositionStateOnInactiveMediaSession(tab) {
diff --git a/dom/media/mediacontrol/tests/browser/head.js b/dom/media/mediacontrol/tests/browser/head.js
index cac96c0bff..7c6a1e37e4 100644
--- a/dom/media/mediacontrol/tests/browser/head.js
+++ b/dom/media/mediacontrol/tests/browser/head.js
@@ -195,6 +195,58 @@ function checkOrWaitUntilMediaStartedPlaying(tab, elementId) {
}
/**
+ * Set the playback rate on a media element.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would check
+ * @param {string} elementId
+ * The element Id of the media which we would check
+ * @param {number} rate
+ * The playback rate to set
+ * @return {Promise}
+ * Resolve when the playback rate has been set
+ */
+function setPlaybackRate(tab, elementId, rate) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [elementId, rate],
+ (Id, rate) => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ video.playbackRate = rate;
+ }
+ );
+}
+
+/**
+ * Set the time on a media element.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would check
+ * @param {string} elementId
+ * The element Id of the media which we would check
+ * @param {number} currentTime
+ * The time to set
+ * @return {Promise}
+ * Resolve when the time has been set
+ */
+function setCurrentTime(tab, elementId, currentTime) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [elementId, currentTime],
+ (Id, currentTime) => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ video.currentTime = currentTime;
+ }
+ );
+}
+
+/**
* Returns a promise that resolves when the specific media stops playing.
*
* @param {tab} tab
@@ -390,6 +442,18 @@ function waitUntilMediaControllerAmountChanged() {
}
/**
+ * Wait until the position state that would be displayed on the virtual control
+ * interface changes. we would observe that by listening for
+ * `media-position-state-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-position-state-changed`
+ */
+function waitUntilPositionStateChanged() {
+ return BrowserUtils.promiseObserved("media-position-state-changed");
+}
+
+/**
* check if the media controll from given tab is active. If not, return a
* promise and resolve it when controller become active.
*/
@@ -400,3 +464,20 @@ async function checkOrWaitUntilControllerBecomeActive(tab) {
}
await new Promise(r => (controller.onactivated = r));
}
+
+/**
+ * Logs all `positionstatechange` events in a tab.
+ */
+function logPositionStateChangeEvents(tab) {
+ tab.linkedBrowser.browsingContext.mediaController.addEventListener(
+ "positionstatechange",
+ event =>
+ info(
+ `got position state: ${JSON.stringify({
+ duration: event.duration,
+ playbackRate: event.playbackRate,
+ position: event.position,
+ })}`
+ )
+ );
+}