From 10e89eb28164387bbd432b5032a7da7b3d969b21 Mon Sep 17 00:00:00 2001 From: sleepyfran Date: Wed, 30 Oct 2024 09:06:58 +0100 Subject: [PATCH] Fix Spotify player sync problems 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. --- .../infrastructure/spotify-player/index.ts | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/infrastructure/spotify-player/index.ts b/packages/infrastructure/spotify-player/index.ts index 0f1773d..3d58558 100644 --- a/packages/infrastructure/spotify-player/index.ts +++ b/packages/infrastructure/spotify-player/index.ts @@ -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, ) => - Stream.async((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.