在这里插入图片描述

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


🔍 一、第三方库概述与应用场景

📱 1.1 为什么需要视频播放功能?

在移动互联网时代,视频内容已经成为应用中最受欢迎的媒体形式之一。无论是短视频应用、在线教育平台、企业培训系统,还是社交媒体应用,视频播放功能都是不可或缺的核心功能。

想象一下这样的场景:用户打开你的在线教育应用,浏览课程列表,点击感兴趣的课程视频。视频开始加载,用户可以看到播放进度条、控制按钮,可以随时暂停、快进、调节播放速度。整个播放过程流畅自然,用户可以专注于学习内容,而不是被糟糕的播放体验所困扰。

这就是 video_player 库要解决的问题。它提供了一套完整的视频播放解决方案,让开发者可以轻松实现跨平台的视频播放功能,同时保持良好的用户体验。

📋 1.2 video_player 是什么?

video_player 是 Flutter 官方维护的核心插件,专门用于在应用内播放视频内容。它支持多种视频源(网络视频、本地文件、Asset 资源),提供了丰富的播放控制功能,并且可以与其他 Flutter Widget 无缝集成。

在 OpenHarmony 平台上,video_player 同样提供了完整的支持,让开发者可以无缝地使用这套 API 来实现视频播放功能。

🎯 1.3 核心功能特性

功能特性 详细说明 OpenHarmony 支持
网络视频播放 支持 HTTP/HTTPS 协议的网络视频 ✅ 完全支持
本地文件播放 播放设备存储中的视频文件 ✅ 完全支持
Asset 资源播放 播放应用内置的视频资源 ✅ 完全支持
播放控制 播放、暂停、跳转、速度调节 ✅ 完全支持
循环播放 设置视频循环播放 ✅ 完全支持
状态监听 实时监听播放状态和进度 ✅ 完全支持
视频信息获取 获取时长、宽高比等信息 ✅ 完全支持

💡 1.4 典型应用场景

在实际的应用开发中,video_player 有着广泛的应用场景:

短视频应用:用户可以浏览、播放短视频内容,支持滑动切换、自动播放等功能。

在线教育平台:学生可以观看课程视频,支持进度记录、倍速播放、章节跳转等功能。

企业培训系统:员工可以观看培训视频,支持考核、进度追踪等功能。

社交媒体应用:用户可以分享和观看视频动态,支持点赞、评论、转发等社交功能。


🏗️ 二、系统架构设计

📐 2.1 整体架构

为了构建一个可维护、可扩展的视频播放系统,我们采用分层架构设计:

┌─────────────────────────────────────────────────────────┐
│                    UI 层 (展示层)                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │  播放界面   │  │  控制面板   │  │  进度条     │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
├─────────────────────────────────────────────────────────┤
│                  服务层 (业务逻辑)                       │
│  ┌─────────────────────────────────────────────────┐   │
│  │              VideoPlayerService                  │   │
│  │  • 统一的视频播放接口                            │   │
│  │  • 播放状态管理                                  │   │
│  │  • 进度追踪与记录                                │   │
│  │  • 错误处理与重试                                │   │
│  └─────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────┤
│                  基础设施层 (底层实现)                   │
│  ┌─────────────────────────────────────────────────┐   │
│  │              video_player 插件                   │   │
│  │  • VideoPlayerController - 视频控制器            │   │
│  │  • VideoPlayer - 视频显示组件                    │   │
│  │  • VideoPlayerValue - 播放状态值                 │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

📊 2.2 数据模型设计

为了更好地管理视频播放的状态和配置,我们设计了一套数据模型:

/// 视频源类型
enum VideoSourceType {
  network,   // 网络视频
  file,      // 本地文件
  asset,     // Asset 资源
}

/// 视频播放状态
enum VideoPlayState {
  idle,       // 空闲
  loading,    // 加载中
  ready,      // 准备就绪
  playing,    // 播放中
  paused,     // 已暂停
  buffering,  // 缓冲中
  error,      // 错误
  completed,  // 播放完成
}

/// 视频信息模型
class VideoInfo {
  /// 视频标题
  final String title;
  
  /// 视频描述
  final String? description;
  
  /// 视频源地址
  final String source;
  
  /// 视频源类型
  final VideoSourceType sourceType;
  
  /// 视频封面
  final String? thumbnail;
  
  /// 视频时长
  final Duration? duration;

  const VideoInfo({
    required this.title,
    this.description,
    required this.source,
    required this.sourceType,
    this.thumbnail,
    this.duration,
  });
}

/// 播放进度信息
class PlaybackProgress {
  /// 当前播放位置
  final Duration position;
  
  /// 视频总时长
  final Duration duration;
  
  /// 缓冲进度
  final Duration buffered;
  
  /// 播放速度
  final double speed;

  const PlaybackProgress({
    required this.position,
    required this.duration,
    required this.buffered,
    this.speed = 1.0,
  });
  
  /// 播放进度百分比 (0.0 - 1.0)
  double get progress {
    if (duration.inMilliseconds == 0) return 0;
    return position.inMilliseconds / duration.inMilliseconds;
  }
  
  /// 缓冲进度百分比 (0.0 - 1.0)
  double get bufferedProgress {
    if (duration.inMilliseconds == 0) return 0;
    return buffered.inMilliseconds / duration.inMilliseconds;
  }
  
  /// 格式化的当前位置
  String get formattedPosition => _formatDuration(position);
  
  /// 格式化的总时长
  String get formattedDuration => _formatDuration(duration);
  
  static String _formatDuration(Duration duration) {
    final hours = duration.inHours;
    final minutes = duration.inMinutes.remainder(60);
    final seconds = duration.inSeconds.remainder(60);
    
    if (hours > 0) {
      return '${hours.toString().padLeft(2, '0')}:'
             '${minutes.toString().padLeft(2, '0')}:'
             '${seconds.toString().padLeft(2, '0')}';
    }
    return '${minutes.toString().padLeft(2, '0')}:'
           '${seconds.toString().padLeft(2, '0')}';
  }
}

📦 三、项目配置与依赖安装

📥 3.1 添加依赖

在 Flutter 项目中使用 video_player,需要在 pubspec.yaml 文件中添加依赖。由于我们要支持 OpenHarmony 平台,需要使用适配版本的仓库。

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

dependencies:
  flutter:
    sdk: flutter
  
  # video_player - 视频播放插件
  # 使用 OpenHarmony 适配版本
  video_player:
    git:
      url: "https://atomgit.com/openharmony-tpc/flutter_packages.git"
      path: "packages/video_player/video_player"

配置说明

  • git 方式引用:因为 OpenHarmony 适配版本需要从指定的 Git 仓库获取
  • url:指向开源鸿蒙 TPC 维护的 flutter_packages 仓库
  • path:指定仓库中 video_player 包的具体路径
  • 本项目基于 video_player@2.7.1 开发,适配 Flutter 3.27.5-ohos-1.0.4

🔧 3.2 权限配置

在 OpenHarmony 平台上播放网络视频需要配置网络权限。

步骤一:配置权限声明

ohos/entry/src/main/module.json5 文件中添加权限声明:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:internet_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}
步骤二:添加字符串资源

ohos/entry/src/main/resources/base/element/string.json 文件中添加权限说明字符串:

{
  "string": [
    {
      "name": "internet_reason",
      "value": "用于播放网络视频和加载视频资源"
    }
  ]
}

📁 3.3 Asset 资源配置(可选)

如果需要使用 Asset 视频资源,需要在 pubspec.yaml 中声明资源文件:

flutter:
  assets:
    - assets/videos/

🛠️ 四、核心服务实现

🎬 4.1 视频播放服务

首先,我们实现一个视频播放服务,封装 video_player 的底层 API:

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

/// 视频播放服务
/// 
/// 该服务封装了 video_player 的底层 API,提供统一的视频播放接口。
/// 所有方法都是实例方法,通过单例模式访问。
class VideoPlayerService {
  /// 单例实例
  static final VideoPlayerService _instance = VideoPlayerService._internal();
  
  /// 获取单例实例
  static VideoPlayerService get instance => _instance;
  
  /// 私有构造函数
  VideoPlayerService._internal();

  /// 当前视频控制器
  VideoPlayerController? _controller;
  
  /// 获取当前控制器
  VideoPlayerController? get controller => _controller;
  
  /// 是否已初始化
  bool get isInitialized => _controller?.value.isInitialized ?? false;
  
  /// 是否正在播放
  bool get isPlaying => _controller?.value.isPlaying ?? false;
  
  /// 是否正在缓冲
  bool get isBuffering => _controller?.value.isBuffering ?? false;
  
  /// 是否循环播放
  bool get isLooping => _controller?.value.isLooping ?? false;
  
  /// 获取视频时长
  Duration get duration => _controller?.value.duration ?? Duration.zero;
  
  /// 获取当前播放位置
  Duration get position => _controller?.value.position ?? Duration.zero;
  
  /// 获取视频宽高比
  double get aspectRatio => _controller?.value.aspectRatio ?? 16 / 9;
  
  /// 获取播放速度
  double get playbackSpeed => _controller?.value.playbackSpeed ?? 1.0;
  
  /// 获取音量
  double get volume => _controller?.value.volume ?? 1.0;

  /// 初始化网络视频
  /// 
  /// [url] 视频地址
  /// [autoPlay] 是否自动播放
  /// [looping] 是否循环播放
  Future<bool> initializeNetworkVideo(
    String url, {
    bool autoPlay = false,
    bool looping = false,
  }) async {
    await _disposeController();
    
    try {
      _controller = VideoPlayerController.networkUrl(Uri.parse(url));
      
      await _controller!.initialize();
      
      if (looping) {
        _controller!.setLooping(true);
      }
      
      if (autoPlay) {
        _controller!.play();
      }
      
      return true;
    } catch (e) {
      debugPrint('初始化网络视频失败: $e');
      return false;
    }
  }

  /// 初始化本地文件视频
  /// 
  /// [file] 视频文件
  /// [autoPlay] 是否自动播放
  /// [looping] 是否循环播放
  Future<bool> initializeFileVideo(
    File file, {
    bool autoPlay = false,
    bool looping = false,
  }) async {
    await _disposeController();
    
    try {
      _controller = VideoPlayerController.file(file);
      
      await _controller!.initialize();
      
      if (looping) {
        _controller!.setLooping(true);
      }
      
      if (autoPlay) {
        _controller!.play();
      }
      
      return true;
    } catch (e) {
      debugPrint('初始化本地视频失败: $e');
      return false;
    }
  }

  /// 初始化 Asset 视频
  /// 
  /// [assetPath] Asset 资源路径
  /// [autoPlay] 是否自动播放
  /// [looping] 是否循环播放
  Future<bool> initializeAssetVideo(
    String assetPath, {
    bool autoPlay = false,
    bool looping = false,
  }) async {
    await _disposeController();
    
    try {
      _controller = VideoPlayerController.asset(assetPath);
      
      await _controller!.initialize();
      
      if (looping) {
        _controller!.setLooping(true);
      }
      
      if (autoPlay) {
        _controller!.play();
      }
      
      return true;
    } catch (e) {
      debugPrint('初始化 Asset 视频失败: $e');
      return false;
    }
  }

  /// 播放
  void play() {
    _controller?.play();
  }

  /// 暂停
  void pause() {
    _controller?.pause();
  }

  /// 切换播放/暂停
  void togglePlayPause() {
    if (isPlaying) {
      pause();
    } else {
      play();
    }
  }

  /// 跳转到指定位置
  /// 
  /// [position] 目标位置
  void seekTo(Duration position) {
    _controller?.seekTo(position);
  }

  /// 跳转到进度位置
  /// 
  /// [progress] 进度值 (0.0 - 1.0)
  void seekToProgress(double progress) {
    final targetPosition = Duration(
      milliseconds: (duration.inMilliseconds * progress).round(),
    );
    seekTo(targetPosition);
  }

  /// 快进
  /// 
  /// [seconds] 快进秒数
  void fastForward(int seconds) {
    final newPosition = position + Duration(seconds: seconds);
    if (newPosition < duration) {
      seekTo(newPosition);
    } else {
      seekTo(duration);
    }
  }

  /// 快退
  /// 
  /// [seconds] 快退秒数
  void rewind(int seconds) {
    final newPosition = position - Duration(seconds: seconds);
    if (newPosition > Duration.zero) {
      seekTo(newPosition);
    } else {
      seekTo(Duration.zero);
    }
  }

  /// 设置播放速度
  /// 
  /// [speed] 播放速度 (0.25, 0.5, 1.0, 1.5, 2.0 等)
  void setPlaybackSpeed(double speed) {
    _controller?.setPlaybackSpeed(speed);
  }

  /// 设置循环播放
  /// 
  /// [looping] 是否循环
  void setLooping(bool looping) {
    _controller?.setLooping(looping);
  }

  /// 设置音量
  /// 
  /// [volume] 音量 (0.0 - 1.0)
  void setVolume(double volume) {
    _controller?.setVolume(volume.clamp(0.0, 1.0));
  }

  /// 添加状态监听器
  /// 
  /// [listener] 监听回调
  void addListener(VoidCallback listener) {
    _controller?.addListener(listener);
  }

  /// 移除状态监听器
  /// 
  /// [listener] 监听回调
  void removeListener(VoidCallback listener) {
    _controller?.removeListener(listener);
  }

  /// 获取播放进度信息
  PlaybackProgress getProgress() {
    return PlaybackProgress(
      position: position,
      duration: duration,
      buffered: Duration.zero,
      speed: playbackSpeed,
    );
  }

  /// 释放控制器
  Future<void> _disposeController() async {
    await _controller?.dispose();
    _controller = null;
  }

  /// 释放资源
  Future<void> dispose() async {
    await _disposeController();
  }
}

📋 4.2 播放进度信息类

/// 播放进度信息
class PlaybackProgress {
  final Duration position;
  final Duration duration;
  final Duration buffered;
  final double speed;

  const PlaybackProgress({
    required this.position,
    required this.duration,
    required this.buffered,
    this.speed = 1.0,
  });

  double get progress {
    if (duration.inMilliseconds == 0) return 0;
    return position.inMilliseconds / duration.inMilliseconds;
  }

  double get bufferedProgress {
    if (duration.inMilliseconds == 0) return 0;
    return buffered.inMilliseconds / duration.inMilliseconds;
  }

  String get formattedPosition => _formatDuration(position);
  String get formattedDuration => _formatDuration(duration);

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

📝 五、完整示例代码

下面是一个完整的专业级视频播放系统示例:

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

// ============ 数据模型 ============

enum VideoSourceType { network, file, asset }

enum VideoPlayState { idle, loading, ready, playing, paused, buffering, error, completed }

class VideoInfo {
  final String title;
  final String? description;
  final String source;
  final VideoSourceType sourceType;
  final String? thumbnail;

  const VideoInfo({
    required this.title,
    this.description,
    required this.source,
    required this.sourceType,
    this.thumbnail,
  });
}

class PlaybackProgress {
  final Duration position;
  final Duration duration;
  final double speed;

  const PlaybackProgress({
    required this.position,
    required this.duration,
    this.speed = 1.0,
  });

  double get progress {
    if (duration.inMilliseconds == 0) return 0;
    return position.inMilliseconds / duration.inMilliseconds;
  }

  String get formattedPosition => _formatDuration(position);
  String get formattedDuration => _formatDuration(duration);

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

// ============ 服务类 ============

class VideoPlayerService {
  static final VideoPlayerService _instance = VideoPlayerService._internal();
  static VideoPlayerService get instance => _instance;
  VideoPlayerService._internal();

  VideoPlayerController? _controller;
  VideoPlayerController? get controller => _controller;

  bool get isInitialized => _controller?.value.isInitialized ?? false;
  bool get isPlaying => _controller?.value.isPlaying ?? false;
  bool get isBuffering => _controller?.value.isBuffering ?? false;
  Duration get duration => _controller?.value.duration ?? Duration.zero;
  Duration get position => _controller?.value.position ?? Duration.zero;
  double get aspectRatio => _controller?.value.aspectRatio ?? 16 / 9;
  double get playbackSpeed => _controller?.value.playbackSpeed ?? 1.0;

  Future<bool> initializeNetworkVideo(
    String url, {
    bool autoPlay = false,
    bool looping = false,
  }) async {
    await _disposeController();
    
    try {
      _controller = VideoPlayerController.networkUrl(Uri.parse(url));
      await _controller!.initialize();
      
      if (looping) _controller!.setLooping(true);
      if (autoPlay) _controller!.play();
      
      return true;
    } catch (e) {
      debugPrint('初始化网络视频失败: $e');
      return false;
    }
  }

  Future<bool> initializeFileVideo(
    File file, {
    bool autoPlay = false,
    bool looping = false,
  }) async {
    await _disposeController();
    
    try {
      _controller = VideoPlayerController.file(file);
      await _controller!.initialize();
      
      if (looping) _controller!.setLooping(true);
      if (autoPlay) _controller!.play();
      
      return true;
    } catch (e) {
      debugPrint('初始化本地视频失败: $e');
      return false;
    }
  }

  Future<bool> initializeAssetVideo(
    String assetPath, {
    bool autoPlay = false,
    bool looping = false,
  }) async {
    await _disposeController();
    
    try {
      _controller = VideoPlayerController.asset(assetPath);
      await _controller!.initialize();
      
      if (looping) _controller!.setLooping(true);
      if (autoPlay) _controller!.play();
      
      return true;
    } catch (e) {
      debugPrint('初始化 Asset 视频失败: $e');
      return false;
    }
  }

  void play() => _controller?.play();
  void pause() => _controller?.pause();
  
  void togglePlayPause() {
    if (isPlaying) {
      pause();
    } else {
      play();
    }
  }

  void seekTo(Duration position) => _controller?.seekTo(position);
  
  void seekToProgress(double progress) {
    final targetPosition = Duration(
      milliseconds: (duration.inMilliseconds * progress).round(),
    );
    seekTo(targetPosition);
  }

  void fastForward(int seconds) {
    final newPosition = position + Duration(seconds: seconds);
    seekTo(newPosition < duration ? newPosition : duration);
  }

  void rewind(int seconds) {
    final newPosition = position - Duration(seconds: seconds);
    seekTo(newPosition > Duration.zero ? newPosition : Duration.zero);
  }

  void setPlaybackSpeed(double speed) => _controller?.setPlaybackSpeed(speed);
  void setLooping(bool looping) => _controller?.setLooping(looping);
  void setVolume(double volume) => _controller?.setVolume(volume.clamp(0.0, 1.0));
  
  void addListener(VoidCallback listener) => _controller?.addListener(listener);
  void removeListener(VoidCallback listener) => _controller?.removeListener(listener);

  PlaybackProgress getProgress() {
    return PlaybackProgress(
      position: position,
      duration: duration,
      speed: playbackSpeed,
    );
  }

  Future<void> _disposeController() async {
    await _controller?.dispose();
    _controller = null;
  }

  Future<void> dispose() async {
    await _disposeController();
  }
}

// ============ 应用入口 ============

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '视频播放系统',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
        useMaterial3: true,
      ),
      home: const VideoListPage(),
    );
  }
}

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

  static final List<VideoInfo> _videos = [
    VideoInfo(
      title: '示例视频 1',
      description: '这是一个示例网络视频',
      source: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
      sourceType: VideoSourceType.network,
    ),
    VideoInfo(
      title: '示例视频 2',
      description: '这是另一个示例网络视频',
      source: 'https://media.w3.org/2010/05/bunny/trailer.mp4',
      sourceType: VideoSourceType.network,
    ),
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('视频播放系统'),
        centerTitle: true,
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: _videos.length,
        itemBuilder: (context, index) {
          final video = _videos[index];
          return Card(
            margin: const EdgeInsets.only(bottom: 12),
            child: ListTile(
              leading: Container(
                width: 80,
                height: 60,
                decoration: BoxDecoration(
                  color: Colors.grey.shade300,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: const Icon(Icons.play_circle_outline, size: 32),
              ),
              title: Text(
                video.title,
                style: const TextStyle(fontWeight: FontWeight.w600),
              ),
              subtitle: Text(
                video.description ?? '',
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
              trailing: const Icon(Icons.arrow_forward_ios, size: 16),
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => VideoPlayerPage(video: video),
                  ),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

class VideoPlayerPage extends StatefulWidget {
  final VideoInfo video;

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

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

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  final VideoPlayerService _service = VideoPlayerService.instance;
  
  VideoPlayState _playState = VideoPlayState.idle;
  bool _showControls = true;
  String? _errorMessage;
  
  final List<double> _speedOptions = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
  int _currentSpeedIndex = 2;

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

  Future<void> _initializeVideo() async {
    setState(() => _playState = VideoPlayState.loading);

    bool success = false;
    
    switch (widget.video.sourceType) {
      case VideoSourceType.network:
        success = await _service.initializeNetworkVideo(widget.video.source);
        break;
      case VideoSourceType.file:
        success = await _service.initializeFileVideo(File(widget.video.source));
        break;
      case VideoSourceType.asset:
        success = await _service.initializeAssetVideo(widget.video.source);
        break;
    }

    if (!mounted) return;

    if (success) {
      setState(() => _playState = VideoPlayState.ready);
      _service.addListener(_onVideoStateChanged);
    } else {
      setState(() {
        _playState = VideoPlayState.error;
        _errorMessage = '视频加载失败';
      });
    }
  }

  void _onVideoStateChanged() {
    if (!mounted) return;
    
    final controller = _service.controller;
    if (controller == null) return;

    if (controller.value.isBuffering) {
      setState(() => _playState = VideoPlayState.buffering);
    } else if (controller.value.isPlaying) {
      setState(() => _playState = VideoPlayState.playing);
    } else if (controller.value.position >= controller.value.duration) {
      setState(() => _playState = VideoPlayState.completed);
    } else {
      setState(() => _playState = VideoPlayState.paused);
    }
  }

  
  void dispose() {
    _service.removeListener(_onVideoStateChanged);
    _service.pause();
    super.dispose();
  }

  void _toggleControls() {
    setState(() => _showControls = !_showControls);
  }

  void _togglePlayPause() {
    _service.togglePlayPause();
  }

  void _seekTo(double progress) {
    _service.seekToProgress(progress);
  }

  void _changeSpeed() {
    setState(() {
      _currentSpeedIndex = (_currentSpeedIndex + 1) % _speedOptions.length;
      _service.setPlaybackSpeed(_speedOptions[_currentSpeedIndex]);
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        title: Text(widget.video.title),
        backgroundColor: Colors.black,
        foregroundColor: Colors.white,
      ),
      body: Column(
        children: [
          Expanded(
            child: _buildVideoPlayer(),
          ),
          if (_showControls && _service.isInitialized) _buildControlPanel(),
        ],
      ),
    );
  }

  Widget _buildVideoPlayer() {
    if (_playState == VideoPlayState.loading) {
      return const Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            CircularProgressIndicator(color: Colors.white),
            SizedBox(height: 16),
            Text('加载中...', style: TextStyle(color: Colors.white)),
          ],
        ),
      );
    }

    if (_playState == VideoPlayState.error) {
      return Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Icon(Icons.error_outline, size: 64, color: Colors.red),
            const SizedBox(height: 16),
            Text(
              _errorMessage ?? '加载失败',
              style: const TextStyle(color: Colors.white),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _initializeVideo,
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    if (!_service.isInitialized) {
      return const Center(
        child: Text('视频未初始化', style: TextStyle(color: Colors.white)),
      );
    }

    return GestureDetector(
      onTap: _toggleControls,
      child: Stack(
        alignment: Alignment.center,
        children: [
          Center(
            child: AspectRatio(
              aspectRatio: _service.aspectRatio,
              child: VideoPlayer(_service.controller!),
            ),
          ),
          
          if (_showControls) _buildOverlayControls(),
          
          if (_playState == VideoPlayState.buffering)
            const CircularProgressIndicator(color: Colors.white),
        ],
      ),
    );
  }

  Widget _buildOverlayControls() {
    return Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            Colors.black.withOpacity(0.3),
            Colors.transparent,
            Colors.black.withOpacity(0.3),
          ],
        ),
      ),
      child: Center(
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            IconButton(
              iconSize: 40,
              icon: const Icon(Icons.replay_10, color: Colors.white),
              onPressed: () => _service.rewind(10),
            ),
            const SizedBox(width: 32),
            GestureDetector(
              onTap: _togglePlayPause,
              child: Container(
                width: 72,
                height: 72,
                decoration: BoxDecoration(
                  color: Colors.black.withOpacity(0.5),
                  shape: BoxShape.circle,
                ),
                child: Icon(
                  _service.isPlaying ? Icons.pause : Icons.play_arrow,
                  size: 48,
                  color: Colors.white,
                ),
              ),
            ),
            const SizedBox(width: 32),
            IconButton(
              iconSize: 40,
              icon: const Icon(Icons.forward_10, color: Colors.white),
              onPressed: () => _service.fastForward(10),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildControlPanel() {
    final progress = _service.getProgress();
    
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.grey.shade900,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.3),
            blurRadius: 8,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Row(
            children: [
              Text(
                progress.formattedPosition,
                style: const TextStyle(color: Colors.white, fontSize: 12),
              ),
              Expanded(
                child: Slider(
                  value: progress.progress.clamp(0.0, 1.0),
                  onChanged: _seekTo,
                  activeColor: Colors.red,
                  inactiveColor: Colors.grey.shade700,
                ),
              ),
              Text(
                progress.formattedDuration,
                style: const TextStyle(color: Colors.white, fontSize: 12),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              IconButton(
                icon: const Icon(Icons.replay_10, color: Colors.white),
                onPressed: () => _service.rewind(10),
              ),
              IconButton(
                icon: Icon(
                  _service.isPlaying ? Icons.pause : Icons.play_arrow,
                  color: Colors.white,
                  size: 32,
                ),
                onPressed: _togglePlayPause,
              ),
              IconButton(
                icon: const Icon(Icons.forward_10, color: Colors.white),
                onPressed: () => _service.fastForward(10),
              ),
              TextButton(
                onPressed: _changeSpeed,
                child: Text(
                  '${_speedOptions[_currentSpeedIndex]}x',
                  style: const TextStyle(color: Colors.white),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

🏆 六、最佳实践与注意事项

⚠️ 6.1 资源管理最佳实践

及时释放资源:视频控制器占用大量系统资源,务必在组件销毁时调用 dispose() 方法释放资源。

避免重复初始化:在初始化新视频之前,先释放之前的控制器,避免内存泄漏。

使用单例模式:对于全局视频播放服务,使用单例模式确保只有一个控制器实例。

🔐 6.2 网络视频注意事项

HTTPS 支持:建议使用 HTTPS 协议的视频源,确保传输安全。

错误处理:网络视频可能因网络问题加载失败,需要提供重试机制。

缓存策略:对于频繁播放的视频,考虑实现本地缓存机制。

📱 6.3 OpenHarmony 平台特殊说明

播放器后端:OpenHarmony 使用 AVPlayer 作为底层播放器。

格式支持:支持 MP4、MKV、WebM 等常见视频格式。

权限配置:播放网络视频需要配置 INTERNET 权限。


📌 七、总结

本文通过一个完整的专业级视频播放系统案例,深入讲解了 video_player 第三方库的使用方法与最佳实践:

架构设计:采用分层架构(UI层 → 服务层 → 基础设施层),让代码更清晰,便于维护和测试。

服务封装:统一封装视频播放逻辑,提供语义化的方法名,让调用代码更易读。

状态管理:完善的播放状态管理机制,支持多种播放状态和状态转换。

自定义界面:灵活的控制面板设计,支持自定义播放器 UI。

掌握这些技巧,你就能构建出专业级的视频播放功能,为用户提供流畅、可靠的视频观看体验。


参考资料

Logo

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

更多推荐