引言
最近在学习HarmonyOS,然后遇到一个需求,做一个视频播放器,我想这不简单嘛直接用一个Video组件
ts
Video({
src: this.videoSrc, // 视频的数据源,支持本地视频和网络视频
previewUri: this.previewUri, // 预览图片路径
currentProgressRate: this.curRate, // 视频播放倍速
controller: this.controller // 视频控制器
})
.autoPlay(true) // 是否自动播放
.width("100%")
.objectFit(ImageFit.Contain)
效果
看就是这么简单
但是我看官方文档原文实现视频播放器有两种方法
看起来AVPlayer
好像更强?有点意思🤔,那我们试试AVPlayer
如何实现视频播放器咯🤓
一、AVPlayer✨
1.创建播放器▶️
首先第一步就是创建AVPlayer
ts
this.avPlayer = await media.createAVPlayer()
2.设置播放资源路径🔧
设置播放资源路径 可以是 本地资源 或 网络资源注意:网络资源需要在module.json5中声明ohos.permission.INTERNET权限
ts
// 获取网络资源
// this.avPlayer.url = "http://www.xxx.cn/xxx";
// 获取本地资源 存放在 resource/rawflie 文件夹中
let context = getContext(this) as common.UIAbilityContext
let fileDescriptor = await context.resourceManager.getRawFd("xxx.mp4")
this.avPlayer.fdSrc = fileDescriptor
3.设置播放参数🎈
ts
// 设置音量
this.avPlayer.setVolume(0.5)
// 设置播放倍数
this.avPlayer.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X)
4.播放控制⏯️
播放、暂停、跳转、停止等播放控制操作是视频播放过程中的常用功能
ts
// 视频播放
avPlayer.play();
// 视频暂停
avPlayer.pause();
// 视频停止播放
avPlayer.stop();
好吧简单介绍完毕我们直接开始实战实现一个播放器吧!😏
二、开始实现✨
1.创建 AVPlayer🍍
首先创建ets/controller/VideoController.ets文件
在里面创建AVPlayer 初始代码如下
ts
import { media } from '@kit.MediaKit';
import { PlayerModel } from '../model/PlayerModel'
@Observed
export class VideoController {
public playerModel: PlayerModel // 视频参数数据模型
private surfaceID: string = ''; // surfaceID用于播放画面显示,通过Xcomponent获取 后面我会讲到
public avPlayer: media.AVPlayer | null = null // avPlayer 实例
private fdSrc: string = '' // 本地资源url
private url: string = '' // 网络资源url
private status: number = 2 // 播放状态 1表示播放中 2表示暂停
private seekTime: number = 0 // 用于设置跳转时间
private duration: number = 0 // 视频总时长
constructor() {
this.playerModel = new PlayerModel()
this.createAVPlayer()
}
// 创建 AVPlayer 实例
async createAVPlayer() {
let avPlayer: media.AVPlayer = await media.createAVPlayer();
this.avPlayer = avPlayer;
}
}
2.创建PlayerMode视频参数数据模型📄
创建/ets/model/PlayerMode.ets 文件
PlayerMode为视频参数的数据模型 代码如下
ts
import { media } from '@kit.MediaKit';
@Observed
export class PlayerModel {
videoHeight: string = '25.6%'
videoWidth: string = "100%";
videoMargin: string = '0';
videoPosition: FlexAlign = FlexAlign.Center;
progressVal: number = 0
currentTime: string = ''
totalTime: string = ''
playSeed:media.PlaybackSpeed = 1
}
里面的值你们可以声明为常量 ,我这里就偷偷懒😉,你们不要学我哈
3.注册AVPlayer回调函数🧨
ts
bindState() {
if (!this.avPlayer) {
return
}
// seek操作结果回调函数
this.avPlayer.on('seekDone', (seekDoneTime: number) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
})
// error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
this.avPlayer.on('error', (err: BusinessError) => {
if (!this.avPlayer) {
return
}
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
this.avPlayer.reset(); // 调用reset重置资源,触发idle状态
})
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
if (!this.avPlayer) {
return
}
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info('AVPlayer state idle called.');
this.avPlayer.release(); // 调用release接口销毁实例对象
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info('AVPlayer state initialized called.');
this.avPlayer.surfaceId = this.surfaceId // 通过Xcomponent获取关联AVPlayer 后面我会讲到
this.avPlayer.prepare();
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info('AVPlayer state prepared called.');
this.avPlayer.play(); // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.info('AVPlayer state playing called.');
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info('AVPlayer state paused called.');
break;
case 'completed': // 播放结束后触发该状态机上报
console.info('AVPlayer state completed called.');
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.info('AVPlayer state stopped called.');
this.avPlayer.reset(); // 调用reset接口初始化avplayer状态
break;
case 'released':
console.info('AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
})
}
这是什么呀,你刚才也没有讲,嗯我现在讲了😏
AVPlayer可以设置监听事件,事件类型如下:
事件类型 | 说明 |
---|---|
stateChange | 必要事件,监听播放器的state属性改变。 |
error | 必要事件,监听播放器的错误信息。 |
durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
seekDone | 响应API调用,监听seek()请求完成情况。当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 |
speedDone | 响应API调用,监听setSpeed()请求完成情况。当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。 |
volumeChange | 响应API调用,监听setVolume()请求完成情况。当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。 |
bitrateDone | 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。当使用setBitrate()指定播放比特率后,如果setBitrate操作成功,将上报该事件。 |
availableBitrates | 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。 |
bufferingUpdate | 用于网络播放,监听网络播放缓冲信息。 |
startRenderFrame | 用于视频播放,监听视频播放首帧渲染时间。 |
videoSizeChange | 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。 |
audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 |
感觉挺多的,没事你只需要记住stateChange 和timeUpdate 这几个常用的就行了
介绍一下AVplayer的流程 这张图里面每一个椭圆也就时一个状态也对应
stateChange
状态机
接下来在createAVPlayer函数中添加这个方法
ts
async createAVPlayer() {
// ...
this.bindState()
}
4.添加播放函数▶️
可以在外部调用这些函数播放视频
播放本地资源
ts
/**
* 播放本地资源
* @param surfaceId surfaceId 通过XComponent获取
* @param fdSrcName 本地资源名称
*/
async playFdSrc(surfaceId: string, fdSrcName: string) {
this.surfaceId = surfaceId
if (this.avPlayer === null) {
await this.createAVPlayer();
}
if (this.avPlayer !== null) {
let context = getContext(this) as common.UIAbilityContext
let fileDescriptor = await context.resourceManager.getRawFd(fdSrcName)
console.log("fileDescriptor:" + fileDescriptor)
this.avPlayer.fdSrc = fileDescriptor
}
}
播放网络资源
ts
/**
* 播放网络资源
* @param surfaceId surfaceId 通过XComponent获取
* @param url 网络资源url
*/
async playUrl(surfaceId: string, url: string) {
this.surfaceId = surfaceId
if (this.avPlayer === null) {
await this.createAVPlayer();
}
if (this.avPlayer !== null) {
this.avPlayer.url = url;
}
}
5.介绍XComponent组件😀
可用于EGL/OpenGLES和媒体数据写入,并显示在XComponent组件
通过surfaceId 属性可以将视频画面关联到XComponent组件上 让XComponent显示实现视频的画面,这样做的好处是可以高度自定义播放界面、增强交互性比如在XComponent上添加手势识别功能
ts
controller: XComponentController = new XComponentController()
// ...
XComponent({
id: "", // 唯一标识
type: XComponentType.SURFACE, // 组件类型 surface component
controller: this.controller // 绑定控制器
})
.onLoad(async () => {
// 设置XComponent持有Surface的宽度和高度
await this.controller!.setXComponentSurfaceSize({
surfaceWidth: 1980,
surfaceHeight: 1080
})
// 获取 XComponent的SurfaceId 用于关联视频画面
let surfaceId = this.controller.getXComponentSurfaceId()
})
6.使用XComponent显示视频画面🍕
ts
import { VideoController } from '../controller/VideoController'
import { PlayerModel } from '../model/PlayerModel'
@Entry
@Component
struct Index {
/**视频控制器*/
videoController: VideoController = new VideoController()
/**XComponent控制器*/
controller: XComponentController = new XComponentController()
/**视频参数数据模型*/
@State playerModel: PlayerModel = this.videoController.playerModel
/**本地文件名称*/
@State fdSrcName: string = 'go_home.mp4'
/**网络视频路径*/
@State url: string = ''
build() {
Column() {
XComponent({
id: "", // 唯一标识
type: XComponentType.SURFACE, // 组件类型 surface component
controller: this.controller
})
.onLoad(async () => {
// 设置XComponent持有Surface的宽度和高度
await this.controller!.setXComponentSurfaceSize({
surfaceWidth: 1980,
surfaceHeight: 1080
})
// 获取 XComponent的SurfaceId 用于关联视频画面
let surfaceId = this.controller.getXComponentSurfaceId()
// 调用播放视频方法 关联视频
this.videoController.playFdSrc(surfaceId, this.fdSrcName)
})
}
.backgroundColor(Color.Black)
.height('100%')
.width('100%')
}
}
好接下来我们运行一下
可以是可以了但是样式有点怪怪的🤔
接下来我们修改一下尺寸
7.修改视频尺寸✒️
我们在VideoController 添加如下方法
ts
/**
* 根据视频大小设置播放页面大小
*/
setVideoSize() {
if (this.avPlayer === null) {
return;
}
if (this.avPlayer.height > this.avPlayer.width) {
// 如果视频高度大于宽度(竖屏),则设置宽度和高度都为100%
this.playerModel.videoWidth = '100%';
this.playerModel.videoHeight = '100%';
this.playerModel.videoPosition = FlexAlign.Center;
this.playerModel.videoMargin = '7.2%';
} else {
// 如果视频宽度大于高度(横屏),则设置宽度为100% 高度为25.6%
this.playerModel.videoWidth = "100%";
this.playerModel.videoHeight = '25.6%';
this.playerModel.videoPosition = FlexAlign.Center;
this.playerModel.videoMargin = '0';
}
}
将计算后的参数都保存到 PlayerModel 中
然后在状态机回调函数中的switch 的prepared子句中调用这个方法设置大小
ts
this.avPlayer.on('stateChange', async (state: media.AVPlayerState, reason: media.StateChangeReason) => {
// ...
switch (state) {
// ...
case 'prepared': // prepare 调用成功后上报该状态机
// 设置视频宽高大小
this.setVideoSize()
// 调用播放接口开始播放
this.avPlayer.play();
break;
// ...
}
})
然后设置XComponent组件的宽高
ts
Column() {
XComponent({
id: "", // 唯一标识
type: XComponentType.SURFACE, // 组件类型 surface component
controller: this.controller
})
.onLoad(async () => {
// 设置XComponent持有Surface的宽度和高度
await this.controller!.setXComponentSurfaceSize({
surfaceWidth: 1980,
surfaceHeight: 1080
})
// 获取 XComponent的SurfaceId 用于关联视频画面
let surfaceId = this.controller.getXComponentSurfaceId()
// 调用播放视频方法 关联视频
this.videoController.playFdSrc(surfaceId, this.fdSrcName)
})
.width(this.playerModel.videoWidth) // 设置宽度
.height(this.playerModel.videoHeight) // 设置高度
}
.justifyContent(this.playerModel.videoPosition)
.backgroundColor(Color.Black)
.height('100%')
.width('100%')
哎呦不错哦!
8.添加播放/暂停方法⏯️
在VideoController中添加以下三个方法
ts
// 播放
play() {
if (this.avPlayer !== null) {
this.avPlayer.play();
}
}
// 暂停
pause() {
if (this.avPlayer !== null) {
this.avPlayer.pause();
}
}
// 播放暂停切换
switchPlayOrPause() {
if (this.avPlayer === null) {
return;
}
// 根据status播放状态切换
if (this.status === 1) {
this.avPlayer.pause();
} else {
this.avPlayer.play();
}
}
然后在状态机变化回调函数中中添加以下代码 切换status状态的代码
ts
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state: media.AVPlayerState, reason: media.StateChangeReason) => {
// ...
switch (state) {
// ...
case 'playing': // play成功调用后触发该状态机上报
console.info('AVPlayer state playing called.');
this.status = 1
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info('AVPlayer state paused called.');
this.status = 2
break;
// ...
}
})
在XComponent组件下添加播放切换按钮
ts
XComponent({
id: "", // 唯一标识
type: XComponentType.SURFACE, // 组件类型 surface component
controller: this.controller
})
//...
Row({ space: 4 }) {
Button("播放/暂停")
.onClick(() => {
this.videoController.switchPlayOrPause()
})
}.padding(6)
有点意思😉
9.添加进度条🍡
在VideoController中添加以下方法
ts
initProgress(time: number) {
// 将当前进度毫秒数转换为秒
let nowSeconds = Math.floor(time / 1000)
// 将当前视频总毫秒数转换为 总秒数
let totalSeconds = Math.floor(this.duration / 1000);
// 进行格式化
this.playerModel.currentTime = DateFormatUtil.secondToTime(nowSeconds);
this.playerModel.totalTime = DateFormatUtil.secondToTime(totalSeconds);
this.playerModel.progressVal = Math.floor(nowSeconds * 100 / totalSeconds);
console.log("progressVal:" + this.playerModel.progressVal);
}
DateFormatUtil 为格式化时间工具类
创建/ets/common/DateFormatUtil 文件,代码如下
ts
class DateFormatUtil {
/**
* 将给定的秒数转换为格式化的时间字符串。
* @function secondToTime
* @param {number} seconds - 要转换的秒数。
* @returns {string} 格式化后的时间字符串,格式为"小时:分钟:秒"。如果小时数为 0,则只显示"分钟:秒";如果分钟数也为 0,则显示"00:秒"。
*/
secondToTime(seconds: number): string {
let hourUnit = 60 * 60;
let hour = Math.floor(seconds / hourUnit);
let minute = Math.floor((seconds - hour * hourUnit) / 60);
let second = seconds - hour * hourUnit - minute * 60;
if (hour > 0) {
return `${this.padding(hour.toString())}:
${this.padding(minute.toString())}:${this.padding(second.toString())}`;
}
if (minute > 0) {
return `${this.padding(minute.toString())}:${this.padding(second.toString())}`;
} else {
return `00:${this.padding(second.toString())}`;
}
}
/**
* 对给定的数字字符串进行前导零填充,确保字符串长度为 2。
* @param {string} num - 要进行填充的数字字符串。
* @returns {string} 长度为 2 的字符串,如果传入的字符串长度小于 2,则在前面填充零。
*/
padding(num: string): string {
return num.padStart(2,'0');
}
}
export default new DateFormatUtil();
secondToTime方法 格式化后的时间字符串,格式为"小时:分钟:秒"。如果小时数为 0,则只显示"分钟:秒";如果分钟数也为 0,则显示"00:秒, 效果
在添加监听状态回调函数中添加如下代码:
ts
bindState() {
// ...
// 监听进度条播放
this.avPlayer.on("timeUpdate",(time) => {
this.changeProgress(time)
})
// ...
}
这样每次视频播放进度更新时都会触发更新进度条
接着在状态机的prepared子句中获取视频总时长
ts
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state: media.AVPlayerState, reason: media.StateChangeReason) => {
// ...
switch (state) {
// ...
case 'prepared': // prepare 调用成功后上报该状态机
// ...
// 获取总时长
this.duration = this.avPlayer.duration;
break;
// ...
}
})
接下来我们在播放暂停按钮下添加Slider组件
ts
Slider({
value: this.playerModel.progressVal,
step: 1,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.trackColor("#888888")
Text("当前进度:" +
this.playerModel.currentTime
)
.fontColor(Color.White)
Text("总时长:" +
this.playerModel.totalTime
)
.fontColor(Color.White)
我们看看效果
哟可以哦😏
10.拖动进度条🥖
在VideoController中添加如下方法:
ts
/**
* 设置跳转时间
* @param value
* @param mode
*/
setSeekTime(value: number, mode: SliderChangeMode) {
// 判断当前 Slider 的模式 是否是移动中
if (mode === SliderChangeMode.Moving) {
this.playerModel.progressVal = value
this.playerModel.currentTime = DateFormatUtil.secondToTime(Math.floor(value * this.duration / 100 / 1000))
this.avPlayer.play()
}
if (mode === SliderChangeMode.End || mode === SliderChangeMode.Click) {
this.seekTime = value * this.duration / 100
if (this.avPlayer !== null) {
this.avPlayer.seek(this.seekTime, media.SeekMode.SEEK_NEXT_SYNC)
this.avPlayer.play()
}
}
}
给Slider添加onChange方法
ts
Slider({
value: this.playerModel.progressVal,
step: 1,
style: SliderStyle.OutSet
})
.blockColor(Color.White)
.trackColor("#888888")
.onChange((value: number, mode: SliderChangeMode) => {
this.videoController.setSeekTime(value, mode)
})
试试效果
不错呢😉
11.设置播放倍速⬆️
这个简单 在VideoController中添加如下方法
ts
setSpeed(playSpeed: media.PlaybackSpeed) {
this.playerModel.playSeed = playSpeed
this.avPlayer?.setSpeed(this.playerModel.playSeed)
}
然后在Slider 组件下添加Button按钮进行测试
ts
Row() {
Button("1x")
.onClick(() =>{
this.videoController.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X)
})
Button("1.5x")
.onClick(() =>{
this.videoController.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_50_X)
})
Button("2x")
.onClick(() =>{
this.videoController.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X)
})
}
来我们看看最终效果
有点意思😀,样式你们可以自己定义一下我就偷偷懒吧
好啦就这样吧,总结一下
后面可以自己扩展,设置按钮样式,布局,设置手势调整音量等
总结📚
- AVPlayer和Video组件的对比
- AVPlayer:功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。
- Video组件:封装了视频播放的基础能力,需要设置数据源以及基础信息即可播放视频,但相对扩展能力较弱。Video组件由ArkUI提供能力,相关指导请参考UI开发文档
- 根据视频时横屏还是竖屏设置XComponent大小
- 创建PlayerMode统一管理视频参数
- 通过AVPlayer的API设置了播放资源、播放参数、播放控制(播放\暂停\跳转)等
- 使用on("stateChnage")方法监听状态变化,执行相应的操作
- 后续可以自己在VideoController中扩展,比如设置音量就调用setVolume()等
参考资料:
我刚学HarmonyOS不久,这个播放器是我看官方文档和案例源码才学会的,第一次写技术文章,满满的成就感😀,我想把我所学到的知识分享一下给大家,给我点个关注or点赞or收藏支持一下吧😉