SwiftAudioEx icon indicating copy to clipboard operation
SwiftAudioEx copied to clipboard

Main thread blocked by synchronous property query on not-yet-loaded property

Open horielov-abcz opened this issue 9 months ago • 1 comments
trafficstars

Describe the bug Main thread is blocked by synchronous property queries on not-yet-loaded asset properties for HTTP(S) assets, causing significant performance issues and UI freezing, especially on slow networks:

Main thread blocked by synchronous property query on not-yet-loaded property (assetProperty_CommonMetadata) for HTTP(S) asset. This could have been a problem if this asset were being read from a slow network.

Main thread blocked by synchronous property query on not-yet-loaded property (assetProperty_AvailableMetadataFormats) for HTTP(S) asset. This could have been a problem if this asset were being read from a slow network.

Main thread blocked by synchronous property query on not-yet-loaded property (Duration) for HTTP(S) asset. This could have been a problem if this asset were being read from a slow network.

To Reproduce I will provide minimal code to reproduce later... The key point is to play stream item, clear the player before it fully loaded and try to play item again.

Additional context It appears that pendingAsset.loadValuesAsynchronously and DispatchQueue.main.async in the AVPlayerWrapper load() method are causing issues. Migrating to the new async/await assets.load(.type) seems to be a solution. I used AI assistance to migrate and verify the solution, and I can confirm that this migration resolves the issue. However, I'm not deeply familiar with implementing this solution in a production environment. Has anyone performed such a migration?

Here is the migrated code (I think this code has its own problems and needs to be reworked):

  // AVPlayerWrapper line 230
  func load() {
      if (state == .failed) {
          recreateAVPlayer()
      } else {
          clearCurrentItem()
      }
      
      if let url = url {
          let pendingAsset = AVURLAsset(url: url, options: urlOptions)
          asset = pendingAsset
          state = .loading
          
          Task { @MainActor in
              do {
                  // Load common metadata
                  let commonMetadata = try await pendingAsset.load(.commonMetadata)
                  if !commonMetadata.isEmpty {
                      delegate?.AVWrapper(didReceiveCommonMetadata: commonMetadata)
                  }
                  
                  // Load chapter metadata
                  let chapterLocales = try await pendingAsset.load(.availableChapterLocales)
                  if chapterLocales.count > 0 {
                      for locale in chapterLocales {
                        let chapters = try await pendingAsset.loadChapterMetadataGroups(withTitleLocale: locale)
                          delegate?.AVWrapper(didReceiveChapterMetadata: chapters)
                      }
                  } else {
                      // Fallback to timed metadata if no chapters
                      let formats = try await pendingAsset.load(.availableMetadataFormats)
                      for format in formats {
                          let timeRange = CMTimeRange(
                              start: CMTime(seconds: 0, preferredTimescale: 1000),
                              end: try await pendingAsset.load(.duration)
                          )
                          let metadataItems = try await pendingAsset.loadMetadata(for: format)
                          let group = AVTimedMetadataGroup(items: metadataItems, timeRange: timeRange)
                          delegate?.AVWrapper(didReceiveTimedMetadata: [group])
                      }
                  }
                  
                  // Check if asset is playable
                  let isPlayable = try await pendingAsset.load(.isPlayable)
                  guard isPlayable else {
                      playbackFailed(error: AudioPlayerError.PlaybackError.itemWasUnplayable)
                      return
                  }
                  
                  // Create player item
                  let item = AVPlayerItem(asset: pendingAsset)
                  self.item = item
                  item.preferredForwardBufferDuration = bufferDuration
                  avPlayer.replaceCurrentItem(with: item)
                  startObservingAVPlayer(item: item)
                  applyAVPlayerRate()
                  
                  // Seek to initial time if needed
                  if let initialTime = timeToSeekToAfterLoading {
                      timeToSeekToAfterLoading = nil
                      seek(to: initialTime)
                  }
              } catch {
                  playbackFailed(error: AudioPlayerError.PlaybackError.failedToLoadKeyValue)
              }
          }
      }
  }

horielov-abcz avatar Jan 23 '25 12:01 horielov-abcz

bump. We are facing some issues getting the itemWasUnplayable using the RN-track-player on some devices with ios>=18. Can't repro it on my end but I am guessing the deprecation is the cause

prabhav-mehra avatar Feb 09 '25 23:02 prabhav-mehra