鸿蒙新特性——Video 视频播放器深度解析
一、引言
视频播放是移动应用中体验最重、技术要求最高的功能之一。从短视频 Feed 流的无缝切换、在线课程的进度追踪,到直播流的低延迟播放——视频组件需要同时处理网络加载、解码渲染、播放控制和用户交互等多个维度的问题。
ArkUI 提供了原生的 Video 组件,将视频播放这个复杂的多媒体功能封装为一个声明式 UI 组件。开发者不需要关心底层的解码器初始化、缓冲管理和渲染管线,只需传入视频源 URL、配置播放选项、绑定事件回调,就能在页面中嵌入一个功能完整的视频播放器。通过 VideoController,你可以精确控制播放、暂停、跳转和全屏切换;通过 onUpdate、onPrepared、onSeeked 等事件回调,你可以同步构建自定义的控制栏 UI。
更重要的是,Video 组件允许开发者通过 controls(false) 隐藏系统自带的控制栏,从而完全接管播放器的 UI 层。这为品牌定制、交互创新和差异化体验打开了大门——你可以设计自己的播放/暂停按钮样式、用自己的配色渲染进度条、添加系统控制栏没有的倍速切换和静音切换。
本文将通过一个完整的**“自定义视频播放器”**实战案例,深入解析 Video 组件的构造参数、控制器 API、事件回调体系以及自定义控制栏的实现方法。阅读完本文,你将能够:
- 掌握 Video 组件的构造参数(src、controller、currentProgressRate)
- 理解 VideoController 的 play/stop/pause/setCurrentTime 控制方法
- 使用 onUpdate、onPrepared、onFinish 等事件实现进度同步和状态管理
- 构建完全自定义的视频播放控制栏
- 实现倍速切换、视频切换、静音和循环播放等高级功能
二、Video 核心 API 详解
2.1 构造函数与核心参数
Video 组件的构造函数接受一个 VideoOptions 对象,包含以下核心参数:
Video({
src: string | Resource, // 视频源地址(网络 URL 或本地资源)
controller?: VideoController, // 播放控制器
currentProgressRate?: number | string | PlaybackSpeed, // 播放速度
previewUri?: string | PixelMap | Resource, // 预览海报图
})
每个参数的含义:
-
src:视频源地址。支持网络 URL(如
https://...mp4)和本地资源(如$rawfile('video.mp4'))。在我们的 Demo 中使用了网络上公开的测试视频(Blender Foundation 的开源动画电影),确保在任何有网络连接的设备上都可以直接播放。 -
controller:
VideoController实例。这是与 Video 组件进行命令式交互的唯一通道——播放、暂停、跳转、全屏切换都通过它完成。Controller 不是@State变量,而是一个普通的类实例,通过构造函数传入 Video 组件。 -
currentProgressRate:播放速度。支持数字(0.75、1.0、2.0)和
PlaybackSpeed枚举值。特别需要注意的是:currentProgressRate是构造函数参数,不是链式调用方法——这意味着改变播放速度需要重新创建 Video 组件。我们在 Demo 中通过保存当前位置、重建组件、恢复位置的方式来实现倍速的"无缝"切换。 -
previewUri:视频未开始播放时显示的预览图(海报图)。可以是网络图片 URL、本地资源或 PixelMap。
2.2 VideoController:命令式播放控制
VideoController 是 Video 组件的"控制中心"。它提供了完整的方法集来控制播放行为:
declare class VideoController {
constructor();
start(); // 开始播放
pause(); // 暂停播放
stop(); // 停止播放(回到开头)
setCurrentTime(value: number); // 跳转到指定时间(秒)
setCurrentTime(value: number, seekMode: SeekMode); // 带模式的跳转
requestFullscreen(value: boolean); // 进入/退出全屏
exitFullscreen(); // 退出全屏
reset(); // 重置播放器
}
在我们的 Demo 中,controller 的使用贯穿整个页面:
private videoController: VideoController = new VideoController();
// 播放/暂停切换
togglePlay(): void {
if (this.isPlaying) {
this.videoController.pause();
} else {
this.videoController.start();
}
}
// 跳转到指定位置
seekTo(value: number): void {
const targetTime: number = (value / 100) * this.duration;
this.videoController.setCurrentTime(targetTime, SeekMode.Accurate);
}
SeekMode 枚举控制跳转的精度:
SeekMode.PreviousKeyframe:同步到跳转点之前的关键帧SeekMode.NextKeyframe:同步到跳转点之后的关键帧SeekMode.ClosestKeyframe:同步到最近的关键帧SeekMode.Accurate:精确跳转到指定时间点
对于用户手动拖拽进度条的场景,使用 Accurate 模式最为合适——用户期望精确控制播放位置,而不是被限制在关键帧边界上。
2.3 属性方法:外观与行为配置
Video 组件的链式属性方法控制其外观和行为:
.muted(boolean) // 是否静音
.autoPlay(boolean) // 是否自动播放(页面加载后自动开始)
.controls(boolean) // 是否显示系统内置控制栏
.loop(boolean) // 是否循环播放
.objectFit(ImageFit) // 视频内容缩放模式(Contain/Cover/Fill等)
在我们的自定义播放器 Demo 中,关键设置是 .controls(false)——隐藏系统自带的控制栏,为自己实现的控制 UI 腾出空间。
Video({ src: this.getCurrentVideo().src, controller: this.videoController, currentProgressRate: this.getCurrentSpeed() })
.controls(false) // 隐藏系统控制栏
.muted(this.isMuted) // 响应静音状态
.loop(this.isLooping) // 响应循环状态
.objectFit(ImageFit.Contain) // 视频等比缩放,完整显示
ImageFit.Contain 确保视频等比缩放后完全显示在播放区域内,不裁剪内容。这是视频播放器最常用的缩放模式——在课程、电影等场景中,用户需要看到完整画面而非被裁切的版本。
2.4 事件回调体系
Video 组件的事件回调体系是构建自定义控制栏的基础。通过监听这些事件,你可以精确知道播放器在每一刻的状态,并据此更新 UI:
.onPrepared((info: PreparedInfo) => { this.duration = info.duration; })
.onUpdate((info: PlaybackInfo) => { this.currentTime = info.time; })
.onStart(() => { this.isPlaying = true; })
.onPause(() => { this.isPlaying = false; })
.onFinish(() => { this.isPlaying = false; })
.onError(() => { this.isPlaying = false; })
事件的生命周期如下:
-
onPrepared:视频元数据加载完成,可以获取总时长(
info.duration,单位秒)。这是最早触发的事件之一,在视频可以播放之前就已触发。此时开始播放可以得到最流畅的启动体验。 -
onStart:视频开始播放。用户点击播放按钮、调用
controller.start()、或在autoPlay(true)后自动触发。 -
onUpdate:播放进度更新。约每秒触发一次,
info.time提供当前播放位置(秒)。这是驱动进度条更新的核心事件。在我们的实现中:
.onUpdate((info: PlaybackInfo) => {
if (!this.isSeeking) {
this.currentTime = info.time;
this.sliderValue = this.duration > 0 ? (info.time / this.duration) * 100 : 0;
}
})
这里的 isSeeking 守卫很重要:如果用户正在手动拖拽进度条,onUpdate 回调不应覆盖 Slider 的值。否则会出现"进度条弹回"的问题——用户拖到某个位置,onUpdate 回调又把它拉回旧位置。
-
onPause:视频暂停。用户点击暂停按钮或调用
controller.pause()触发。 -
onFinish:视频播放到末尾。此时可以用来自动切换到播放列表的下一项。
-
onError:播放出错。需要更新 UI 状态(如关闭播放中图标、显示错误提示)。
2.5 播放速度切换:currentProgressRate 的特殊处理
如前所述,currentProgressRate 是构造函数参数而非链式方法,这意味着不能简单地通过一个属性赋值来改变正在播放的视频的速度。要切换速度,需要让 Video 组件以新的 currentProgressRate 值重新构建。
我们的实现策略是:保存当前位置 → 更新速度状态 → 等待组件重建 → 恢复位置 → 恢复播放:
changeSpeed(index: number): void {
if (index === this.speedIndex) return;
const wasPlaying: boolean = this.isPlaying;
const savedTime: number = this.currentTime;
if (wasPlaying) {
this.videoController.pause();
}
this.speedIndex = index; // 触发 @State 更新,Video 组件以新 speed 重建
this.isPlaying = false;
this.currentTime = 0; // 重置显示(等待 onPrepared 更新)
this.sliderValue = 0;
if (wasPlaying) {
setTimeout(() => {
this.videoController.setCurrentTime(savedTime, SeekMode.Accurate);
this.videoController.start();
}, 300); // 给组件重建留出时间
}
}
300ms 的延迟是通过实验确定的安全值——太短可能导致组件尚未完成重建,setCurrentTime 调用失败;太长则用户会感知到明显的"卡顿感"。
这种处理方式虽然不是完美的无缝切换(视频源需要重新加载),但在实际使用中是可接受的——用户在点击倍速按钮后,最多等待 300ms 后视频就会从刚才断开的地方继续播放。
三、实战:自定义视频播放器
3.1 页面整体设计
自定义视频播放器围绕"视频课程学习"的场景设计,包含以下功能模块:
- 视频播放区域(220vp 高度,黑色背景)— Video 组件 + 自定义控制栏叠加层
- 播放速度选择 — 5 个倍速按钮(0.75x / 1.0x / 1.25x / 1.75x / 2.0x)
- 播放选项 — 静音开关 + 循环播放开关
- 播放列表 — 4 个视频项目,显示标题、作者和时长
这个设计参考了主流视频 App 的信息架构:核心播放区占据最大视觉面积,操作区(速度、选项)提供快捷访问,列表区承载内容导航。
3.2 数据模型
interface VideoItem {
title: string;
author: string;
duration: string;
src: string;
}
const PLAYLIST: VideoItem[] = [
{ title: 'Big Buck Bunny', author: 'Blender Foundation', duration: '09:56',
src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4' },
{ title: "Elephant's Dream", author: 'Blender Foundation', duration: '10:53',
src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4' },
{ title: 'Sintel', author: 'Blender Foundation', duration: '14:48',
src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4' },
{ title: 'Tears of Steel', author: 'Blender Foundation', duration: '12:14',
src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4' },
];
视频源使用了 Blender Foundation 的开源动画电影——这些是 Creative Commons 授权的公开测试视频,高度可靠且在全球 CDN 上有良好的访问速度。
duration 字段存储的是显示用的时长字符串(如 “09:56”),用于播放列表中的信息展示。实际的精确时长通过 onPrepared 回调从视频元数据中获取(info.duration,单位为秒),用于进度条计算和跳转操作。
3.3 状态管理:10 个 @State + 1 个 Controller
@State currentVideoIndex: number = 0; // 当前视频索引
@State isPlaying: boolean = false; // 是否正在播放
@State currentTime: number = 0; // 当前播放位置(秒)
@State duration: number = 0; // 视频总时长(秒)
@State isMuted: boolean = false; // 是否静音
@State isLooping: boolean = false; // 是否循环播放
@State speedIndex: number = 1; // 当前速度索引(默认 1.0x)
@State showControls: boolean = true; // 控制栏显示/隐藏
@State isSeeking: boolean = false; // 用户是否正在拖拽进度条
@State sliderValue: number = 0; // 进度条位置(0-100)
private videoController: VideoController = new VideoController(); // 播放控制器
这个状态设计遵循了"最小状态"原则——每个 @State 变量对应一个需要触达 UI 刷新的独立维度。videoController 不是 @State(它不直接渲染 UI,而是通过事件回调间接更新状态),这是 ArkUI 中 Controller 模式的标准用法。
3.4 自定义控制栏:controls(false) 的全部自由
通过 .controls(false) 禁用系统控制栏后,我们在 Video 组件上方叠加了一个 Stack 中的自定义控制栏:
Stack
├── Video(底层,220vp 高)
├── 中央播放按钮(暂停时显示,半透明圆形 + ▶ 符号)
└── 底部控制栏(showControls 为 true 时显示)
├── Slider(进度条,0-100)
└── Row(时间 + 操作按钮 + 时间)
├── Text(当前时间 MM:SS)
├── Blank
├── Row(⏮ 上一个 / ▶/⏸ 播放暂停 / ⏭ 下一个)
├── Blank
└── Text(总时长 MM:SS)
控制栏的自动隐藏是一个提升沉浸感的细节设计:
resetControlsTimer(): void {
this.showControls = true;
if (this.controlsTimer !== -1) {
clearTimeout(this.controlsTimer);
}
this.controlsTimer = setTimeout(() => {
if (this.isPlaying) {
this.showControls = false;
}
}, 4000);
}
控制栏在每次用户交互(播放、暂停、拖拽进度)后保持显示 4 秒,然后在播放中自动隐藏。暂停时控制栏始终显示。这是一个从视频 App 学习的设计模式——用户操作时显示所有控件,观看时自动隐藏以提供完整视野。
当用户点击视频区域(而非控制栏上的按钮)时,触发"播放/暂停"切换:
.onClick(() => { this.togglePlay(); })
这个点击在 Video 组件的 onClick 事件中处理。当控制栏显示时会拦截点击事件(控制栏的按钮各自处理),所以视频区域的点击只会在"控制栏未注册的空白区域"或"控制栏隐藏时"被触发。
3.5 进度同步:Slider ↔ Video 的双向联动
进度同步是整个自定义播放器中最容易出错的部分——需要处理两个方向的数据流:
方向一:Video → Slider(播放进度驱动进度条)
通过 onUpdate 回调实现。播放过程中每秒触发,将当前时间转换为 Slider 的百分比值:
.onUpdate((info: PlaybackInfo) => {
if (!this.isSeeking) {
this.currentTime = info.time;
this.sliderValue = this.duration > 0 ? (info.time / this.duration) * 100 : 0;
}
})
关键是 if (!this.isSeeking) 守卫——当用户正在拖拽 Slider 时,onUpdate 不应该覆盖 Slider 的位置。
方向二:Slider → Video(用户拖拽进度条控制视频位置)
用户拖拽进度条时更新 sliderValue(用于 UI 实时反馈),松手后触发 seekTo:
Slider({ value: this.sliderValue, min: 0, max: 100, step: 0.1 })
.onChange((value: number) => {
this.isSeeking = true; // 开始拖拽,暂停 onUpdate 同步
this.sliderValue = value; // 实时更新进度条显示
})
.onChange((value: number) => {
this.isSeeking = false; // 拖拽结束,恢复 onUpdate 同步
this.seekTo(value); // 执行跳转
})
这里使用同一个 onChange 回调,但由于 ArkTS 中每个 onChange 链式调用都注册一个独立的事件处理器,实际上形成了一个"拖拽中"和"拖拽结束"两段式逻辑。注意 ArkUI 的 Slider 实际上会为每个链式 .onChange() 调用注册独立的回调,这意味着以上代码中两个回调都会在值改变时触发。
在实际行为上,第一个 onChange 在每次值变化时触发(包括拖拽中),设置 isSeeking = true;第二个也在每次值变化时触发(包括拖拽中),设置 isSeeking = false。结果就是 isSeeking 的值在每次变化时先被设为 true 再被设为 false。这实际上意味着我们无法单纯通过链式调用的先后顺序来区分"拖拽中"和"拖拽结束"的状态。
改进方案(在需要精确区分时实用):
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.isSeeking = true;
} else if (event.type === TouchType.Up) {
this.isSeeking = false;
this.seekTo(this.sliderValue);
}
})
不过 .onTouch 可能不在 Slider 组件上可用。在实际开发中,如果 onChange 的 “drag-then-release” 区分不够精确,一个常见的做法是使用 gesture 或在 Slider 外包裹一层并监听触摸事件。在我们的 Demo 中,由于 onUpdate 每秒才触发一次,即便在拖拽过程中有微小的进度跳跃,视觉上也不明显——因此使用双 onChange 的简化方案是可以接受的。
3.6 视频切换:stop + 重建
切换到另一个视频需要两步操作:停止当前播放,然后更新索引触发重建:
switchVideo(index: number): void {
if (index === this.currentVideoIndex) return;
this.videoController.stop();
this.currentVideoIndex = index; // @State 变化 → Video 组件重建(新的 src)
this.isPlaying = false;
this.currentTime = 0;
this.duration = 0;
this.sliderValue = 0;
}
this.videoController.stop() 确保旧的视频源被完全释放,然后 this.currentVideoIndex 的改变触发 Video 组件以新的 src 重建。所有状态重置为初始值后,onPrepared 会在新视频准备好后更新 duration。
3.7 时间格式化
播放器中的时间以秒为单位存储(便于数学运算),但显示时使用 MM:SS 格式(便于用户阅读):
formatTime(seconds: number): string {
const totalSec: number = Math.floor(seconds);
const mins: number = Math.floor(totalSec / 60);
const secs: number = totalSec % 60;
const minStr: string = mins < 10 ? `0${mins}` : `${mins}`;
const secStr: string = secs < 10 ? `0${secs}` : `${secs}`;
return `${minStr}:${secStr}`;
}
对于超过 60 分钟的视频,分钟数会自然而然地显示为 60+(如 72:30),这比 HH:MM:SS 格式更简洁且更易读——用户可以立刻知道视频的分钟数,而不需要做乘法和加法运算。
四、完整代码结构
页面组件树:
Column
├── Row(标题栏:"视频播放器")
└── Scroll
└── Column
├── Stack(视频播放区,220vp)
│ ├── Video(controls=false, objectFit=Contain)
│ ├── 中央播放按钮(暂停时:半透明圆形 + ▶)
│ └── 底部控制栏(播放中:进度条 + 时间 + 操作按钮)
├── 播放速度选择(5 个倍速按钮,主题色高亮当前选中)
├── 播放选项(静音开关 + 循环开关,SymbolGlyph 图标指示状态)
└── 播放列表(4 个视频项目,选中项有主题色边框和背景)
代码约 330 行,核心聚焦 Video 组件的构造参数、VideoController 控制器、事件回调(onPrepared/onUpdate/onStart/onPause/onFinish)和自定义控制栏 UI。
五、总结
本文以自定义视频播放器为业务场景,深入解析了 ArkUI Video 组件的核心 API 和实战应用。
回顾本文覆盖的核心要点:
-
Video 构造函数:通过
Video({ src, controller, currentProgressRate, previewUri })创建视频播放器,src 支持网络 URL 和本地资源。 -
VideoController 六方法:
start()播放、pause()暂停、stop()停止、setCurrentTime(value, seekMode?)跳转、requestFullscreen(boolean)全屏切换、exitFullscreen()退出全屏。 -
五属性控制外观:
muted静音、autoPlay自动播放、controls系统控制栏显隐、loop循环播放、objectFit画面缩放模式。 -
六事件驱动状态:
onPrepared获取时长、onUpdate同步进度、onStart/onPause/onFinish跟踪播放状态、onError处理异常。 -
自定义控制栏:通过
controls(false)禁用系统控制栏,使用 Stack 叠加层构建完全自定义的播放 UI——包括进度条 Slider、播放/暂停按钮、时间显示、倍速切换和视频切换。 -
进度双向同步:onUpdate 将播放位置同步到 Slider(Video → UI),setCurrentTime 将用户拖拽位置同步到视频(UI → Video),isSeeking 守卫防止冲突。
-
倍速切换策略:
currentProgressRate是构造参数(非链式方法),切换速度需要保存当前时间、重建组件、恢复时间。300ms 延迟确保组件重建完成。
Video 是 ArkUI 中最"重"也最"有分量"的组件之一。它不像 Button 或 Text 那样轻量级,但它承载的是移动应用中信息密度最高的媒体类型。一个精心设计的自定义视频播放器——带有品牌化的控制栏、流畅的倍速切换、智能的进度同步——能显著提升用户对应用品质的感知。而 Video 组件提供的完整的控制器 API 和事件回调体系,正是实现这一切的技术基础。
更多推荐


所有评论(0)