欢迎光临
我们一直在努力

Flutter/OpenHarmony 三方库适配实战:video_player 视频播放

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 的主要流程是:

  • 通过 TextureRegistry 注册纹理并取得 texture id。
  • 根据 Asset、网络 URI 或 fd 准备播放源。
  • 使用 AVMetadataExtractor 或 AVImageGenerator 读取首帧和媒体信息。
  • 将首帧 PixelMap 设置为纹理背景,避免初始化前出现纯黑区域。
  • 创建 VideoPlayer 和对应 EventChannel。
  • 调用 createAVPlayer() 并等待播放器进入可用状态。
  • 创建成功后返回的 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 经验交流。

    赞(0)
    未经允许不得转载:171主机测评 » Flutter/OpenHarmony 三方库适配实战:video_player 视频播放
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址