Flutter/OpenHarmony 三方库适配实战:video_player 视频播放
一、项目信息
| 仓库名称 | video_player |
| 仓库地址 | https://atomgit.com/oh-flutter/video_player |
| 基于版本 | video_player 2.11.1 |
| 适配 TAG | video_player-v2.11.1-ohos-1.0.2 |
| OHOS 插件包 | video_player_ohos |
| OHOS 原生入口 | VideoPlayerPlugin |
| Dart 注册类 | OhosVideoPlayer |
| 原生播放器 | @ohos.multimedia.media.AVPlayer |
| 视频显示方式 | Flutter Texture |
video_player 的公开 API 由 VideoPlayerController 提供,平台差异集中在 VideoPlayerPlatform。OHOS 适配只需要实现同一套平台契约:
- create 创建播放器并返回 texture/player id。
- play、pause、seekTo、setVolume 控制播放。
- videoEventsFor 返回初始化、缓冲、播放状态和播放完成事件。
- buildView 返回可嵌入 Flutter 页面的视频画面。
- dispose 释放播放器和原生纹理资源。
二、整体架构
本次适配采用 Pigeon 生成的强类型通道:
VideoPlayerController
|
v
VideoPlayerPlatform
|
v
OhosVideoPlayer(Dart 实现)
|
v
OhosVideoPlayerApi(Pigeon BasicMessageChannel)
|
v
VideoPlayerApiImpl
|
v
AVPlayer + Flutter TextureRegistry
|
v
每个播放器独立 EventChannel
使用 Pigeon 而不是手写 Map 参数,可以减少字段名、空值和类型转换错误。播放控制 通过 Pigeon 通道,播放状态通过 EventChannel 流式返回,两者职责清晰。
三、OHOS 插件声明
video_player_ohos/pubspec.yaml 将 OHOS 包声明为 video_player 的平台实现:
name: video_player_ohos
version: 1.0.0+2
flutter:
plugin:
implements: video_player
platforms:
ohos:
dartPluginClass: OhosVideoPlayer
package: io.flutter.plugins.videoplayer
pluginClass: VideoPlayerPlugin
Dart 注册类设置平台实例:
class OhosVideoPlayer extends VideoPlayerPlatform {
OhosVideoPlayer({OhosVideoPlayerApi? pluginApi})
: _api = pluginApi ?? OhosVideoPlayerApi();
final OhosVideoPlayerApi _api;
static void registerWith() {
VideoPlayerPlatform.instance = OhosVideoPlayer();
}
}
业务继续使用:
final controller = VideoPlayerController.asset('assets/video.mp4');
await controller.initialize();
await controller.play();
业务代码不会感知底层是 AVPlayer 还是 Texture。
四、统一不同数据源
Dart 层把 Asset、网络地址和本地文件转换为 CreateMessage:
Future<int?> _create(DataSource dataSource) async {
String? asset;
String? uri;
switch (dataSource.sourceType) {
case DataSourceType.asset:
asset = dataSource.asset;
break;
case DataSourceType.network:
uri = dataSource.uri;
break;
case DataSourceType.file:
uri = 'fd://${await VideoPlayerOhosChannel.getFileFdByPath(
dataSource.uri!,
)}';
break;
default:
uri = dataSource.uri;
}
return _api.create(CreateMessage(
asset: asset,
uri: uri,
httpHeaders: dataSource.httpHeaders,
viewType: PlatformVideoViewType.textureView,
));
}
本地文件不会把 Dart File 对象传给原生层,而是通过辅助通道打开文件并返回文件 描述符。原生侧使用文件描述符创建播放器:
async onMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
if (call.method === 'getFileFdByPath') {
try {
const filePath = call.argument('filePath') as string;
const file = fs.openSync(filePath);
result.success(file.fd);
} catch (e) {
result.success(–1);
}
}
}
网络地址直接交给 AVPlayer,请求头通过播放器配置传入。
五、创建 AVPlayer 和 Flutter Texture
原生插件在 Ability 绑定阶段创建 VideoPlayerApiImpl:
onAttachedToAbility(binding: AbilityPluginBinding): void {
if (this.videoPlayerApiImpl == null) {
this.videoPlayerApiImpl = new VideoPlayerApiImpl(
this.flutterState!,
binding,
);
this.videoPlayerApiImpl.setup(this.pluginBinding!.getBinaryMessenger());
}
}
VideoPlayerApiImpl.create 的主要流程是:
创建成功后返回的 texture id 同时作为 Flutter Texture 的 id:
Widget buildView(int playerId) {
return Texture(textureId: playerId);
}
因此 Flutter 页面可以直接嵌入原生视频画面,并继续使用普通 Widget 控制播放按钮、 进度条和音量。
六、播放控制
VideoPlayerApiImpl 根据 playerId 找到原生播放器并调用:
play(playerId: number): void {
const videoPlayer = this.videoPlayers.get(playerId);
videoPlayer?.play();
}
pause(playerId: number): void {
const videoPlayer = this.videoPlayers.get(playerId);
videoPlayer?.pause();
}
seekTo(playerId: number, positionMs: number): void {
const videoPlayer = this.videoPlayers.get(playerId);
videoPlayer?.seekTo(positionMs);
}
setVolume(playerId: number, volume: number): void {
const videoPlayer = this.videoPlayers.get(playerId);
videoPlayer?.setVolume(volume);
}
所有方法都需要检查 playerId 是否存在。页面销毁与异步控制可能并发,直接使用 非空对象会导致应用崩溃。
七、事件流
每个播放器创建独立事件通道:
flutter.io/videoPlayer/videoEvents<textureId>
Dart 侧把原生 Map 转换为标准 VideoEvent:
Stream<VideoEvent> videoEventsFor(int playerId) {
return _eventChannelFor(playerId)
.receiveBroadcastStream()
.map((dynamic raw) {
final event = raw as Map<dynamic, dynamic>;
switch (event['event']) {
case 'initialized':
return VideoEvent(
eventType: VideoEventType.initialized,
duration: Duration(
milliseconds: (event['duration'] as num).toInt(),
),
size: Size(
(event['width'] as num).toDouble(),
(event['height'] as num).toDouble(),
),
);
case 'completed':
return VideoEvent(eventType: VideoEventType.completed);
case 'bufferingStart':
return VideoEvent(eventType: VideoEventType.bufferingStart);
case 'bufferingEnd':
return VideoEvent(eventType: VideoEventType.bufferingEnd);
default:
return VideoEvent(eventType: VideoEventType.unknown);
}
});
}
事件通道按 texture id 隔离,可以避免多个播放器互相收到错误的初始化或完成事件。
八、资源释放
播放器销毁必须同时释放原生播放器和纹理:
dispose(playerId: number): void {
const videoPlayer = this.videoPlayers.get(playerId);
if (videoPlayer == null) {
return;
}
videoPlayer.release();
this.flutterState?.getTextureRegistry().unregisterTexture(playerId);
this.videoPlayers.remove(playerId);
}
Ability 分离或引擎销毁时,插件会清理全部播放器:
onDetachedFromEngine(binding: FlutterPluginBinding): void {
this.disposeAll();
}
onDetachedFromAbility(): void {
this.disposeAll();
}
disposeAll 同时解绑辅助 MethodChannel 并清空实现对象。业务页面也应在 dispose() 中释放 VideoPlayerController。
九、Dart 使用示例
class VideoDemo extends StatefulWidget {
const VideoDemo({super.key});
State<VideoDemo> createState() => _VideoDemoState();
}
class _VideoDemoState extends State<VideoDemo> {
late final VideoPlayerController _controller;
void initState() {
super.initState();
_controller = VideoPlayerController.network(
'https://example.com/video.mp4',
httpHeaders: const <String, String>{
'Authorization': 'Bearer token',
},
);
_controller.initialize().then((_) {
_controller.setLooping(true);
_controller.play();
setState(() {});
});
}
Widget build(BuildContext context) {
if (!_controller.value.isInitialized) {
return const CircularProgressIndicator();
}
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
}
网络播放还需要宿主 HAP 声明:
{
module: {
requestPermissions: [
{ name: 'ohos.permission.INTERNET' },
],
},
}
十、测试结果
执行:
flutter analyze –no-pub lib test
flutter test
结果:
- 静态分析无问题。
- 17 个 Dart 测试通过。
- 示例 HAP 构建、安装和启动成功。
- Asset、Remote 和 LocalFile 三种数据源入口均显示。

十一、遗留问题
- 模拟器只验证基础播放 UI 和本地/网络数据源入口,编码兼容性仍需真机批量验证。
- DRM、HDR、画中画和复杂音轨切换应按业务场景单独测试。
- 仓库中部分旧视频资源是 Git LFS 指针,本次远端快照不上传这些大文件,测试时应 使用业务自己的小体积视频或重新准备测试资产。
- 后台播放和音频焦点行为需要根据宿主应用的后台配置验证。
十二、代码提交与协作
欢迎加入 CPF-Flutter 鸿蒙社区,参与 Flutter OHOS 经验交流。




