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

本文基于flutter3.27.5开发

在这里插入图片描述

一、video_player 库概述

视频播放是现代移动应用的核心功能之一,无论是短视频应用、在线教育平台还是娱乐媒体应用,都需要强大的视频播放能力。在 Flutter for OpenHarmony 应用开发中,video_player 是官方推荐的视频播放插件,提供了完整的视频播放解决方案。

video_player 库特点

video_player 库基于 Flutter 平台接口实现,提供了以下核心特性:

多格式支持:支持 MP4、WebM、HLS、DASH 等主流视频格式,满足各种场景需求。

多数据源:支持网络视频、本地文件、Asset 资源等多种数据源。

播放控制:提供播放、暂停、停止、跳转、倍速播放等完整的播放控制能力。

状态监听:实时监听播放状态、缓冲状态、播放进度等信息。

跨平台一致:统一的 API 接口,在不同平台上表现一致。

功能支持对比

功能 Android iOS OpenHarmony
网络视频播放
本地文件播放
Asset 资源播放
播放控制
倍速播放
音量控制
循环播放
HLS/DASH 流媒体

使用场景:短视频应用、在线教育平台、视频会议、媒体播放器等。


二、安装与配置

2.1 添加依赖

在项目的 pubspec.yaml 文件中添加 video_player 依赖:

dependencies:
  video_player:
    git:
      url: https://atomgit.com/openharmony-tpc/flutter_packages.git
      path: packages/video_player/video_player

然后执行以下命令获取依赖:

flutter pub get

2.2 权限配置

video_player 需要网络权限才能播放网络视频。在 module.json5 中添加:

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

string.json 中添加权限说明:

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

三、核心 API 详解

3.1 VideoPlayerController 类

VideoPlayerController 是视频播放的核心控制器,负责管理视频的加载、播放、暂停等操作。

class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
  // 构造方法
  VideoPlayerController.networkUrl(Uri url, {VideoFormat? formatHint});
  VideoPlayerController.asset(String dataSource, {String? package});
  VideoPlayerController.file(File file);
  VideoPlayerController.contentUri(Uri contentUri);
  
  // 核心方法
  Future<void> initialize();
  Future<void> play();
  Future<void> pause();
  Future<void> seekTo(Duration position);
  Future<void> setPlaybackSpeed(double speed);
  Future<void> setVolume(double volume);
  Future<void> setLooping(bool looping);
  void dispose();
}

3.2 构造方法详解

3.2.1 VideoPlayerController.networkUrl

从网络 URL 创建视频控制器。

VideoPlayerController.networkUrl(Uri url, {VideoFormat? formatHint})

参数说明

url 参数是视频的网络地址,必须是一个有效的 URI。

formatHint 参数是视频格式提示,用于流媒体格式识别。

使用示例

// 播放普通网络视频
final controller = VideoPlayerController.networkUrl(
  Uri.parse('https://example.com/video.mp4'),
);

// 播放 HLS 流媒体
final hlsController = VideoPlayerController.networkUrl(
  Uri.parse('https://example.com/video.m3u8'),
  formatHint: VideoFormat.hls,
);

// 播放 DASH 流媒体
final dashController = VideoPlayerController.networkUrl(
  Uri.parse('https://example.com/video.mpd'),
  formatHint: VideoFormat.dash,
);
3.2.2 VideoPlayerController.asset

从 Asset 资源创建视频控制器。

VideoPlayerController.asset(String dataSource, {String? package})

参数说明

dataSource 参数是 Asset 资源路径。

package 参数是资源所在的包名(用于插件资源)。

使用示例

// 播放应用内 Asset 视频
final controller = VideoPlayerController.asset('assets/videos/intro.mp4');

// 播放插件包中的视频
final pluginController = VideoPlayerController.asset(
  'assets/videos/tutorial.mp4',
  package: 'my_plugin',
);

注意:Asset 视频需要在 pubspec.yaml 中声明:

flutter:
  assets:
    - assets/videos/
3.2.3 VideoPlayerController.file

从本地文件创建视频控制器。

VideoPlayerController.file(File file)

参数说明

file 参数是本地文件对象。

使用示例

import 'dart:io';

// 播放本地文件
final file = File('/path/to/video.mp4');
final controller = VideoPlayerController.file(file);

3.3 initialize 方法

initialize 方法用于初始化视频控制器,必须在播放前调用。

Future<void> initialize()

返回值:返回一个 Future,初始化完成后完成。

使用示例

final controller = VideoPlayerController.networkUrl(
  Uri.parse('https://example.com/video.mp4'),
);

try {
  await controller.initialize();
  print('视频初始化成功');
  print('视频时长: ${controller.duration}');
  print('视频尺寸: ${controller.size}');
} catch (e) {
  print('视频初始化失败: $e');
}

重要提示

  1. initialize 方法是异步的,必须使用 await 等待完成
  2. 初始化完成后才能获取视频时长、尺寸等信息
  3. 初始化失败时会抛出异常,建议使用 try-catch 处理

3.4 play 方法

play 方法用于开始或恢复播放视频。

Future<void> play()

使用示例

// 开始播放
await controller.play();

// 播放前检查初始化状态
if (controller.value.isInitialized) {
  await controller.play();
}

3.5 pause 方法

pause 方法用于暂停播放视频。

Future<void> pause()

使用示例

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

// 根据当前状态切换播放/暂停
if (controller.value.isPlaying) {
  await controller.pause();
} else {
  await controller.play();
}

3.6 seekTo 方法

seekTo 方法用于跳转到指定播放位置。

Future<void> seekTo(Duration position)

参数说明

position 参数是目标播放位置。

使用示例

// 跳转到 30 秒位置
await controller.seekTo(Duration(seconds: 30));

// 跳转到视频开头
await controller.seekTo(Duration.zero);

// 快进 10 秒
final newPosition = controller.position + Duration(seconds: 10);
await controller.seekTo(newPosition);

// 快退 10 秒
final newPosition = controller.position - Duration(seconds: 10);
if (newPosition < Duration.zero) {
  await controller.seekTo(Duration.zero);
} else {
  await controller.seekTo(newPosition);
}

3.7 setPlaybackSpeed 方法

setPlaybackSpeed 方法用于设置播放速度。

Future<void> setPlaybackSpeed(double speed)

参数说明

speed 参数是播放速度倍率,1.0 为正常速度。

使用示例

// 正常速度
await controller.setPlaybackSpeed(1.0);

// 1.5 倍速
await controller.setPlaybackSpeed(1.5);

// 2 倍速
await controller.setPlaybackSpeed(2.0);

// 0.5 倍速(慢放)
await controller.setPlaybackSpeed(0.5);

OpenHarmony 平台说明:OpenHarmony 支持以下固定倍速值:

  • 0.125、0.25、0.5、0.75、1.0、1.25、1.5、1.75、2.0、3.0

设置其他倍速值时,系统会自动选择最接近的支持值。

3.8 setVolume 方法

setVolume 方法用于设置音量。

Future<void> setVolume(double volume)

参数说明

volume 参数是音量值,范围 0.0(静音)到 1.0(最大音量)。

使用示例

// 静音
await controller.setVolume(0.0);

// 最大音量
await controller.setVolume(1.0);

// 50% 音量
await controller.setVolume(0.5);

3.9 setLooping 方法

setLooping 方法用于设置是否循环播放。

Future<void> setLooping(bool looping)

参数说明

looping 参数为 true 时循环播放,为 false 时播放一次后停止。

使用示例

// 开启循环播放
await controller.setLooping(true);

// 关闭循环播放
await controller.setLooping(false);

// 检查是否循环播放
if (controller.value.isLooping) {
  print('当前为循环播放模式');
}

3.10 dispose 方法

dispose 方法用于释放视频控制器资源。

void dispose()

使用示例


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

重要提示

  1. 必须在 Widget 销毁时调用 dispose 释放资源
  2. 未释放的控制器会导致内存泄漏
  3. 释放后不能再使用该控制器

3.11 常用属性

OpenHarmony 版本的 video_player 中,播放状态相关属性需要通过 value 访问:

// 当前播放位置(通过 value 访问)
Duration get value.position;

// 视频总时长(通过 value 访问)
Duration get value.duration;

// 视频尺寸(通过 value 访问)
Size get value.size;

// 视频宽高比(通过 value 访问)
double get value.aspectRatio;

// 当前播放状态
VideoPlayerValue get value;

使用示例

// 获取播放进度百分比
final progress = controller.value.position.inMilliseconds / controller.value.duration.inMilliseconds;

// 获取视频尺寸
final width = controller.value.size.width;
final height = controller.value.size.height;

// 获取宽高比
final ratio = controller.value.aspectRatio;

// 检查是否正在播放
if (controller.value.isPlaying) {
  print('视频正在播放');
}

// 检查是否初始化完成
if (controller.value.isInitialized) {
  print('视频已初始化');
}

// 检查是否缓冲中
if (controller.value.isBuffering) {
  print('视频正在缓冲');
}

重要提示:OpenHarmony 版本的 positiondurationsizeaspectRatio 等属性需要通过 controller.value 访问,而不是直接通过 controller 访问。


四、数据模型详解

4.1 VideoPlayerValue 类

VideoPlayerValue 类包含视频播放器的所有状态信息。

class VideoPlayerValue {
  const VideoPlayerValue({
    this.duration = Duration.zero,
    this.position = Duration.zero,
    this.isPlaying = false,
    this.isLooping = false,
    this.isBuffering = false,
    this.volume = 1.0,
    this.playbackSpeed = 1.0,
    this.errorDescription,
    this.size = Size.zero,
    this.isInitialized = false,
  });
  
  final Duration duration;          // 视频总时长
  final Duration position;          // 当前播放位置
  final bool isPlaying;             // 是否正在播放
  final bool isLooping;             // 是否循环播放
  final bool isBuffering;           // 是否正在缓冲
  final double volume;              // 当前音量
  final double playbackSpeed;       // 当前播放速度
  final String? errorDescription;   // 错误描述
  final Size size;                  // 视频尺寸
  final bool isInitialized;         // 是否已初始化
}

4.2 VideoFormat 枚举

VideoFormat 枚举定义了视频格式类型。

enum VideoFormat {
  dash,   // MPEG-DASH 流媒体
  hls,    // HTTP Live Streaming
  ss,     // Smooth Streaming
  other,  // 其他格式
}

4.3 DataSourceType 枚举

DataSourceType 枚举定义了数据源类型。

enum DataSourceType {
  asset,      // Asset 资源
  network,    // 网络资源
  file,       // 本地文件
  contentUri, // Content URI(仅 Android)
}

4.4 VideoPlayer 组件

VideoPlayer 是用于显示视频的 Widget。

class VideoPlayer extends StatefulWidget {
  const VideoPlayer(this.controller, {super.key});
  
  final VideoPlayerController controller;
}

使用示例

VideoPlayer(controller);

4.5 VideoProgressIndicator 组件

VideoProgressIndicator 是视频进度条组件。

class VideoProgressIndicator extends StatefulWidget {
  const VideoProgressIndicator(
    this.controller, {
    super.key,
    this.colors,
    this.allowScrubbing = false,
    this.padding = const EdgeInsets.only(top: 5.0),
  });
  
  final VideoPlayerController controller;
  final VideoProgressColors? colors;      // 进度条颜色
  final bool allowScrubbing;              // 是否允许拖动
  final EdgeInsets padding;               // 内边距
}

使用示例

VideoProgressIndicator(
  controller,
  allowScrubbing: true,
  colors: VideoProgressColors(
    playedColor: Colors.red,
    bufferedColor: Colors.grey,
    backgroundColor: Colors.black,
  ),
);

4.6 VideoProgressColors 类

VideoProgressColors 类定义进度条颜色。

class VideoProgressColors {
  const VideoProgressColors({
    this.playedColor = const Color.fromRGBO(255, 0, 0, 0.7),
    this.bufferedColor = const Color.fromRGBO(50, 50, 200, 0.2),
    this.backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5),
  });
  
  final Color playedColor;      // 已播放部分颜色
  final Color bufferedColor;    // 已缓冲部分颜色
  final Color backgroundColor;  // 背景颜色
}

五、OpenHarmony 平台实现原理

5.1 原生 API 映射

video_player 在 OpenHarmony 平台上使用 @ohos.multimedia.media 模块实现:

Flutter API OpenHarmony API
initialize media.createAVPlayer
play avPlayer.play
pause avPlayer.pause
seekTo avPlayer.seek
setPlaybackSpeed avPlayer.setSpeed
setVolume avPlayer.setVolume
setLooping avPlayer.setLoop

5.2 AVPlayer 状态机

OpenHarmony AVPlayer 采用状态机模式管理播放状态:

idle -> initialized -> prepared -> playing -> paused -> completed
                       ↓
                     stopped

状态说明

状态 说明
idle 空闲状态,初始状态
initialized 已设置数据源,未准备
prepared 准备完成,可以播放
playing 正在播放
paused 已暂停
completed 播放完成
stopped 已停止
error 发生错误

5.3 视频渲染实现

OpenHarmony 使用 XComponent 进行视频渲染:

XComponent({
  id: 'videoPlayer',
  type: 'surface',
  libraryname: 'avplayer',
})
.onLoad(() => {
  // 获取 surfaceId 并关联到 AVPlayer
  avPlayer.surfaceId = surfaceId;
});

5.4 倍速播放实现

OpenHarmony AVPlayer 支持固定的播放速度:

// 支持的速度值
const speeds = [0.125, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 3.0];

// 设置播放速度
avPlayer.setSpeed(speed);

六、实战案例

6.1 基础视频播放

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

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

  
  State<BasicVideoPlayer> createState() => _BasicVideoPlayerState();
}

class _BasicVideoPlayerState extends State<BasicVideoPlayer> {
  late VideoPlayerController _controller;
  bool _isInitialized = false;

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基础视频播放')),
      body: Center(
        child: _isInitialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: VideoPlayer(_controller),
              )
            : const CircularProgressIndicator(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _controller.value.isPlaying
                ? _controller.pause()
                : _controller.play();
          });
        },
        child: Icon(
          _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      ),
    );
  }
}

6.2 带控制条的视频播放器

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

class ControlledVideoPlayer extends StatefulWidget {
  final String videoUrl;

  const ControlledVideoPlayer({super.key, required this.videoUrl});

  
  State<ControlledVideoPlayer> createState() => _ControlledVideoPlayerState();
}

class _ControlledVideoPlayerState extends State<ControlledVideoPlayer> {
  late VideoPlayerController _controller;
  bool _isInitialized = false;

  
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
    _controller.addListener(() {
      setState(() {});
    });
    _controller.initialize().then((_) {
      setState(() {
        _isInitialized = true;
      });
    });
  }

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        title: const Text('视频播放器'),
        backgroundColor: Colors.transparent,
      ),
      body: _isInitialized
          ? Column(
              children: [
                Expanded(
                  child: Center(
                    child: AspectRatio(
                      aspectRatio: _controller.value.aspectRatio,
                      child: VideoPlayer(_controller),
                    ),
                  ),
                ),
                _buildControls(),
              ],
            )
          : const Center(
              child: CircularProgressIndicator(color: Colors.white),
            ),
    );
  }

  Widget _buildControls() {
    return Container(
      color: Colors.black87,
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Row(
            children: [
              Text(
                _formatDuration(_controller.position),
                style: const TextStyle(color: Colors.white),
              ),
              Expanded(
                child: Slider(
                  value: _controller.position.inSeconds.toDouble(),
                  max: _controller.duration.inSeconds.toDouble(),
                  onChanged: (value) {
                    _controller.seekTo(Duration(seconds: value.toInt()));
                  },
                ),
              ),
              Text(
                _formatDuration(_controller.duration),
                style: const TextStyle(color: Colors.white),
              ),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              IconButton(
                icon: const Icon(Icons.replay_10, color: Colors.white),
                onPressed: () {
                  final newPosition = _controller.position - const Duration(seconds: 10);
                  _controller.seekTo(newPosition < Duration.zero ? Duration.zero : newPosition);
                },
              ),
              IconButton(
                icon: Icon(
                  _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
                  color: Colors.white,
                  size: 48,
                ),
                onPressed: () {
                  _controller.value.isPlaying ? _controller.pause() : _controller.play();
                },
              ),
              IconButton(
                icon: const Icon(Icons.forward_10, color: Colors.white),
                onPressed: () {
                  final newPosition = _controller.position + const Duration(seconds: 10);
                  _controller.seekTo(newPosition > _controller.duration ? _controller.duration : newPosition);
                },
              ),
            ],
          ),
        ],
      ),
    );
  }
}

七、常见问题

7.1 视频无法播放?

原因

  1. 网络权限未配置
  2. URL 无效或无法访问
  3. 视频格式不支持
  4. 未等待初始化完成

解决方案

// 确保初始化完成后再播放
await _controller.initialize();
await _controller.play();

7.2 如何实现全屏播放?


void initState() {
  super.initState();
  SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.landscapeLeft,
    DeviceOrientation.landscapeRight,
  ]);
}


void dispose() {
  SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  super.dispose();
}

7.3 OpenHarmony 倍速播放限制

OpenHarmony 仅支持固定倍速值:0.125、0.25、0.5、0.75、1.0、1.25、1.5、1.75、2.0、3.0。

设置其他值时系统会自动选择最接近的支持值。


八、完整代码示例

以下是一个完整的可运行示例,展示了 video_player 库的核心功能:
在这里插入图片描述

main.dart

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

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Player Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
        useMaterial3: true,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  final List<VideoItem> videos = const [
    VideoItem(
      title: 'Sintel 预告片',
      url: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
      thumbnail: 'https://picsum.photos/400/225?random=1',
    ),
    VideoItem(
      title: 'Big Buck Bunny',
      url: 'https://media.w3.org/2010/05/bunny/trailer.mp4',
      thumbnail: 'https://picsum.photos/400/225?random=2',
    ),
    VideoItem(
      title: '测试视频',
      url: 'https://www.w3schools.com/html/mov_bbb.mp4',
      thumbnail: 'https://picsum.photos/400/225?random=3',
    ),
  ];

  HomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('视频播放器'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: videos.length,
        itemBuilder: (context, index) {
          final video = videos[index];
          return Card(
            clipBehavior: Clip.antiAlias,
            margin: const EdgeInsets.only(bottom: 16),
            child: InkWell(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => VideoPlayerPage(
                      videoUrl: video.url,
                      title: video.title,
                    ),
                  ),
                );
              },
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Stack(
                    alignment: Alignment.center,
                    children: [
                      Image.network(
                        video.thumbnail,
                        width: double.infinity,
                        height: 180,
                        fit: BoxFit.cover,
                        errorBuilder: (context, error, stackTrace) {
                          return Container(
                            width: double.infinity,
                            height: 180,
                            color: Colors.grey[300],
                            child: const Icon(Icons.video_library, size: 48),
                          );
                        },
                      ),
                      Container(
                        decoration: BoxDecoration(
                          color: Colors.black.withOpacity(0.5),
                          shape: BoxShape.circle,
                        ),
                        padding: const EdgeInsets.all(16),
                        child: const Icon(
                          Icons.play_arrow,
                          color: Colors.white,
                          size: 48,
                        ),
                      ),
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.all(12),
                    child: Text(
                      video.title,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

class VideoPlayerPage extends StatefulWidget {
  final String videoUrl;
  final String title;

  const VideoPlayerPage({
    super.key,
    required this.videoUrl,
    required this.title,
  });

  
  State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  late VideoPlayerController _controller;
  bool _isInitialized = false;
  bool _showControls = true;
  bool _hasError = false;

  static const List<double> _playbackSpeeds = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];

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

  Future<void> _initPlayer() async {
    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));

    _controller.addListener(() {
      if (_controller.value.hasError) {
        setState(() {
          _hasError = true;
        });
      }
    });

    try {
      await _controller.initialize();
      setState(() {
        _isInitialized = true;
      });
    } catch (e) {
      setState(() {
        _hasError = true;
      });
    }
  }

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

    if (hours > 0) {
      return '$hours:${twoDigits(minutes)}:${twoDigits(seconds)}';
    }
    return '${twoDigits(minutes)}:${twoDigits(seconds)}';
  }

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        title: Text(widget.title),
        backgroundColor: Colors.transparent,
        foregroundColor: Colors.white,
      ),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    if (_hasError) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, color: Colors.red, size: 64),
            const SizedBox(height: 16),
            const Text(
              '视频加载失败',
              style: TextStyle(color: Colors.white, fontSize: 18),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _hasError = false;
                  _isInitialized = false;
                });
                _initPlayer();
              },
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    if (!_isInitialized) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(color: Colors.white),
            SizedBox(height: 16),
            Text(
              '正在加载视频...',
              style: TextStyle(color: Colors.white, fontSize: 16),
            ),
          ],
        ),
      );
    }

    return GestureDetector(
      onTap: () {
        setState(() {
          _showControls = !_showControls;
        });
      },
      child: Stack(
        alignment: Alignment.bottomCenter,
        children: [
          Center(
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: VideoPlayer(_controller),
            ),
          ),
          if (_showControls) _buildControls(),
        ],
      ),
    );
  }

  Widget _buildControls() {
    return Container(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [Colors.transparent, Colors.black87],
        ),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          _buildProgressBar(),
          _buildControlButtons(),
        ],
      ),
    );
  }

  Widget _buildProgressBar() {
    final position = _controller.value.position;
    final duration = _controller.value.duration;

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(
        children: [
          Text(
            _formatDuration(position),
            style: const TextStyle(color: Colors.white, fontSize: 12),
          ),
          Expanded(
            child: SliderTheme(
              data: SliderTheme.of(context).copyWith(
                thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
                trackHeight: 3,
                activeTrackColor: Theme.of(context).colorScheme.primary,
                inactiveTrackColor: Colors.white30,
                thumbColor: Theme.of(context).colorScheme.primary,
              ),
              child: Slider(
                value: position.inSeconds.toDouble().clamp(0, duration.inSeconds.toDouble()),
                max: duration.inSeconds.toDouble(),
                onChanged: (value) {
                  _controller.seekTo(Duration(seconds: value.toInt()));
                },
              ),
            ),
          ),
          Text(
            _formatDuration(duration),
            style: const TextStyle(color: Colors.white, fontSize: 12),
          ),
        ],
      ),
    );
  }

  Widget _buildControlButtons() {
    final position = _controller.value.position;
    final duration = _controller.value.duration;

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          IconButton(
            icon: const Icon(Icons.replay_10, color: Colors.white, size: 28),
            onPressed: () {
              final newPosition = position - const Duration(seconds: 10);
              _controller.seekTo(
                newPosition < Duration.zero ? Duration.zero : newPosition,
              );
            },
          ),
          IconButton(
            icon: Icon(
              _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
              color: Colors.white,
              size: 48,
            ),
            onPressed: () {
              _controller.value.isPlaying ? _controller.pause() : _controller.play();
              setState(() {});
            },
          ),
          IconButton(
            icon: const Icon(Icons.forward_10, color: Colors.white, size: 28),
            onPressed: () {
              final newPosition = position + const Duration(seconds: 10);
              _controller.seekTo(
                newPosition > duration ? duration : newPosition,
              );
            },
          ),
          PopupMenuButton<double>(
            icon: Container(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              decoration: BoxDecoration(
                color: Colors.white24,
                borderRadius: BorderRadius.circular(4),
              ),
              child: Text(
                '${_controller.value.playbackSpeed}x',
                style: const TextStyle(color: Colors.white, fontSize: 12),
              ),
            ),
            onSelected: (speed) {
              _controller.setPlaybackSpeed(speed);
              setState(() {});
            },
            itemBuilder: (context) {
              return _playbackSpeeds.map((speed) {
                return PopupMenuItem(
                  value: speed,
                  child: Text(
                    '${speed}x',
                    style: TextStyle(
                      fontWeight: _controller.value.playbackSpeed == speed
                          ? FontWeight.bold
                          : FontWeight.normal,
                    ),
                  ),
                );
              }).toList();
            },
          ),
        ],
      ),
    );
  }
}

class VideoItem {
  final String title;
  final String url;
  final String thumbnail;

  const VideoItem({
    required this.title,
    required this.url,
    required this.thumbnail,
  });
}

运行此示例后,您将看到一个完整的视频播放器演示界面,包含视频列表、播放控制、进度条、倍速播放等功能。点击视频卡片即可进入播放页面。


九、总结

video_player 是 Flutter 官方推荐的视频播放插件,为 OpenHarmony 应用提供了完整的视频播放能力。

优点

  1. 多格式支持:支持主流视频格式
  2. 跨平台一致:统一的 API 接口
  3. 完整控制:播放、暂停、跳转、倍速等
  4. 状态监听:实时获取播放状态

适用场景

  • 短视频应用
  • 在线教育平台
  • 视频会议应用
  • 媒体播放器

最佳实践

  1. 及时释放不再使用的控制器
  2. 做好错误处理
  3. OpenHarmony 使用固定倍速值
  4. 处理应用生命周期事件

十、参考资料

Logo

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

更多推荐