Hooks
No hooks found in any category.
useMediaSession
sensorsInstallation
npx usehooks-cli@latest add use-media-session
Description
A hook for integrating with the Media Session API to control media playback from system UI, notifications, and hardware media keys. Allows setting metadata and handling media control actions.
Parameters
Name | Type | Default | Description |
---|---|---|---|
options? | UseMediaSessionOptions | - | Configuration options for media session |
Parameter Properties
options properties:
Name | Type | Description |
---|---|---|
metadata? | MediaMetadataInit | Initial metadata for the media session |
playbackState? | MediaSessionPlaybackState | Initial playback state ('none', 'paused', 'playing') |
actionHandlers? | Partial<Record<MediaSessionAction, MediaSessionActionHandler>> | Initial action handlers for media controls |
Return Type
UseMediaSessionReturn
Property | Type | Description |
---|---|---|
isSupported | boolean | Whether the Media Session API is supported |
setMetadata | (metadata: MediaMetadataInit) => void | Set media metadata (title, artist, album, artwork) |
setPlaybackState | (state: MediaSessionPlaybackState) => void | Set the current playback state |
setActionHandler | (action: MediaSessionAction, handler: MediaSessionActionHandler | null) => void | Set or remove an action handler for media controls |
clearActionHandlers | () => void | Clear all action handlers |
Examples
Basic Audio Player
Simple audio player with media session integration
1import { useMediaSession } from '@usehooks/use-media-session';
2import { useRef, useState } from 'react';
3
4function AudioPlayer() {
5 const audioRef = useRef<HTMLAudioElement>(null);
6 const [isPlaying, setIsPlaying] = useState(false);
7 const [currentTrack, setCurrentTrack] = useState(0);
8
9 const tracks = [
10 {
11 title: 'Song One',
12 artist: 'Artist Name',
13 album: 'Album Name',
14 src: '/audio/song1.mp3',
15 artwork: [{ src: '/images/album1.jpg', sizes: '512x512', type: 'image/jpeg' }]
16 },
17 {
18 title: 'Song Two',
19 artist: 'Artist Name',
20 album: 'Album Name',
21 src: '/audio/song2.mp3',
22 artwork: [{ src: '/images/album2.jpg', sizes: '512x512', type: 'image/jpeg' }]
23 }
24 ];
25
26 const {
27 isSupported,
28 setMetadata,
29 setPlaybackState,
30 setActionHandler
31 } = useMediaSession();
32
33 const play = () => {
34 audioRef.current?.play();
35 setIsPlaying(true);
36 setPlaybackState('playing');
37 };
38
39 const pause = () => {
40 audioRef.current?.pause();
41 setIsPlaying(false);
42 setPlaybackState('paused');
43 };
44
45 const nextTrack = () => {
46 const next = (currentTrack + 1) % tracks.length;
47 setCurrentTrack(next);
48 updateMetadata(next);
49 };
50
51 const prevTrack = () => {
52 const prev = (currentTrack - 1 + tracks.length) % tracks.length;
53 setCurrentTrack(prev);
54 updateMetadata(prev);
55 };
56
57 const updateMetadata = (trackIndex: number) => {
58 const track = tracks[trackIndex];
59 setMetadata({
60 title: track.title,
61 artist: track.artist,
62 album: track.album,
63 artwork: track.artwork
64 });
65 };
66
67 // Set up media session handlers
68 React.useEffect(() => {
69 if (isSupported) {
70 setActionHandler('play', play);
71 setActionHandler('pause', pause);
72 setActionHandler('nexttrack', nextTrack);
73 setActionHandler('previoustrack', prevTrack);
74
75 // Set initial metadata
76 updateMetadata(currentTrack);
77 }
78 }, [isSupported, currentTrack]);
79
80 return (
81 <div>
82 <audio
83 ref={audioRef}
84 src={tracks[currentTrack].src}
85 onPlay={() => {
86 setIsPlaying(true);
87 setPlaybackState('playing');
88 }}
89 onPause={() => {
90 setIsPlaying(false);
91 setPlaybackState('paused');
92 }}
93 onEnded={nextTrack}
94 />
95
96 <div>
97 <h3>{tracks[currentTrack].title}</h3>
98 <p>{tracks[currentTrack].artist} - {tracks[currentTrack].album}</p>
99 </div>
100
101 <div>
102 <button onClick={prevTrack}>⏮</button>
103 <button onClick={isPlaying ? pause : play}>
104 {isPlaying ? '⏸' : '▶️'}
105 </button>
106 <button onClick={nextTrack}>⏭</button>
107 </div>
108
109 {!isSupported && (
110 <p>Media Session API not supported</p>
111 )}
112 </div>
113 );
114}
Podcast Player
Podcast player with seek controls and chapter support
1import { useMediaSession } from '@usehooks/use-media-session';
2import { useRef, useState, useEffect } from 'react';
3
4function PodcastPlayer() {
5 const audioRef = useRef<HTMLAudioElement>(null);
6 const [isPlaying, setIsPlaying] = useState(false);
7 const [currentTime, setCurrentTime] = useState(0);
8 const [duration, setDuration] = useState(0);
9
10 const podcast = {
11 title: 'Episode 42: React Hooks Deep Dive',
12 artist: 'Tech Podcast',
13 album: 'Season 3',
14 src: '/audio/podcast-episode.mp3',
15 artwork: [
16 { src: '/images/podcast-cover.jpg', sizes: '512x512', type: 'image/jpeg' }
17 ]
18 };
19
20 const {
21 setMetadata,
22 setPlaybackState,
23 setActionHandler
24 } = useMediaSession({
25 metadata: podcast,
26 playbackState: 'paused'
27 });
28
29 const play = () => {
30 audioRef.current?.play();
31 setIsPlaying(true);
32 setPlaybackState('playing');
33 };
34
35 const pause = () => {
36 audioRef.current?.pause();
37 setIsPlaying(false);
38 setPlaybackState('paused');
39 };
40
41 const seekBackward = () => {
42 if (audioRef.current) {
43 audioRef.current.currentTime = Math.max(0, audioRef.current.currentTime - 10);
44 }
45 };
46
47 const seekForward = () => {
48 if (audioRef.current) {
49 audioRef.current.currentTime = Math.min(
50 audioRef.current.duration,
51 audioRef.current.currentTime + 30
52 );
53 }
54 };
55
56 const seekTo = (details: { seekTime?: number }) => {
57 if (audioRef.current && details.seekTime !== undefined) {
58 audioRef.current.currentTime = details.seekTime;
59 }
60 };
61
62 // Set up media session handlers
63 useEffect(() => {
64 setActionHandler('play', play);
65 setActionHandler('pause', pause);
66 setActionHandler('seekbackward', seekBackward);
67 setActionHandler('seekforward', seekForward);
68 setActionHandler('seekto', seekTo);
69 }, []);
70
71 // Update time
72 useEffect(() => {
73 const audio = audioRef.current;
74 if (!audio) return;
75
76 const updateTime = () => setCurrentTime(audio.currentTime);
77 const updateDuration = () => setDuration(audio.duration);
78
79 audio.addEventListener('timeupdate', updateTime);
80 audio.addEventListener('loadedmetadata', updateDuration);
81
82 return () => {
83 audio.removeEventListener('timeupdate', updateTime);
84 audio.removeEventListener('loadedmetadata', updateDuration);
85 };
86 }, []);
87
88 const formatTime = (time: number) => {
89 const minutes = Math.floor(time / 60);
90 const seconds = Math.floor(time % 60);
91 return `${minutes}:${seconds.toString().padStart(2, '0')}`;
92 };
93
94 return (
95 <div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
96 <audio
97 ref={audioRef}
98 src={podcast.src}
99 onPlay={() => {
100 setIsPlaying(true);
101 setPlaybackState('playing');
102 }}
103 onPause={() => {
104 setIsPlaying(false);
105 setPlaybackState('paused');
106 }}
107 />
108
109 <div style={{ textAlign: 'center', marginBottom: '20px' }}>
110 <img
111 src={podcast.artwork[0].src}
112 alt="Podcast cover"
113 style={{ width: '200px', height: '200px', borderRadius: '8px' }}
114 />
115 <h3>{podcast.title}</h3>
116 <p>{podcast.artist}</p>
117 </div>
118
119 <div style={{ marginBottom: '20px' }}>
120 <input
121 type="range"
122 min={0}
123 max={duration || 0}
124 value={currentTime}
125 onChange={(e) => {
126 const time = parseFloat(e.target.value);
127 if (audioRef.current) {
128 audioRef.current.currentTime = time;
129 }
130 }}
131 style={{ width: '100%' }}
132 />
133 <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px' }}>
134 <span>{formatTime(currentTime)}</span>
135 <span>{formatTime(duration)}</span>
136 </div>
137 </div>
138
139 <div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
140 <button onClick={seekBackward}>⏪ 10s</button>
141 <button onClick={isPlaying ? pause : play} style={{ fontSize: '24px' }}>
142 {isPlaying ? '⏸' : '▶️'}
143 </button>
144 <button onClick={seekForward}>30s ⏩</button>
145 </div>
146 </div>
147 );
148}
Dynamic Metadata Updates
Update media session metadata dynamically based on content
1import { useMediaSession } from '@usehooks/use-media-session';
2import { useState, useEffect } from 'react';
3
4function DynamicMediaSession() {
5 const [currentContent, setCurrentContent] = useState('music');
6 const [isActive, setIsActive] = useState(false);
7
8 const {
9 isSupported,
10 setMetadata,
11 setPlaybackState,
12 setActionHandler,
13 clearActionHandlers
14 } = useMediaSession();
15
16 const contentTypes = {
17 music: {
18 title: 'Beautiful Song',
19 artist: 'Amazing Artist',
20 album: 'Great Album',
21 artwork: [{ src: '/music-cover.jpg', sizes: '512x512', type: 'image/jpeg' }]
22 },
23 podcast: {
24 title: 'Tech Talk Episode 15',
25 artist: 'Tech Podcast Network',
26 album: 'Season 2',
27 artwork: [{ src: '/podcast-cover.jpg', sizes: '512x512', type: 'image/jpeg' }]
28 },
29 audiobook: {
30 title: 'Chapter 3: The Journey Begins',
31 artist: 'Famous Author',
32 album: 'Epic Novel',
33 artwork: [{ src: '/book-cover.jpg', sizes: '512x512', type: 'image/jpeg' }]
34 }
35 };
36
37 const handlePlay = () => {
38 setIsActive(true);
39 setPlaybackState('playing');
40 console.log('Playing:', currentContent);
41 };
42
43 const handlePause = () => {
44 setIsActive(false);
45 setPlaybackState('paused');
46 console.log('Paused:', currentContent);
47 };
48
49 const handleStop = () => {
50 setIsActive(false);
51 setPlaybackState('none');
52 console.log('Stopped:', currentContent);
53 };
54
55 // Update metadata when content type changes
56 useEffect(() => {
57 if (isSupported) {
58 const metadata = contentTypes[currentContent as keyof typeof contentTypes];
59 setMetadata(metadata);
60 }
61 }, [currentContent, isSupported]);
62
63 // Set up action handlers
64 useEffect(() => {
65 if (isSupported) {
66 setActionHandler('play', handlePlay);
67 setActionHandler('pause', handlePause);
68 setActionHandler('stop', handleStop);
69
70 return () => {
71 clearActionHandlers();
72 };
73 }
74 }, [isSupported]);
75
76 if (!isSupported) {
77 return <div>Media Session API not supported</div>;
78 }
79
80 return (
81 <div style={{ padding: '20px' }}>
82 <h2>Dynamic Media Session</h2>
83
84 <div style={{ marginBottom: '20px' }}>
85 <label>Content Type: </label>
86 <select
87 value={currentContent}
88 onChange={(e) => setCurrentContent(e.target.value)}
89 >
90 <option value="music">Music</option>
91 <option value="podcast">Podcast</option>
92 <option value="audiobook">Audiobook</option>
93 </select>
94 </div>
95
96 <div style={{ marginBottom: '20px' }}>
97 <h3>Current Metadata:</h3>
98 <pre style={{ backgroundColor: '#f5f5f5', padding: '10px', borderRadius: '4px' }}>
99 {JSON.stringify(contentTypes[currentContent as keyof typeof contentTypes], null, 2)}
100 </pre>
101 </div>
102
103 <div>
104 <button onClick={handlePlay} disabled={isActive}>
105 Play
106 </button>
107 <button onClick={handlePause} disabled={!isActive}>
108 Pause
109 </button>
110 <button onClick={handleStop}>
111 Stop
112 </button>
113 </div>
114
115 <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
116 <p>Use your device's media controls or notification to test the integration!</p>
117 </div>
118 );
119}
Dependencies
react
Notes
- •Only available in browsers that support the Media Session API (Chrome 57+, Firefox 82+)
- •Requires active media element for full functionality
- •Media controls appear in system notifications, lock screen, and hardware keys
- •Artwork should be provided in multiple sizes for best compatibility
- •Action handlers are automatically cleared on component unmount
- •Some actions may not be available on all platforms
Implementation
1'use client';
2
3import { useEffect, useCallback, useRef } from "react";
4
5interface MediaImage {
6 src: string;
7 sizes?: string;
8 type?: string;
9}
10
11interface MediaMetadataInit {
12 title?: string;
13 artist?: string;
14 album?: string;
15 artwork?: MediaImage[];
16}
17
18type MediaSessionAction =
19 | "play"
20 | "pause"
21 | "stop"
22 | "seekbackward"
23 | "seekforward"
24 | "seekto"
25 | "skipad"
26 | "previoustrack"
27 | "nexttrack";
28
29type MediaSessionPlaybackState = "none" | "paused" | "playing";
30
31type MediaSessionActionHandler = (details?: any) => void;
32
33interface UseMediaSessionOptions {
34 metadata?: MediaMetadataInit;
35 playbackState?: MediaSessionPlaybackState;
36 actionHandlers?: Partial<
37 Record<MediaSessionAction, MediaSessionActionHandler>
38 >;
39}
40
41interface UseMediaSessionReturn {
42 isSupported: boolean;
43 setMetadata: (metadata: MediaMetadataInit) => void;
44 setPlaybackState: (state: MediaSessionPlaybackState) => void;
45 setActionHandler: (
46 action: MediaSessionAction,
47 handler: MediaSessionActionHandler | null
48 ) => void;
49 clearActionHandlers: () => void;
50}
51
52const useMediaSession = (
53 options?: UseMediaSessionOptions
54): UseMediaSessionReturn => {
55 const actionHandlersRef = useRef<Set<MediaSessionAction>>(new Set());
56
57 const isSupported =
58 typeof navigator !== "undefined" && "mediaSession" in navigator;
59
60 const setMetadata = useCallback(
61 (metadata: MediaMetadataInit) => {
62 if (!isSupported) return;
63
64 try {
65 navigator.mediaSession.metadata = new MediaMetadata(metadata);
66 } catch (error) {
67 console.warn("Failed to set media metadata:", error);
68 }
69 },
70 [isSupported]
71 );
72
73 const setPlaybackState = useCallback(
74 (state: MediaSessionPlaybackState) => {
75 if (!isSupported) return;
76
77 try {
78 navigator.mediaSession.playbackState = state;
79 } catch (error) {
80 console.warn("Failed to set playback state:", error);
81 }
82 },
83 [isSupported]
84 );
85
86 const setActionHandler = useCallback(
87 (action: MediaSessionAction, handler: MediaSessionActionHandler | null) => {
88 if (!isSupported) return;
89
90 try {
91 navigator.mediaSession.setActionHandler(action, handler);
92 if (handler) {
93 actionHandlersRef.current.add(action);
94 } else {
95 actionHandlersRef.current.delete(action);
96 }
97 } catch (error) {
98 console.warn(`Failed to set action handler for ${action}:`, error);
99 }
100 },
101 [isSupported]
102 );
103
104 const clearActionHandlers = useCallback(() => {
105 if (!isSupported) return;
106
107 actionHandlersRef.current.forEach((action) => {
108 try {
109 navigator.mediaSession.setActionHandler(action, null);
110 } catch (error) {
111 console.warn(`Failed to clear action handler for ${action}:`, error);
112 }
113 });
114 actionHandlersRef.current.clear();
115 }, [isSupported]);
116
117 // Set initial metadata if provided
118 useEffect(() => {
119 if (options?.metadata) {
120 setMetadata(options.metadata);
121 }
122 }, [setMetadata, options?.metadata]);
123
124 // Set initial playback state if provided
125 useEffect(() => {
126 if (options?.playbackState) {
127 setPlaybackState(options.playbackState);
128 }
129 }, [setPlaybackState, options?.playbackState]);
130
131 // Set initial action handlers if provided
132 useEffect(() => {
133 if (options?.actionHandlers) {
134 Object.entries(options.actionHandlers).forEach(([action, handler]) => {
135 if (handler) {
136 setActionHandler(action as MediaSessionAction, handler);
137 }
138 });
139 }
140
141 // Cleanup function to clear all action handlers when component unmounts
142 return () => {
143 clearActionHandlers();
144 };
145 }, [setActionHandler, clearActionHandlers, options?.actionHandlers]);
146
147 return {
148 isSupported,
149 setMetadata,
150 setPlaybackState,
151 setActionHandler,
152 clearActionHandlers,
153 };
154};
155
156export { useMediaSession };
157export type {
158 MediaImage,
159 MediaMetadataInit,
160 MediaSessionAction,
161 MediaSessionPlaybackState,
162 MediaSessionActionHandler,
163 UseMediaSessionOptions,
164 UseMediaSessionReturn,
165};
166