Repository navigation
Description
Steps to reproduce
- Create a new Flutter app (
flutter create --platforms=ios repro). - Add
video_playerand pin the iOS implementation:flutter pub add video_player video_player_avfoundation:2.12.2. - Replace
lib/main.dartwith the code sample below. flutter runon an iOS simulator.
The app crashes almost immediately (on the 2nd iteration in my runs).
Pinning video_player_avfoundation: 2.12.1 with the exact same code runs 197+ iterations without a crash, so this is a regression from 2.12.2 (#13197, "Convert player classes to Swift").
In a real app this shows up as a carousel with video pages: when a page is swiped away, the widget resets the video with seekTo(Duration.zero) and then disposes the controller. The app crashes a few seconds after the home screen loads.
Expected results
seekTo followed by dispose should not crash. The pending seek should be ignored, or complete with an error, once the player is disposed.
Actual results
The process aborts with:
Fatal error: Attempted to read an unowned reference but the object was already destroyed
Code sample
Code sample
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() => runApp(const MaterialApp(home: ReproPage()));
class ReproPage extends StatefulWidget {
const ReproPage({super.key});
@override
State<ReproPage> createState() => _ReproPageState();
}
class _ReproPageState extends State<ReproPage> {
int _iterations = 0;
@override
void initState() {
super.initState();
_run();
}
Future<void> _run() async {
while (mounted) {
final controller = VideoPlayerController.networkUrl(
Uri.parse('https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4'),
);
await controller.initialize();
await controller.play();
// Same pattern as a carousel page being swiped away:
// reset the video and dispose it without awaiting the seek.
controller.seekTo(Duration.zero);
await controller.dispose();
setState(() => _iterations++);
debugPrint('iteration $_iterations');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('iterations: $_iterations')));
}
}Analysis
In 2.12.2, VideoPlayerInstanceBridge holds the player as private unowned let player: VideoPlayer (VideoPlayer.swift). The only strong reference to the player is playersByIdentifier in the plugin, and that entry is removed from onDisposed during dispose().
The seekTo handler in the generated Messages.g.swift is asynchronous. It defers the call into a task:
seekToChannel.setMessageHandler { message, reply in
...
Task { @MainActor in
do {
try await api.seek(to: positionArg)pause and dispose are handled synchronously in the message handler. When seekTo and dispose arrive back to back:
- The
seekTomessage enqueues aTask. - The
disposemessage runs synchronously, removes the player fromplayersByIdentifier, and the player is deallocated. - The task then runs
VideoPlayerInstanceBridge.seek(to:), which loads theunownedreference to the destroyed player and triggersswift_abortRetainUnowned.
The other async bridge methods (getAudioTracks, selectAudioTrack, video track methods, etc.) have the same exposure.
A weak reference with an early return (or a "disposed" error) in the bridge would avoid the crash.
Logs
Crash report (triggered thread)
Exception Type: EXC_CRASH (SIGABRT)
0 libsystem_kernel.dylib __pthread_kill
1 libsystem_pthread.dylib pthread_kill
2 libsystem_c.dylib abort
3 libswiftCore.dylib swift::fatalErrorv(unsigned int, char const*, char*)
4 libswiftCore.dylib swift::fatalError(unsigned int, char const*, ...)
5 libswiftCore.dylib swift::swift_abortRetainUnowned(void const*)
6 libswiftCore.dylib swift_unknownObjectUnownedLoadStrong
7 Runner.debug.dylib VideoPlayerInstanceBridge.seek(to:)
8 Runner.debug.dylib protocol witness for VideoPlayerInstanceApi.seek(to:) in conformance VideoPlayerInstanceBridge
9 Runner.debug.dylib closure #1 in closure #6 in static VideoPlayerInstanceApiSetup.setUp(binaryMessenger:api:messageChannelSuffix:)
10 Runner.debug.dylib partial apply for closure #1 in closure #6 in static VideoPlayerInstanceApiSetup.setUp(binaryMessenger:api:messageChannelSuffix:)
11 Runner.debug.dylib thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A)
12 Runner.debug.dylib partial apply for thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A)
13 libswift_Concurrency.dylib completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*)
Flutter Doctor output
Environment
Flutter 3.47.7 • channel stable
Xcode 27.1
iOS Simulator: iPhone 14 Pro Max, iOS 27.0
video_player 2.14.1
video_player_avfoundation 2.12.2 (crashes) / 2.12.1 (no crash)
Activity
stuartmorgan commentedon Oct 22, 2026
I forgot that there was a closure scope in Pigeon code that could extend the bridge lifetime very briefly when I made that unowned; I was thinking the only owning ref would ever be the player. I'll need to make it weak and add more boilerplate to handle dummy return values.
stuartmorgan commentedon Oct 22, 2026
While this is a real plugin bug, this:
plus the reported behavior sounds like you are calling
disposewithout awaiting theseekTo. Callingdisposeon an object you are still interacting with is generally not something you should be doing.[video_player] Fix crash regression with async calls on iOS/macOS (fl…
github-actions commentedon Nov 7, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.