Closed kimfucious closed 2 years ago
There is a selector that allows the user to selected any of the available devices and stores that in a @State variable
I discourage you from storing any of the devices in another variable precisely because the available devices can change frequently. If the device stored in this variable is no longer in the list of available devices returned by availableDevices
, then you can't use it for API calls anymore. Instead, you should use the availableDevices
method to display a list/menu of the devices to a user; then, when they select a device from that list/menu, you should immediately transfer playback to that device.
Further, the device selector in the View is a Swift UI menu, when the menu is opened, it runs getAvailableDevices
How do you detect when the menu is opened? Post the code.
Selecting .smartphone device in the menu sometimes plays/transfers the track (if not more than a few seconds have passed).
When it doesn't work, one of two things can happen:
- If the track is playing, nothing happens until I switch to the Spotify app where it seems to "catch up" and begin to continue the track, playing on the phone
- If the track is paused, and I just wait, maybe after 10 or 20 seconds, the track starts playing in the app
The underlying issue for this behavior is the application lifecycle on iOS. When the user switches from one app to another, the former app is suspended after a short interval, except for certain special cases, such as if the app is playing music. When the Spotify iOS app is not playing music and you switch from it to your app, it will be suspended very shortly after. A consequence of this is that it is no longer an active or available device (although the Spotify web API may incorrectly report it as active/available). When you issue a web API command on your app that targets the Spotify iOS app on the same device while it is suspended, then the command will only take effect after you switch back to the Spotify iOS app because it will no longer be suspended at that point.
HI @Peter-Schorn, thanks for your detailed reply.
I discourage you from storing any of the devices in another variable precisely because the available devices can change frequently...
I understand your point, however, the app needs to display what the currently selected device is to the user regardless of it's current state of availability or activeness. This is why, before performing any action on the selected device (e.g. play, transfer, etc.), I perform a getAvailableDevices, check if the selected device is the result, and use that status (if it's there) to transferPlayback or open the Spotify app if the selected device is not there.
After pondering your reply, I did a little test to check how the Spotify app behaves:
I wonder how the heck they do that? 🤔 My guess is that the app knows the phone is always there when the Spotify app is running and it's contantly polling the selectedDevice.
Here's my transfer playback scenario in my app once more:
.first
).smartphone
, run the transferPlayback
method. This always works when the device is my computer running the macOS Spotify app even when the device is not active..smartphone
&& the selectedDevice is active, run the transferPlayback
method using the ID from the matched result with the play
parameter set to true; otherwise, open the Spotify app.Again, the problem is that after a few seconds of a track being paused and the user starts at step 1 above, and the device is a smartphone, and it's active, the app transferPlayback method throws the error:
SpotifyError(message: "Not found.", statusCode: 404)
I log this on the line immediately before the transferPlayback is called and the error is thrown.
Device(id: Optional("cbf47231224e87ab30353bdaa645aexxxxxxxxxx"), isActive: true, isPrivateSession: false, isRestricted: false, name: "Kim’s iPhone", type: SpotifyWebAPI.DeviceType.smartphone, volumePercent: Optional(100))
How do you detect when the menu is opened? Post the code.
At first I was using .onAppear
on one menu item that is always in the menu, but I opted for using .onTapGesture
instead:
SpotifyDetailMenuView(
availableDevices: $availableDevices,
selectedDevice: $selectedDevice,
isPlaying: $isPlaying,
isPaused: $isPaused,
nowPlayingTrack: $nowPlayingTrack,
handlePlayThisTrack: self.handlePlayThisTrack,
item: item
)
.onTapGesture(perform: {
Task.init {
Logger.log(.info, "Polling Available Devices")
await pollAvailableDevices()
}
})
In the above, pollAvailableDevices()
sets up a timer to run getAvailableDevices every second for 5 seconds, after whih the timer ends itself (I haven't figured out how to run an action, like cancelling the timer, when closing a Swift UI menu yet). Results from getAvailable devices are populated into the $availableDevices @State variable (which I understand you discourage, but in this scenario the results are pretty fresh, I believe). This is kinda similar to the sheet that gets opened when you click on the device selector in the Spotify app, I imagine, as there's a spinner next to where it say "Select a Device".
The above is obviously flawed. And I think that there's a reason they are using a sheet here instead of a menu, as menus don't seem to dynamically update. I can only see changes in the menu after closing and re-opening, even though I can see availableDevices changing, using an .onChange
. I may need to change out this menu for a sheet, to see for sure.
The underlying issue for this behavior is the application lifecycle on iOS... I understand what you're saying, however, in the scenario that I've attempted to describe, the user remains in my app when the Spotify 404 error occurs.
So in the case where the user attempts to play or transfer playback to a device of type, .smartphone
after a track has been paused for a few seconds, even with the "freshest" results from getAvailableDevices and the smartphone being in those results and claiming to be active", the user doesn't leave the app. Only an error is thrown. At which point, I catch that error and then open the track in the Spotify app when the caught error has the message of "Not found."
For the record, although not relevant except that it pertains to the iOS app lifecycle, I'm using the following to "configure spotify" when the user returns to the app. The same thing happens .onAppear
.
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
Logger.log(.info, "Spotify detail view became active!")
Task.init {
await configureSpotify()
}
}```
I think I'm almost ready to just open the Spotify app whenever there's the result returned from getAvailableDevices does not include a device that is something other than .smartphone
.
I wonder how the heck they do that? 🤔 My guess is that the app knows the phone is always there when the Spotify app is running and it's contantly polling the selectedDevice.
More likely, each Spotify client (the iOS app, desktop app, etc.) communicates with a backend server when it is running and broadcasts its availability. The client currently playing music sends information about the current playback to the server as well. The clients likely establish two-way connections—meaning the server can send information to a client without the client first requesting it. This eliminates the need for the client to constantly "poll" the server for, say, the available devices, like we have to do with the API.
I understand what you're saying, however, in the scenario that I've attempted to describe, the user remains in my app when the Spotify 404 error occurs.
Again, the problem is that after a few seconds of a track being paused and the user starts at step 1 above, and the device is a smartphone, and it's active, the app transferPlayback method throws the error:
The user is using your app, not the Spotify app. Therefore, if the Spotify app is not playing music, then it's suspended. In other words, it's frozen. It cannot run any code. This is why you cannot play music on the Spotify app (without making it the foreground app first). This also explains why the music sometimes begins playing only after you switch back to the Spotify app. At this point the Spotify app becomes unfrozen and can respond to messages sent to it, such as a request to play music.
even with the "freshest" results from getAvailableDevices and the smartphone being in those results and claiming to be active
As I said before:
When the Spotify iOS app is not playing music and you switch from it to your app, it will be suspended very shortly after. A consequence of this is that it is no longer an active or available device (although the Spotify web API may incorrectly report it as active/available).
The last part is a bug because the system notifies an app when it's about to be suspended, so the Spotify app should change its status at this point to unavailable. But it doesn't do this, so the API reports the device as still active or available, even though it's neither.
Please re-read my previous post.
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in Logger.log(.info, "Spotify detail view became active!") Task.init { await configureSpotify() } }
SwiftUI has its own new API for responding to lifecycle events. See this article.
Therefore, if the Spotify app is not playing music, then it's suspended... The last part is a bug because...
Ah, I get you now. That is very clarifying 💡
I was thinking my app centric, not considering that the Spotify app was being suspended when nothing is playing.
Thank you very much for taking the time to undestand and provide insight on this issue!
HI @Peter-Schorn,
I've got an edge case scenario that you might be able to shed some light on.
In brief:
.smartphone
and.computer
..first
on the results of getAvailbleDevices for theid
of the selected device in state.pausePlayback
method.If play is tapped when the selected device is of type,
.computer
, whether or not that device is active, the song will play using thetransferPlayback
method using the result offirst
from item 7 above. This always works.Here's the problem If play is tapped when the selected device is of type,
.smartphone
, even with that device being active, the song will only sometimes play using thetransferPlayback
method with the id fromfirst
from item 7 above.When this does not play, I can catch the error in a do/catch, and the error message is:
When this occurs, I handle this in the catch block by opening the Spotify app to the track that is paused, using:
Oddly enough, when the Spotify app opens, the track continues where it left off when paused.
Further, the device selector in the View is a Swift UI menu, when the menu is opened, it runs getAvailableDevices and populates menu items for each device, so that the menu is always "fresh".
Tapping on an available device in the menu, will attempt to play the track using the transferPlayback method.
Similar to the above, the
.computer
always works.Here's were it gets weird
Selecting
.smartphone
device in the menu sometimes plays/transfers the track (if not more than a few seconds have passed).When it doesn't work, one of two things can happen:
For the record, the play method seems to have the same issue in that it throws the aforementioned error in the same circumstances that the transferPlayback method does with
.smartPhone
devices.I know that's probably way more than you wanted to read, but it's an interesting edge case that I was wondering if you'd come across before.
I am intentionally not playing on the first available active device, in this scenario, as in some scenarios the user wants to play the track on their phone.
While opening up the app when the error mentioned above is thrown is a workaround, I was hoping to see if there was a solution that provided a better user experience.
As always, I love your library, appreciate your insight, and know that underlying issues may just be because of how the Spotify playback API works.
So thanks for any ideas you have to share.