flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.12k stars 27.43k forks source link

[video_player][android] DecoderInitializationException when _VideoAppLifeCycle resumed #154559

Closed qinjing100 closed 1 month ago

qinjing100 commented 2 months ago

Steps to reproduce

  1. run code samples in debug and release
  2. init VideoPlayerController from local file
  3. play or pause video
  4. click share button to share video file
  5. dismiss share

Expected results

  1. video keep play or pause state

Actual results

MediaCodecRenderer: Failed to initialize decoder: OMX.MTK.VIDEO.DECODER.AVC
MediaCodecRenderer: DecoderInitializationException: Decoder init failed: OMX.MTK.VIDEO.DECODER.AVC, Format(1, null, null, video/avc, avc1.64001F, -1, null, [720, 1280, 30.0, ColorInfo(BT709, Limited range, SDR SMPTE 170M, false, 8bit Luma, 8bit Chroma)], [-1, -1])

Code sample

Code sample ```dart import 'dart:io'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import 'package:share_plus/share_plus.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart'; class ButterFlyAssetVideo extends StatefulWidget { @override ButterFlyAssetVideoState createState() => ButterFlyAssetVideoState(); } class ButterFlyAssetVideoState extends State { late VideoPlayerController _controller; String fp = '/storage/emulated/0/Download/1134977543617888510.mp4'; @override void initState() { super.initState(); _controller = VideoPlayerController.file(File(fp)); _controller.addListener(() { setState(() {}); }); _controller.setLooping(true); _controller.initialize().then((_) => setState(() {})); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SingleChildScrollView( child: Column( children: [ Container( padding: const EdgeInsets.only(top: 20.0), ), const Text('With assets mp4'), Container( padding: const EdgeInsets.all(20), child: AspectRatio( aspectRatio: _controller.value.aspectRatio, child: Stack( alignment: Alignment.bottomCenter, children: [ VideoPlayer(_controller), _ControlsOverlay(controller: _controller), VideoProgressIndicator(_controller, allowScrubbing: true), ], ), ), ), ], ), ); } } class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({required this.controller}); static const List _exampleCaptionOffsets = [ Duration(seconds: -10), Duration(seconds: -3), Duration(seconds: -1, milliseconds: -500), Duration(milliseconds: -250), Duration.zero, Duration(milliseconds: 250), Duration(seconds: 1, milliseconds: 500), Duration(seconds: 3), Duration(seconds: 10), ]; static const List _examplePlaybackRates = [ 0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0, ]; final VideoPlayerController controller; @override Widget build(BuildContext context) { return Stack( children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 50), reverseDuration: const Duration(milliseconds: 200), child: controller.value.isPlaying ? const SizedBox.shrink() : const ColoredBox( color: Colors.black26, child: Center( child: Icon( Icons.play_arrow, color: Colors.white, size: 100.0, semanticLabel: 'Play', ), ), ), ), GestureDetector( onTap: () { controller.value.isPlaying ? controller.pause() : controller.play(); }, ), Align( alignment: Alignment.topLeft, child: PopupMenuButton( initialValue: controller.value.captionOffset, tooltip: 'Caption Offset', onSelected: (Duration delay) { controller.setCaptionOffset(delay); }, itemBuilder: (BuildContext context) { return >[ for (final Duration offsetDuration in _exampleCaptionOffsets) PopupMenuItem( value: offsetDuration, child: Text('${offsetDuration.inMilliseconds}ms'), ) ]; }, child: Padding( padding: const EdgeInsets.symmetric( // Using less vertical padding as the text is also longer // horizontally, so it feels like it would need more spacing // horizontally (matching the aspect ratio of the video). vertical: 12, horizontal: 16, ), child: Text('${controller.value.captionOffset.inMilliseconds}ms'), ), ), ), Align( alignment: Alignment.topRight, child: PopupMenuButton( initialValue: controller.value.playbackSpeed, tooltip: 'Playback speed', onSelected: (double speed) { controller.setPlaybackSpeed(speed); }, itemBuilder: (BuildContext context) { return >[ for (final double speed in _examplePlaybackRates) PopupMenuItem( value: speed, child: Text('${speed}x'), ) ]; }, child: Padding( padding: const EdgeInsets.symmetric( // Using less vertical padding as the text is also longer // horizontally, so it feels like it would need more spacing // horizontally (matching the aspect ratio of the video). vertical: 12, horizontal: 16, ), child: Text('${controller.value.playbackSpeed}x'), ), ), ), Positioned( // alignment: Alignment.bottomRight, bottom: 32, right: 8, child: IconButton( onPressed: () async { var medias = List.filled( 1, '/storage/emulated/0/Download/1134977543617888510.mp4'); // share file try { await shareWithResult(medias); } catch (e) { debugPrint('share error: $e'); } }, icon: const Icon(Icons.share_outlined, color: Colors.white, size: 36), ), ), ], ); } Future shareWithResult(List medias) async { ShareResult shareResult; final files = []; for (var i = 0; i < medias.length; i++) { var mt = lookupMimeType(medias[i]); debugPrint('file: ${medias[i]}, type: $mt'); XFile xfile; if (mt != null && mt.startsWith('image')) { debugPrint('add image file to share!'); xfile = XFile(medias[i], name: basename(medias[i]), mimeType: mt); } else { xfile = XFile(medias[i], name: basename(medias[i])); } // only support share one file per time if (files.isEmpty) { files.add(xfile); } } String status = 'share result: '; try { shareResult = await Share.shareXFiles( files, text: 'share text', subject: 'share subject', ); if (shareResult.status == ShareResultStatus.success) { status = "$status: share succeed."; } else if (shareResult.status == ShareResultStatus.dismissed) { status = "$status: share dismissed."; } else { status = "$status: share unavailable!"; } } catch (e) { debugPrint('share error: $e'); } } } ```

Screenshots or Video

Screenshots / Video demonstration ### 1. play video from file ### 2. click share > image ### 3. dissmiss share, resume play video page > image ### 4. state error, play or pause button can not work, and video progress indicator keep moving > image

Logs

Logs ```console [+1460 ms] I/flutter (19615): file: /storage/emulated/0/Download/1134977543617888510.mp4, type: video/mp4 [ +563 ms] V/CCodec (19615): watch for 1 codecs [ ] D/PipelineWatcher(19615): DEBUG: elapsed 2 / 2 [+1727 ms] D/BufferPoolAccessor2.0(19615): bufferpool2 0xb40000773a80d028 : 6(49152 size) total buffers - 5(40960 size) used buffers - 1/7 (recycle/alloc) - 6/44 (fetch/transfer) [ ] D/BufferPoolAccessor2.0(19615): evictor expired: 1, evicted: 1 [ +55 ms] I/FA (19615): Application backgrounded at: timestamp_millis: 1725375039034 [+12961 ms] I/ExoPlayerImpl(19615): Init 6fa0e3a [AndroidXMedia3/1.4.0] [PD2164, V2164A, vivo, 30] [ +42 ms] W/AString (19615): ctor got NULL, using empty string instead [ ] D/MediaCodec(19615): CreateByComponentName,componentName is:OMX.MTK.VIDEO.DECODER.AVC [ ] I/ACodec (19615): mIsVivoLogEnable = 0 [ +4 ms] I/OMXClient(19615): IOmx service obtained [ +3 ms] D/MediaCodec(19615): flushMediametrics [ ] D/SurfaceUtils(19615): connecting to surface 0xb4000073e82d8010, reason connectToSurface [ ] E/BufferQueueProducer(19615): [ImageReader-1x1f22m5-19615-0](id:4c9f00000000,api:3,p:19615,c:19615) connect: already connected (cur=3 req=3) [ ] E/SurfaceUtils(19615): Failed to connect to surface 0xb4000073e82d8010, err -22 [ ] E/MediaCodec(19615): nativeWindowConnect returned an error: Invalid argument (-22) [ ] E/MediaCodec(19615): configure failed with err 0xffffffea, resetting... [ +3 ms] I/ACodec (19615): mIsVivoLogEnable = 0 [ ] I/OMXClient(19615): IOmx service obtained [ +6 ms] W/MediaCodecRenderer(19615): Failed to initialize decoder: OMX.MTK.VIDEO.DECODER.AVC [ ] W/MediaCodecRenderer(19615): java.lang.IllegalArgumentException [ ] W/MediaCodecRenderer(19615): at android.media.MediaCodec.native_configure(Native Method) [ ] W/MediaCodecRenderer(19615): at android.media.MediaCodec.configure(MediaCodec.java:2158) [ ] W/MediaCodecRenderer(19615): at android.media.MediaCodec.configure(MediaCodec.java:2074) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter$Factory.createAdapter(SynchronousMediaCodecAdapter.java:53) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.mediacodec.DefaultMediaCodecAdapterFactory.createAdapter(DefaultMediaCodecAdapterFactory.java:139) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.initCodec(MediaCodecRenderer.java:1225) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecWithFallback(MediaCodecRenderer.java:1137) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecOrBypass(MediaCodecRenderer.java:588) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.onInputFormatChanged(MediaCodecRenderer.java:1602) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.onInputFormatChanged(MediaCodecVideoRenderer.java:1182) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.readSourceOmittingSampleData(MediaCodecRenderer.java:1042) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:860) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.render(MediaCodecVideoRenderer.java:1018) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:1136) [ ] W/MediaCodecRenderer(19615): at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:561) [ ] W/MediaCodecRenderer(19615): at android.os.Handler.dispatchMessage(Handler.java:102) [ ] W/MediaCodecRenderer(19615): at android.os.Looper.loop(Looper.java:257) [ ] W/MediaCodecRenderer(19615): at android.os.HandlerThread.run(HandlerThread.java:67) [ ] E/MediaCodecVideoRenderer(19615): Video codec error [ ] E/MediaCodecVideoRenderer(19615): androidx.media3.exoplayer.mediacodec.MediaCodecRenderer$DecoderInitializationException: Decoder init failed: OMX.MTK.VIDEO.DECODER.AVC, Format(1, null, null, video/avc, avc1.64001F, -1, null, [720, 1280, 30.0, ColorInfo(BT709, Limited range, SDR SMPTE 170M, false, 8bit Luma, 8bit Chroma)], [-1, -1]) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecWithFallback(MediaCodecRenderer.java:1144) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecOrBypass(MediaCodecRenderer.java:588) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.onInputFormatChanged(MediaCodecRenderer.java:1602) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.onInputFormatChanged(MediaCodecVideoRenderer.java:1182) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.readSourceOmittingSampleData(MediaCodecRenderer.java:1042) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:860) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.render(MediaCodecVideoRenderer.java:1018) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:1136) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:561) [ ] E/MediaCodecVideoRenderer(19615): at android.os.Handler.dispatchMessage(Handler.java:102) [ ] E/MediaCodecVideoRenderer(19615): at android.os.Looper.loop(Looper.java:257) [ ] E/MediaCodecVideoRenderer(19615): at android.os.HandlerThread.run(HandlerThread.java:67) [ ] E/MediaCodecVideoRenderer(19615): Caused by: java.lang.IllegalArgumentException [ ] E/MediaCodecVideoRenderer(19615): at android.media.MediaCodec.native_configure(Native Method) [ ] E/MediaCodecVideoRenderer(19615): at android.media.MediaCodec.configure(MediaCodec.java:2158) [ ] E/MediaCodecVideoRenderer(19615): at android.media.MediaCodec.configure(MediaCodec.java:2074) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter$Factory.createAdapter(SynchronousMediaCodecAdapter.java:53) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.DefaultMediaCodecAdapterFactory.createAdapter(DefaultMediaCodecAdapterFactory.java:139) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.initCodec(MediaCodecRenderer.java:1225) [ ] E/MediaCodecVideoRenderer(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecWithFallback(MediaCodecRenderer.java:1137) [ ] E/MediaCodecVideoRenderer(19615): ... 11 more [ ] E/ExoPlayerImplInternal(19615): Playback error [ ] E/ExoPlayerImplInternal(19615): androidx.media3.exoplayer.ExoPlaybackException: MediaCodecVideoRenderer error, index=0, format=Format(1, null, null, video/avc, avc1.64001F, -1, null, [720, 1280, 30.0, ColorInfo(BT709, Limited range, SDR SMPTE 170M, false, 8bit Luma, 8bit Chroma)], [-1, -1]), format_supported=YES [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:640) [ ] E/ExoPlayerImplInternal(19615): at android.os.Handler.dispatchMessage(Handler.java:102) [ ] E/ExoPlayerImplInternal(19615): at android.os.Looper.loop(Looper.java:257) [ ] E/ExoPlayerImplInternal(19615): at android.os.HandlerThread.run(HandlerThread.java:67) [ ] E/ExoPlayerImplInternal(19615): Caused by: androidx.media3.exoplayer.mediacodec.MediaCodecRenderer$DecoderInitializationException: Decoder init failed: OMX.MTK.VIDEO.DECODER.AVC, Format(1, null, null, video/avc, avc1.64001F, -1, null, [720, 1280, 30.0, ColorInfo(BT709, Limited range, SDR SMPTE 170M, false, 8bit Luma, 8bit Chroma)], [-1, -1]) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecWithFallback(MediaCodecRenderer.java:1144) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecOrBypass(MediaCodecRenderer.java:588) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.onInputFormatChanged(MediaCodecRenderer.java:1602) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.onInputFormatChanged(MediaCodecVideoRenderer.java:1182) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.readSourceOmittingSampleData(MediaCodecRenderer.java:1042) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:860) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.render(MediaCodecVideoRenderer.java:1018) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:1136) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:561) [ ] E/ExoPlayerImplInternal(19615): ... 3 more [ ] E/ExoPlayerImplInternal(19615): Caused by: java.lang.IllegalArgumentException [ ] E/ExoPlayerImplInternal(19615): at android.media.MediaCodec.native_configure(Native Method) [ ] E/ExoPlayerImplInternal(19615): at android.media.MediaCodec.configure(MediaCodec.java:2158) [ ] E/ExoPlayerImplInternal(19615): at android.media.MediaCodec.configure(MediaCodec.java:2074) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter$Factory.createAdapter(SynchronousMediaCodecAdapter.java:53) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.DefaultMediaCodecAdapterFactory.createAdapter(DefaultMediaCodecAdapterFactory.java:139) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.initCodec(MediaCodecRenderer.java:1225) [ ] E/ExoPlayerImplInternal(19615): at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecWithFallback(MediaCodecRenderer.java:1137) [ ] E/ExoPlayerImplInternal(19615): ... 11 more [ ] D/MediaCodec(19615): flushMediametrics [ +7 ms] D/MediaCodec(19615): flushMediametrics [+1669 ms] I/TRuntime.CctTransportBackend(19615): Making request to: https://firebaselogging-pa.googleapis.com/v1/firelog/legacy/batchlog ```

Flutter Doctor output

Doctor output ```console [✓] Flutter (Channel stable, 3.24.0, on macOS 14.6.1 23G93 darwin-x64, locale zh-Hans-HK) • Flutter version 3.24.0 on channel stable at /Users/bruce/fvm/versions/3.24.0 • Upstream repository https://github.com/flutter/flutter.git • Framework revision 80c2e84975 (5 weeks ago), 2024-07-30 23:06:49 +0700 • Engine revision b8800d88be • Dart version 3.5.0 • DevTools version 2.37.2 • Pub download mirror https://pub.flutter-io.cn • Flutter download mirror https://storage.flutter-io.cn [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/bruce/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11572160) • All Android licenses accepted. [!] Xcode - develop for iOS and macOS (Xcode 15.3) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 15E204a ✗ Unable to get list of installed Simulator runtimes. ! CocoaPods 1.11.3 out of date (1.13.0 is recommended). CocoaPods is a package manager for iOS or macOS platform code. Without CocoaPods, plugins will not work on iOS or macOS. For more info, see https://flutter.dev/to/platform-plugins To update CocoaPods, see https://guides.cocoapods.org/using/getting-started.html#updating-cocoapods [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2023.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 17.0.10+0-17.0.10b1087.21-11572160) [✓] VS Code (version 1.92.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.94.0 [✓] Connected device (3 available) • V2164A (mobile) • 344393009600178 • android-arm64 • Android 11 (API 30) • macOS (desktop) • macos • darwin-x64 • macOS 14.6.1 23G93 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 128.0.6613.114 [✓] Network resources • All expected network resources are available. ! Doctor found issues in 1 category. ```
darshankawar commented 1 month ago

Thanks for the report @qinjing100 share_plus and mime are third party packages. Does the same behavior persists without using these package's code implementation and using the intent chooser that Android provides to share the video ? If so, please provide us updated code sample.

qinjing100 commented 1 month ago

@darshankawar Thanks for your reply. Because Flutter Official share plugin has been replaced by the share_plus, I do not know how to use intent chooser that Android provides manually.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:share_plus/share_plus.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart';

class ButterFlyAssetVideo extends StatefulWidget {
  @override
  ButterFlyAssetVideoState createState() => ButterFlyAssetVideoState();
}

class ButterFlyAssetVideoState extends State<ButterFlyAssetVideo>
    with WidgetsBindingObserver {
  late VideoPlayerController _controller;

  String fp = '/storage/emulated/0/Download/833095631108348966.mp4';

  @override
  void initState() {
    super.initState();

    // _controller = VideoPlayerController.file(File(fp),);

    // _controller.addListener(() {
    //   setState(() {});
    // });
    // _controller.setLooping(true);
    // _controller.initialize().then((_) => setState(() {}));
    // _controller.play();
    WidgetsBinding.instance.addObserver(this);
    initialize();
  }

  initialize() {
    _controller = VideoPlayerController.file(File(fp),
        videoPlayerOptions: VideoPlayerOptions(
          allowBackgroundPlayback: true,
        ));

    _controller.addListener(() {
      setState(() {});
    });
    _controller.setLooping(true);
    _controller.initialize().then((_) => setState(() {}));
    _controller.play();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    debugPrint("didChangeAppLifecycleState $state");
    switch (state) {
      case AppLifecycleState.inactive:
        {
          _controller.dispose();
        }
        break;

      case AppLifecycleState.paused:
        {
          _controller.dispose();
        }
        break;

      case AppLifecycleState.detached:
        {
          _controller.dispose();
        }
        break;

      case AppLifecycleState.resumed:
        {
          initialize();
        }
        break;

      default:
        {
          //statements;
        }
        break;
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          Container(
            padding: const EdgeInsets.only(top: 20.0),
          ),
          const Text('With assets mp4'),
          Container(
            padding: const EdgeInsets.all(20),
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: Stack(
                alignment: Alignment.bottomCenter,
                children: <Widget>[
                  VideoPlayer(_controller),
                  _ControlsOverlay(controller: _controller),
                  VideoProgressIndicator(_controller, allowScrubbing: true),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _ControlsOverlay extends StatelessWidget {
  const _ControlsOverlay({required this.controller});

  static const List<Duration> _exampleCaptionOffsets = <Duration>[
    Duration(seconds: -10),
    Duration(seconds: -3),
    Duration(seconds: -1, milliseconds: -500),
    Duration(milliseconds: -250),
    Duration.zero,
    Duration(milliseconds: 250),
    Duration(seconds: 1, milliseconds: 500),
    Duration(seconds: 3),
    Duration(seconds: 10),
  ];
  static const List<double> _examplePlaybackRates = <double>[
    0.25,
    0.5,
    1.0,
    1.5,
    2.0,
    3.0,
    5.0,
    10.0,
  ];

  final VideoPlayerController controller;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 50),
          reverseDuration: const Duration(milliseconds: 200),
          child: controller.value.isPlaying
              ? const SizedBox.shrink()
              : const ColoredBox(
                  color: Colors.black26,
                  child: Center(
                    child: Icon(
                      Icons.play_arrow,
                      color: Colors.white,
                      size: 100.0,
                      semanticLabel: 'Play',
                    ),
                  ),
                ),
        ),
        GestureDetector(
          onTap: () {
            controller.value.isPlaying ? controller.pause() : controller.play();
          },
        ),
        Align(
          alignment: Alignment.topLeft,
          child: PopupMenuButton<Duration>(
            initialValue: controller.value.captionOffset,
            tooltip: 'Caption Offset',
            onSelected: (Duration delay) {
              controller.setCaptionOffset(delay);
            },
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<Duration>>[
                for (final Duration offsetDuration in _exampleCaptionOffsets)
                  PopupMenuItem<Duration>(
                    value: offsetDuration,
                    child: Text('${offsetDuration.inMilliseconds}ms'),
                  )
              ];
            },
            child: Padding(
              padding: const EdgeInsets.symmetric(
                // Using less vertical padding as the text is also longer
                // horizontally, so it feels like it would need more spacing
                // horizontally (matching the aspect ratio of the video).
                vertical: 12,
                horizontal: 16,
              ),
              child: Text('${controller.value.captionOffset.inMilliseconds}ms'),
            ),
          ),
        ),
        Align(
          alignment: Alignment.topRight,
          child: PopupMenuButton<double>(
            initialValue: controller.value.playbackSpeed,
            tooltip: 'Playback speed',
            onSelected: (double speed) {
              controller.setPlaybackSpeed(speed);
            },
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<double>>[
                for (final double speed in _examplePlaybackRates)
                  PopupMenuItem<double>(
                    value: speed,
                    child: Text('${speed}x'),
                  )
              ];
            },
            child: Padding(
              padding: const EdgeInsets.symmetric(
                // Using less vertical padding as the text is also longer
                // horizontally, so it feels like it would need more spacing
                // horizontally (matching the aspect ratio of the video).
                vertical: 12,
                horizontal: 16,
              ),
              child: Text('${controller.value.playbackSpeed}x'),
            ),
          ),
        ),
        Positioned(
          // alignment: Alignment.bottomRight,
          bottom: 32,
          right: 8,
          child: IconButton(
            onPressed: () async {
              var medias = List<String>.filled(
                  1, '/storage/emulated/0/Download/833095631108348966.mp4');
              // share file
              try {
                await shareWithResult(medias);
              } catch (e) {
                debugPrint('share error: $e');
              }
            },
            icon:
                const Icon(Icons.share_outlined, color: Colors.white, size: 36),
          ),
        ),
      ],
    );
  }

  Future<void> shareWithResult(List<String> medias) async {
    ShareResult shareResult;

    final files = <XFile>[];
    for (var i = 0; i < medias.length; i++) {
      var mt = lookupMimeType(medias[i]);
      debugPrint('file: ${medias[i]}, type: $mt');
      XFile xfile;
      if (mt != null && mt.startsWith('image')) {
        debugPrint('add image file to share!');
        xfile = XFile(medias[i], name: basename(medias[i]), mimeType: mt);
      } else {
        xfile = XFile(medias[i], name: basename(medias[i]));
      }
      // only support share one file per time
      if (files.isEmpty) {
        files.add(xfile);
      }
    }
    String status = 'share result: ';
    try {
      shareResult = await Share.shareXFiles(
        files,
        text: 'share text',
        subject: 'share subject',
      );
      if (shareResult.status == ShareResultStatus.success) {
        status = "$status: share succeed.";
      } else if (shareResult.status == ShareResultStatus.dismissed) {
        status = "$status: share dismissed.";
      } else {
        status = "$status: share unavailable!";
      }
    } catch (e) {
      debugPrint('share error: $e');
    }
  }
}

If i reinitialize the controller manually when AppLifecycleState resumed back, the problem disappear. But another problem occur, the video plays from zero (because of the reinitialize operation).

So I think of there is something bug in the _VideoAppLifeCycleObserver of the video controller.

Thanks a lot for your time.

darshankawar commented 1 month ago

Thanks for the update. The code sample assumes and uses local path to download the video and play, but at my end it won't work. Is there a way to use file from asset or from network directly which still shows the issue ?

qinjing100 commented 1 month ago

@darshankawar Thanks for your reply. I update the sample code .

// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

/// An example of using the plugin, controlling lifecycle and playback of the
/// video.

import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(
    MaterialApp(
      home: _App(),
    ),
  );
}

class _App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        key: const ValueKey<String>('home_page'),
        appBar: AppBar(
          title: const Text('Video player example'),
          actions: <Widget>[
            IconButton(
              key: const ValueKey<String>('push_tab'),
              icon: const Icon(Icons.navigation),
              onPressed: () {
                Navigator.push<_PlayerVideoAndPopPage>(
                  context,
                  MaterialPageRoute<_PlayerVideoAndPopPage>(
                    builder: (BuildContext context) => _PlayerVideoAndPopPage(),
                  ),
                );
              },
            )
          ],
          bottom: const TabBar(
            isScrollable: true,
            tabs: <Widget>[
              Tab(
                icon: Icon(Icons.cloud),
                text: 'Remote',
              ),
              Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'),
              Tab(icon: Icon(Icons.list), text: 'List example'),
            ],
          ),
        ),
        body: TabBarView(
          children: <Widget>[
            _BumbleBeeRemoteVideo(),
            _ButterFlyAssetVideo(),
            _ButterFlyAssetVideoInList(),
          ],
        ),
      ),
    );
  }
}

class _ButterFlyAssetVideoInList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        const _ExampleCard(title: 'Item a'),
        const _ExampleCard(title: 'Item b'),
        const _ExampleCard(title: 'Item c'),
        const _ExampleCard(title: 'Item d'),
        const _ExampleCard(title: 'Item e'),
        const _ExampleCard(title: 'Item f'),
        const _ExampleCard(title: 'Item g'),
        Card(
            child: Column(children: <Widget>[
          Column(
            children: <Widget>[
              const ListTile(
                leading: Icon(Icons.cake),
                title: Text('Video video'),
              ),
              Stack(
                  alignment: FractionalOffset.bottomRight +
                      const FractionalOffset(-0.1, -0.1),
                  children: <Widget>[
                    _ButterFlyAssetVideo(),
                    Image.asset('assets/flutter-mark-square-64.png'),
                  ]),
            ],
          ),
        ])),
        const _ExampleCard(title: 'Item h'),
        const _ExampleCard(title: 'Item i'),
        const _ExampleCard(title: 'Item j'),
        const _ExampleCard(title: 'Item k'),
        const _ExampleCard(title: 'Item l'),
      ],
    );
  }
}

/// A filler card to show the video in a list of scrolling contents.
class _ExampleCard extends StatelessWidget {
  const _ExampleCard({required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          ListTile(
            leading: const Icon(Icons.airline_seat_flat_angled),
            title: Text(title),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: OverflowBar(
              alignment: MainAxisAlignment.end,
              spacing: 8.0,
              children: <Widget>[
                TextButton(
                  child: const Text('BUY TICKETS'),
                  onPressed: () {
                    /* ... */
                  },
                ),
                TextButton(
                  child: const Text('SELL TICKETS'),
                  onPressed: () {
                    /* ... */
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _ButterFlyAssetVideo extends StatefulWidget {
  @override
  _ButterFlyAssetVideoState createState() => _ButterFlyAssetVideoState();
}

class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> {
  late VideoPlayerController _controller;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4');

    _controller.addListener(() {
      setState(() {});
    });
    _controller.setLooping(true);
    _controller.initialize().then((_) => setState(() {}));
    _controller.play();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          Container(
            padding: const EdgeInsets.only(top: 20.0),
          ),
          const Text('With assets mp4'),
          Container(
            padding: const EdgeInsets.all(20),
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: Stack(
                alignment: Alignment.bottomCenter,
                children: <Widget>[
                  VideoPlayer(_controller),
                  _ControlsOverlay(controller: _controller),
                  VideoProgressIndicator(_controller, allowScrubbing: true),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _BumbleBeeRemoteVideo extends StatefulWidget {
  @override
  _BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState();
}

class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
  late VideoPlayerController _controller;

  Future<ClosedCaptionFile> _loadCaptions() async {
    final String fileContents = await DefaultAssetBundle.of(this.context)
        .loadString('assets/bumble_bee_captions.vtt');
    return WebVTTCaptionFile(
        fileContents); // For vtt files, use WebVTTCaptionFile
  }

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(
      Uri.parse(
          'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'),
      closedCaptionFile: _loadCaptions(),
      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
    );

    _controller.addListener(() {
      setState(() {});
    });
    _controller.setLooping(true);
    _controller.initialize();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          Container(padding: const EdgeInsets.only(top: 20.0)),
          const Text('With remote mp4'),
          Container(
            padding: const EdgeInsets.all(20),
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: Stack(
                alignment: Alignment.bottomCenter,
                children: <Widget>[
                  VideoPlayer(_controller),
                  ClosedCaption(text: _controller.value.caption.text),
                  _ControlsOverlay(controller: _controller),
                  VideoProgressIndicator(_controller, allowScrubbing: true),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _ControlsOverlay extends StatelessWidget {
  const _ControlsOverlay({required this.controller});

  static const List<Duration> _exampleCaptionOffsets = <Duration>[
    Duration(seconds: -10),
    Duration(seconds: -3),
    Duration(seconds: -1, milliseconds: -500),
    Duration(milliseconds: -250),
    Duration.zero,
    Duration(milliseconds: 250),
    Duration(seconds: 1, milliseconds: 500),
    Duration(seconds: 3),
    Duration(seconds: 10),
  ];
  static const List<double> _examplePlaybackRates = <double>[
    0.25,
    0.5,
    1.0,
    1.5,
    2.0,
    3.0,
    5.0,
    10.0,
  ];

  final VideoPlayerController controller;

  static const String videoName = 'Butterfly-209.mp4';

  Future<String> copyVideo(String filename) async {
    final tempDir = await getTemporaryDirectory();
    ByteData bytes = await rootBundle.load("assets/$filename");
    final assetPath = '${tempDir.path}/$filename';
    File file = await File(assetPath).create();
    await file.writeAsBytes(bytes.buffer.asUint8List());
    debugPrint('file path: ${file.path}');
    return file.path;
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 50),
          reverseDuration: const Duration(milliseconds: 200),
          child: controller.value.isPlaying
              ? const SizedBox.shrink()
              : const ColoredBox(
                  color: Colors.black26,
                  child: Center(
                    child: Icon(
                      Icons.play_arrow,
                      color: Colors.white,
                      size: 100.0,
                      semanticLabel: 'Play',
                    ),
                  ),
                ),
        ),
        GestureDetector(
          onTap: () {
            controller.value.isPlaying ? controller.pause() : controller.play();
          },
        ),
        Align(
          alignment: Alignment.topLeft,
          child: PopupMenuButton<Duration>(
            initialValue: controller.value.captionOffset,
            tooltip: 'Caption Offset',
            onSelected: (Duration delay) {
              controller.setCaptionOffset(delay);
            },
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<Duration>>[
                for (final Duration offsetDuration in _exampleCaptionOffsets)
                  PopupMenuItem<Duration>(
                    value: offsetDuration,
                    child: Text('${offsetDuration.inMilliseconds}ms'),
                  )
              ];
            },
            child: Padding(
              padding: const EdgeInsets.symmetric(
                // Using less vertical padding as the text is also longer
                // horizontally, so it feels like it would need more spacing
                // horizontally (matching the aspect ratio of the video).
                vertical: 12,
                horizontal: 16,
              ),
              child: Text('${controller.value.captionOffset.inMilliseconds}ms'),
            ),
          ),
        ),
        Align(
          alignment: Alignment.topRight,
          child: PopupMenuButton<double>(
            initialValue: controller.value.playbackSpeed,
            tooltip: 'Playback speed',
            onSelected: (double speed) {
              controller.setPlaybackSpeed(speed);
            },
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<double>>[
                for (final double speed in _examplePlaybackRates)
                  PopupMenuItem<double>(
                    value: speed,
                    child: Text('${speed}x'),
                  )
              ];
            },
            child: Padding(
              padding: const EdgeInsets.symmetric(
                // Using less vertical padding as the text is also longer
                // horizontally, so it feels like it would need more spacing
                // horizontally (matching the aspect ratio of the video).
                vertical: 12,
                horizontal: 16,
              ),
              child: Text('${controller.value.playbackSpeed}x'),
            ),
          ),
        ),
        Positioned(
          // alignment: Alignment.bottomRight,
          bottom: 30,
          right: 8,
          child: IconButton(
            onPressed: () async {
              final tempFilePath = await copyVideo(videoName);
              debugPrint('temp file path: $tempFilePath');
              var medias = List<String>.filled(1, tempFilePath);
              // share
              try {
                await shareWithResult(medias);
              } catch (e) {
                debugPrint('share error: $e');
              }
            },
            icon: const Icon(Icons.share_outlined, color: Colors.red, size: 30),
          ),
        ),
      ],
    );
  }

  Future<void> shareWithResult(List<String> medias) async {
    ShareResult shareResult;
    final files = <XFile>[];
    for (var i = 0; i < medias.length; i++) {
      XFile xfile;
      xfile = XFile(medias[i], name: basename(medias[i]));
      // only support share one file per time
      if (files.isEmpty) {
        files.add(xfile);
      }
    }
    String status = 'share result: ';
    try {
      shareResult = await Share.shareXFiles(
        files,
        text: 'share text',
        subject: 'share subject',
      );

      if (shareResult.status == ShareResultStatus.success) {
        status = "$status: ${shareResult.raw}";
      } else if (shareResult.status == ShareResultStatus.dismissed) {
        status = "$status: shareDismissed.";
      } else {
        status = "$status: shareUnavailable!";
      }
    } catch (e) {
      debugPrint('share error: $e');
    }
  }
}

class _PlayerVideoAndPopPage extends StatefulWidget {
  @override
  _PlayerVideoAndPopPageState createState() => _PlayerVideoAndPopPageState();
}

class _PlayerVideoAndPopPageState extends State<_PlayerVideoAndPopPage> {
  late VideoPlayerController _videoPlayerController;
  bool startedPlaying = false;

  @override
  void initState() {
    super.initState();

    _videoPlayerController =
        VideoPlayerController.asset('assets/Butterfly-209.mp4');
    _videoPlayerController.addListener(() {
      if (startedPlaying && !_videoPlayerController.value.isPlaying) {
        Navigator.pop(this.context);
      }
    });
  }

  @override
  void dispose() {
    _videoPlayerController.dispose();
    super.dispose();
  }

  Future<bool> started() async {
    await _videoPlayerController.initialize();
    await _videoPlayerController.play();
    startedPlaying = true;
    return true;
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: FutureBuilder<bool>(
          future: started(),
          builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
            if (snapshot.data ?? false) {
              return AspectRatio(
                aspectRatio: _videoPlayerController.value.aspectRatio,
                child: VideoPlayer(_videoPlayerController),
              );
            } else {
              return const Text('waiting for video to load');
            }
          },
        ),
      ),
    );
  }
}

Asset Tab, I add a Share button, you can have a try. First click the red share button, then dissmiss share. then the video state become abnormal, play or pause not work, the video progress indicator keep moving.

Thanks for your time.

darshankawar commented 1 month ago

Thanks for the update. I see below after clicking on share button and then dismissing:

https://github.com/user-attachments/assets/cc294d55-ce80-48f9-b6f7-4d83986b0bcb

Stable : 3.24.2
Master: 3.25.0-1.0.pre.269
qinjing100 commented 1 month ago

@darshankawar Thanks for your reply. I found on flutter 3.22.3 it works well. maybe there is something bug of _VideoAppLifeCycle (video_player 2.9.1) with flutter sdk (3.24.0 +) appLifeCycle. hope some one can fix it as soon as possible.

gmstyle commented 1 month ago

Same issue for me but when put the screen off or when put the app in background. The app stop playing video. Works well with flutter 3.22.3.

dfdgsdfg commented 1 month ago

Another workaround

# pubspec.yaml

dependencies:
  video_player: ^2.9.1

dependency_overrides:
  video_player_android: 2.7.1

Maybe video_player_andriod's Impeller Support is the problem?

video_player_android changelog

## 2.7.3.    <-- broken with latest video_palyer & flutter

* Updates Media3-ExoPlayer to 1.4.1.

## 2.7.2.    <-- broken with latest video_palyer & flutter

* Updates minimum supported SDK version to Flutter 3.24/Dart 3.5.

* Re-adds Impeller support.

## 2.7.1.    <----------- work well with latest video_palyer & flutter

* Revert Impeller support.

## 2.7.0.     <-- not tested

* Re-adds [support for Impeller](https://docs.flutter.dev/release/breaking-changes/android-surface-plugins).

## 2.6.0.      <----------- work well with latest video_palyer & flutter

* Adds RTSP support.
jesswrd commented 1 month ago

@matanlurey Do you think this is related to the work you had to do to support Impeller?

matanlurey commented 1 month ago

I have a 1-line fix for the share/share_plus regression that should be easy to test and land:

 final class VideoPlayer implements TextureRegistry.SurfaceProducer.Callback {
@@ -86,8 +88,8 @@ final class VideoPlayer implements TextureRegistry.SurfaceProducer.Callback {

   @RestrictTo(RestrictTo.Scope.LIBRARY)
   public void onSurfaceCreated() {
-    exoPlayer = createVideoPlayer();
     if (savedStateDuring != null) {
+      exoPlayer = createVideoPlayer();
       savedStateDuring.restore(exoPlayer);
       savedStateDuring = null;
     }

Longer-term I want to rename onSurfaceCreated to onSurfaceAvailable and add better docs

github-actions[bot] commented 1 month ago

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.