useHooks.iov4.1.2
DocsBlogGitHub
Hooks
No hooks found in any category.

useMediaSession

sensors

Installation

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

NameTypeDefaultDescription
options?UseMediaSessionOptions-Configuration options for media session

Parameter Properties

options properties:

NameTypeDescription
metadata?MediaMetadataInitInitial metadata for the media session
playbackState?MediaSessionPlaybackStateInitial playback state ('none', 'paused', 'playing')
actionHandlers?Partial<Record<MediaSessionAction, MediaSessionActionHandler>>Initial action handlers for media controls

Return Type

UseMediaSessionReturn
PropertyTypeDescription
isSupportedbooleanWhether the Media Session API is supported
setMetadata(metadata: MediaMetadataInit) => voidSet media metadata (title, artist, album, artwork)
setPlaybackState(state: MediaSessionPlaybackState) => voidSet the current playback state
setActionHandler(action: MediaSessionAction, handler: MediaSessionActionHandler | null) => voidSet or remove an action handler for media controls
clearActionHandlers() => voidClear 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