From 59f930caf01e94f7ad2e6e9254ac9366c36b103f Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Wed, 9 Oct 2024 19:30:44 +0500 Subject: [PATCH 1/6] feat. better handling of media keys in browser --- packages/player/src/use-playback.ts | 74 +++++++++++++++----- packages/studio/src/components/PlayPause.tsx | 1 + 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/packages/player/src/use-playback.ts b/packages/player/src/use-playback.ts index 53da6350f91..92cfc43ec99 100644 --- a/packages/player/src/use-playback.ts +++ b/packages/player/src/use-playback.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ -import {useContext, useEffect, useRef} from 'react'; -import {Internals} from 'remotion'; -import {calculateNextFrame} from './calculate-next-frame.js'; -import {useIsBackgrounded} from './is-backgrounded.js'; -import {usePlayer} from './use-player.js'; +import { useCallback, useContext, useEffect, useRef } from 'react'; +import { Internals } from 'remotion'; +import { calculateNextFrame } from './calculate-next-frame.js'; +import { useIsBackgrounded } from './is-backgrounded.js'; +import { usePlayer } from './use-player.js'; export const usePlayback = ({ loop, @@ -12,6 +12,7 @@ export const usePlayback = ({ inFrame, outFrame, frameRef, + browserMediaControlsEnabled }: { loop: boolean; playbackRate: number; @@ -19,10 +20,11 @@ export const usePlayback = ({ inFrame: number | null; outFrame: number | null; frameRef: React.MutableRefObject; + browserMediaControlsEnabled?: boolean }) => { const config = Internals.useUnsafeVideoConfig(); const frame = Internals.Timeline.useTimelinePosition(); - const {playing, pause, emitter} = usePlayer(); + const { playing, pause, play, emitter } = usePlayer(); const setFrame = Internals.Timeline.useTimelineSetFrame(); const buffering = useRef(null); @@ -40,6 +42,44 @@ export const usePlayback = ({ ); } + // the functions to interact with the media session API + const setupMediaSession = useCallback((browserMediaControls: boolean | undefined) => { + if ('mediaSession' in navigator) { + // I could've checked for the input option here and return rightaway, but it would not override the default buggy behavior + + navigator.mediaSession.setActionHandler("play", () => { + if (browserMediaControls) { + play() + } + }); + navigator.mediaSession.setActionHandler("pause", () => { + if (browserMediaControls) { + pause() + } + }); + + } + }, + [pause, play]); + + const cleanupMediaSession = () => { + if ('mediaSession' in navigator) { + navigator.mediaSession.metadata = null; + navigator.mediaSession.setActionHandler('play', null); + navigator.mediaSession.setActionHandler('pause', null); + } + }; + + useEffect(() => { + // add the media session controls in accordance with the config + setupMediaSession(browserMediaControlsEnabled); + return () => { + cleanupMediaSession() + } + }, [setupMediaSession, browserMediaControlsEnabled]) + + // complete code for media session API + useEffect(() => { const onBufferClear = context.listenForBuffering(() => { buffering.current = performance.now(); @@ -67,13 +107,13 @@ export const usePlayback = ({ let hasBeenStopped = false; let reqAnimFrameCall: | { - type: 'raf'; - id: number; - } + type: 'raf'; + id: number; + } | { - type: 'timeout'; - id: Timer; - } + type: 'timeout'; + id: Timer; + } | null = null; let startedTime = performance.now(); let framesAdvanced = 0; @@ -99,7 +139,7 @@ export const usePlayback = ({ const actualFirstFrame = inFrame ?? 0; const currentFrame = frameRef.current; - const {nextFrame, framesToAdvance, hasEnded} = calculateNextFrame({ + const { nextFrame, framesToAdvance, hasEnded } = calculateNextFrame({ time, currentFrame, playbackSpeed: playbackRate, @@ -116,7 +156,7 @@ export const usePlayback = ({ nextFrame !== frameRef.current && (!hasEnded || moveToBeginningWhenEnded) ) { - setFrame((c) => ({...c, [config.id]: nextFrame})); + setFrame((c) => ({ ...c, [config.id]: nextFrame })); } if (hasEnded) { @@ -153,7 +193,7 @@ export const usePlayback = ({ id: setTimeout(callback, 1000 / config.fps), }; } else { - reqAnimFrameCall = {type: 'raf', id: requestAnimationFrame(callback)}; + reqAnimFrameCall = { type: 'raf', id: requestAnimationFrame(callback) }; } }; @@ -199,7 +239,7 @@ export const usePlayback = ({ return; } - emitter.dispatchTimeUpdate({frame: frameRef.current as number}); + emitter.dispatchTimeUpdate({ frame: frameRef.current as number }); lastTimeUpdateEvent.current = frameRef.current; }, 250); @@ -207,6 +247,6 @@ export const usePlayback = ({ }, [emitter, frameRef]); useEffect(() => { - emitter.dispatchFrameUpdate({frame}); + emitter.dispatchFrameUpdate({ frame }); }, [emitter, frame]); }; diff --git a/packages/studio/src/components/PlayPause.tsx b/packages/studio/src/components/PlayPause.tsx index 855d2e35be9..c5b76775ad4 100644 --- a/packages/studio/src/components/PlayPause.tsx +++ b/packages/studio/src/components/PlayPause.tsx @@ -63,6 +63,7 @@ export const PlayPause: React.FC<{ inFrame, outFrame, frameRef: remotionInternal_currentFrameRef, + browserMediaControlsEnabled: true }); const isStill = useIsStill(); From 4467d0bf377b3300d3bb5b18605c879730915154 Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Wed, 9 Oct 2024 21:20:54 +0500 Subject: [PATCH 2/6] fix formatting --- packages/player/src/use-playback.ts | 81 ++++++++++---------- packages/studio/src/components/PlayPause.tsx | 2 +- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/packages/player/src/use-playback.ts b/packages/player/src/use-playback.ts index 92cfc43ec99..70aa5265cec 100644 --- a/packages/player/src/use-playback.ts +++ b/packages/player/src/use-playback.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ -import { useCallback, useContext, useEffect, useRef } from 'react'; -import { Internals } from 'remotion'; -import { calculateNextFrame } from './calculate-next-frame.js'; -import { useIsBackgrounded } from './is-backgrounded.js'; -import { usePlayer } from './use-player.js'; +import {useCallback, useContext, useEffect, useRef} from 'react'; +import {Internals} from 'remotion'; +import {calculateNextFrame} from './calculate-next-frame.js'; +import {useIsBackgrounded} from './is-backgrounded.js'; +import {usePlayer} from './use-player.js'; export const usePlayback = ({ loop, @@ -12,7 +12,7 @@ export const usePlayback = ({ inFrame, outFrame, frameRef, - browserMediaControlsEnabled + browserMediaControlsEnabled, }: { loop: boolean; playbackRate: number; @@ -20,11 +20,11 @@ export const usePlayback = ({ inFrame: number | null; outFrame: number | null; frameRef: React.MutableRefObject; - browserMediaControlsEnabled?: boolean + browserMediaControlsEnabled?: boolean; }) => { const config = Internals.useUnsafeVideoConfig(); const frame = Internals.Timeline.useTimelinePosition(); - const { playing, pause, play, emitter } = usePlayer(); + const {playing, pause, play, emitter} = usePlayer(); const setFrame = Internals.Timeline.useTimelineSetFrame(); const buffering = useRef(null); @@ -43,24 +43,25 @@ export const usePlayback = ({ } // the functions to interact with the media session API - const setupMediaSession = useCallback((browserMediaControls: boolean | undefined) => { - if ('mediaSession' in navigator) { - // I could've checked for the input option here and return rightaway, but it would not override the default buggy behavior - - navigator.mediaSession.setActionHandler("play", () => { - if (browserMediaControls) { - play() - } - }); - navigator.mediaSession.setActionHandler("pause", () => { - if (browserMediaControls) { - pause() - } - }); - - } - }, - [pause, play]); + const setupMediaSession = useCallback( + (browserMediaControls: boolean | undefined) => { + if ('mediaSession' in navigator) { + // I could've checked for the input option here and return rightaway, but it would not override the default buggy behavior + + navigator.mediaSession.setActionHandler('play', () => { + if (browserMediaControls) { + play(); + } + }); + navigator.mediaSession.setActionHandler('pause', () => { + if (browserMediaControls) { + pause(); + } + }); + } + }, + [pause, play], + ); const cleanupMediaSession = () => { if ('mediaSession' in navigator) { @@ -74,9 +75,9 @@ export const usePlayback = ({ // add the media session controls in accordance with the config setupMediaSession(browserMediaControlsEnabled); return () => { - cleanupMediaSession() - } - }, [setupMediaSession, browserMediaControlsEnabled]) + cleanupMediaSession(); + }; + }, [setupMediaSession, browserMediaControlsEnabled]); // complete code for media session API @@ -107,13 +108,13 @@ export const usePlayback = ({ let hasBeenStopped = false; let reqAnimFrameCall: | { - type: 'raf'; - id: number; - } + type: 'raf'; + id: number; + } | { - type: 'timeout'; - id: Timer; - } + type: 'timeout'; + id: Timer; + } | null = null; let startedTime = performance.now(); let framesAdvanced = 0; @@ -139,7 +140,7 @@ export const usePlayback = ({ const actualFirstFrame = inFrame ?? 0; const currentFrame = frameRef.current; - const { nextFrame, framesToAdvance, hasEnded } = calculateNextFrame({ + const {nextFrame, framesToAdvance, hasEnded} = calculateNextFrame({ time, currentFrame, playbackSpeed: playbackRate, @@ -156,7 +157,7 @@ export const usePlayback = ({ nextFrame !== frameRef.current && (!hasEnded || moveToBeginningWhenEnded) ) { - setFrame((c) => ({ ...c, [config.id]: nextFrame })); + setFrame((c) => ({...c, [config.id]: nextFrame})); } if (hasEnded) { @@ -193,7 +194,7 @@ export const usePlayback = ({ id: setTimeout(callback, 1000 / config.fps), }; } else { - reqAnimFrameCall = { type: 'raf', id: requestAnimationFrame(callback) }; + reqAnimFrameCall = {type: 'raf', id: requestAnimationFrame(callback)}; } }; @@ -239,7 +240,7 @@ export const usePlayback = ({ return; } - emitter.dispatchTimeUpdate({ frame: frameRef.current as number }); + emitter.dispatchTimeUpdate({frame: frameRef.current as number}); lastTimeUpdateEvent.current = frameRef.current; }, 250); @@ -247,6 +248,6 @@ export const usePlayback = ({ }, [emitter, frameRef]); useEffect(() => { - emitter.dispatchFrameUpdate({ frame }); + emitter.dispatchFrameUpdate({frame}); }, [emitter, frame]); }; diff --git a/packages/studio/src/components/PlayPause.tsx b/packages/studio/src/components/PlayPause.tsx index c5b76775ad4..bb9a171a2a4 100644 --- a/packages/studio/src/components/PlayPause.tsx +++ b/packages/studio/src/components/PlayPause.tsx @@ -63,7 +63,7 @@ export const PlayPause: React.FC<{ inFrame, outFrame, frameRef: remotionInternal_currentFrameRef, - browserMediaControlsEnabled: true + browserMediaControlsEnabled: true, }); const isStill = useIsStill(); From 63af56274277f56998e858464f4ac3245345985e Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 10 Oct 2024 16:32:24 +0200 Subject: [PATCH 3/6] implement all modes --- packages/player/src/Player.tsx | 13 ++ packages/player/src/PlayerUI.tsx | 4 + packages/player/src/browser-mediasession.ts | 134 +++++++++++++++++++ packages/player/src/use-playback.ts | 51 ++----- packages/studio/src/components/PlayPause.tsx | 4 +- 5 files changed, 165 insertions(+), 41 deletions(-) create mode 100644 packages/player/src/browser-mediasession.ts diff --git a/packages/player/src/Player.tsx b/packages/player/src/Player.tsx index 04b885c970c..82de5d3e57e 100644 --- a/packages/player/src/Player.tsx +++ b/packages/player/src/Player.tsx @@ -25,6 +25,7 @@ import type { import type {PosterFillMode, RenderLoading, RenderPoster} from './PlayerUI.js'; import PlayerUI from './PlayerUI.js'; import {PLAYER_COMP_ID, SharedPlayerContexts} from './SharedPlayerContext.js'; +import type {BrowserMediaControlsBehavior} from './browser-mediasession.js'; import {PLAYER_CSS_CLASSNAME} from './player-css-classname.js'; import type {PlayerRef} from './player-methods.js'; import type {RenderVolumeSlider} from './render-volume-slider.js'; @@ -85,6 +86,7 @@ export type PlayerProps< readonly bufferStateDelayInMilliseconds?: number; readonly hideControlsWhenPointerDoesntMove?: boolean | number; readonly overflowVisible?: boolean; + readonly browserMediaControlsBehavior?: BrowserMediaControlsBehavior; } & CompProps & PropsIfHasProps; @@ -146,6 +148,7 @@ const PlayerFn = < hideControlsWhenPointerDoesntMove = true, overflowVisible = false, renderMuteButton, + browserMediaControlsBehavior: passedBrowserMediaControlsBehavior, ...componentProps }: PlayerProps, ref: MutableRefObject, @@ -348,6 +351,15 @@ const PlayerFn = < const actualInputProps = useMemo(() => inputProps ?? {}, [inputProps]); + const browserMediaControlsBehavior: BrowserMediaControlsBehavior = + useMemo(() => { + return ( + passedBrowserMediaControlsBehavior ?? { + mode: 'prevent-media-session', + } + ); + }, [passedBrowserMediaControlsBehavior]); + return ( diff --git a/packages/player/src/PlayerUI.tsx b/packages/player/src/PlayerUI.tsx index 363f28c6588..35b749a2a86 100644 --- a/packages/player/src/PlayerUI.tsx +++ b/packages/player/src/PlayerUI.tsx @@ -18,6 +18,7 @@ import type { RenderPlayPauseButton, } from './PlayerControls.js'; import {Controls} from './PlayerControls.js'; +import type {BrowserMediaControlsBehavior} from './browser-mediasession.js'; import { calculateCanvasTransformation, calculateContainerStyle, @@ -87,6 +88,7 @@ const PlayerUI: React.ForwardRefRenderFunction< readonly bufferStateDelayInMilliseconds: number; readonly hideControlsWhenPointerDoesntMove: boolean | number; readonly overflowVisible: boolean; + readonly browserMediaControlsBehavior: BrowserMediaControlsBehavior; } > = ( { @@ -123,6 +125,7 @@ const PlayerUI: React.ForwardRefRenderFunction< bufferStateDelayInMilliseconds, hideControlsWhenPointerDoesntMove, overflowVisible, + browserMediaControlsBehavior, }, ref, ) => { @@ -159,6 +162,7 @@ const PlayerUI: React.ForwardRefRenderFunction< inFrame, outFrame, frameRef: player.remotionInternal_currentFrameRef, + browserMediaControlsBehavior, }); useEffect(() => { diff --git a/packages/player/src/browser-mediasession.ts b/packages/player/src/browser-mediasession.ts new file mode 100644 index 00000000000..0e1128ee2cd --- /dev/null +++ b/packages/player/src/browser-mediasession.ts @@ -0,0 +1,134 @@ +import {useEffect} from 'react'; +import type {VideoConfig} from 'remotion'; +import {usePlayer} from './use-player.js'; + +export type BrowserMediaControlsBehavior = + | { + mode: 'do-nothing'; + } + | { + mode: 'prevent-media-session'; + } + | { + mode: 'register-media-session'; + }; + +export const useBrowserMediaSession = ({ + browserMediaControlsBehavior, + videoConfig, + playbackRate, +}: { + browserMediaControlsBehavior: BrowserMediaControlsBehavior; + videoConfig: VideoConfig | null; + playbackRate: number; +}) => { + const {playing, pause, play, emitter, getCurrentFrame, seek} = usePlayer(); + + useEffect(() => { + if (playing) { + navigator.mediaSession.playbackState = 'playing'; + } else { + navigator.mediaSession.playbackState = 'paused'; + } + }, [playing]); + + useEffect(() => { + const onTimeUpdate = () => { + if (!videoConfig) { + return; + } + + if (navigator.mediaSession) { + navigator.mediaSession.setPositionState({ + duration: videoConfig.durationInFrames / videoConfig.fps, + playbackRate, + position: getCurrentFrame() / videoConfig.fps, + }); + } + }; + + emitter.addEventListener('timeupdate', onTimeUpdate); + + return () => { + emitter.removeEventListener('timeupdate', onTimeUpdate); + }; + }, [emitter, getCurrentFrame, playbackRate, videoConfig]); + + useEffect(() => { + if (!navigator.mediaSession) { + return; + } + + if (browserMediaControlsBehavior.mode === 'do-nothing') { + return; + } + + navigator.mediaSession.setActionHandler('play', () => { + if (browserMediaControlsBehavior.mode === 'register-media-session') { + play(); + } + }); + navigator.mediaSession.setActionHandler('pause', () => { + if (browserMediaControlsBehavior.mode === 'register-media-session') { + pause(); + } + }); + navigator.mediaSession.setActionHandler('seekto', (event) => { + if ( + browserMediaControlsBehavior.mode === 'register-media-session' && + event.seekTime !== undefined && + videoConfig + ) { + seek(Math.round(event.seekTime * videoConfig.fps)); + } + }); + + navigator.mediaSession.setActionHandler('seekbackward', () => { + if ( + browserMediaControlsBehavior.mode === 'register-media-session' && + videoConfig + ) { + seek( + Math.max(0, Math.round((getCurrentFrame() - 10) * videoConfig.fps)), + ); + } + }); + + navigator.mediaSession.setActionHandler('seekforward', () => { + if ( + browserMediaControlsBehavior.mode === 'register-media-session' && + videoConfig + ) { + seek( + Math.max( + videoConfig.durationInFrames - 1, + Math.round((getCurrentFrame() + 10) * videoConfig.fps), + ), + ); + } + }); + + navigator.mediaSession.setActionHandler('previoustrack', () => { + if (browserMediaControlsBehavior.mode === 'register-media-session') { + seek(0); + } + }); + + return () => { + navigator.mediaSession.metadata = null; + navigator.mediaSession.setActionHandler('play', null); + navigator.mediaSession.setActionHandler('pause', null); + navigator.mediaSession.setActionHandler('seekto', null); + navigator.mediaSession.setActionHandler('seekbackward', null); + navigator.mediaSession.setActionHandler('seekforward', null); + navigator.mediaSession.setActionHandler('previoustrack', null); + }; + }, [ + browserMediaControlsBehavior.mode, + getCurrentFrame, + pause, + play, + seek, + videoConfig, + ]); +}; diff --git a/packages/player/src/use-playback.ts b/packages/player/src/use-playback.ts index 70aa5265cec..bcd1c4382ba 100644 --- a/packages/player/src/use-playback.ts +++ b/packages/player/src/use-playback.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ -import {useCallback, useContext, useEffect, useRef} from 'react'; +import {useContext, useEffect, useRef} from 'react'; import {Internals} from 'remotion'; +import type {BrowserMediaControlsBehavior} from './browser-mediasession.js'; +import {useBrowserMediaSession} from './browser-mediasession.js'; import {calculateNextFrame} from './calculate-next-frame.js'; import {useIsBackgrounded} from './is-backgrounded.js'; import {usePlayer} from './use-player.js'; @@ -12,7 +14,7 @@ export const usePlayback = ({ inFrame, outFrame, frameRef, - browserMediaControlsEnabled, + browserMediaControlsBehavior, }: { loop: boolean; playbackRate: number; @@ -20,11 +22,11 @@ export const usePlayback = ({ inFrame: number | null; outFrame: number | null; frameRef: React.MutableRefObject; - browserMediaControlsEnabled?: boolean; + browserMediaControlsBehavior: BrowserMediaControlsBehavior; }) => { const config = Internals.useUnsafeVideoConfig(); const frame = Internals.Timeline.useTimelinePosition(); - const {playing, pause, play, emitter} = usePlayer(); + const {playing, pause, emitter} = usePlayer(); const setFrame = Internals.Timeline.useTimelineSetFrame(); const buffering = useRef(null); @@ -42,42 +44,11 @@ export const usePlayback = ({ ); } - // the functions to interact with the media session API - const setupMediaSession = useCallback( - (browserMediaControls: boolean | undefined) => { - if ('mediaSession' in navigator) { - // I could've checked for the input option here and return rightaway, but it would not override the default buggy behavior - - navigator.mediaSession.setActionHandler('play', () => { - if (browserMediaControls) { - play(); - } - }); - navigator.mediaSession.setActionHandler('pause', () => { - if (browserMediaControls) { - pause(); - } - }); - } - }, - [pause, play], - ); - - const cleanupMediaSession = () => { - if ('mediaSession' in navigator) { - navigator.mediaSession.metadata = null; - navigator.mediaSession.setActionHandler('play', null); - navigator.mediaSession.setActionHandler('pause', null); - } - }; - - useEffect(() => { - // add the media session controls in accordance with the config - setupMediaSession(browserMediaControlsEnabled); - return () => { - cleanupMediaSession(); - }; - }, [setupMediaSession, browserMediaControlsEnabled]); + useBrowserMediaSession({ + browserMediaControlsBehavior, + playbackRate, + videoConfig: config, + }); // complete code for media session API diff --git a/packages/studio/src/components/PlayPause.tsx b/packages/studio/src/components/PlayPause.tsx index bb9a171a2a4..17efcfe1a3c 100644 --- a/packages/studio/src/components/PlayPause.tsx +++ b/packages/studio/src/components/PlayPause.tsx @@ -63,7 +63,9 @@ export const PlayPause: React.FC<{ inFrame, outFrame, frameRef: remotionInternal_currentFrameRef, - browserMediaControlsEnabled: true, + browserMediaControlsBehavior: { + mode: 'register-media-session', + }, }); const isStill = useIsStill(); From e3ad6c35659400bb08069e83dcfbc5ba9867ffb3 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 10 Oct 2024 16:58:27 +0200 Subject: [PATCH 4/6] document behavior --- packages/docs/docs/player/TableOfContents.tsx | 4 + packages/docs/docs/player/api.mdx | 5 + packages/docs/docs/player/media-keys.mdx | 129 ++++++++++++++++++ packages/docs/sidebars.js | 1 + packages/docs/src/data/articles.ts | 7 + .../articles-docs-player-media-keys.png | Bin 0 -> 60766 bytes packages/player/src/browser-mediasession.ts | 26 +++- 7 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 packages/docs/docs/player/media-keys.mdx create mode 100644 packages/docs/static/generated/articles-docs-player-media-keys.png diff --git a/packages/docs/docs/player/TableOfContents.tsx b/packages/docs/docs/player/TableOfContents.tsx index d9c0ce562e3..01ead978838 100644 --- a/packages/docs/docs/player/TableOfContents.tsx +++ b/packages/docs/docs/player/TableOfContents.tsx @@ -67,6 +67,10 @@ export const PlayerGuide: React.FC = () => { Custom controls
Recipes for custom Play buttons, volume sliders, etc.
+ + Media Keys +
Control what happens when users presses ⏯️
+
); diff --git a/packages/docs/docs/player/api.mdx b/packages/docs/docs/player/api.mdx index b9080a06851..b9ebbc4e53a 100644 --- a/packages/docs/docs/player/api.mdx +++ b/packages/docs/docs/player/api.mdx @@ -534,6 +534,11 @@ This allows you to flexibly implement custom UI for the buffer state. Makes the Player render things outside of the canvas. Useful if you have interactive elements in the video such as draggable elements. +### `browserMediaControlsBehavior` + +Controls what happens when the user presses the Play/Pause button on their keyboard or uses other controls such as Chromes built-in controls. +See [Media Keys Behavior](/docs/player/media-keys) for more information. + ## `PlayerRef` You may attach a ref to the player and control it in an imperative manner. diff --git a/packages/docs/docs/player/media-keys.mdx b/packages/docs/docs/player/media-keys.mdx new file mode 100644 index 00000000000..e26ee646811 --- /dev/null +++ b/packages/docs/docs/player/media-keys.mdx @@ -0,0 +1,129 @@ +--- +image: /generated/articles-docs-player-media-keys.png +id: media-keys +sidebar_label: 'Media Keys' +title: 'Media Key Behavior (Web MediaSession API)' +crumb: '@remotion/player' +--- + +# Media Keys Behavior + +This document is about the behavior when a user: + +- Presses the Play/Pause (⏯️) or Previous track (⏪) button on their keyboard +- Uses other controls such as Chromes built-in controls next to the user avatar to control the playback + +These behaviors are controlled by the [`browserMediaControlsBehavior`](/docs/player/player#browsermediacontrolsbehavior) prop. +The underlying Web API used is the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API). + +## Modes + +### `prevent-media-session` (default) + +```tsx twoslash +const otherProps = { + compositionHeight: 720, + compositionWidth: 1280, + inputProps: {}, + durationInFrames: 120, + fps: 30, + component: () => null, +}; +// ---cut--- + +import {Player} from '@remotion/player'; + +export const MyComp: React.FC = () => { + return ( + + ); +}; +``` + +In this mode, Remotion will not act on the user's media keys, but map any keybord action to a no-op. +This prevents that the Player is paused but the audio tags get resumed by pressing the Play/Pause button on the keyboard, which was a problem prior to Remotion v4.0.221. + +### `register-media-session` + +```tsx twoslash +const otherProps = { + compositionHeight: 720, + compositionWidth: 1280, + inputProps: {}, + durationInFrames: 120, + fps: 30, + component: () => null, +}; +// ---cut--- + +import {Player} from '@remotion/player'; + +export const MyComp: React.FC = () => { + return ( + + ); +}; +``` + +In this mode, Remotion will use the Media Session API to register handlers: + +- When the user presses the Play/Pause button on the keyboard + - Toggle the Remotion Player's state +- When the user presses the Previous track button on the keyboard + - Seek to the beginning of the video +- When the user presses the Fast Forward button on the keyboard + - Seek 10 seconds forward +- When the user presses the Rewind button on the keyboard + - Seek 10 seconds backward + +Also, Remotion will react to seeking events and inform the device about the current playback position and duration. + +### `do-nothing` + +```tsx twoslash +const otherProps = { + compositionHeight: 720, + compositionWidth: 1280, + inputProps: {}, + durationInFrames: 120, + fps: 30, + component: () => null, +}; +// ---cut--- + +import {Player} from '@remotion/player'; + +export const MyComp: React.FC = () => { + return ( + + ); +}; +``` + +Reverts to the behavior prior to Remotion v4.0.221. +Remotion will not react to any media keys, leaving the browser to handle the media keys. +This leads to the problem that the user can resume any media tag by pressing the Play/Pause button on the keyboard, without the Remotion Player also resuming. + +## When using multiple ``'s + +Remotion's `register-media-session` handler is supposed to only work with 1 Player mounted. +It is not defined which Player reacts to the media keys. + +Whe working with multiple Players, set one to `do-nothing` mode and the other to `register-media-session` mode to explicitly set the Media Keys for only 1 Player. + +If you want all Players to react to the media keys, you need to use `do-nothing` mode and implement this behavior yourself with the Player API. diff --git a/packages/docs/sidebars.js b/packages/docs/sidebars.js index 60a72c1ccc4..c5109ed144a 100644 --- a/packages/docs/sidebars.js +++ b/packages/docs/sidebars.js @@ -739,6 +739,7 @@ module.exports = { 'player/premounting', 'player/best-practices', 'player/custom-controls', + 'player/media-keys', ], }, diff --git a/packages/docs/src/data/articles.ts b/packages/docs/src/data/articles.ts index e2437f42552..9bacb41c096 100644 --- a/packages/docs/src/data/articles.ts +++ b/packages/docs/src/data/articles.ts @@ -3737,6 +3737,13 @@ export const articles = [ compId: 'articles-docs-player-custom-controls', crumb: '@remotion/player', }, + { + id: 'media-keys', + title: 'Media Key Behavior (Web MediaSession API)', + relativePath: 'docs/player/media-keys.mdx', + compId: 'articles-docs-player-media-keys', + crumb: '@remotion/player', + }, { id: 'player/index', title: '@remotion/player', diff --git a/packages/docs/static/generated/articles-docs-player-media-keys.png b/packages/docs/static/generated/articles-docs-player-media-keys.png new file mode 100644 index 0000000000000000000000000000000000000000..7eb9d81ea7882f2345ebef4866361a14143a74a1 GIT binary patch literal 60766 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 { + if (!navigator.mediaSession) { + return; + } + + if (browserMediaControlsBehavior.mode === 'do-nothing') { + return; + } + if (playing) { navigator.mediaSession.playbackState = 'playing'; } else { navigator.mediaSession.playbackState = 'paused'; } - }, [playing]); + }, [browserMediaControlsBehavior.mode, playing]); useEffect(() => { + if (!navigator.mediaSession) { + return; + } + + if (browserMediaControlsBehavior.mode === 'do-nothing') { + return; + } + const onTimeUpdate = () => { if (!videoConfig) { return; @@ -52,7 +68,13 @@ export const useBrowserMediaSession = ({ return () => { emitter.removeEventListener('timeupdate', onTimeUpdate); }; - }, [emitter, getCurrentFrame, playbackRate, videoConfig]); + }, [ + browserMediaControlsBehavior.mode, + emitter, + getCurrentFrame, + playbackRate, + videoConfig, + ]); useEffect(() => { if (!navigator.mediaSession) { From 464f5f156426b749ac8ba38608ed928498067d28 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 10 Oct 2024 16:59:24 +0200 Subject: [PATCH 5/6] Update media-keys.mdx --- packages/docs/docs/player/media-keys.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/docs/docs/player/media-keys.mdx b/packages/docs/docs/player/media-keys.mdx index e26ee646811..981bc06c3b0 100644 --- a/packages/docs/docs/player/media-keys.mdx +++ b/packages/docs/docs/player/media-keys.mdx @@ -127,3 +127,7 @@ It is not defined which Player reacts to the media keys. Whe working with multiple Players, set one to `do-nothing` mode and the other to `register-media-session` mode to explicitly set the Media Keys for only 1 Player. If you want all Players to react to the media keys, you need to use `do-nothing` mode and implement this behavior yourself with the Player API. + +## In the Remotion Studio + +The behavior is set to `register-media-session` as of v4.0.221 and previously it behaved like `do-nothing`. From 5486e45d6aa9558e8240ba1b220e643a5c1d6f3f Mon Sep 17 00:00:00 2001 From: Hunain Ahmed <89797440+hunxjunedo@users.noreply.github.com> Date: Fri, 11 Oct 2024 01:32:57 +0500 Subject: [PATCH 6/6] spell fix in media-keys.mdx --- packages/docs/docs/player/media-keys.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/docs/player/media-keys.mdx b/packages/docs/docs/player/media-keys.mdx index 981bc06c3b0..4e6356577f4 100644 --- a/packages/docs/docs/player/media-keys.mdx +++ b/packages/docs/docs/player/media-keys.mdx @@ -124,7 +124,7 @@ This leads to the problem that the user can resume any media tag by pressing the Remotion's `register-media-session` handler is supposed to only work with 1 Player mounted. It is not defined which Player reacts to the media keys. -Whe working with multiple Players, set one to `do-nothing` mode and the other to `register-media-session` mode to explicitly set the Media Keys for only 1 Player. +When working with multiple Players, set one to `do-nothing` mode and the other to `register-media-session` mode to explicitly set the Media Keys for only 1 Player. If you want all Players to react to the media keys, you need to use `do-nothing` mode and implement this behavior yourself with the Player API.