如何在 flutter 中显示我保存在路径中的视频?

How to display a video I saved in a path in flutter?

您好,我尝试在从相机录制视频并将此视频保存在设备中后显示视频 (path: /data/user/0/com.XXXXX.flutter_video_test/app_flutter/Movies/2019-11-08.mp4)

我使用了flutter.dev中https://pub.dev/packages/video_player的例子。当我完整地写路径时还可以,但是当我使用字符串变量做同样的事情时,我就出错了。

这是问题的一部分

String dirPath;


  Future<String> load_path_video() async {
    final Directory extDir = await getApplicationDocumentsDirectory();

    setState(() {
        dirPath = '${extDir.path}/Movies/2019-11-08.mp4';
// if I print ($dirPath) I have /data/user/0/com.XXXXX.flutter_video_test/app_flutter/Movies/2019-11-08.mp4
    });
  }


  @override
  void initState() {
    // TODO: implement initState
    load_path_video();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(

      body:  ListView(

        children: <Widget>[

          Container(
            padding: const EdgeInsets.all(20),
            child: NetworkPlayerLifeCycle(
                '$dirPath', // with the String dirPath I have error but if I use the same path but write like this  /data/user/0/com.XXXXX.flutter_video_test/app_flutter/Movies/2019-11-08.mp4 it's ok ... why? 
                    (BuildContext context,
                    VideoPlayerController controller) =>
                    AspectRatioVideo(controller)),
          ),

        ],
      ),
    );
  }
}

这里是我使用字符串变量时的错误

E/ExoPlayerImplInternal(20818): Source error.
E/ExoPlayerImplInternal(20818): com.google.android.exoplayer2.upstream.FileDataSource$FileDataSourceException: java.io.FileNotFoundException: null: open failed: ENOENT (No such file or directory)
E/ExoPlayerImplInternal(20818):     at com.google.android.exoplayer2.upstream.FileDataSource.open(FileDataSource.java:73)
E/ExoPlayerImplInternal(20818):     at com.google.android.exoplayer2.upstream.DefaultDataSource.open(DefaultDataSource.java:250)
E/ExoPlayerImplInternal(20818):     at com.google.android.exoplayer2.upstream.StatsDataSource.open(StatsDataSource.java:83)
E/ExoPlayerImplInternal(20818):     at com.google.android.exoplayer2.source.ExtractorMediaPeriod$ExtractingLoadable.load(ExtractorMediaPeriod.java:885)
E/ExoPlayerImplInternal(20818):     at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:381)
E/ExoPlayerImplInternal(20818):     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
E/ExoPlayerImplInternal(20818):     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
E/ExoPlayerImplInternal(20818):     at java.lang.Thread.run(Thread.java:776)
E/ExoPlayerImplInternal(20818): Caused by: java.io.FileNotFoundException: null: open failed: ENOENT (No such file or directory)
E/ExoPlayerImplInternal(20818):     at libcore.io.IoBridge.open(IoBridge.java:455)
E/ExoPlayerImplInternal(20818):     at java.io.RandomAccessFile.<init>(RandomAccessFile.java:247)
E/ExoPlayerImplInternal(20818):     at java.io.RandomAccessFile.<init>(RandomAccessFile.java:128)
E/ExoPlayerImplInternal(20818):     at com.google.android.exoplayer2.upstream.FileDataSource.open(FileDataSource.java:65)
E/ExoPlayerImplInternal(20818):     ... 7 more
E/ExoPlayerImplInternal(20818): Caused by: android.system.ErrnoException: open failed: ENOENT (No such file or directory)
E/ExoPlayerImplInternal(20818):     at libcore.io.Posix.open(Native Method)
E/ExoPlayerImplInternal(20818):     at libcore.io.BlockGuardOs.open(BlockGuardOs.java:187)
E/ExoPlayerImplInternal(20818):     at libcore.io.IoBridge.open(IoBridge.java:441)
E/ExoPlayerImplInternal(20818):     ... 10 more
I/flutter (20818): Video player had error com.google.android.exoplayer2.ExoPlaybackException: com.google.android.exoplayer2.upstream.FileDataSource$FileDataSourceException: java.io.FileNotFoundException: null: open failed: ENOENT (No such file or directory)

从错误来看,您似乎正在尝试显示一个不存在的文件 (No such file or directory),这让我认为您收到错误是因为 dirPathnull.

我建议您在代码中添加一个 if,如下所示:

@override
  Widget build(BuildContext context) {

    return Scaffold(
      body:  ListView(
        children: <Widget>[
          if(dirPath != null)
              Container(
                padding: const EdgeInsets.all(20),
                child: NetworkPlayerLifeCycle(
                    dirPath,
                    (BuildContext context,
                    VideoPlayerController controller) =>
                    AspectRatioVideo(controller)),
          ),

        ],
      ),
    );
  }

这样,只有在 dirPath 不是 null 时才会显示 Container 小部件。

对您的代码的一个观察: - 当你的 dirPath 已经是一个字符串时,你不需要使用 '$dirPath',你可以直接使用变量

在 initState() await load_path_video() 期间,dirPath 仍然为 null
您需要一个 bool loading 来检查 await getApplicationDocumentsDirectory 是否完成
您可以复制粘贴 运行 下面的完整代码并确保文件在路径

代码片段

Future<String> load_path_video() async {
    loading = true;
    final Directory extDir = await getApplicationDocumentsDirectory();

    setState(() {
      dirPath = '${extDir.path}/Movies/2019-11-08.mp4';
      print(dirPath);
      loading = false;
      // if I print ($dirPath) I have /data/user/0/com.XXXXX.flutter_video_test/app_flutter/Movies/2019-11-08.mp4
    });
  }

 Container(
            padding: const EdgeInsets.all(20),
            child: loading
                ? CircularProgressIndicator()
                : NetworkPlayerLifeCycle(
                    '$dirPath', // with the String dirPath I have error but if I use the same path but write like this  /data/user/0/com.XXXXX.flutter_video_test/app_flutter/Movies/2019-11-08.mp4 it's ok ... why ?
                    (BuildContext context, VideoPlayerController controller) =>
                        AspectRatioVideo(controller)),

工作演示

完整的工作代码

import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:video_player/video_player.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  String dirPath;
  bool loading = false;

  Future<String> load_path_video() async {
    loading = true;
    final Directory extDir = await getApplicationDocumentsDirectory();

    setState(() {
      dirPath = '${extDir.path}/Movies/2019-11-08.mp4';
      print(dirPath);
      loading = false;
      // if I print ($dirPath) I have /data/user/0/com.XXXXX.flutter_video_test/app_flutter/Movies/2019-11-08.mp4
    });
  }

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  void initState() {
    // TODO: implement initState
    load_path_video();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      body: ListView(
        children: <Widget>[
          Container(
            padding: const EdgeInsets.all(20),
            child: loading
                ? CircularProgressIndicator()
                : NetworkPlayerLifeCycle(
                    '$dirPath', // with the String dirPath I have error but if I use the same path but write like this  /data/user/0/com.XXXXX.flutter_video_test/app_flutter/Movies/2019-11-08.mp4 it's ok ... why ?
                    (BuildContext context, VideoPlayerController controller) =>
                        AspectRatioVideo(controller)),
          ),
        ],
      ),
    );
  }
}

class VideoPlayPause extends StatefulWidget {
  VideoPlayPause(this.controller);

  final VideoPlayerController controller;

  @override
  State createState() {
    return _VideoPlayPauseState();
  }
}

class _VideoPlayPauseState extends State<VideoPlayPause> {
  _VideoPlayPauseState() {
    listener = () {
      setState(() {});
    };
  }

  FadeAnimation imageFadeAnim =
      FadeAnimation(child: const Icon(Icons.play_arrow, size: 100.0));
  VoidCallback listener;

  VideoPlayerController get controller => widget.controller;

  @override
  void initState() {
    super.initState();
    controller.addListener(listener);
    controller.setVolume(1.0);
    controller.play();
  }

  @override
  void deactivate() {
    controller.setVolume(0.0);
    controller.removeListener(listener);
    super.deactivate();
  }

  @override
  Widget build(BuildContext context) {
    final List<Widget> children = <Widget>[
      GestureDetector(
        child: VideoPlayer(controller),
        onTap: () {
          if (!controller.value.initialized) {
            return;
          }
          if (controller.value.isPlaying) {
            imageFadeAnim =
                FadeAnimation(child: const Icon(Icons.pause, size: 100.0));
            controller.pause();
          } else {
            imageFadeAnim =
                FadeAnimation(child: const Icon(Icons.play_arrow, size: 100.0));
            controller.play();
          }
        },
      ),
      Align(
        alignment: Alignment.bottomCenter,
        child: VideoProgressIndicator(
          controller,
          allowScrubbing: true,
        ),
      ),
      Center(child: imageFadeAnim),
      Center(
          child: controller.value.isBuffering
              ? const CircularProgressIndicator()
              : null),
    ];

    return Stack(
      fit: StackFit.passthrough,
      children: children,
    );
  }
}

class FadeAnimation extends StatefulWidget {
  FadeAnimation(
      {this.child, this.duration = const Duration(milliseconds: 500)});

  final Widget child;
  final Duration duration;

  @override
  _FadeAnimationState createState() => _FadeAnimationState();
}

class _FadeAnimationState extends State<FadeAnimation>
    with SingleTickerProviderStateMixin {
  AnimationController animationController;

  @override
  void initState() {
    super.initState();
    animationController =
        AnimationController(duration: widget.duration, vsync: this);
    animationController.addListener(() {
      if (mounted) {
        setState(() {});
      }
    });
    animationController.forward(from: 0.0);
  }

  @override
  void deactivate() {
    animationController.stop();
    super.deactivate();
  }

  @override
  void didUpdateWidget(FadeAnimation oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.child != widget.child) {
      animationController.forward(from: 0.0);
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return animationController.isAnimating
        ? Opacity(
            opacity: 1.0 - animationController.value,
            child: widget.child,
          )
        : Container();
  }
}

typedef Widget VideoWidgetBuilder(
    BuildContext context, VideoPlayerController controller);

abstract class PlayerLifeCycle extends StatefulWidget {
  PlayerLifeCycle(this.dataSource, this.childBuilder);

  final VideoWidgetBuilder childBuilder;
  final String dataSource;
}

/// A widget connecting its life cycle to a [VideoPlayerController] using
/// a data source from the network.
class NetworkPlayerLifeCycle extends PlayerLifeCycle {
  NetworkPlayerLifeCycle(String dataSource, VideoWidgetBuilder childBuilder)
      : super(dataSource, childBuilder);

  @override
  _NetworkPlayerLifeCycleState createState() => _NetworkPlayerLifeCycleState();
}

/// A widget connecting its life cycle to a [VideoPlayerController] using
/// an asset as data source
class AssetPlayerLifeCycle extends PlayerLifeCycle {
  AssetPlayerLifeCycle(String dataSource, VideoWidgetBuilder childBuilder)
      : super(dataSource, childBuilder);

  @override
  _AssetPlayerLifeCycleState createState() => _AssetPlayerLifeCycleState();
}

abstract class _PlayerLifeCycleState extends State<PlayerLifeCycle> {
  VideoPlayerController controller;

  @override

  /// Subclasses should implement [createVideoPlayerController], which is used
  /// by this method.
  void initState() {
    super.initState();
    controller = createVideoPlayerController();
    controller.addListener(() {
      if (controller.value.hasError) {
        print(controller.value.errorDescription);
      }
    });
    controller.initialize();
    controller.setLooping(true);
    controller.play();
  }

  @override
  void deactivate() {
    super.deactivate();
  }

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

  @override
  Widget build(BuildContext context) {
    return widget.childBuilder(context, controller);
  }

  VideoPlayerController createVideoPlayerController();
}

class _NetworkPlayerLifeCycleState extends _PlayerLifeCycleState {
  @override
  VideoPlayerController createVideoPlayerController() {
    return VideoPlayerController.network(widget.dataSource);
  }
}

class _AssetPlayerLifeCycleState extends _PlayerLifeCycleState {
  @override
  VideoPlayerController createVideoPlayerController() {
    return VideoPlayerController.asset(widget.dataSource);
  }
}

/// A filler card to show the video in a list of scrolling contents.
Widget buildCard(String title) {
  return Card(
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        ListTile(
          leading: const Icon(Icons.airline_seat_flat_angled),
          title: Text(title),
        ),
        // TODO(jackson): Remove when deprecation is on stable branch
        // ignore: deprecated_member_use
        ButtonTheme.bar(
          child: ButtonBar(
            children: <Widget>[
              FlatButton(
                child: const Text('BUY TICKETS'),
                onPressed: () {
                  /* ... */
                },
              ),
              FlatButton(
                child: const Text('SELL TICKETS'),
                onPressed: () {
                  /* ... */
                },
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

class VideoInListOfCards extends StatelessWidget {
  VideoInListOfCards(this.controller);

  final VideoPlayerController controller;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        buildCard("Item a"),
        buildCard("Item b"),
        buildCard("Item c"),
        buildCard("Item d"),
        buildCard("Item e"),
        buildCard("Item f"),
        buildCard("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>[
                    AspectRatioVideo(controller),
                    Image.asset('assets/flutter-mark-square-64.png'),
                  ]),
            ],
          ),
        ])),
        buildCard("Item h"),
        buildCard("Item i"),
        buildCard("Item j"),
        buildCard("Item k"),
        buildCard("Item l"),
      ],
    );
  }
}

class AspectRatioVideo extends StatefulWidget {
  AspectRatioVideo(this.controller);

  final VideoPlayerController controller;

  @override
  AspectRatioVideoState createState() => AspectRatioVideoState();
}

class AspectRatioVideoState extends State<AspectRatioVideo> {
  VideoPlayerController get controller => widget.controller;
  bool initialized = false;

  VoidCallback listener;

  @override
  void initState() {
    super.initState();
    listener = () {
      if (!mounted) {
        return;
      }
      if (initialized != controller.value.initialized) {
        initialized = controller.value.initialized;
        setState(() {});
      }
    };
    controller.addListener(listener);
  }

  @override
  Widget build(BuildContext context) {
    if (initialized) {
      return Center(
        child: AspectRatio(
          aspectRatio: controller.value.aspectRatio,
          child: VideoPlayPause(controller),
        ),
      );
    } else {
      return Container();
    }
  }
}