SvenTiigi / YouTubePlayerKit

A Swift Package to easily play YouTube videos 📺
https://sventiigi.github.io/YouTubePlayerKit/
MIT License
717 stars 64 forks source link

YouTubePlayer's PlaybackState is temporarily out-of-sync with WebView after 'load' and 'cue' function calls #92

Closed acosmicflamingo closed 6 months ago

acosmicflamingo commented 6 months ago

What happened?

When using Swift concurrency functionality of YouTubePlayerKit, I thought it would be safe to assume that youtubePlayer.state would reflect the true state of YouTubePlayer's webView property, but it appears that there is a slight delay.

My attempts to address this behavior (https://github.com/SvenTiigi/YouTubePlayerKit/pull/91) actually are trying to fix a symptom instead of the underlying root cause (sorry for the recent GitHub activity hehehe).

What are the steps to reproduce?

Task { @MainActor in
  do {
    let urlString = "https://www.youtube.com/watch?v=omguEZ7jy5E"
    guard let source = YouTubePlayer.Source.url(urlString) else { return }

    try await youTubePlayer.cue(source: source)

    print("Before: ", await youTubePlayer.state)
    try await Task.sleep(for: .seconds(1.0))
    print("After:", await youTubePlayer.state)
  } catch {}
}

What is the expected behavior?

Expected:

Before: Optional(YouTubePlayerKit.YouTubePlayer.State.error(The owner of the requested video does not allow it to be played in embedded players.))
After: Optional(YouTubePlayerKit.YouTubePlayer.State.error(The owner of the requested video does not allow it to be played in embedded players.))

Actual:

Before:  Optional(YouTubePlayerKit.YouTubePlayer.State.ready)
After: Optional(YouTubePlayerKit.YouTubePlayer.State.error(The owner of the requested video does not allow it to be played in embedded players.))

I don't know whether this is just a limitation of the JavaScript API, since it appears that this logic is called between the "Before" and "After" print statements, but I would have expected that if I'm utilizing Swift Concurrency, I can safely do this:

Task { @MainActor in
  do {
    let urlString = "https://www.youtube.com/watch?v=omguEZ7jy5E"
    guard let source = YouTubePlayer.Source.url(urlString) else { return }

    try await youTubePlayer.cue(source: source)
    switch youTubePlayer.state {
      case .ready:
        print("It will play!")
      case .error:
        print("This URL will not play, sorry")
    }
  } catch {}
}
acosmicflamingo commented 6 months ago

I apologize for all the recent activity; I have come to understand more how the library behaves and it appears that the behavior is quite different than what I initially expected.

Turns out that the update actually will occur not when the completion closures are called but within WKNavigationDelegate. That is where the JavaScript event will make it's way to other publishers.

In addition, I have found a way to get the behavior I want by doing something like this (in case anyone else hits the issues I was seeing):

youtubePlayer.cue(source: source)
youtubePlayer.reload()  // a tip I saw Sven mention in a GitHub issue ticket somewhere

for await state in await youtubePlayerClient.statePublisher.values {
  switch state {
    case .ready:
      print("I'M READY!!!")
    default:
      print(":(")
  }

  // Kill async stream to prevent picking up any state updates that
  // occur outside the cueing scope
  break
}

Great work @SvenTiigi ! It's amazing what it can do :)