🎧

Safari で Spotify Web Playback SDK を再生させる

に公開

Spotify では、Web上で簡単に音源の再生ができるSDKが公開されていますが、これがSafari上でうまく動作せず、音源の再生ができなかったのでその対処法をメモがてら書いていきます
つらつらと書いていますが、対応方法はこちらのセクションに書いています

そもそもなんで再生されないの?

非同期処理で再生が走る場合、音源の再生がサスペンドされていそうなため

音源再生時の非同期処理

SpotifyのWeb SDK Playback(長いので以降 Player)で音源を再生するにはどういう実装をしたらいいのかちょっと見ていきます
Web上のUIから特定の音源を再生したい場合は以下の実装が必要になります

  1. Player の初期化・認証
  2. ユーザーのクリックイベントから再生用のエンドポイントに初期化したPlayerのdeviceIDを添えてリクエストを送る

2のリクエストを送ると、1で初期化したPlayerが機能して音源が鳴り始める想定です
このとおりに実装してみると、Chrome では問題なく再生できましたが Safari 上では再生できませんでした
2の部分の実装のみ軽く書くと以下のような感じです

/** 
  * Player の初期化はここでは書きませんが、以下のチュートリアルがわかりやすいです
  * https://developer.spotify.com/documentation/web-playback-sdk/tutorials/getting-started
  */

export const Player = () => {
  const handleClick = () => {
    fetch('https://api.spotify.com/v1/me/player/play?device_id=xxxxxx', {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
      body: JSON.stringify({
       context_uri: 'album_uri_xxxxxxx'
      }),
      });
  }
  
  return (
   <button onClick={handleClick}>play song</button>
  )
}

どうやら Safari では、非同期処理で再生が走る場合、次の再生処理まで音源の再生がサスペンドされるようでした(awaitをつけても挙動は変わりませんでした)
なので、上記の実装を以下のように変えると問題なく機能します

export const Player = () => {
  // 初期化したPlayerを用意する
  const { player } = usePlayer()
  
  // マウント時にAPIを読んで、サスペンド状態にしておく
  useEffect(() => {
    // handleClick の処理をこっちに移す
    fetch('https://api.spotify.com/v1/me/player/play?device_id=xxxxxx', {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
      body: JSON.stringify({
        context_uri: 'album_uri_xxxxxxx'
      }),
    });
  }, [])
  
  // サスペンドされた音源を再生する
  const handleClick = () => {
    player.resume()
  }
  
  return (
   <button onClick={handleClick}>play song</button>
  )
}

上の実装じゃできないこと

上の実装では、マウント時に特定の音源をサスペンドしておくことでクリック時に問題なく動作させることができています
しかし、実際のユースケースでは以下のUIのようにユーザーのアクションを起因として音源を特定して再生しています
マウント時に音源指定することは、このユースケースでは少し難しそうです
player-sample

できるようにするには

このサスペンドは、あらかじめ何かしらの音源を再生しておくことで回避することができます
そのため、音源再生の非同期処理を呼ぶ前に無音の音源をあらかじめ再生しておくことで、サスペンドを解除することができます
以下のように実装を変えてみると、問題なく音源が再生されました🙌

export const Player = () => {
  const handleClick = () => {
    /**
     * Hack:
     * Safariでは、一度どこかで再生されていないとプレイヤーがロックされるため、
     * 無音の音源を再生してロックを解除する
     */
    const audio = new Audio("/audio/silence.mp3");
    audio.play();

    // この状態で呼ぶと音源が再生される
    fetch('https://api.spotify.com/v1/me/player/play?device_id=xxxxxx', {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
      body: JSON.stringify({
        context_uri: 'album_uri_xxxxxxx'
      }),
    });
  }
  
  return (
   <button onClick={handleClick}>play song</button>
  )
}

おわりに

音源周りの実装はあまりしたことがなかったので戸惑いましたが、学びになりました
ハックぽい対処法なので、もっと良い手法や修正箇所などあればコメントいただけると嬉しいです🙏

Discussion