Skip to content

[video_player_avfoundation] 2.12.2: "Attempted to read an unowned reference" crash in VideoPlayerInstanceBridge.seek(to:) when seekTo is followed by dispose #195056

Closed
flutter/packages
#13383

Description

@Xelorium

Steps to reproduce

  1. Create a new Flutter app (flutter create --platforms=ios repro).
  2. Add video_player and pin the iOS implementation: flutter pub add video_player video_player_avfoundation:2.12.2.
  3. Replace lib/main.dart with the code sample below.
  4. flutter run on 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:

  1. The seekTo message enqueues a Task.
  2. The dispose message runs synchronously, removes the player from playersByIdentifier, and the player is deallocated.
  3. The task then runs VideoPlayerInstanceBridge.seek(to:), which loads the unowned reference to the destroyed player and triggers swift_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

  1. added
    c: regressionIt was better in the past than it is now
    c: crashStack traces logged to the console
    platform-iosiOS applications specifically
    platform-macosBuilding on or for macOS specifically
    packageflutter/packages repository. See also p: labels.
    c: fatal crashCrashes that terminate the process
    team-iosOwned by iOS platform team
    on Oct 22, 2026
  2. self-assigned this
    on Oct 22, 2026
  3. stuartmorgan commented on Oct 22, 2026

    @stuartmorgan
    Contributor

    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.

  4. stuartmorgan commented on Oct 22, 2026

    @stuartmorgan
    Contributor

    While this is a real plugin bug, this:

    when a page is swiped away, the widget resets the video with seekTo(Duration.zero) and then disposes the controller.

    plus the reported behavior sounds like you are calling dispose without awaiting the seekTo. Calling dispose on an object you are still interacting with is generally not something you should be doing.

  5. added
    P2Important issues not at the top of the work list
    triaged-iosTriaged by iOS platform team
    on Oct 23, 2026
  6. github-actions commented on Nov 7, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  7. locked as resolved and limited conversation to collaborators on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listc: crashStack traces logged to the consolec: fatal crashCrashes that terminate the processc: regressionIt was better in the past than it is nowp: video_playerThe Video Player pluginpackageflutter/packages repository. See also p: labels.platform-iosiOS applications specificallyplatform-macosBuilding on or for macOS specificallyteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions