Skip to content

Commit

Permalink
Fix Spotify player sync problems
Browse files Browse the repository at this point in the history
Fixes #7. There were quite a few problems with the implementation, namely:
- The player listeners were not properly being set because the `consumeCommandsInBackground`
  was called before setting up listeners without forking the effect into another fiber,
  essentially not calling the setup of the listeners until that effect was done (which was never).
- Even if the listeners were properly setup, since we were exposing the raw queue in the `observe`
  function, the subscribers of the stream would only get whichever event were currently in the queue
  and then the stream would close. This was fixed by casting the queue to a stream.
- And even after all the previous problems were fixed, the listener setup was not done correctly
  and we were not correctly handling `play` updates or tracks finishing.
sleepyfran committed Oct 30, 2024
1 parent 66a1065 commit 10e89eb
Showing 1 changed file with 27 additions and 22 deletions.
49 changes: 27 additions & 22 deletions packages/infrastructure/spotify-player/index.ts
Original file line number Diff line number Diff line change
@@ -102,18 +102,14 @@ const make = Effect.gen(function* () {
),
Match.tag("PlayerReady", ({ player, deviceId }) =>
Effect.log("Player is ready, setting up listeners").pipe(
Effect.andThen(() =>
setupListeners(player, mediaPlayerEventQueue),
),
Effect.andThen(() =>
consumeCommandsInBackground(
{ authInfo, deviceId },
{ playerApi, player },
commandQueue,
).pipe(
Effect.andThen(() =>
setupListeners(player, mediaPlayerEventQueue),
),
Effect.andThen(() =>
Effect.log("Player ready and listeners set up"),
),
),
),
),
@@ -138,7 +134,7 @@ const make = Effect.gen(function* () {
playTrack: (trackId) => commandQueue.offer(PlayTrack({ trackId })),
togglePlayback: commandQueue.offer(TogglePlayback()),
stop: commandQueue.offer(Stop()),
observe: mediaPlayerEventQueue,
observe: Stream.fromQueue(mediaPlayerEventQueue),
dispose: commandQueue.offer(Dispose()),
};
}),
@@ -193,25 +189,34 @@ const setupListeners = (
player: Spotify.Player,
mediaPlayerEventQueue: Queue.Enqueue<MediaPlayerEvent>,
) =>
Stream.async<MediaPlayerEvent>((emit) => {
Effect.sync(() => {
player.addListener("player_state_changed", (state) => {
if (state.paused) {
emit.single("trackPaused");
}

if (state.position === 0) {
emit.single("trackPlaying");
const currentTrackId = state.track_window.current_track.id;
const previousTrackID = state.track_window.previous_tracks[0]?.id;

/*
There's no reliable way of detecting when a track has ended via this listener
(or any other API provided via the SDK/Web API). However when a song finishes
there's an event that adds the track that was just played to the previous_tracks
array, so if we detect that the current track is the same as the previous track
and the position is 0 while simultaneously being paused, we can assume that the
previous track has ended.
*/
if (
state.position === 0 &&
state.paused &&
currentTrackId === previousTrackID
) {
return mediaPlayerEventQueue.unsafeOffer("trackEnded");
}

if (state.position === state.track_window.current_track.duration_ms) {
emit.single("trackEnded");
if (state.paused) {
mediaPlayerEventQueue.unsafeOffer("trackPaused");
} else {
mediaPlayerEventQueue.unsafeOffer("trackPlaying");
}
});
}).pipe(
Stream.runForEach((event: MediaPlayerEvent) =>
mediaPlayerEventQueue.offer(event),
),
);
});

/**
* Implementation of the media player service using the Spotify Web Playback SDK.

0 comments on commit 10e89eb

Please sign in to comment.