Flutter視頻播放封裝歷程

本文基于官方視頻播放plugin進(jìn)行封裝
https://github.com/flutter/plugins/tree/master/packages/video_player/video_player

在日常的開(kāi)發(fā)中洞就,難免會(huì)遇到視頻開(kāi)發(fā)需求楷兽;隨著Flutter技術(shù)日漸活躍所以在所難逃會(huì)有視頻功能的需求吧秕,如果完完整整把官方提供的video_player功能直接搬進(jìn)來(lái)使用會(huì)發(fā)現(xiàn)在很多地方需要進(jìn)一步封裝蒂秘。

一、集成視頻播放功能
首先悟泵,由于公司的Android采用的是ijkplayer來(lái)實(shí)現(xiàn)視頻播放功能而官方的這個(gè)插件采用的是exoplayer勾邦,所以在集成的時(shí)候我們把VideoPlayerPlugin對(duì)應(yīng)的類進(jìn)行改造把相關(guān)的exoplayer換成自家App中的ijkplayer刻蟹,這樣子做的好處不僅僅可以減少因?yàn)橐肓?code>exoplayer給App帶來(lái)了更大的包大小,而且也可以復(fù)用原生端的代碼淘捡。

其次藕各,需要大概了解一些官方的視頻播放插件原理,而對(duì)于原理可以用一句話來(lái)概括就是:外接紋理 Texture焦除;
Flutter端中的Textture類的定義如下:

class Texture extends LeafRenderObjectWidget {
  const Texture({
    Key key,
    @required this.textureId,
  }) : assert(textureId != null),
       super(key: key);
}

所以也就說(shuō)每一個(gè)紋理Textture對(duì)應(yīng)著一個(gè)必須的textureId激况,這就是實(shí)現(xiàn)視頻播放供的關(guān)鍵點(diǎn);對(duì)于原生如何生成textureId膘魄,可以大概看下官方的插件的源碼:

TextureRegistry textures = registrar.textures();
TextureRegistry.SurfaceTextureEntry textureEntry = textures.createSurfaceTexture();
textureId = textureEntry.id()

問(wèn)題1:什么時(shí)候生成這個(gè)textureId呢乌逐?
當(dāng)我們?cè)贔lutter端調(diào)用視頻初始化的時(shí)候,會(huì)調(diào)用plugin的create方法:

final Map<dynamic, dynamic> response = await _channel.invokeMethod(
  'create',
  dataSourceDescription,
);
_textureId = response['textureId'];

問(wèn)題2:獲取了textureId是如何進(jìn)行傳遞數(shù)據(jù)呢创葡?
當(dāng)Flutter端調(diào)用create的時(shí)候浙踢,原生端會(huì)生成一個(gè)textureId并注冊(cè)了一個(gè)新的EventChannel;至此視頻播放的相關(guān)數(shù)據(jù)如:initialized(初始化)灿渴、completed(播放完成)洛波、bufferingUpdate(進(jìn)度更新)、bufferingStart(緩沖開(kāi)始)骚露、bufferingEnd(緩沖結(jié)束)就會(huì)回調(diào)給Flutter端具體的每一個(gè)視頻Widget蹬挤,從而實(shí)現(xiàn)下一步邏輯。

原生端如何生成一個(gè)新的EventChannel

TextureRegistry.SurfaceTextureEntry textureEntry = textures.createSurfaceTexture();
String eventChannelName = "flutter.io/videoPlayer/videoEvents" + textureEntry.id();
EventChannel eventChannel =
        new EventChannel(
                registrar.messenger(), eventChannelName);

Flutter端如何監(jiān)聽(tīng):

void eventListener(dynamic event) {
   final Map<dynamic, dynamic> map = event;
   switch (map['event']) {
     case 'initialized':
       value = value.copyWith(
         duration: Duration(milliseconds: map['duration']),
         size: Size(map['width']?.toDouble() ?? 0.0,
             map['height']?.toDouble() ?? 0.0),
       );
       initializingCompleter.complete(null);
       _applyLooping();
       _applyVolume();
       _applyPlayPause();
       break;
       ......
   }
 }
 void errorListener(Object obj) {
   final PlatformException e = obj;
   LogUtil.d("----------- ErrorListener Code = ${e.code}");
   value = VideoPlayerValue.erroneous(e.code);
   _timer?.cancel();
 }

 _eventSubscription = _eventChannelFor(_textureId)
     .receiveBroadcastStream()
     .listen(eventListener, onError: errorListener);
 return initializingCompleter.future;
}

EventChannel _eventChannelFor(int textureId) {
 return EventChannel('flutter.io/videoPlayer/videoEvents$textureId');
}

當(dāng)然對(duì)于類似暫停/播放/快進(jìn)...等一些需要觸發(fā)操作的走的邏輯有點(diǎn)不同棘幸;走的是跟調(diào)用create方法的同一個(gè)plugin(即"flutter.io/videoPlayer"對(duì)應(yīng)的plugin):

// 如播放/暫停調(diào)用方式
final MethodChannel _channel = const MethodChannel('flutter.io/videoPlayer')
_channel.invokeMethod( 'play', <String, dynamic>{'textureId': _textureId});

至此焰扳;就可以實(shí)現(xiàn)了Flutter的視頻播放功能了。

二、視頻列表界面實(shí)現(xiàn)
在Flutter跟普通的列表式界面一樣使用一個(gè)ListView這種可滑動(dòng)的控件即可實(shí)現(xiàn)吨悍;但是對(duì)于視頻界面有點(diǎn)不同的是需要處理當(dāng)當(dāng)前播放的item不可見(jiàn)的時(shí)候需要暫停播放光绕,當(dāng)點(diǎn)擊另一個(gè)視頻的時(shí)候前一個(gè)播放中的視頻需要暫停。

首先畜份,針對(duì)點(diǎn)擊另一個(gè)視頻的時(shí)候前一個(gè)播放中的視頻需要暫停的處理方式:
這種情況還是比較好處理诞帐,我這里的處理方式是給每一個(gè)視頻的Widget都注冊(cè)一個(gè)點(diǎn)擊回調(diào),當(dāng)另一個(gè)點(diǎn)擊播放的時(shí)候遍歷回調(diào)時(shí)發(fā)現(xiàn)當(dāng)前視頻處于播放中就執(zhí)行暫停操作:

/// 控制當(dāng)點(diǎn)擊播放的時(shí)候上一個(gè)視頻需要暫停
playCallback = () {
  if (controller.value.isPlaying) {
    setState(() {
      controller.pause();
    });
  }
};
VideoPlayerController.playCallbacks.add(playCallback);

其次爆雹,滑動(dòng)的時(shí)候當(dāng)item不可見(jiàn)的時(shí)候需要停止播放停蕉;相對(duì)這種情況主要的原理就是獲取可滑動(dòng)視圖的Rect(區(qū)域),然后當(dāng)視頻Rect的底部小于可滑動(dòng)視圖的頂部或者當(dāng)前的視頻的視圖的頂部小于可滑動(dòng)視圖的底部就執(zhí)行暫停操作钙态。
問(wèn)題1:如何獲取Widget的Rect呢慧起?

  /// 返回對(duì)應(yīng)的Rect區(qū)域...
  static Rect getRectFromKey(BuildContext currentContext) {
    var object = currentContext?.findRenderObject();
    var translation = object?.getTransformTo(null)?.getTranslation();
    var size = object?.semanticBounds?.size;

    if (translation != null && size != null) {
      return new Rect.fromLTWH(translation.x, translation.y, size.width, size.height);
    } else {
      return null;
    }
  }

問(wèn)題2:如何根據(jù)滑動(dòng)來(lái)判斷滑出了屏幕呢?
給視頻Widget注冊(cè)一個(gè)監(jiān)聽(tīng)册倒,當(dāng)滑動(dòng)的時(shí)候進(jìn)行回調(diào)判斷:

/// 滑動(dòng)ListView的時(shí)候進(jìn)行回調(diào)給視頻Widget
scrollController = ScrollController();
scrollController.addListener(() {
  if (videoScrollController.scrollOffsetCallbacks.isNotEmpty) {
    for (ScrollOffsetCallback callback in videoScrollController.scrollOffsetCallbacks) {
      callback();
    }
  }
});

當(dāng)視頻Widget接收到滑動(dòng)回調(diào)的時(shí)候:

scrollOffsetCallback = () {
itemRect = VideoScrollController.getRectFromKey(videoBuildContext);
      /// 狀態(tài)欄 + 標(biāo)題欄的高度(存在一點(diǎn)偏差)
      int toolBarAndStatusBarHeight = 44 + 25;
      if (itemRect != null && videoScrollController.rect != null &&
          (itemRect.top > videoScrollController.rect.bottom || itemRect.bottom - toolBarAndStatusBarHeight < videoScrollController.rect.top)) {
        if (controller.value.isPlaying) {
          setState(() {
            LogUtil.d("=============== 正在播放中蚓挤,被移出屏幕外需要暫停播放 ======");
            controller.pause();
          });
        }
      }
    };
videoScrollController?.scrollOffsetCallbacks?.add(scrollOffsetCallback);

至此,真的列表式視頻界面就解決了相關(guān)滑動(dòng)或者點(diǎn)擊的時(shí)候執(zhí)行暫停/播放的功能了驻子。

三灿意、全屏切換
對(duì)于視頻功能很常見(jiàn)的就是全屏的需求,所以在Flutter端自然也是少不了這種功能了崇呵;針對(duì)全屏的功能參考了開(kāi)源庫(kù)https://github.com/brianegan/chewie的思路缤剧。
而主要的原理還是利用每個(gè)紋理TextturetextureId唯一的原理,跟原生的扣View的方式有一些差別域慷,按照開(kāi)源庫(kù)的整體代碼還是比較簡(jiǎn)單沒(méi)有非常多的麻煩問(wèn)題:

/// 退出全屏
  _popFullScreenWidget() {
    Navigator.of(context).pop();
  }

/// 切換至全屏狀態(tài)
  _pushFullScreenWidget() async {
    final TransitionRoute<Null> route = new PageRouteBuilder<Null>(
      settings: new RouteSettings(isInitialRoute: false),
      pageBuilder: _fullScreenRoutePageBuilder,
    );

    SystemChrome.setEnabledSystemUIOverlays([]);
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.landscapeLeft,
      DeviceOrientation.landscapeRight,
    ]);
    await Navigator.of(context).push(route);
    SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
      DeviceOrientation.landscapeLeft,
      DeviceOrientation.landscapeRight,
    ]);
  }

那么荒辕,以上大概就是在Flutter端開(kāi)發(fā)時(shí)常見(jiàn)的需求功能了;當(dāng)然在具體的或者更變態(tài)的視頻需求時(shí)需要基于該方案再進(jìn)一步完善犹褒。
附上幾張demo的效果圖:


在這里插入圖片描述
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抵窒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子叠骑,更是在濱河造成了極大的恐慌李皇,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件座云,死亡現(xiàn)場(chǎng)離奇詭異疙赠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)朦拖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門圃阳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人璧帝,你說(shuō)我怎么就攤上這事捍岳。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵锣夹,是天一觀的道長(zhǎng)页徐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)银萍,這世上最難降的妖魔是什么变勇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮贴唇,結(jié)果婚禮上搀绣,老公的妹妹穿的比我還像新娘。我一直安慰自己戳气,他們只是感情好链患,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著瓶您,像睡著了一般麻捻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上呀袱,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天贸毕,我揣著相機(jī)與錄音,去河邊找鬼压鉴。 笑死崖咨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的油吭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼署拟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼婉宰!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起推穷,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤心包,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后馒铃,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蟹腾,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年区宇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了娃殖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡议谷,死狀恐怖炉爆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤芬首,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布赴捞,位于F島的核電站,受9級(jí)特大地震影響郁稍,放射性物質(zhì)發(fā)生泄漏赦政。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一耀怜、第九天 我趴在偏房一處隱蔽的房頂上張望昼钻。 院中可真熱鬧,春花似錦封寞、人聲如沸然评。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)碗淌。三九已至,卻和暖如春抖锥,著一層夾襖步出監(jiān)牢的瞬間亿眠,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工磅废, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纳像,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓拯勉,卻偏偏與公主長(zhǎng)得像竟趾,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宫峦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容