Flutter視訊庫chewie的使用

mwq30123發表於2020-03-30

chewie介紹

video_player外掛提供了對視訊播放的底層訪問。Chewie對video_player進行封裝,提供了一套友好的控制UI。

chewie真的很棒,使用非常簡潔。chewie提供了兩套完善的UI風格樣式,以及很大限度的自定義UI樣式。但某些場景下,不能滿足我們的需要,那就得進行功能擴充套件了。自定義UI樣式和功能擴充套件,就得對庫原始碼有一定了解。

chewie使用方式

先貼一個效果圖

Flutter視訊庫chewie的使用

新增依賴 pubspec.yaml

dependencies:
  chewie: ^0.9.8+1
  video_player: ^0.10.2+5
複製程式碼

封裝一個widget

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"

<application
        android:name="io.flutter.app.FlutterApplication"
        android:label="flutter_sample"
        android:usesCleartextTraffic="true"
        tools:targetApi="m"
        android:icon="@mipmap/ic">
複製程式碼

到這裡我們就能愉快地使用Chewie我們的視訊了。

當然了,我們肯定不能就這麼滿足了。

chewie 原始碼解析

首先進入Chewie的構造方法,在檔案chewie_player.dart中,我們發現就一個controller引數,那構造的引數就放在了ChewieController中了。

Chewie({
    Key key,
    this.controller,
  })  : assert(controller != null, 'You must provide a chewie controller'),
        super(key: key);
複製程式碼

chewie_player.dart 程式碼繼續往下翻,找到了ChewieController類。類的開頭有一段註釋介紹,說了ChewieController的功能。

同時也提到播放狀態的變化不歸它管,播放狀態得找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

相關文章