Skip to content

[video_player] Convert player classes to Swift - #13197

Merged
auto-submit[bot] merged 25 commits into
flutter:mainfrom
stuartmorgan:video-player-ios-swift-player
Oct 21, 2026
Merged

auto-submit[bot] merged 25 commits into
flutter:mainfrom
stuartmorgan:video-player-ios-swift-player

Conversation

@stuartmorgan

@stuartmorgan stuartmorgan commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Converts VideoPlayer and TextureBasedVideoPlayer to Swift. Also converts AVAssetTrackUtils since it simplifies calling from Swift, and that's a very simple conversion, as well as the AV* class wrappers, in order to replace deprecated methods with newer, Swift-friendlier alternatives. As part of the conversion, adopts Swift Pigeon for the instance-level APIs, so that all of the Pigeon generation in Swift-based (and combines the Pigeon definition files now that they can share a single config). Also adopts Pigeon's new async support to make handling async code simpler, so does require some test changes for the API surface change.

Intentionally left to later PRs, to limit scope:

  • The remaining utility files in the video_player_avfoundation_objc subtarget (although these should be relatively simple)
  • The code in video_player_avfoundation_ios and video_player_avfoundation_macos
  • Switching the manual event channel code in FVPEventBridge to Pigeon's Swift event channel support

As part of the conversion, the minimum Flutter version has been changed to 3.47, which allows dropping versions of iOS and macOS that are no longer supported in 3.47, and thus avoiding the need to convert legacy codepaths that only existing for those no-longer-supported versions.

Updates CI's iOS version to target for deprecation checks to iOS 15, rather than 14, to allow using things that require iOS 15 without failing that check (which overrides the deployment version, but is intended to raise it, not lower it).

Also replaces a number of methods that are deprecated in iOS 16, but for which the replacements were added in iOS 15.

Part of flutter/flutter#119105

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request converts the native player implementation of the AVFoundation video player plugin from Objective-C to Swift, updating the minimum supported SDK versions to Flutter 3.47/Dart 3.13, iOS 15.0, and macOS 12.0. It consolidates the Pigeon-generated message APIs into a single file and refactors the player classes (VideoPlayer and TextureBasedVideoPlayer) and tests accordingly. Feedback on the changes includes addressing a potential retain cycle in nested asynchronous closures within VideoPlayer.swift, removing an unused error variable, and utilizing Swift's native comparison operators for CMTime instead of C-style functions.

@stuartmorgan

Copy link
Copy Markdown
Contributor Author

Note for review: if you want to do a side-by-side comparison with the Obj-C code, you'll want to do that with the commit before 66e328f, and then review from there on separately. I did the initial conversion by editing the code inline, so it had the same structure and order, but then in that commit when I was doing final cleanup I reordered the file since the method order was a bit random.

@stuartmorgan stuartmorgan added override: no versioning needed Override the check requiring version bumps for most changes override: no changelog needed Override the check requiring CHANGELOG updates for most changes labels Oct 12, 2026
@stuartmorgan

Copy link
Copy Markdown
Contributor Author

version/changelog override: the incidental changes to image_picker_ios are #pragma directives to suppress warnings, so have no runtime impact.

@stuartmorgan stuartmorgan removed override: no versioning needed Override the check requiring version bumps for most changes override: no changelog needed Override the check requiring CHANGELOG updates for most changes labels Oct 13, 2026
@stuartmorgan

Copy link
Copy Markdown
Contributor Author

Removed the image picker incidental changes since #13224 will solve that problem as a separate PR, which is cleaner. This is now blocked from landing on that (or a similar) PR landing, but can still be reviewed as-is.

@stuartmorgan stuartmorgan left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think I got everything. Replacing the deprecated methods required some non-trivial refactoring, and pulled another file into the conversion, so it'll need non-trivial re-review.

let asset = currentItem.asset

// Get tracks from media selection (for HLS streams)
let audioGroup = asset.mediaSelectionGroup(forMediaCharacteristic: .audible)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in both locations. This required making both audio functions async in the Pigeon definition, so required some restructuring.

(The code will get less convoluted when the plugin switches to Pigeon's new async support, but that should be another PR.)

from: option.commonMetadata, withKey: AVMetadataKey.commonKeyTitle, keySpace: .common)
let commonMetadataTitle: String? = titleItems.first?.stringValue

let isSelected = currentSelection?.isEqual(option) ?? false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. I also inlined all of these values since the local variables aren't adding anything.

return
}

urlAsset.loadValuesAsynchronously(forKeys: [assetVariantsKey]) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All three instances have been replaced now. This involved migrating FVPAVFactory.* to Swift, since load(_:isolation) is an async function using isolated.

(Maybe there's some way to declare that in Obj-C that would auto-bridge correctly, but I don't know it, and all the code needs to be converted anyway.)

let status = urlAsset.statusOfValue(forKey: assetVariantsKey, error: nil)

if status == .loaded {
let variants = urlAsset.variants

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Replaced with load(.variants).


private func itemDidPlayToEndTime(_ item: AVPlayerItem) {
if isLooping {
item.seek(to: CMTime.zero) { _ in }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed; fixed all instances.

// Fixes https://github.com/flutter/flutter/issues/48670
if time.isIndefinite { return -9_223_372_036_854_775_807 }
if time.timescale == 0 { return 0 }
return time.value * 1000 / Int64(time.timescale)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

videoComposition.renderSize = CGSize(width: width, height: height)

videoComposition.sourceTrackIDForFrameTiming = videoTrack.trackID
if frameDurationIsValid(videoTrack.minFrameDuration) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Replaced with load(.minFrameDuration)

// Prefer more detailed error information from tracks loading.
var nsError: NSError?
var error: Error?
if player.currentItem?.asset.statusOfValue(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

(error as? NSError)?.localizedFailureReason,
underlyingError?.localizedDescription,
underlyingError?.localizedFailureReason,
].compactMap({ $0 })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reverted to NSMutableOrderedSet.

@stuartmorgan stuartmorgan added the override: no versioning needed Override the check requiring version bumps for most changes label Oct 15, 2026
@stuartmorgan

Copy link
Copy Markdown
Contributor Author

version override: the app-facing package change doesn't need to be published immediately since the main package itself does still support older OS versions via resolution to older implementations on older OS versions.

}

var duration: CMTime {
return avAsset.duration

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was confused, because I ran min-16 analyze locally before I uploaded this, to make sure I'd caught everything, and it passed. I thought maybe I had messed that up, but CI is green even though I merged in your bump to iOS 16 min.

So I checked the logs, and sure enough it's complaining about things:

/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:132:52: warning: 'naturalSize' was deprecated in iOS 16.0: Use load(.naturalSize) instead
            transform: transform, size: videoTrack.naturalSize)
                                                   ^
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:183:46: warning: 'duration' was deprecated in iOS 16.0: Use load(.duration) instead
    let duration = player.currentItem?.asset.duration.value ?? 0
                                             ^
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:247:44: warning: 'languageCode' was deprecated in iOS 16: renamed to 'language.languageCode.identifier'
              languageCode: option.locale?.languageCode,
                                           ^
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:247:44: note: use 'language.languageCode.identifier' instead
              languageCode: option.locale?.languageCode,
                                           ^~~~~~~~~~~~
                                           language.languageCode.identifier
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:249:54: warning: 'stringValue' was deprecated in iOS 16.0: Use load(.stringValue) instead
              commonMetadataTitle: titleItems.first?.stringValue)
                                                     ^
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:493:28: warning: 'naturalSize' was deprecated in iOS 16.0: Use load(.naturalSize) instead
    var width = videoTrack.naturalSize.width
                           ^
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:494:29: warning: 'naturalSize' was deprecated in iOS 16.0: Use load(.naturalSize) instead
    var height = videoTrack.naturalSize.height
                            ^
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:498:26: warning: 'naturalSize' was deprecated in iOS 16.0: Use load(.naturalSize) instead
      width = videoTrack.naturalSize.height
                         ^
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:499:27: warning: 'naturalSize' was deprecated in iOS 16.0: Use load(.naturalSize) instead
      height = videoTrack.naturalSize.width
                          ^
/Volumes/Work/s/w/ir/x/w/packages/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/VideoPlayer.swift:609:60: warning: 'duration' was deprecated in iOS 16.0: Use load(.duration) instead
    return convertCMTimeToMillis(player.currentItem?.asset.duration ?? .zero)

Oops

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Replaced with load(.duration). I've now correctly verified locally that the the PR is free of deprecation warnings at an iOS 16 level.

func selectAudioTrack(
atIndex trackIndex: Int64, completion: @escaping (Result<Void, Error>) -> Void
) {
guard let currentItem = player.currentItem else { return }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this need to call the completion? Will it hang?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ugh, good catch. I forgot to re-audit all the early returns of the methods I made async.

Once this lands I'll try to fast-follow with a PR to switch to Pigeons async` Swift support to eliminate the possibility of this class of bugs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

guard let currentItem = player.currentItem else { return }
let asset = currentItem.asset

asset.loadMediaSelectionGroup(for: .audible) { audioGroup, _ in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this need to be in a Task { @MainActor like getVideoTracks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed and converted to async version as above.

let asset = currentItem.asset

// Get tracks from media selection (for HLS streams)
asset.loadMediaSelectionGroup(for: .audible) { audioGroup, _ in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same, does this need to be in a Task { @MainActor ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Somehow I had it in my head that all these AV* classes were main-thread only, so assumed this callback would be on the main thread since it's passing in an AV class, but apparently I had at some point incorrectly extrapolated that from AVPlayer/AVPlayerItem. So yes, this is potentially doing AVPlayerItem access from a random background thread 😬

It turns out there's an async variant of this method, so I'll switch to that (in a @MainActor Task) so it's easier to reason about everything.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

(The Task adds a little boilerplate over the callback version, but that will go away as soon as these are async Pigeon methods.)

/// Note: Apple does not provide a constant for this key.
/// AVURLAsset.variants is documented via Apple's current async-loading API docs:
/// https://developer.apple.com/documentation/avfoundation/avpartialasyncproperty/variants
private let assetVariantsKey = "variants"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now unused.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed.

let currentItem = player.currentItem
assert(
currentItem?.status == .readyToPlay,
"reportInitializedIfReadyToPlay was called when the item wasn't ready to play.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same, this is the old objc name, use #function or hardcode

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

assert(!isInitialized, "reportInitializedIfReadyToPlay should only be called once.")
isInitialized = true
eventListener?.videoPlayerDidInitialize(
withDuration: duration(), size: currentItem?.presentationSize ?? CGSize.zero)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
withDuration: duration(), size: currentItem?.presentationSize ?? CGSize.zero)
withDuration: duration(), size: currentItem?.presentationSize ?? .zero)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

}

/// Protocol for abstracting access to an AVAsset, to enable unit testing.
protocol WrappedAVAsset: AnyObject {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this need to be a AnyObject or can WrappedAVAsset become a struct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Technically only WrappedAVPlayerItem needs this (see below) currently, but I added it to all of them for consistency. I can remove it from most of them if you prefer though.

}

/// Protocol for abstracting access to an AVPlayerItem, to enable unit testing.
protocol WrappedAVPlayerItem: AnyObject {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same, does this need to be a AnyObject or can WrappedAVPlayerItem become a struct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was needed because if WrappedAVPlayerItem doesn't have class semantics, then anything using the protocol has to follow struct semantics, which means that a let variable--say, the player item passed to VideoPlayer's init--becomes fully immutable rather than just not being reassignable as a variable. So playerItem.videoComposition = someNewComposition wouldn't work on a passed-in wrapped player item, even though it would on an actual player item.

@stuartmorgan
stuartmorgan requested a review from jmagman October 16, 2026 03:10
"\(#function) was called when the item wasn't ready to play.")
assert(!isInitialized, "\(#function) should only be called once.")
isInitialized = true
Task { @MainActor in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this changes event ordering. isInitialized is set, reportStatus calls updatePlayingState()-> player.play() -> rate KVO -> videoPlayerDidSetPlaying synchronously, and initialized only goes out when this task runs. The dart side can see isPlaying before initialized. In theory if dispose() runs first, currentItem is nil, duration() returns 0, then initialized would be sent after videoPlayerWasDisposed.

Since it's .readyToPlay duration should already be loaded. if case .loaded(let d) = asset.status(of: .duration) makes this synchronous.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

From the docs it doesn't seem like we can assume that specific properties are loaded just because it's ready to play, so I think we would still need to handle the non-loaded cases even though they would probably be rarely (if ever) called in practice.

I went the other direction instead and have more comprehensively adopted async, including at the Pigeon API level, so that we don't have sync method deferring work with Tasks, and instead can await as necessary to handle ordering. (There are potentially some ordering changes around KVO callbacks, which become async instead of sync, but I don't think we should be relying on exact call timing of those anyway.)

I have that pretty much done, but I need to iron out the unit tests that use autoreleasepool blocks to force deallocation at specific times, because they don't support async calls. Hopefully I'll have a new version up tomorrow.

}

func seek(to position: Int64, completion: @escaping (Result<Void, Error>) -> Void) {
Task { @MainActor in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The seek used to be issued synchronously from pigeon, but now it's issued after an await. seekTo(x) followed by play() from dart would reach AVPlayer as play-then-seek.
Same as previous comment in reportInitialized, I think you can sync status(of: .duration) the duration instead of loading it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The seek used to be issued synchronously from pigeon, but now it's issued after an await. seekTo(x) followed by play() from dart would reach AVPlayer as play-then-seek.

This is true, but seekTo and play are async on the Dart side, so if you write seekTo(x); play() instead of await seekTo(x); await play() you are already playing with fire. Even though the method was returning right away on the native side, the Dart Future wouldn't complete until the completion(...) call at the end of the Task, so correct Dart code would work.

This should be clearer in the new code, where seek is an anync method, rather than a synchronous method that dispatches to a Task that calls a completion handler.

}
completion(.success(trackData))
} catch {
completion(.success([]))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is the catch here really a success that looks like no tracks? Maybe at least log the error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Now that these functions are all async, I made them async throws and let any failures fall up into the Pigeon handler that will turn them into channel errors.

currentItem.select(option, in: audioGroup)
}
}
} catch {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same, should this catch be a .failure instead of a success that looks like it was selected?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed as above.

mediaSelectionTracks: mediaSelectionTracks)))
} catch {
completion(
.success(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same, is this catch really a success?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed as above.

// Fixes https://github.com/flutter/flutter/issues/48670
if time.isIndefinite { return -9_223_372_036_854_775_807 }
if time.timescale == 0 { return 0 }
return CMTimeConvertScale(time, timescale: 1000, method: .roundHalfAwayFromZero).value

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you want to round towards 0 to keep it the same ms.

Suggested change
return CMTimeConvertScale(time, timescale: 1000, method: .roundHalfAwayFromZero).value
return CMTimeConvertScale(time, timescale: 1000, method: .roundTowardZero).value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was assuming that the old behavior was for expediency rather than a specific intent to truncate, but changed to roundTowardZero to preserve behavior.

Comment on lines +588 to +595
if let currentItem = player.currentItem {
switch currentItem.asset.status(of: .tracks) {
case .failed(let nsError):
error = nsError
case .notYetLoaded, .loading, .loaded(_):
break
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit, Swift cleverness (feel free to ignore any of these that aren't behavioral btw)

Suggested change
if let currentItem = player.currentItem {
switch currentItem.asset.status(of: .tracks) {
case .failed(let nsError):
error = nsError
case .notYetLoaded, .loading, .loaded(_):
break
}
}
if case .failed(let trackError)? = player.currentItem?.asset.status(of: .tracks) {
error = trackError
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice! if case is a pattern I am vaguely aware exists in both Dart and Swift, but haven't internalized yet. Changed.

/// Wraps the AVAudioSession method of the same name.
func setCategory(
_ category: AVAudioSession.Category,
with options: AVAudioSession.CategoryOptions) throws

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AVAudioSession doesn't have the with

Suggested change
with options: AVAudioSession.CategoryOptions) throws
options: AVAudioSession.CategoryOptions) throws

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

func setCategory(
_ category: AVAudioSession.Category, with options: AVAudioSession.CategoryOptions
) throws {
return try AVAudioSession.sharedInstance().setCategory(category, options: options)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return try AVAudioSession.sharedInstance().setCategory(category, options: options)
try AVAudioSession.sharedInstance().setCategory(category, options: options)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@jmagman

jmagman commented Oct 16, 2026

Copy link
Copy Markdown
Member

No longer left for later in the description:

  • Adopting the new Pigeon async/await support

@stuartmorgan
stuartmorgan requested a review from jmagman October 20, 2026 14:11
// - https://github.com/flutter/flutter/issues/124937
//
// Failing to de-register results in a crash in [AVPlayer willChangeValueForKey:].
@Test func doesNotCrashOnRateObservationAfterDisposal() async throws {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason this test is deleted instead of having the autoreleasepool code adjusted like the test below is that when I went to update it, I realized the test was a lie now anyways; leaving the KVO observers registered won't be a crash any more anyway, because in the new block-based version the behavior would be that the block is called, it finds that self is nil, and then it no-ops. So this test would never fail even with the bug present (exhibit A, the earlier iteration of this patch that did in fact leave them registered), and since it's a no-op anyway now I don't see how we could actually test it (nor is it critical to test like it was when it would crash).

@jmagman jmagman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The last few things are very minor nits, feel free to ignore.

Thank you so much for working on this @stuartmorgan!

playToEndObserver = NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime, object: item, queue: .main
) { [weak self] notification in
Task { @MainActor in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit, I think this is already on the main queue a few lines above so you don't need the extra hop:

Suggested change
Task { @MainActor in
MainActor.assumeIsolated {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed; I forgot this one had that argument.

(We could probably get away with doing the same thing in the KVO callbacks too, since they fire synchronously on the calling thread and player and player item should be MainActor-only, but it felt a bit risky which is why I used the Task instead. If there ever are ordering issues with these, that could be revisited. I'll actually add a comment about that just in case.)


// MARK: FlutterTexture

func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

fyi I think this will need to become nonisolated when we start enforcing swift 6 (I believe it's run on the raster thread?), but not worth changing for this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, this class is split over several threads, so will need work to fully annotate. I punted on everything but the overridden methods for now since there wasn't a bunch of new async stuff in this class.

}
}

@MainActor func setEventListener(_ eventListener: FVPVideoEventListener) async {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit since VideoPlayer is @MainActor the funcs don't need to be annotated as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oops, I thought I did a pass over the file when I switched from trying to do individual annotations to making the whole class MainActor, but apparently not since there were a bunch of them left. Removed everywhere.

private var keyValueObservations: [NSKeyValueObservation] = []
private var playToEndObserver: NSObjectProtocol?

/// Initializes a new instance of FVPVideoPlayer with the given AVPlayerItem, AV factory, and view

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
/// Initializes a new instance of FVPVideoPlayer with the given AVPlayerItem, AV factory, and view
/// Initializes a new instance of VideoPlayer with the given AVPlayerItem, AV factory, and view

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

/// AVPlayerItem is ready to play and the event listener is set to a non-nil value, whichever occurs
/// last.
///
/// This flag is set to YES when the "initialized" message is first sent, and is never set to NO

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
/// This flag is set to YES when the "initialized" message is first sent, and is never set to NO
/// This flag is set to true when the "initialized" message is first sent, and is never set to false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@stuartmorgan stuartmorgan added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 21, 2026
@auto-submit
auto-submit Bot merged commit 2ef3d18 into flutter:main Oct 21, 2026
13 checks passed
GhagSagar23 pushed a commit to Kryonex-Labs/flutter that referenced this pull request Oct 22, 2026
flutter/packages@1383427...93baed9

2026-10-22 [email protected] [material_ui] Port flutter/flutter
flutter#186777 MaterialColor test (flutter/packages#12858)
2026-10-21 [email protected] [rfw] Migrate to material_ui
(flutter/packages#12811)
2026-10-21 [email protected] [cupertino_ui] Fix the installing
command of the cupertino_ui package (flutter/packages#13141)
2026-10-21 [email protected]
[cupertino_ui] Unskip web tests that pass on CanvasKit and Skwasm:
date_picker_test, spell_check_suggestions_toolbar_test
(flutter/packages#13367)
2026-10-21 [email protected] [video_player] Convert player classes
to Swift (flutter/packages#13197)
2026-10-21 [email protected] [camera_windows] Fall back to a physical
stream for cameras that reject preferred-stream selectors
(flutter/packages#12748)
2026-10-21 [email protected] [google_sign_in_ios, video_player] Specify
types for pattern variable declarations (flutter/packages#13362)
2026-10-21 [email protected] [google_maps_flutter_web] Issue 64073
implement my location (flutter/packages#12186)
2026-10-21 [email protected] [material_ui]
Unskip web tests that pass on CanvasKit and Skwasm: app_bar_theme_test,
input_decorator_test, paginated_data_table_test,
spell_check_suggestions_toolbar_test (flutter/packages#13352)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
flutter/packages@1383427...93baed9

2026-10-22 [email protected] [material_ui] Port flutter/flutter
flutter#186777 MaterialColor test (flutter/packages#12858)
2026-10-21 [email protected] [rfw] Migrate to material_ui
(flutter/packages#12811)
2026-10-21 [email protected] [cupertino_ui] Fix the installing
command of the cupertino_ui package (flutter/packages#13141)
2026-10-21 [email protected]
[cupertino_ui] Unskip web tests that pass on CanvasKit and Skwasm:
date_picker_test, spell_check_suggestions_toolbar_test
(flutter/packages#13367)
2026-10-21 [email protected] [video_player] Convert player classes
to Swift (flutter/packages#13197)
2026-10-21 [email protected] [camera_windows] Fall back to a physical
stream for cameras that reject preferred-stream selectors
(flutter/packages#12748)
2026-10-21 [email protected] [google_sign_in_ios, video_player] Specify
types for pattern variable declarations (flutter/packages#13362)
2026-10-21 [email protected] [google_maps_flutter_web] Issue 64073
implement my location (flutter/packages#12186)
2026-10-21 [email protected] [material_ui]
Unskip web tests that pass on CanvasKit and Skwasm: app_bar_theme_test,
input_decorator_test, paginated_data_table_test,
spell_check_suggestions_toolbar_test (flutter/packages#13352)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD override: no versioning needed Override the check requiring version bumps for most changes p: video_player platform-ios platform-macos

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants