本文基於官方視頻播放plugin進行封裝 github.com/flutter/plu…git
在平常的開發中,不免會遇到視頻開發需求;隨着Flutter技術日漸活躍因此在所難逃會有視頻功能的需求,若是完完整整把官方提供的video_player
功能直接搬進來使用會發如今不少地方須要進一步封裝。github
1、集成視頻播放功能bash
首先,因爲公司的Android
採用的是ijkplayer
來實現視頻播放功能而官方的這個插件採用的是exoplayer
,因此在集成的時候咱們把VideoPlayerPlugin
對應的類進行改造把相關的exoplayer
換成自家App中的ijkplayer
,這樣子作的好處不只僅能夠減小由於引入了exoplayer
給App帶來了更大的包大小,並且也能夠複用原生端的代碼。app
其次,須要大概瞭解一些官方的視頻播放插件原理,而對於原理能夠用一句話來歸納就是:外接紋理 Texture
; 在Flutter
端中的Textture
類的定義以下:async
class Texture extends LeafRenderObjectWidget {
const Texture({
Key key,
@required this.textureId,
}) : assert(textureId != null),
super(key: key);
}
複製代碼
因此也就說每個紋理Textture
對應着一個必須的textureId
,這就是實現視頻播放供的關鍵點;對於原生如何生成textureId
,能夠大概看下官方的插件的源碼:ide
TextureRegistry textures = registrar.textures();
TextureRegistry.SurfaceTextureEntry textureEntry = textures.createSurfaceTexture();
textureId = textureEntry.id()
複製代碼
問題1:何時生成這個textureId
呢? 當咱們在Flutter端調用視頻初始化的時候,會調用plugin的create
方法:oop
final Map<dynamic, dynamic> response = await _channel.invokeMethod(
'create',
dataSourceDescription,
);
_textureId = response['textureId'];
複製代碼
問題2:獲取了textureId
是如何進行傳遞數據呢? 當Flutter端調用create
的時候,原生端會生成一個textureId
並註冊了一個新的EventChannel
;至此視頻播放的相關數據如:initialized
(初始化)、completed
(播放完成)、bufferingUpdate
(進度更新)、bufferingStart
(緩衝開始)、bufferingEnd
(緩衝結束)就會回調給Flutter端具體的每個視頻Widget
,從而實現下一步邏輯。ui
原生端如何生成一個新的EventChannel
:this
TextureRegistry.SurfaceTextureEntry textureEntry = textures.createSurfaceTexture();
String eventChannelName = "flutter.io/videoPlayer/videoEvents" + textureEntry.id();
EventChannel eventChannel =
new EventChannel(
registrar.messenger(), eventChannelName);
複製代碼
Flutter端如何監聽:spa
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');
}
複製代碼
固然對於相似暫停/播放/快進...
等一些須要觸發操做的走的邏輯有點不一樣;走的是跟調用create
方法的同一個plugin
(即"flutter.io/videoPlayer"對應的plugin):
// 如播放/暫停調用方式
final MethodChannel _channel = const MethodChannel('flutter.io/videoPlayer')
_channel.invokeMethod( 'play', <String, dynamic>{'textureId': _textureId});
複製代碼
至此;就能夠實現了Flutter的視頻播放功能了。
2、視頻列表界面實現
在Flutter跟普通的列表式界面同樣使用一個ListView
這種可滑動的控件便可實現;可是對於視頻界面有點不一樣的是須要處理當當前播放的item不可見的時候須要暫停播放,當點擊另外一個視頻的時候前一個播放中的視頻須要暫停。
首先,針對點擊另外一個視頻的時候前一個播放中的視頻須要暫停的處理方式: 這種狀況仍是比較好處理,我這裏的處理方式是給每個視頻的Widget
都註冊一個點擊回調,當另外一個點擊播放的時候遍歷回調時發現當前視頻處於播放中就執行暫停操做:
/// 控制當點擊播放的時候上一個視頻須要暫停
playCallback = () {
if (controller.value.isPlaying) {
setState(() {
controller.pause();
});
}
};
VideoPlayerController.playCallbacks.add(playCallback);
複製代碼
其次,滑動的時候當item不可見的時候須要中止播放;相對這種狀況主要的原理就是獲取可滑動視圖的Rect
(區域),而後當視頻Rect
的底部小於可滑動視圖的頂部或者當前的視頻的視圖的頂部小於可滑動視圖的底部就執行暫停操做。 問題1:如何獲取Widget的Rect
呢?
/// 返回對應的Rect區域...
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;
}
}
複製代碼
問題2:如何根據滑動來判斷滑出了屏幕呢? 給視頻Widget註冊一個監聽,當滑動的時候進行回調判斷:
/// 滑動ListView的時候進行回調給視頻Widget
scrollController = ScrollController();
scrollController.addListener(() {
if (videoScrollController.scrollOffsetCallbacks.isNotEmpty) {
for (ScrollOffsetCallback callback in videoScrollController.scrollOffsetCallbacks) {
callback();
}
}
});
複製代碼
當視頻Widget接收到滑動回調的時候:
scrollOffsetCallback = () {
itemRect = VideoScrollController.getRectFromKey(videoBuildContext);
/// 狀態欄 + 標題欄的高度(存在一點誤差)
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);
複製代碼
至此,真的列表式視頻界面就解決了相關滑動或者點擊的時候執行暫停/播放的功能了。
3、全屏切換
對於視頻功能很常見的就是全屏的需求,因此在Flutter端天然也是少不了這種功能了;針對全屏的功能參考了開源庫github.com/brianegan/c…的思路。 而主要的原理仍是利用每一個紋理Textture
的textureId
惟一的原理,跟原生的扣View
的方式有一些差異,按照開源庫的總體代碼仍是比較簡單沒有很是多的麻煩問題:
/// 退出全屏
_popFullScreenWidget() {
Navigator.of(context).pop();
}
/// 切換至全屏狀態
_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端開發時常見的需求功能了;固然在具體的或者更變態的視頻需求時須要基於該方案再進一步完善。 附上幾張demo的效果圖: