chromium/ash/webui/focus_mode/untrusted_resources/player.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

const TRUSTED_ORIGIN = 'chrome://focus-mode-media';

interface Track {
  // The URL of the audio data.
  mediaUrl: string;
  // The track thumbnail in the form of a data URL.
  thumbnailUrl: string;
  // The title.
  title: string;
  // The artist.
  artist: string;
}

function isTrack(a: any): a is Track {
  return (
      a && typeof a == 'object' && typeof a.mediaUrl == 'string' &&
      typeof a.thumbnailUrl == 'string' && typeof a.title == 'string' &&
      typeof a.artist == 'string');
}

interface Command {
  cmd: string;
  arg: any;
}

function isCommand(a: any): a is Command {
  return a && typeof a == 'object' && typeof a.cmd == 'string';
}

function sendTrackRequest() {
  parent.postMessage({cmd: 'gettrack'}, TRUSTED_ORIGIN);
}

interface PlaybackStatus {
  state: string;
  position: number;
  initial: boolean;
}
let playbackStatus: PlaybackStatus|null = null;

function replyPlaybackStatus(newState: string|null) {
  // Do not send status update if the track has not been loaded yet.
  if (!playbackStatus || (playbackStatus.state == 'none' && newState == null)) {
    return;
  }

  if (newState != null) {
    playbackStatus.state = newState;
  }
  playbackStatus.position = getPlayerElement().currentTime;

  parent.postMessage(
      {
        cmd: 'replyplaybackstatus',
        state: playbackStatus.state,
        position: playbackStatus.position,
        initial: playbackStatus.initial,
      },
      TRUSTED_ORIGIN);

  playbackStatus.initial = false;
}

function getPlayerElement(): HTMLAudioElement {
  return document.getElementById('player') as HTMLAudioElement;
}

function loadTrack(track: Track) {
  const p = getPlayerElement();
  p.src = track.mediaUrl;

  const metadata: any = {
    title: track.title,
    artist: track.artist,
  };
  if (track.thumbnailUrl) {
    metadata.artwork = [{src: track.thumbnailUrl}];
  }
  navigator.mediaSession.metadata = new MediaMetadata(metadata);
  playbackStatus = {
    state: 'none',
    position: 0,
    initial: true,
  };
}

globalThis.addEventListener('load', () => {
  getPlayerElement().addEventListener('play', () => {
    replyPlaybackStatus('playing');
  });

  getPlayerElement().addEventListener('pause', () => {
    replyPlaybackStatus('paused');
  });

  getPlayerElement().addEventListener('ended', () => {
    replyPlaybackStatus('ended');
    sendTrackRequest();
  });

  // Registering this makes the "next track" button show up in the media
  // controls. We do not support going to the previous track.
  navigator.mediaSession.setActionHandler('nexttrack', () => {
    replyPlaybackStatus('switchedtonext');
    sendTrackRequest();
  });

  sendTrackRequest();
});

globalThis.addEventListener('message', (event: MessageEvent) => {
  if (event.origin != TRUSTED_ORIGIN) {
    return;
  }

  const data = event.data;
  if (isCommand(data)) {
    if (data.cmd == 'play' && isTrack(data.arg)) {
      loadTrack(data.arg);
    } else if (data.cmd == 'queryplaybackstatus') {
      replyPlaybackStatus(null);
    }
  }
});