Skip to content

Load AVAsset properties asynchronously - #26041

Open
crazytonyli wants to merge 4 commits into
trunkfrom
task/zero-warnings-avasset-async
Open

crazytonyli wants to merge 4 commits into
trunkfrom
task/zero-warnings-avasset-async

Conversation

@crazytonyli

@crazytonyli crazytonyli commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

The media upload gate and exporters read synchronous AVAsset properties (duration, isExportable, tracks, naturalSize, preferredTransform), which are deprecated since iOS 16. This PR replaces them with load(_:), which makes those code paths async.

A few things worth a closer look:

  1. MediaVideoExporter now exports with AVAssetExportSession.export(to:as:). It's back-deployed to iOS 13, so it works with our deployment target. Cancelling the returned Progress cancels the export task, and the exporter still reports videoExportSessionCancelled, which Aztec relies on to remove the attachment silently.
  2. Blog.canUploadVideo(from:) is async and nonisolated(nonsending), so it reads the blog on the caller's actor, like any other access to the blog's properties.
  3. When the duration can't be loaded, it's treated as zero. That's what the deprecated synchronous duration returned, so the upload gate and the exporter behave as before.

@wpmobilebot

wpmobilebot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number34646
VersionPR #26041
Bundle IDorg.wordpress.alpha
Commiteee87cc
Installation URL6oumr33giijso
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number34646
VersionPR #26041
Bundle IDcom.jetpack.alpha
Commiteee87cc
Installation URL1e3e8l2pkebm8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

The success handler already ran on the main thread, but the failure handler ran on the exporter's background thread.
Export videos with the async export API, which cancels with the task. If the duration can't be loaded, the export continues, as it did with the synchronous duration property.
@crazytonyli
crazytonyli force-pushed the task/zero-warnings-avasset-async branch from ef1b75f to eee87cc Compare September 24, 2026 03:25
@crazytonyli
crazytonyli marked this pull request as ready for review September 24, 2026 03:29
@crazytonyli crazytonyli added this to the 27.4 milestone Sep 24, 2026
let observer = VideoSessionProgressObserver(videoSession: session, progressHandler: { value in
progress.completedUnitCount = Int64(Float(MediaExportProgressUnits.done) * value)
})
defer { observer.stop() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stop() doesn't prevent one more progressHandler call. VideoSessionProgressObserver.work() calls the handler before checking interrupt, so a tick that's already queued on the global queue still runs up to 100ms after this defer.

When the export fails or is cancelled, that tick lands after fail() has set completedUnitCount = totalUnitCount and writes it back to done * session.progress (e.g. 40) — so the progress ends unfinished, which is what the fail() doc comment says it prevents. The window between stop() and fail() is microseconds, so this is the likely ordering, not an edge case.

Not a regression — the old code left progress partial on export failure too — but it's a one-line fix to make fail() hold:

private func work() {
    DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(100)) {
        guard !self.interrupt else { return }
        self.progressHandler(self.videoSession.progress)
        if self.videoSession.progress != 1 {
            self.work()
        }
    }
}

(interrupt is still an unsynchronized Bool read across threads, but that's pre-existing, and with the guard first a stale read only costs one extra tick.)

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants