Flutter视频库chewie的使用

chewie介绍

video_player插件提供了对视频播放的底层访问。Chewie对video_player进行封装,提供了一套友好的控制UI。android

chewie真的很棒,使用很是简洁。chewie提供了两套完善的UI风格样式,以及很大限度的自定义UI样式。但某些场景下,不能知足咱们的须要,那就得进行功能扩展了。自定义UI样式和功能扩展,就得对库源码有必定了解。git

chewie使用方式

先贴一个效果图:左边是默认效果,右边是固定尺寸的效果。github

添加依赖 pubspec.yamlbash

dependencies:
  chewie: ^0.9.8+1
  video_player: ^0.10.2+5
复制代码

封装一个widgetmarkdown

class ChewieVideoWidget1 extends StatefulWidget {
  //https://nico-android-apk.oss-cn-beijing.aliyuncs.com/landscape.mp4
  final String playUrl;

  ChewieVideoWidget1(this.playUrl);
  @override
  _ChewieVideoWidget1State createState() => _ChewieVideoWidget1State();
}

class _ChewieVideoWidget1State extends State<ChewieVideoWidget1> {
  VideoPlayerController _videoPlayerController;
  ChewieController _chewieController;

  @override
  void initState() {
    super.initState();
    _videoPlayerController = VideoPlayerController.network(widget.playUrl);
    _chewieController = ChewieController(
      videoPlayerController: _videoPlayerController,
      autoPlay: true,
    //aspectRatio: 3 / 2.0,
    //customControls: CustomControls(),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Chewie(controller: _chewieController,),
    );
  }
}
复制代码

最后注意,Android9.0后没法播放http的视频资源,须要在AndroidManifest.xml中配置android:usesCleartextTraffic="true"app

<application
        android:name="io.flutter.app.FlutterApplication"
        android:label="flutter_sample"
        android:usesCleartextTraffic="true"
        tools:targetApi="m"
        android:icon="@mipmap/ic">
复制代码

到这里咱们就能愉快地使用Chewie咱们的视频了。less

固然了,咱们确定不能就这么知足了。async

chewie 源码解析

首先进入Chewie的构造方法,在文件chewie_player.dart中,咱们发现就一个controller参数,那构造的参数就放在了ChewieController中了。ide

Chewie({
    Key key,
    this.controller,
  })  : assert(controller != null, 'You must provide a chewie controller'),
        super(key: key);
复制代码

chewie_player.dart 代码继续往下翻,找到了ChewieController类。类的开头有一段注释介绍,说了ChewieController的功能。oop

同时也提到播放状态的变化不归它管,播放状态得找VideoPlayerController要。实际上与VideoPlayerController的互动,做者放在了MaterialControls/CupertinoControls中。

咱们先来看看找到了ChewieController类,截取一部分代码。从构造方法发现,该有视频配置几乎都有,好比说自动播放,循环,全屏,seekTo,禁音等等。同时还能够配置进度条颜色,甚至自定义的控制widget。

///ChewieController用于配置和驱动Chewie播放组件。
///它提供了控制播放的方法,例如[pause]和[play],
///以及控制播放器呈现的方法,例如[enterFullScreen]或[exitFullScreen]。
///此外,你还能够监听ChewieController呈现形式变化,例如进入和退出全屏模式。
///若是要监听播放状态的变化(好比播放器的进度变化),还得依靠VideoPlayerController。

class ChewieController extends ChangeNotifier {
  ChewieController({
    //video_player库中须要的VideoPlayerController
    this.videoPlayerController, 
    //容器宽高比。不设置的话,会根据屏幕计算默认的长宽比。
    //思考要设置成视频长宽比怎么作?
    this.aspectRatio,
    this.autoInitialize = false,
    this.autoPlay = false,
    this.startAt,
    this.looping = false,
    this.materialProgressColors,//进度条颜色
    this.customControls,//自定义控制层样式
    this.allowFullScreen = true,//是否容许全屏
    this.allowMuting = true,//...
    this.routePageBuilder = null,
    ...
  }) : assert(videoPlayerController != null,
            'You must provide a controller to play a video') {
    _initialize();
  }
复制代码

在ChewieController的构造方法中调用了_initialize(),省略了一部分代码。咱们能够看到,若是设置了autoPlay,那视频这个时候就能播放了。此时完成了对VideoPlayerController配置和操做。如今video_player用起来了!

Future _initialize() async {
    await videoPlayerController.setLooping(looping);
    if ((autoInitialize || autoPlay) &&
        !videoPlayerController.value.initialized) {
      await videoPlayerController.initialize();
    }
    if (autoPlay) {
      await videoPlayerController.play();
    }
    if (startAt != null) {
      await videoPlayerController.seekTo(startAt);
    }
    ....
  }
复制代码

接下来,产生了疑问:

  • 控制ControlWidget在哪?怎么初始化的?
  • 控制widget与ChewieController/VideoPlayerController如何交互的?

咱们从新回到Chewie类中,找到它的build(BuildContext)方法。看一个Widget类的源码,首先是构造方法,其次就是build方法了。咱们看到了两个东西:

  • _ChewieControllerProvider
  • PlayerWithControls
@override
  Widget build(BuildContext context) {
    return _ChewieControllerProvider(
      controller: widget.controller,
      child: PlayerWithControls(),
    );
  }
复制代码

_ChewieControllerProvider。看到Provider,那必然就是InheritedWidget。这个地方就很清晰了。ChewieController经过InheritedWidget进行了共享。child(PlayerWithControls),能够经过widget树,向上找到ChewieController,同时拿到ChewieController中的VideoPlayerController

static ChewieController of(BuildContext context) {
    final chewieControllerProvider =
        context.inheritFromWidgetOfExactType(_ChewieControllerProvider)
            as _ChewieControllerProvider;

    return chewieControllerProvider.controller;
  }

class _ChewieControllerProvider extends InheritedWidget {
  const _ChewieControllerProvider({
    Key key,
    @required this.controller,
    @required Widget child,
  })  : assert(controller != null),
        assert(child != null),
        super(key: key, child: child);

  final ChewieController controller;

  @override
  bool updateShouldNotify(_ChewieControllerProvider old) =>
      controller != old.controller;
}
复制代码

controller的访问解决了,那再看看控制widget:PlayerWithControls。选取了部分代码,而且加了注释解读。

class PlayerWithControls extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
  //拿到_ChewieControllerProvider共享的ChewieController
    final ChewieController chewieController = ChewieController.of(context);

    return Center(
      child: Container(
        //设置宽度:使用屏幕的宽度
        width: MediaQuery.of(context).size.width,
        //使用配置的长宽比aspectRatio,若是没有默认值,就经过屏幕尺寸计算。
        //咱们能够看到,始终都是用了固定的长宽比
        child: AspectRatio(
          aspectRatio:
              chewieController.aspectRatio ?? _calculateAspectRatio(context),
          child: _buildPlayerWithControls(chewieController, context),
        ),
      ),
    );
  }

  Container _buildPlayerWithControls(
      ChewieController chewieController, BuildContext context) {
    return Container(
      child: Stack(
        children: <Widget>[
        //构建播放器Widget,
        //VideoPlayer构造使用了chewieController中的videoPlayerController。
          Center(
            child: AspectRatio(
              aspectRatio: chewieController.aspectRatio ??
                  _calculateAspectRatio(context),
              child: VideoPlayer(chewieController.videoPlayerController),
            ),
          ),
          //构建控制widget,customControls/meterialControls/cupertinoControls
          _buildControls(context, chewieController),
        ],
      ),
    );
  }
  //计算长宽比。
  double _calculateAspectRatio(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final width = size.width;
    final height = size.height;

    return width > height ? width / height : height / width;
  }
}
复制代码

咱们进入 _buildControls(BuildContext)。进入其中一个Controls:MaterialControls。你在界面看到的开始/暂停/全屏/进度条等等控件就都在这了。截取的部分源码,有省略。 UI与底层的交互其实就两部分:首先是读取底层数据渲染UI和设置对底层的控制事件。其次是获取到底层的数据变化从而从新绘制UI。

1.播放按钮widget使用VideoPlayerController.VideoPlayerValue数据构建,接着按钮点击操做VideoPlayerController的过程。

///在build(BuildContext)中,咱们找到了构建Widget的内容。
  @override
  Widget build(BuildContext context) {
    child: Column(
        children: <Widget>[
            _buildHitArea(),
            _buildBottomBar(context),
  }
  
  ///进入_buildBottomBar(BuildContext),。
  AnimatedOpacity _buildBottomBar(BuildContext context,) {
    return AnimatedOpacity(
        child: Row(
          children: <Widget>[
            _buildPlayPause(controller),
           _buildProgressBar(),
  }
  
  //进入 _buildPlayPause(controller)
  GestureDetector _buildPlayPause(VideoPlayerController controller) {
    return GestureDetector(
      onTap: _playPause,
      child: Container(
        child: Icon(
          controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
  }
  
  //播放widget 触发的点击事件。
  void _playPause() {
    setState(() {
      if (controller.value.isPlaying) {
        controller.pause();
      } else {
        controller.play();
      }
    });
  }
复制代码

2.经过VideoPlayerController.addListener监听变化,当变化时,取出VideoPlayerController.VideoPlayerValue。 setState,更新_latestValue,从而达到刷新UI的操做。

@override
void didChangeDependencies() {
    if (_oldController != chewieController) {
      _dispose();
      _initialize();
    }
    super.didChangeDependencies();
}
  
Future<Null> _initialize() async {
    controller.addListener(_updateState);
    _updateState();
}

void _updateState() {
    setState(() {
      _latestValue = controller.value;
    });
  }
复制代码

VideoPlayerValue包含了实时的播放器数据。

VideoPlayerValue({
    @required this.duration,
    this.size,
    this.position = const Duration(),
    this.buffered = const <DurationRange>[],
    this.isPlaying = false,
    this.isLooping = false,
    this.isBuffering = false,
    this.volume = 1.0,
    this.errorDescription,
  });
复制代码

到这里咱们分析了部分主流程的代码。固然了,Chewie库远不止这些。

了解了这些,咱们就能够入门了。能够尝试解决下面的问题了。

如何自定义控制层样式?

这个问题就比较简单了,构建ChewieController时,咱们能够设置customControls。咱们能够参考MaterialControls,写写咱们本身的controls。在这个代码仓库里有参考代码custom_controls.dart。代码在这里

ChewieController(
      videoPlayerController: _videoPlayerController,
      autoPlay: true,
      customControls: CustomControls(),
    );
复制代码

如何在固定尺寸容器内显示视频?

设想这样的场景:咱们但愿在一个300*300的容器内播放视频,且视频不该当出现变形。

翻看源码发现,Chewie只提供了改变长宽比aspectRatio的机会。容器的widget是写死的PlayerWithControls。而从上面的源码能够看到的宽度是固定屏幕的宽度。我猜测是做者不想外部调用破坏到内部自己的结构,尽量的保证原子性和高复用性。

那若是咱们要实现效果就只能把 Chewie的代码copy出来,而后进行改造了。改造部分以下。 0.把Chewie库中全部代码复制出来,同时去掉pubspec.yaml中的chewie依赖

video_player: ^0.10.2+5
screen: 0.0.5
#chewie: ^0.9.8+1
复制代码

1.chewie_player.dart

class Chewie extends StatefulWidget {
  ///增长一个课配置的child
  Widget child ;
 
  Chewie({
    Key key,
    this.controller,
    this.child,
  })  : assert(controller != null, 'You must provide a chewie controller'),
        super(key: key);

 }
  
class ChewieState extends State<Chewie> {
   @override
  Widget build(BuildContext context) {
    return _ChewieControllerProvider(
      controller: widget.controller,
      //不在写死PlayerWithControls
      child: widget.child??PlayerWithControls(),
    );
  }
}
复制代码

2.构造Chewie地方增长child属性

Chewie(
      controller: _chewieController,
      child: CustomPlayerWithControls(),
    )
复制代码

3.自定义的控制Widget。custom_player_with_controls.dart 这里

class CustomPlayerWithControls extends StatelessWidget {
  final double width;
  final double height;
  ///入参增长容器宽和高
  CustomPlayerWithControls({
    Key key,
    this.width = 300,
    this.height = 300,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final ChewieController chewieController = ChewieController.of(context);
    return _buildPlayerWithControls(chewieController, context);
  }
  Container _buildPlayerWithControls(
      ChewieController chewieController, BuildContext context) {
    return Container(
      width: width,
      height: height,
      child: Stack(
        children: <Widget>[
        //自定义的部分主要在这个容器里面了。
          VideoPlayerContainer(width, height),
          _buildControls(context, chewieController),
        ],
      ),
    );
  }

  Widget _buildControls(
    BuildContext context,
    ChewieController chewieController,
  ) {
    return chewieController.showControls &&
            chewieController.customControls != null
        ? chewieController.customControls
        : Container();
  }
}

///与源码中的PlayerWithControls相比,VideoPlayerContainer继承了StatefulWidget,监听videoPlayerController的变化,拿到视频宽高比。
class VideoPlayerContainer extends StatefulWidget {
  final double maxWidth;
  final double maxHeight;

///根据入参的宽高,计算获得容器宽高比
  double _viewRatio;

  VideoPlayerContainer(
    this.maxWidth,
    this.maxHeight, {
    Key key,
  })  : _viewRatio = maxWidth / maxHeight,
        super(key: key);

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

class _VideoPlayerContainerState extends State<VideoPlayerContainer> {
  double _aspectRatio;

  ChewieController chewieController;

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

  void _dispose() {
    chewieController.videoPlayerController.removeListener(_updateState);
  }

  @override
  void didChangeDependencies() {
    final _oldController = chewieController;
    chewieController = ChewieController.of(context);
    if (_oldController != chewieController) {
      _dispose();
      chewieController.videoPlayerController.addListener(_updateState);
      _updateState();
    }
    super.didChangeDependencies();
  }

  void _updateState() {
    VideoPlayerValue value = chewieController?.videoPlayerController?.value;
    if (value != null) {
      double newAspectRatio = value.size != null ? value.aspectRatio : null;
      if (newAspectRatio != null && newAspectRatio != _aspectRatio) {
        setState(() {
          _aspectRatio = newAspectRatio;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_aspectRatio == null) {
      return Container();
    }
    double width;
    double height;
    ///两个宽高比进行比较,保证VideoPlayer不超出容器,且不会产生变形
    if (_aspectRatio > widget._viewRatio) {
      width = widget.maxWidth;
      height = width / _aspectRatio;
    } else {
      height = widget.maxHeight;
      width = height * _aspectRatio;
    }

    return Center(
      child: Container(
        width: width,
        height: height,
        child: VideoPlayer(chewieController.videoPlayerController),
      ),
    );
  }
}
复制代码

chewie的使用就介绍到这了。 全部代码都在这里。 入口文件: main_video.dart

相关文章
相关标签/搜索