Skip to content

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

Description

@horielov-abcz

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)
              }
          }
      }
  }

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions