欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


一、video_player 组件概述

视频播放是移动应用中最常见的功能之一,无论是短视频应用、在线教育、直播还是多媒体展示,都离不开视频播放能力。在 Flutter for OpenHarmony 应用开发中,video_player 是一个功能强大的视频播放插件,提供了完整的视频播放控制功能,支持多种数据源、播放控制和事件监听。

📋 video_player 组件特点

特点 说明
多数据源支持 支持网络 URL、本地文件、应用资源文件
播放控制 播放、暂停、停止、循环播放
音量控制 支持音量调节
倍速播放 支持 0.125× 到 3.0× 的多档倍速
进度控制 支持定位播放进度
缓冲状态 实时监控视频缓冲状态
完整事件 初始化、缓冲、播放状态、完成等事件监听
鸿蒙适配 专门为 OpenHarmony 平台进行了适配,API 12+

支持的视频格式

OpenHarmony 平台基于 AVPlayer 实现,支持以下主流视频格式:

格式类型 说明
MP4 最常见的视频格式
HLS HTTP Live Streaming 直播流
DASH Dynamic Adaptive Streaming
SS Smooth Streaming

💡 使用场景:短视频应用、在线视频、教育平台、直播应用、多媒体展示等。


二、OpenHarmony 平台适配说明

2.1 兼容性信息

本项目基于 video_player@2.10.0 开发,适配 Flutter 3.7.12-ohos-1.0.6,OpenHarmony API 12+。

平台 支持版本
Android SDK 16+
iOS 11.0+
Web 任意版本
OpenHarmony API 12+

2.2 权限要求

在 OpenHarmony 平台上,如果播放网络视频,需要配置网络权限:

权限名称 权限等级 用途
ohos.permission.INTERNET normal 访问网络视频

注意:网络权限属于 normal 级别,可以直接使用,无需特殊配置。


三、项目配置与安装

3.1 添加依赖配置

首先,需要在你的 Flutter 项目的 pubspec.yaml 文件中添加 video_player 依赖。

打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:

dependencies:
  flutter:
    sdk: flutter

  # 添加 video_player 依赖(OpenHarmony 适配版本)
  video_player:
    git:
      url: https://atomgit.com/openharmony-tpc/flutter_packages
      path: packages/video_player/video_player

3.2 下载依赖

配置完成后,需要在项目根目录执行以下命令下载依赖:

flutter pub get

3.3 权限配置

如果需要播放网络视频,在 OpenHarmony 平台上需要配置网络权限:

ohos/entry/src/main/module.json5:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:network_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

配置字段说明:

  • name:权限名称
    • ohos.permission.INTERNET:访问网络权限
  • reason:权限说明,引用字符串资源
  • usedScene:使用场景配置

ohos/entry/src/main/resources/base/element/string.json:

{
  "string": [
    {
      "name": "network_reason",
      "value": "使用网络播放视频"
    }
  ]
}

⚠️ 提示:如果只播放本地视频或资源视频,则不需要配置网络权限。


四、video_player 基础用法

4.1 导入库

在使用 video_player 之前,需要先导入库:

import 'package:video_player/video_player.dart';

导入后,你就可以使用 video_player 提供的所有类和方法了。

4.2 VideoPlayerController 核心概念

VideoPlayerController 是 video_player 的核心控制器,负责管理视频的播放、暂停、进度等所有操作。

控制器初始化
late VideoPlayerController _controller;


void initState() {
  super.initState();
  _controller = VideoPlayerController.networkUrl(
    Uri.parse('https://example.com/video.mp4'),
  )..initialize().then((_) {
      // 初始化完成后更新 UI
      setState(() {});
    });
}


void dispose() {
  _controller.dispose();
  super.dispose();
}
控制器状态

通过 _controller.value 可以获取控制器的当前状态:

VideoPlayerValue value = _controller.value;

// 常用状态
bool isInitialized = value.isInitialized;     // 是否已初始化
bool isPlaying = value.isPlaying;             // 是否正在播放
bool isBuffering = value.isBuffering;         // 是否正在缓冲
Duration duration = value.duration;           // 视频总时长
Duration position = value.position;           // 当前播放位置
Size size = value.size;                       // 视频尺寸
double aspectRatio = value.aspectRatio;       // 宽高比
double playbackSpeed = value.playbackSpeed;   // 播放速度
List<DurationRange> buffered = value.buffered; // 缓冲范围

4.3 创建控制器

video_player 支持三种数据源:

4.3.1 网络视频
_controller = VideoPlayerController.networkUrl(
  Uri.parse('https://media.w3.org/2010/05/sintel/trailer.mp4'),
);
4.3.2 资源视频

将视频文件放在 assets 目录中,然后在 pubspec.yaml 中配置:

flutter:
  assets:
    - assets/video1.mp4

使用方式:

_controller = VideoPlayerController.asset('assets/video1.mp4');
4.3.3 本地文件
_controller = VideoPlayerController.file(
  File('/path/to/local/video.mp4'),
);

⚠️ 注意:Web 平台不支持 dart:io,所以不能使用 VideoPlayerController.file

4.4 播放控制

播放与暂停
// 开始播放
await _controller.play();

// 暂停播放
await _controller.pause();

// 切换播放/暂停
if (_controller.value.isPlaying) {
  await _controller.pause();
} else {
  await _controller.play();
}
循环播放
await _controller.setLooping(true);
音量控制
// 设置音量,范围 0.0 到 1.0
await _controller.setVolume(0.5);
倍速播放
// 设置播放速度
await _controller.setPlaybackSpeed(1.5);

OpenHarmony 平台支持的倍速:

  • 0.125×、0.25×、0.5×、0.75×、1.0×、1.25×、1.5×、1.75×、2.0×、3.0×
进度控制
// 定位到指定位置
await _controller.seekTo(Duration(seconds: 30));

// 获取当前播放位置
Duration position = _controller.value.position;

五、UI 展示与交互

5.1 VideoPlayer Widget

VideoPlayer 是用于展示视频的 Widget:

VideoPlayer(_controller)

通常配合 AspectRatio 使用以保持正确的宽高比:

AspectRatio(
  aspectRatio: _controller.value.aspectRatio,
  child: VideoPlayer(_controller),
)

5.2 进度条

使用 VideoProgressIndicator 显示播放进度:

VideoProgressIndicator(
  _controller,
  allowScrubbing: true,  // 允许拖动进度条
)

5.3 完整播放器 UI

下面是一个完整的视频播放器 UI 示例:

class VideoPlayerScreen extends StatefulWidget {
  const VideoPlayerScreen({super.key});

  
  State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  late VideoPlayerController _controller;

  
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(
      Uri.parse('https://media.w3.org/2010/05/sintel/trailer.mp4'),
    )..initialize().then((_) {
        setState(() {});
      });

    _controller.addListener(() {
      setState(() {});
    });
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('视频播放')),
      body: Center(
        child: _controller.value.isInitialized
            ? Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  AspectRatio(
                    aspectRatio: _controller.value.aspectRatio,
                    child: Stack(
                      alignment: Alignment.bottomCenter,
                      children: [
                        VideoPlayer(_controller),
                        _buildPlayPauseOverlay(),
                        VideoProgressIndicator(_controller, allowScrubbing: true),
                      ],
                    ),
                  ),
                  _buildControls(),
                ],
              )
            : const CircularProgressIndicator(),
      ),
    );
  }

  Widget _buildPlayPauseOverlay() {
    return GestureDetector(
      onTap: () {
        setState(() {
          _controller.value.isPlaying
              ? _controller.pause()
              : _controller.play();
        });
      },
      child: _controller.value.isPlaying
          ? const SizedBox.shrink()
          : Container(
              color: Colors.black26,
              child: const Center(
                child: Icon(Icons.play_arrow, color: Colors.white, size: 100),
              ),
            ),
    );
  }

  Widget _buildControls() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          IconButton(
            icon: Icon(_controller.value.isPlaying ? Icons.pause : Icons.play_arrow),
            onPressed: () {
              setState(() {
                _controller.value.isPlaying
                    ? _controller.pause()
                    : _controller.play();
              });
            },
          ),
          IconButton(
            icon: const Icon(Icons.replay_10),
            onPressed: () {
              final position = _controller.value.position;
              _controller.seekTo(Duration(
                seconds: position.inSeconds - 10,
              ));
            },
          ),
          IconButton(
            icon: const Icon(Icons.forward_10),
            onPressed: () {
              final position = _controller.value.position;
              _controller.seekTo(Duration(
                seconds: position.inSeconds + 10,
              ));
            },
          ),
          PopupMenuButton<double>(
            initialValue: _controller.value.playbackSpeed,
            tooltip: '播放速度',
            onSelected: (double speed) {
              _controller.setPlaybackSpeed(speed);
            },
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<double>>[
                const PopupMenuItem<double>(value: 0.5, child: Text('0.5x')),
                const PopupMenuItem<double>(value: 1.0, child: Text('1.0x')),
                const PopupMenuItem<double>(value: 1.5, child: Text('1.5x')),
                const PopupMenuItem<double>(value: 2.0, child: Text('2.0x')),
              ];
            },
            child: Text('${_controller.value.playbackSpeed}x'),
          ),
        ],
      ),
    );
  }
}

六、完整示例应用

在这里插入图片描述

下面是一个功能完整的视频播放应用,包含网络视频、资源视频和本地视频的播放:

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

void main() => runApp(const VideoPlayerApp());

class VideoPlayerApp extends StatelessWidget {
  const VideoPlayerApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Player Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6366F1)),
        useMaterial3: true,
      ),
      home: const VideoHomePage(),
    );
  }
}

class VideoHomePage extends StatelessWidget {
  const VideoHomePage({super.key});

  
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('视频播放示例'),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.insert_drive_file), text: '资源视频'),
              Tab(icon: Icon(Icons.cloud), text: '网络视频'),
              Tab(icon: Icon(Icons.info), text: '视频信息'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            AssetVideoPage(),
            NetworkVideoPage(),
            VideoInfoPage(),
          ],
        ),
      ),
    );
  }
}

class AssetVideoPage extends StatefulWidget {
  const AssetVideoPage({super.key});

  
  State<AssetVideoPage> createState() => _AssetVideoPageState();
}

class _AssetVideoPageState extends State<AssetVideoPage> {
  late VideoPlayerController _controller;

  
  void initState() {
    super.initState();
    _controller = VideoPlayerController.asset('assets/video1.mp4')
      ..initialize().then((_) {
        setState(() {});
      })
      ..setLooping(true);

    _controller.addListener(() {
      setState(() {});
    });
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Center(
      child: _controller.value.isInitialized
          ? Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                AspectRatio(
                  aspectRatio: _controller.value.aspectRatio,
                  child: VideoPlayer(_controller),
                ),
                VideoProgressIndicator(_controller, allowScrubbing: true),
                _buildControls(),
              ],
            )
          : const CircularProgressIndicator(),
    );
  }

  Widget _buildControls() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          IconButton(
            icon: Icon(_controller.value.isPlaying ? Icons.pause : Icons.play_arrow),
            onPressed: () {
              setState(() {
                _controller.value.isPlaying
                    ? _controller.pause()
                    : _controller.play();
              });
            },
          ),
          IconButton(
            icon: const Icon(Icons.stop),
            onPressed: () {
              _controller.pause();
              _controller.seekTo(Duration.zero);
            },
          ),
          PopupMenuButton<double>(
            initialValue: _controller.value.playbackSpeed,
            onSelected: (double speed) {
              _controller.setPlaybackSpeed(speed);
            },
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<double>>[
                const PopupMenuItem<double>(value: 0.5, child: Text('0.5x')),
                const PopupMenuItem<double>(value: 1.0, child: Text('1.0x')),
                const PopupMenuItem<double>(value: 1.5, child: Text('1.5x')),
                const PopupMenuItem<double>(value: 2.0, child: Text('2.0x')),
              ];
            },
            child: Text('${_controller.value.playbackSpeed}x'),
          ),
        ],
      ),
    );
  }
}

class NetworkVideoPage extends StatefulWidget {
  const NetworkVideoPage({super.key});

  
  State<NetworkVideoPage> createState() => _NetworkVideoPageState();
}

class _NetworkVideoPageState extends State<NetworkVideoPage> {
  late VideoPlayerController _controller;
  bool _isBuffering = false;

  
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(
      Uri.parse('https://media.w3.org/2010/05/sintel/trailer.mp4'),
    )..initialize().then((_) {
        setState(() {});
      });

    _controller.addListener(() {
      setState(() {
        _isBuffering = _controller.value.isBuffering;
      });
    });
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Center(
      child: _controller.value.isInitialized
          ? Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                AspectRatio(
                  aspectRatio: _controller.value.aspectRatio,
                  child: Stack(
                    alignment: Alignment.center,
                    children: [
                      VideoPlayer(_controller),
                      if (_isBuffering)
                        const CircularProgressIndicator(
                          valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                        ),
                      _buildPlayPauseOverlay(),
                    ],
                  ),
                ),
                VideoProgressIndicator(_controller, allowScrubbing: true),
                _buildControls(),
              ],
            )
          : const CircularProgressIndicator(),
    );
  }

  Widget _buildPlayPauseOverlay() {
    return GestureDetector(
      onTap: () {
        setState(() {
          _controller.value.isPlaying
              ? _controller.pause()
              : _controller.play();
        });
      },
      child: AnimatedSwitcher(
        duration: const Duration(milliseconds: 50),
        child: _controller.value.isPlaying
            ? const SizedBox.shrink()
            : Container(
                color: Colors.black26,
                child: const Center(
                  child: Icon(
                    Icons.play_arrow,
                    color: Colors.white,
                    size: 100,
                  ),
                ),
              ),
      ),
    );
  }

  Widget _buildControls() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          IconButton(
            icon: Icon(_controller.value.isPlaying ? Icons.pause : Icons.play_arrow),
            onPressed: () {
              setState(() {
                _controller.value.isPlaying
                    ? _controller.pause()
                    : _controller.play();
              });
            },
          ),
          IconButton(
            icon: const Icon(Icons.replay_10),
            onPressed: () {
              final position = _controller.value.position;
              _controller.seekTo(Duration(
                seconds: position.inSeconds - 10,
              ));
            },
          ),
          IconButton(
            icon: const Icon(Icons.forward_10),
            onPressed: () {
              final position = _controller.value.position;
              _controller.seekTo(Duration(
                seconds: position.inSeconds + 10,
              ));
            },
          ),
          Text(_formatDuration(_controller.value.position)),
          const Text('/'),
          Text(_formatDuration(_controller.value.duration)),
        ],
      ),
    );
  }

  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final minutes = twoDigits(duration.inMinutes.remainder(60));
    final seconds = twoDigits(duration.inSeconds.remainder(60));
    return '$minutes:$seconds';
  }
}

class VideoInfoPage extends StatefulWidget {
  const VideoInfoPage({super.key});

  
  State<VideoInfoPage> createState() => _VideoInfoPageState();
}

class _VideoInfoPageState extends State<VideoInfoPage> {
  late VideoPlayerController _controller;

  
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(
      Uri.parse('https://media.w3.org/2010/05/sintel/trailer.mp4'),
    )..initialize().then((_) {
        setState(() {});
      });
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    if (!_controller.value.isInitialized) {
      return const Center(child: CircularProgressIndicator());
    }

    final value = _controller.value;

    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildInfoCard('视频尺寸', '${value.size.width.toInt()} x ${value.size.height.toInt()}'),
        const SizedBox(height: 16),
        _buildInfoCard('视频时长', _formatDuration(value.duration)),
        const SizedBox(height: 16),
        _buildInfoCard('当前进度', _formatDuration(value.position)),
        const SizedBox(height: 16),
        _buildInfoCard('播放状态', value.isPlaying ? '播放中' : '已暂停'),
        const SizedBox(height: 16),
        _buildInfoCard('缓冲状态', value.isBuffering ? '缓冲中' : '已缓冲'),
        const SizedBox(height: 16),
        _buildInfoCard('播放速度', '${value.playbackSpeed}x'),
        const SizedBox(height: 16),
        _buildInfoCard('宽高比', value.aspectRatio.toStringAsFixed(2)),
        const SizedBox(height: 16),
        _buildInfoCard('缓冲范围', _formatBufferedRanges(value.buffered)),
      ],
    );
  }

  Widget _buildInfoCard(String title, String value) {
    return Card(
      child: ListTile(
        title: Text(title),
        subtitle: Text(value),
        leading: const Icon(Icons.info_outline),
      ),
    );
  }

  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final minutes = twoDigits(duration.inMinutes.remainder(60));
    final seconds = twoDigits(duration.inSeconds.remainder(60));
    return '$minutes:$seconds';
  }

  String _formatBufferedRanges(List<DurationRange> buffered) {
    if (buffered.isEmpty) return '无';
    return buffered.map((range) {
      final start = _formatDuration(range.start);
      final end = _formatDuration(range.end);
      return '$start - $end';
    }).join(', ');
  }
}

七、API 参考

7.1 VideoPlayerController 主要方法

方法 返回值 说明
initialize() Future <void> 初始化视频播放器
play() Future <void> 开始播放
pause() Future <void> 暂停播放
setLooping(bool looping) Future <void> 设置循环播放
setVolume(double volume) Future <void> 设置音量(0.0-1.0)
setPlaybackSpeed(double speed) Future <void> 设置播放速度
seekTo(Duration position) Future <void> 定位到指定位置
dispose() void 释放资源

7.2 VideoPlayerValue 属性

属性 类型 说明
duration Duration 视频总时长
position Duration 当前播放位置
size Size 视频尺寸
isInitialized bool 是否已初始化
isPlaying bool 是否正在播放
isBuffering bool 是否正在缓冲
playbackSpeed double 播放速度
aspectRatio double 宽高比
buffered List <DurationRange> 已缓冲的范围
errorDescription String? 错误描述(如有)

7.3 构造函数

构造函数 参数 说明
VideoPlayerController.asset() String asset 从资源文件创建
VideoPlayerController.networkUrl() Uri url 从网络 URL 创建
VideoPlayerController.file() File file 从本地文件创建

八、常见问题与解决方案

8.1 视频无法播放

问题描述:视频加载后无法播放。

可能原因

  1. 视频格式不支持
  2. 网络连接问题
  3. 视频文件损坏
  4. 权限未配置(网络视频)

解决方案

_controller.addListener(() {
  if (_controller.value.hasError) {
    print('视频播放错误: ${_controller.value.errorDescription}');
  }
});

8.2 黑屏无画面

问题描述:初始化完成后显示黑屏。

解决方案

// 确保 AspectRatio 正确设置
AspectRatio(
  aspectRatio: _controller.value.aspectRatio,
  child: VideoPlayer(_controller),
)

// 或者检查视频尺寸
if (_controller.value.size.isEmpty) {
  print('视频尺寸异常');
}

8.3 播放卡顿

问题描述:视频播放过程中频繁卡顿。

解决方案

// 监听缓冲状态
_controller.addListener(() {
  if (_controller.value.isBuffering) {
    print('视频正在缓冲...');
  }
});

8.4 内存泄漏

问题描述:页面退出后视频仍在播放。

解决方案


void dispose() {
  _controller.dispose();  // 必须释放控制器
  super.dispose();
}

九、最佳实践

9.1 正确管理生命周期

class VideoWidget extends StatefulWidget {
  
  State<VideoWidget> createState() => _VideoWidgetState();
}

class _VideoWidgetState extends State<VideoWidget> {
  late VideoPlayerController _controller;

  
  void initState() {
    super.initState();
    _initializeController();
  }

  void _initializeController() {
    _controller = VideoPlayerController.networkUrl(
      Uri.parse('https://example.com/video.mp4'),
    )..initialize().then((_) {
        setState(() {});
      });
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 页面可见时恢复播放
    if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
      _controller.play();
    }
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

9.2 预加载视频

// 在页面初始化时预先加载视频
Future<void> preloadVideo(String url) async {
  final controller = VideoPlayerController.networkUrl(Uri.parse(url));
  await controller.initialize();
  // 使用时直接使用已初始化的控制器
}

9.3 错误处理

_controller.addListener(() {
  if (_controller.value.hasError) {
    // 显示错误提示
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('视频播放失败: ${_controller.value.errorDescription}')),
    );
  }
});

9.4 性能优化

// 暂停不可见的视频

  void deactivate() {
  _controller.pause();
  super.deactivate();
}

// 释放不再使用的视频
void _releaseVideo() {
  _controller.pause();
  _controller.seekTo(Duration.zero);
}


Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐