Video / Media Session Sample

Available in Chrome 57+ | View on GitHub | Browse Samples

Background

The Media Session API lets you customize media notifications by providing metadata information for the media your web app is playing. It also allows you to handle media related events such as seeking or track changing which may come from notifications or media keys.

Credits: Media files are © copyright Blender Foundation | www.blender.org .

Live Output


JavaScript Snippet

let video = document.querySelector('video');

let playlist = getAwesomePlaylist();
let index = 0;

function onPlayButtonClick() {
  playVideo();
}

function playVideo() {
  video.src = playlist[index].src;
  video.play()
  .then(_ => updateMetadata())
  .catch(error => log(error.message));
}

function updateMetadata() {
  let track = playlist[index];

  log('Playing ' + track.title + ' track...');
  navigator.mediaSession.metadata = new MediaMetadata({
    title: track.title,
    artist: track.artist,
    artwork: track.artwork,
    chapterInfo: track.chapterInfo,
  });

  // Media is loaded, set the duration.
  updatePositionState();
}

/* Position state (supported since Chrome 81) */

function updatePositionState() {
  if ('setPositionState' in navigator.mediaSession) {
    log('Updating position state...');
    navigator.mediaSession.setPositionState({
      duration: video.duration,
      playbackRate: video.playbackRate,
      position: video.currentTime
    });
  }
}

/* Previous Track & Next Track */

navigator.mediaSession.setActionHandler('previoustrack', function() {
  log('> User clicked "Previous Track" icon.');
  index = (index - 1 + playlist.length) % playlist.length;
  playVideo();
});

navigator.mediaSession.setActionHandler('nexttrack', function() {
  log('> User clicked "Next Track" icon.');
  index = (index + 1) % playlist.length;
  playVideo();
});

video.addEventListener('ended', function() {
  // Play automatically the next track when video ends.
  index = (index - 1 + playlist.length) % playlist.length;
  playVideo();
});

/* Seek Backward & Seek Forward */

let defaultSkipTime = 10; /* Time to skip in seconds by default */

navigator.mediaSession.setActionHandler('seekbackward', function(event) {
  log('> User clicked "Seek Backward" icon.');
  const skipTime = event.seekOffset || defaultSkipTime;
  video.currentTime = Math.max(video.currentTime - skipTime, 0);
  updatePositionState();
});

navigator.mediaSession.setActionHandler('seekforward', function(event) {
  log('> User clicked "Seek Forward" icon.');
  const skipTime = event.seekOffset || defaultSkipTime;
  video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
  updatePositionState();
});

/* Play & Pause */

navigator.mediaSession.setActionHandler('play', async function() {
  log('> User clicked "Play" icon.');
  await video.play();
  // Do something more than just playing video...
});

navigator.mediaSession.setActionHandler('pause', function() {
  log('> User clicked "Pause" icon.');
  video.pause();
  // Do something more than just pausing video...
});

video.addEventListener('play', function() {
  navigator.mediaSession.playbackState = 'playing';
});

video.addEventListener('pause', function() {
  navigator.mediaSession.playbackState = 'paused';
});

/* Stop (supported since Chrome 77) */

try {
  navigator.mediaSession.setActionHandler('stop', function() {
    log('> User clicked "Stop" icon.');
    // TODO: Clear UI playback...
  });
} catch(error) {
  log('Warning! The "stop" media session action is not supported.');
}

/* Seek To (supported since Chrome 78) */

try {
  navigator.mediaSession.setActionHandler('seekto', function(event) {
    log('> User clicked "Seek To" icon.');
    if (event.fastSeek && ('fastSeek' in video)) {
      video.fastSeek(event.seekTime);
      return;
    }
    video.currentTime = event.seekTime;
    updatePositionState();
  });
} catch(error) {
  log('Warning! The "seekto" media session action is not supported.');
}


/* Enter Picture-in-Picture (supported since Chrome 120) */

try {
  navigator.mediaSession.setActionHandler('enterpictureinpicture', async function() {
    log('> User clicked "Enter Picture-in-Picture" icon or we are eligible to automatically enter picture-in-picture.');
    await video.requestPictureInPicture();
  });
} catch(error) {
  log('Warning! The "enterpictureinpicture" media session action is not supported.');
}

/* Utils */

function getAwesomePlaylist() {
  const BASE_URL = 'https://storage.googleapis.com/media-session/';

  return [{
      src: BASE_URL + 'sintel/trailer.mp4',
      title: '"Sintel" Trailer, Durian Open Movie Project',
      artist: 'Blender Foundation',
      artwork: [
        { src: BASE_URL + 'sintel/artwork-96.png',  sizes: '96x96',   type: 'image/png' },
        { src: BASE_URL + 'sintel/artwork-128.png', sizes: '128x128', type: 'image/png' },
        { src: BASE_URL + 'sintel/artwork-192.png', sizes: '192x192', type: 'image/png' },
        { src: BASE_URL + 'sintel/artwork-256.png', sizes: '256x256', type: 'image/png' },
        { src: BASE_URL + 'sintel/artwork-384.png', sizes: '384x384', type: 'image/png' },
        { src: BASE_URL + 'sintel/artwork-512.png', sizes: '512x512', type: 'image/png' },
      ],
      chapterInfo: [{
        title: 'Chapter 1',
        startTime: 0,
        artwork: [
          { src: BASE_URL + 'sintel/chapter1-128.png', sizes: '128x128', type: 'image/png'},
          { src: BASE_URL + 'sintel/chapter1-512.png', sizes: '512x512', type: 'image/png'},
        ]
      }, {
        title: 'Chapter 2',
        startTime: 37,
        artwork: [
          { src: BASE_URL + 'sintel/chapter2-128.png', sizes: '128x128', type: 'image/png'},
          { src: BASE_URL + 'sintel/chapter2-512.png', sizes: '512x512', type: 'image/png'},
        ]
      }]
    }, {
      src: BASE_URL + 'big-buck-bunny/trailer.mov',
      title: '"Big Buck Bunny" Trailer, Peach Open Movie Project',
      artist: 'Blender Foundation',
      artwork: [
        { src: BASE_URL + 'big-buck-bunny/artwork-96.png',  sizes: '96x96',   type: 'image/png' },
        { src: BASE_URL + 'big-buck-bunny/artwork-128.png', sizes: '128x128', type: 'image/png' },
        { src: BASE_URL + 'big-buck-bunny/artwork-192.png', sizes: '192x192', type: 'image/png' },
        { src: BASE_URL + 'big-buck-bunny/artwork-256.png', sizes: '256x256', type: 'image/png' },
        { src: BASE_URL + 'big-buck-bunny/artwork-384.png', sizes: '384x384', type: 'image/png' },
        { src: BASE_URL + 'big-buck-bunny/artwork-512.png', sizes: '512x512', type: 'image/png' },
      ],
      chapterInfo: [{
        title: 'Chapter 1',
        startTime: 0,
        artwork: [
          { src: BASE_URL + 'big-buck-bunny/chapter1-128.png', sizes: '128x128', type: 'image/png'},
          { src: BASE_URL + 'big-buck-bunny/chapter1-512.png', sizes: '512x512', type: 'image/png'},
        ]
      }, {
        title: 'Chapter 2',
        startTime: 16,
        artwork: [
          { src: BASE_URL + 'big-buck-bunny/chapter2-128.png', sizes: '128x128', type: 'image/png'},
          { src: BASE_URL + 'big-buck-bunny/chapter2-512.png', sizes: '512x512', type: 'image/png'},
        ]
      }]
    }, {
      src: BASE_URL + 'elephants-dream/teaser.mp4',
      title: '"Elephants Dream" Teaser, Orange Open Movie Project',
      artist: 'Blender Foundation',
      artwork: [
        { src: BASE_URL + 'elephants-dream/artwork-96.png',  sizes: '96x96',   type: 'image/png' },
        { src: BASE_URL + 'elephants-dream/artwork-128.png', sizes: '128x128', type: 'image/png' },
        { src: BASE_URL + 'elephants-dream/artwork-192.png', sizes: '192x192', type: 'image/png' },
        { src: BASE_URL + 'elephants-dream/artwork-256.png', sizes: '256x256', type: 'image/png' },
        { src: BASE_URL + 'elephants-dream/artwork-384.png', sizes: '384x384', type: 'image/png' },
        { src: BASE_URL + 'elephants-dream/artwork-512.png', sizes: '512x512', type: 'image/png' },
      ],
      chapterInfo: [{
        title: 'Chapter 1',
        startTime: 0,
        artwork: [
          { src: BASE_URL + 'elephants-dream/chapter1-128.png', sizes: '128x128', type: 'image/png'},
          { src: BASE_URL + 'elephants-dream/chapter1-512.png', sizes: '512x512', type: 'image/png'},
        ]
      }, {
        title: 'Chapter 2',
        startTime: 64,
        artwork: [
          { src: BASE_URL + 'elephants-dream/chapter2-128.png', sizes: '128x128', type: 'image/png'},
          { src: BASE_URL + 'elephants-dream/chapter2-512.png', sizes: '512x512', type: 'image/png'},
        ]
      }]
    }, {
      src: BASE_URL + 'caminandes/short.mp4',
      title: '"Caminandes 2: Gran Dillama" - Blender Animated Short',
      artist: 'Blender Foundation',
      artwork: [
        { src: BASE_URL + 'caminandes/artwork-96.png',  sizes: '96x96',   type: 'image/png' },
        { src: BASE_URL + 'caminandes/artwork-128.png', sizes: '128x128', type: 'image/png' },
        { src: BASE_URL + 'caminandes/artwork-192.png', sizes: '192x192', type: 'image/png' },
        { src: BASE_URL + 'caminandes/artwork-256.png', sizes: '256x256', type: 'image/png' },
        { src: BASE_URL + 'caminandes/artwork-384.png', sizes: '384x384', type: 'image/png' },
        { src: BASE_URL + 'caminandes/artwork-512.png', sizes: '512x512', type: 'image/png' },
      ],
      chapterInfo: [{
        title: 'Chapter 1',
        startTime: 0,
        artwork: [
          { src: BASE_URL + 'caminandes/chapter1-128.png', sizes: '128x128', type: 'image/png'},
          { src: BASE_URL + 'caminandes/chapter1-512.png', sizes: '512x512', type: 'image/png'},
        ]
      }, {
        title: 'Chapter 2',
        startTime: 94,
        artwork: [
          { src: BASE_URL + 'caminandes/chapter2-128.png', sizes: '128x128', type: 'image/png'},
          { src: BASE_URL + 'caminandes/chapter2-512.png', sizes: '512x512', type: 'image/png'},
        ]
      }]
    }];
}