HarmonyOS 5鸿蒙场景技术共建能力:基于AVPlayer实现短视频播放
一、前言:
大家好,我是完美句号!欢迎来到 HarmonyOS 5 开发实战系列。本系列致力于为开发者提供实用的技术方案和即拿即用的代码示例,帮助大家快速掌握 HarmonyOS Next 应用开发中的核心功能。在HarmonyOS 5鸿蒙系统中,基于AVPlayer实现短视频播放是一个重要的功能。本文将详细介绍如何使用AVPlayer来实现这一功能。
AVPlayer作为HarmonyOS平台上的强大音视频播放组件,具备出色的兼容性,能轻松应对多种主流格式。它支持AAC、MP3等音频解码格式,以及264/AVC、H.265/HEVC等视频解码格式,同时也能处理MP4、M4A等封装格式。无论是在线视频还是本地媒体,AVPlayer都能游刃有余地播放。 AVPlayer兼容多种音频和视频格式,适用于多种应用场景。
二、AVPlayer简介:
AVPlayer是HarmonyOS平台的核心音视频播放组件,支持主流音频/视频格式的本地及在线播放,广泛应用于系统级应用与第三方开发场景,基于AVPlayer系统播放器实现,适用于短视频播放类应用的开发,指导开发者实现短视频流畅切换,提炼一套可复制的方案,帮助开发者交付极速、流畅的短视频播放体验。
以下是其核心特性:
-
支持AAC、MP3等音频格式,H.264/AVC、H.265/HEVC等视频编码格式,兼容MP4、M4A等封装格式。典型应用包括图库、华为视频、美团等系统级应用。
-
提供两种开发方式:
- ArkTS:快速构建播放界面,支持状态监听、错误捕获和流程控制一体化;
- C/C++ NDK:适用于高性能播放或自定义渲染场景,可深度整合硬件资源。
- ArkTS:快速构建播放界面,支持状态监听、错误捕获和流程控制一体化;
-
适用场景:
- 适用于本地视频播放、在线流媒体播放、音视频转码等场景,支持通过USB/WiFi传输文件,并可扩展至直播资源访问与多任务处理。 其典型应用场景涵盖图库、华为视频、华为音乐等众多领域。
三、基于AVPlayer实现视频基础播控功能:
如何基于AVPlayer系统播放器实现播放本地视频相关功能,指导开发者实现视频加载、播放、暂停、退出、跳转播放、静音播放、循环播放、窗口缩放模式设置、倍速设置、音量设置、字幕挂载等开发场景。

├──entry/src/main/ets // 代码区
│ ├──common
│ │ ├──constants
│ │ │ └──CommonConstants.ets // 公共常量
│ │ └──utils
│ │ ├──GlobalContext.ets // 公共工具类
│ │ └──TimeUtils.ts // 视频时间帮助类
│ ├──views
│ │ ├──languageDialog.ets // 弹幕语言切换弹窗
│ │ ├──ScaleDialog.ets // 窗口缩放模式设置弹窗
│ │ ├──SetVolumn.ets // 设置音量组件
│ │ ├──SpeedDialog.ets // 播放倍速弹窗
│ │ └──VideoOperate.ets // 视频操作组件
│ ├──controller
│ │ └──AvPlayerController.ets // avplayer公共控制类
│ ├──entryability
│ │ └──EntryAbility.ets // 应用入口Ability
│ ├──model
│ │ └──VideoData.ets // 视频数据类
│ └──pages
│ └──Index.ets // 首页视频界面
└──entry/src/main/resources // 应用资源目录
3.1 具体实现:
- 使用media.createAVPlayer()来获取AVPlayer对象;
- 倍速切换:选择不同倍速时调用avPlayer.setSpeed(speed: PlaybackSpeed);
- 暂停、播放:点击暂停、播放按钮时调用avPlayer.pause()、avPlayer.play();
- 视频跳转:在拖动滑动条时调用avPlayer.seek();
- 静音播放:点击静音按钮时调用avPlayer.setMediaMuted();
- 音量设置:为元素添加手势上下滑动监听PanGesture,滑动时时显示AVVolumePanel组件并根据滑动距离计算音量volume值;
- 窗口缩放模式设置:选择不同的窗口缩放模式时设置avPlayer的videoScaleType属性值;
- 长按倍速:为元素添加手势长按监听LongPressGesture,长按时调用avPlayer.setSpeed(speed: PlaybackSpeed);
- 循环播放:在视频prepared状态下,设置avPlayer的loop属性值为true。
- 字幕挂载:视频初始化时调用avPlayer.addSubtitleFromFd()设置外挂字幕资源。

3.2 音视频播放流程示例(以ArkTS为例):
-
调用createAVPlayer创建AVPlayer实例,并初始化至idle状态。
-
设置业务所需的事件监听,特别关注AVPlayer的状态变化。
-
根据状态变化执行相应逻辑,如切换画面、控制播放等。

import { common } from '@kit.AbilityKit';
import { media } from '@kit.MediaKit';
import { audio } from '@kit.AudioKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError, emitter } from '@kit.BasicServicesKit';
import { CommonConstants, VideoDataType } from '../common/constants/CommonConstants';
import { VideoData } from '../model/VideoData';
const TAG = '[AvPlayerController]';
const CASE_ZERO = 0;
const CASE_ONE = 1;
const CASE_TWO = 2;
const CASE_THREE = 3;
@Observed
export class AvPlayerController {
@Track surfaceID: string = '';
@Track isPlaying: boolean = false;
@Track isReady: boolean = false;
@Track currentTime: number = 0;
@Track currentBufferTime: number = 0;
@Track isLoading: boolean = false;
@Track duration: number = 0;
@Track durationTime: number = 0;
@Track currentCaption: string = '';
private avPlayer?: media.AVPlayer;
private curSource?: VideoData;
private context: common.UIAbilityContext | undefined = AppStorage.get('context');
private seekTime?: number;
private isMuted: boolean | undefined = undefined;
private speedSelect: number = 0;
private windowScaleSelect: number = 0;
private index: number = 0;
// [Start create_instance]
// Create an AVPlayer instance
public async initAVPlayer(source: VideoData, surfaceId: string, avPlayer?: media.AVPlayer) {
if (!this.context) {
hilog.info(CommonConstants.LOG_DOMAIN, TAG, `initPlayer failed context not set`);
return
}
this.curSource = source;
if (source.seekTime) {
this.seekTime = source.seekTime;
}
if (source.isMuted) {
this.isMuted = source.isMuted;
}
if (source.index) {
this.index = source.index;
}
if (!this.curSource) {
return;
}
this.surfaceID = surfaceId;
try {
// Creates the avPlayer instance object.
this.avPlayer = avPlayer ? avPlayer : await media.createAVPlayer()
// Creates a callback function for state machine changes.
this.setAVPlayerCallback();
if (!this.context) {
hilog.info(CommonConstants.LOG_DOMAIN, TAG, `initPlayer failed context not set`);
return
}
switch (this.curSource.type) {
case VideoDataType.RAW_FILE:
let fileDescriptor = await this.context.resourceManager.getRawFd(this.curSource.videoSrc);
this.avPlayer.fdSrc = fileDescriptor;
break;
case VideoDataType.URL:
this.avPlayer.url = this.curSource.videoSrc;
break;
case VideoDataType.RAW_M3U8_FILE:
let m3u8Fd = await this.context.resourceManager.getRawFd(this.curSource.videoSrc);
let fdUrl = 'fd://' + m3u8Fd.fd + '?offset=' + m3u8Fd.offset + '&size=' + m3u8Fd.length;
let mediaSource = media.createMediaSourceWithUrl(fdUrl);
mediaSource.setMimeType(media.AVMimeTypes.APPLICATION_M3U8);
let playbackStrategy: media.PlaybackStrategy = { preferredBufferDuration: 20, showFirstFrameOnPrepare: true };
await this.avPlayer.setMediaSource(mediaSource, playbackStrategy);
break;
case VideoDataType.RAW_MP4_FILE:
let mp4Fd = await this.context.resourceManager.getRawFd(this.curSource.videoSrc);
let mp4FdUrl = 'fd://' + mp4Fd.fd;
this.avPlayer.url = mp4FdUrl;
break;
default:
break;
}
// [Start AddCaption]
if (this.curSource.caption) {
let fileDescriptorSub = await this.context.resourceManager.getRawFd(this.curSource.caption);
this.avPlayer.addSubtitleFromFd(fileDescriptorSub.fd, fileDescriptorSub.offset, fileDescriptorSub.length)
.catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`addSubtitleFromFd failed, code is ${err.code}, message is ${err.message}`);
});
}
// [End AddCaption]
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`initPlayer failed, code is ${err.code}, message is ${err.message}`);
}
}
// [End create_instance]
private setAVPlayerCallback() {
if (!this.avPlayer) {
return;
}
this.avPlayer!.on('error', (err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG, `AVPlayer error, code is ${err.code}, message is ${err.message}`);
this.avPlayer!.reset().catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`reset failed, code is ${err.code}, message is ${err.message}`);
});
});
this.avPlayer!.on('durationUpdate', (time: number) => {
this.duration = time;
AppStorage.setOrCreate('DurationTime', time);
});
this.avPlayer.on('timeUpdate', (time: number) => {
this.currentTime = time;
AppStorage.setOrCreate('CurrentTime', time);
});
// The error callback function is triggered when an error occurs during avPlayer operations,
// at which point the reset interface is called to initiate the reset process
this.avPlayer.on('error', (err: BusinessError) => {
if (!this.avPlayer) {
return;
}
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
this.avPlayer.reset().catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`reset failed, code is ${err.code}, message is ${err.message}`);
});
})
this.subtitleUpdateFunction();
this.setStateChangeCallback();
}
private setStateChangeCallback() {
if (!this.avPlayer) {
return;
}
// [Start loop_playback]
// Callback function for state machine changes
this.avPlayer.on('stateChange', async (state) => {
if (!this.avPlayer) {
return;
}
switch (state) {
// [StartExclude loop_playback]
case 'idle': // This state machine is triggered after the reset interface is successfully invoked.
hilog.info(CommonConstants.LOG_DOMAIN, TAG, 'setAVPlayerCallback AVPlayer state idle called.');
break;
case 'initialized': // This status is reported after the playback source is set on the AVPlayer.
// Set the display screen. This parameter is not required when the resource to be played is audio-only.
this.avPlayer.surfaceId = this.surfaceID;
this.avPlayer.prepare().catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`prepare failed, code is ${err.code}, message is ${err.message}`);
});
break;
// [EndExclude loop_playback]
case 'prepared': // This state machine is reported after the prepare interface is successfully invoked.
this.isReady = true;
this.avPlayer.loop = true
// [StartExclude loop_playback]
this.durationTime = this.avPlayer.duration;
this.currentTime = this.avPlayer.currentTime;
this.avPlayer.audioInterruptMode = audio.InterruptMode.SHARE_MODE;
if (this.seekTime) {
this.avPlayer!.seek(this.seekTime!, media.SeekMode.SEEK_CLOSEST);
}
let eventData: emitter.EventData = {
data: {
'percent': this.avPlayer.width / this.avPlayer.height
}
};
emitter.emit(CommonConstants.AVPLAYER_PREPARED, eventData);
if (this.isMuted) {
try {
await this.avPlayer!.setMediaMuted(media.MediaType.MEDIA_TYPE_AUD, this.isMuted!)
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`setMediaMuted failed, code is ${err.code}, message is ${err.message}`);
}
}
this.setWindowScale();
if (this.index === 0) {
this.avPlayer.play().catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`play failed, code is ${err.code}, message is ${err.message}`);
});
}
this.setVideoSpeed();
// [EndExclude loop_playback]
break;
// [StartExclude loop_playback]
case 'playing': // After the play interface is successfully invoked, the state machine is reported.
this.isPlaying = true;
let eventDataTrue: emitter.EventData = {
data: {
'flag': true
}
};
let innerEventTrue: emitter.InnerEvent = {
eventId: 2,
priority: emitter.EventPriority.HIGH
};
emitter.emit(innerEventTrue, eventDataTrue);
break;
case 'completed': // This state machine is triggered to report when the playback ends.
this.currentTime = 0;
let eventDataFalse: emitter.EventData = {
data: {
'flag': false
}
};
let innerEvent: emitter.InnerEvent = {
eventId: 1,
priority: emitter.EventPriority.HIGH
};
emitter.emit(innerEvent, eventDataFalse);
break;
default:
hilog.info(CommonConstants.LOG_DOMAIN, TAG, 'setAVPlayerCallback AVPlayer state unknown called.');
break;
// [EndExclude loop_playback]
}
});
// [End loop_playback]
}
private setWindowScale() {
switch (this.windowScaleSelect) {
case CASE_ZERO:
this.videoScaleFit();
break;
case CASE_ONE:
this.videoScaleFitCrop();
break;
default:
break;
}
}
private setVideoSpeed() {
switch (this.speedSelect) {
case CASE_ZERO:
this.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
break;
case CASE_ONE:
this.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
break;
case CASE_TWO:
this.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
break;
case CASE_THREE:
this.videoSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
break;
default:
break;
}
}
videoPlay(): void {
if (this.avPlayer) {
this.avPlayer.play().catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`play failed, code is ${err.code}, message is ${err.message}`);
});
this.isPlaying = true;
}
}
videoPause(): void {
if (this.avPlayer) {
this.avPlayer.pause().catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`addSubtitleFromFd failed, code is ${err.code}, message is ${err.message}`);
});
this.isPlaying = false;
}
}
// Toggle play/pause state
videoStop(): void {
if (this.avPlayer) {
this.avPlayer.stop().catch((err: BusinessError) => {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoPause failed, code is ${err.code}, message is ${err.message}`);
});
this.isPlaying = false;
}
}
// [Start video_muted_fun]
/**
* Video muted
* @param isMuted
* @returns
*/
async videoMuted(isMuted: boolean): Promise<void> {
if (this.avPlayer) {
try {
this.isMuted = isMuted;
await this.avPlayer!.setMediaMuted(media.MediaType.MEDIA_TYPE_AUD, isMuted)
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoMuted failed, code is ${err.code}, message is ${err.message}`);
}
}
}
// [End video_muted_fun]
// [Start video_speed_fun]
videoSpeed(speed: number): void {
if (this.avPlayer) {
try {
this.avPlayer.setSpeed(speed);
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoSpeed failed, code is ${err.code}, message is ${err.message}`);
}
}
}
// [End video_speed_fun]
videoSeek(seekTime: number): void {
if (this.avPlayer) {
try {
this.avPlayer.seek(seekTime, media.SeekMode.SEEK_CLOSEST);
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoSeek failed, code is ${err.code}, message is ${err.message}`);
}
}
}
async videoReset(): Promise<void> {
if (!this.avPlayer) {
return;
}
try {
await this.avPlayer.reset();
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG, `videoReset failed, code is ${err.code}, message is ${err.message}`);
}
}
async videoRelease(): Promise<void> {
if (!this.avPlayer) {
return;
}
this.avPlayer.release((err) => {
if (err === null) {
hilog.info(CommonConstants.LOG_DOMAIN, TAG, 'videoRelease release success');
} else {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoRelease release filed, code is ${err.code}, message is ${err.message}`);
}
});
}
getDurationTime(): number {
return this.durationTime;
}
getCurrentTime(): number {
return this.currentTime;
}
// [Start window_scale_fun]
/**
* Set window scale mode
*/
videoScaleFit(): void {
if (this.avPlayer) {
try {
this.avPlayer.videoScaleType = media.VideoScaleType.VIDEO_SCALE_TYPE_FIT
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoScaleType_0 failed, code is ${err.code}, message is ${err.message}`);
}
}
}
videoScaleFitCrop(): void {
if (this.avPlayer) {
try {
this.avPlayer.videoScaleType = media.VideoScaleType.VIDEO_SCALE_TYPE_FIT_CROP
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`videoScaleType_1 failed, code is ${err.code}, message is ${err.message}`);
}
}
}
// [End window_scale_fun]
subtitleUpdateFunction(): void {
if (this.avPlayer) {
try {
// [Start RegisterCaptionCallBack]
this.avPlayer.on('subtitleUpdate', (info: media.SubtitleInfo) => {
if (info) {
let text = (!info.text) ? '' : info.text;
this.currentCaption = text; //update current caption content
} else {
this.currentCaption = '';
hilog.error(CommonConstants.LOG_DOMAIN, TAG, 'subtitleUpdate info is null');
}
});
// [End RegisterCaptionCallBack]
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`subtitleUpdateFunction failed, code is ${err.code}, message is ${err.message}`);
}
}
}
// [Start languageSwitch]
async languageChange(languageSelect: number = 0): Promise<void> {
if (this.avPlayer) {
try {
if (this.curSource && this.curSource.caption) {
this.curSource.caption = languageSelect === 0 ? 'captions.srt' : 'en_captions.srt'
this.curSource.seekTime = this.avPlayer.currentTime;
await this.avPlayer.reset();
this.initAVPlayer(this.curSource, this.surfaceID, this.avPlayer);
}
} catch (err) {
hilog.error(CommonConstants.LOG_DOMAIN, TAG,
`languageChange failed, code is ${err.code}, message is ${err.message}`);
}
}
}
// [End languageSwitch]
}
3.2 开发建议与最佳实践:

-
严格遵循状态机规范:在视频处于prepared、paused或completed状态时,才应调用play方法开始播放;若视频已在播放,则应调用pause方法暂停。
-
重视资源释放:播放完成后,务必调用release方法,以确保内存、线程等系统资源得到妥善释放,避免潜在的资源泄漏问题。
-
妥善管理权限:若应用程序需要访问网络或播放媒体,应提前申请相应的权限,以确保应用的正常运行和用户的良好体验。
更多推荐



所有评论(0)