目录:
1、鸿蒙视频功能介绍
鸿蒙提供了两种方式实现视频播放功能:
- 使用video组件实现视频播放
- 使用AVPlayer组件实现视频播放
2、AVPlayer组件实现视频播放
2.1、播放功能的逻辑处理
import media from '@ohos.multimedia.media'
export class VideoAVPlayerClass {
// 创建的播放器应该存在我们的工具类上,这样才能被导出使用
static player: media.AVPlayer | null = null
// 当前播放器播放视频的总时长
static duration: number = 0
// 当前播放器播放的时长
static time: number = 0
// 当前播放器是否播放
static isPlay: boolean = false
// 当前播放器的播放列表
static playList: videoItemType[] = []
// 当前播放的视频索引
static playIndex: number = 0
// surfaceID用于播放画面显示,具体的值需要通过XComponent接口获取
static surfaceId: string = ''
// 播放器上记录上下文
static context: Context|null = null
// 创建播放器的方法
static async init(initParams: InitParams) {
// 存储属性SurfaceID,用于设置播放窗口,显示画面
VideoAVPlayerClass.surfaceId = initParams.surfaceId
// 将上下文 context 存储到播放器类上
VideoAVPlayerClass.context = initParams.context
// 创建播放器实例
VideoAVPlayerClass.player = await media.createAVPlayer()
// ----------------------- 事件监听 --------------------------------------------------------------
// 用于进度条,监听进度条长度,刷新资源时长
VideoAVPlayerClass.avPlayer.on('durationUpdate', (duration: number) => {
console.info('AVPlayer state durationUpdate called. current time: ', duration);
// 获取视频总时长
VideoAVPlayerClass.duration = duration
})
// 用于进度条,监听进度条当前位置,刷新当前时间
VideoAVPlayerClass.avPlayer.on('timeUpdate', (time) =>{
console.info('AVPlayer state timeUpdate called. current time: ', time);
// 获取当前播放时长
VideoAVPlayerClass.time = time
// 更新信息到页面
VideoAVPlayerClass.updateState()
})
// 监听seek生效的事件
VideoAVPlayerClass.avPlayer.on('seekDone', (seekDoneTime: number) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
VideoAVPlayerClass.avPlayer.play()
VideoAVPlayerClass.isPlay = true
})
// 监听视频播放错误事件,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
VideoAVPlayerClass.avPlayer.on('error', (err) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
// 调用reset重置资源,触发idle状态
VideoAVPlayerClass.avPlayer.reset()
})
// 监听播放状态机AVPlayerState切换的事件
VideoAVPlayerClass.avPlayer.on('stateChange', async (state: media.AVPlayerState, reason: media.StateChangeReason) => {
switch (state) {
// 成功调用reset接口后触发该状态机上报
case 'idle':
console.info('AVPlayer state idle called.');
break
// avplayer 设置播放源后触发该状态上报
case 'initialized':
console.info('AVPlayerstate initialized called.');
// 设置显示画面,当播放的资源为纯音频时无需设置
VideoAVPlayerClass.avPlayer.surfaceId = VideoAVPlayerClass.surfaceId
break
// prepare调用成功后上报该状态机
case 'prepared':
console.info('AVPlayer state prepared called.');
break
// play成功调用后触发该状态机上报
case 'playing':
console.info('AVPlayer state playing called.');
break
// pause成功调用后触发该状态机上报
case 'paused':
console.info('AVPlayer state paused called.');
break
// 播放结束后触发该状态机上报
case 'completed':
console.info('AVPlayer state completed called.');
// 当前视频播放完成,自动播放下一个视频哦
if (VideoAVPlayerClass.autoPlayList && VideoAVPlayerClass.playIndex < VideoAVPlayerClass.playList.length) {
VideoAVPlayerClass.playIndex++
VideoAVPlayerClass.playIndex = (VideoAVPlayerClass.playIndex + VideoAVPlayerClass.playList.length) % VideoAVPlayerClass.playList.length
VideoAVPlayerClass.singlePlay(VideoAVPlayerClass.playList[VideoAVPlayerClass.playIndex])
VideoAVPlayerClass.isPlay = true
} else {
VideoAVPlayerClass.isPlay = false
// 停止播放
VideoAVPlayerClass.avPlayer.stop()
}
break
// stop接口成功调用后触发该状态机上报
case 'stopped':
console.info('AVPlayer state stopped called.');
// 调用reset接口初始化avplayer状态
VideoAVPlayerClass.avPlayer.reset()
break
case 'released':
console.info('AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
})
}
// 视频播放
static async play() {
VideoAVPlayerClass.avPlayer.play()
VideoAVPlayerClass.isPlay = true
VideoAVPlayerClass.updateState()
}
// 视频暂停
static pause() {
VideoAVPlayerClass.avPlayer.pause()
VideoAVPlayerClass.isPlay = false
VideoAVPlayerClass.updateState()
}
// 切换视频
static singlePlay(video?: videoItemType) {
if (video) {
let index = VideoAVPlayerClass.playList.findIndex((item: videoItemType) => item.id === video.id)
if (index > -1) {
// 当前要播放的视频在播放列表里
VideoAVPlayerClass.playIndex = index
} else {
// 当前要播放的视频不在播放列表里
VideoAVPlayerClass.playList.push(video)
VideoAVPlayerClass.playIndex = VideoAVPlayerClass.playList.length - 1
}
}
VideoAVPlayerClass.changePlay()
}
static async changePlay() {
// 将播放状态置为闲置
await VideoAVPlayerClass.avPlayer.reset()
// 重置当前播放时长和视频时长
VideoAVPlayerClass.time = 0
VideoAVPlayerClass.duration = 0
VideoAVPlayerClass.avPlayer.url = VideoAVPlayerClass.playList[VideoAVPlayerClass.playIndex].url
VideoAVPlayerClass.updateState()
}
// 更新页面状态
static async updateState() {
const data = {
playState: JSON.stringify({
duration: VideoAVPlayerClass.duration,
time: VideoAVPlayerClass.time,
isPlay: VideoAVPlayerClass.isPlay,
playIndex: VideoAVPlayerClass.playIndex,
playList: VideoAVPlayerClass.playList,
})
}
// 更新页面
emitter.emit({
eventId: EmitEventType.UPDATE_STATE
}, {
data
})
// 存储首选项
const preferences:PreferencesClass = new PreferencesClass(VideoAVPlayerClass.context)
await preferences.setVideoPlayState(JSON.parse(data.playState))
}
}
2.2、页面调用渲染
import emitter from '@ohos.events.emitter';
import PlayingAnimation from '../components/PlayingAnimation';
import { EmitEventType } from '../constants/EventContants';
import { VideoListData } from '../constants/VideoConstants';
import { PlayStateType, PlayStateTypeModel } from '../models/playState';
import { videoItemType } from '../models/video';
import { VideoPlayStateType, VideoPlayStateTypeModel } from '../models/videoPlayState';
import { PreferencesClass } from '../utils/PreferencesClass';
import { VideoAVPlayerClass } from '../utils/VideoAVPlayerClass';
@Preview
@Component
struct Index {
@State
playState: VideoPlayStateType = new VideoPlayStateTypeModel({} as VideoPlayStateType)
xComController: XComponentController = new XComponentController()
surfaceId: string = "" // 定义surfaceId
videoList: videoItemType[] = VideoListData
async aboutToAppear() {
// 从播放器订阅数据
emitter.on({ eventId: EmitEventType.UPDATE_STATE }, (data) => {
this.playState = new VideoPlayStateTypeModel(JSON.parse(data.data.playState))
})
// 从首选项加载数据
const preferences:PreferencesClass = new PreferencesClass(getContext(this))
this.playState = await preferences.getVideoPlayState()
}
aboutToDisappear(){
// 销毁播放器
VideoAVPlayerClass.avPlayer.release()
}
// 时长数字(ms)转字符串
number2time(number: number) {
if (!number) {
return '00:00'
}
const ms: number = number % 1000
const second = (number - ms) / 1000
const s: number = second % 60
if (second > 60) {
const m: number = (second - s) / 60 % 60
return m.toString()
.padStart(2, '0') + ':' + s.toString()
.padStart(2, '0')
}
return '00:' + s.toString()
.padStart(2, '0')
}
build() {
Row() {
Column({ space: 10 }) {
Stack() {
Column() {
Row(){
// 视频播放窗口
XComponent({
id: 'videoXComponent',
type: 'surface',
controller: this.xComController
})
.width('100%')
.height(200)
.onLoad(async () => {
this.xComController.setXComponentSurfaceSize({ surfaceWidth: 1080, surfaceHeight: 1920 });
this.surfaceId = this.xComController.getXComponentSurfaceId()
if (this.surfaceId) {
await VideoAVPlayerClass.init({surfaceId: this.surfaceId, playList: this.videoList, context: getContext(this)})
await VideoAVPlayerClass.singlePlay()
}
})
}
.onClick(() => {
this.playState.isPlay ? VideoAVPlayerClass.pause() : VideoAVPlayerClass.play()
})
// 进度条
Row({space: 6}){
// 当前播放时长
Text(this.number2time(this.playState?.time))
.fontColor($r('app.color.white'))
.visibility(this.playState?.duration ? Visibility.Visible : Visibility.Hidden)
// 进度条
Slider({
value: this.playState.time,
min: 0,
max: this.playState.duration,
})
.trackColor($r('app.color.white'))
.onChange((value: number, mode: SliderChangeMode) => {
// 切换播放进度
VideoAVPlayerClass.seekTime(value)
})
.width("70%")
// 视频总时长
Text(this.number2time(this.playState?.duration))
.fontColor($r('app.color.white'))
.visibility(this.playState?.duration ? Visibility.Visible : Visibility.Hidden)
}
.width('100%')
.height(20)
.margin({
top: 10
})
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height(270)
.padding({
top: 30,
bottom:30
})
.backgroundColor($r('app.color.black'))
.justifyContent(FlexAlign.Start)
// 播放按钮
if (!this.playState.isPlay) {
Image($r('app.media.ic_play'))
.width(48)
.height(48)
.fillColor($r('app.color.white'))
.onClick(() => {
VideoAVPlayerClass.play()
})
}
}
// 视频列表缩略图
List({ space: 10, initialIndex: 0 }) {
ForEach(this.videoList, (item: videoItemType, index: number) => {
ListItem() {
Stack({alignContent: Alignment.Center}){
Image(item.imgUrl)
.width(100)
.height(80)
// .objectFit(ImageFit.Contain)
if (this.playState.playIndex === index) {
Row(){
PlayingAnimation({ recordIng: true })
}
}
}
}
.width(100)
.onClick(() => {
VideoAVPlayerClass.singlePlay(item)
})
}, item => item)
}
.height(100)
.listDirection(Axis.Horizontal) // 排列方向
.edgeEffect(EdgeEffect.Spring) // 滑动到边缘无效果
.onScrollIndex((firstIndex: number, lastIndex: number) => {
console.info('first' + firstIndex)
console.info('last' + lastIndex)
})
}
.width('100%')
.height('100%')
}
.height('100%')
.width('100%')
}
}
export default Index
2.3、缓存播放信息
问题场景:
我们的播放器已经可以正常进行视频的播放和切换了,当我们不小心退出了当前页面,再进入播放页面时,你会发现我们当前播放的视频信息没有了,这是为什么呢?
在当前的实现中,播放页面的信息是通过订阅播放器得到的,如果不播放了,就没有了信息来源的渠道,所以页面的播放信息就没有了。因此我们需要再建立一个信息收集渠道,即使不在播放时,也能获取到最后的播放信息数据。为此,我们使用**@ohos.data.preferences(用户首选项)**来持久化播放信息。
实现一个存储和读取首选项的工具类:
import preferences from '@ohos.data.preferences'
import { videoDefaultState, VideoPlayStateType } from '../models/videoPlayState'
export class PreferencesClass {
StoreName = 'VIDEO_PLAYER'
context: Context
VideoPlayStateKey = "VIDEO_PLAY_STATE"
constructor(context: Context) {
this.context = context
}
// 获取store
async getStore() {
return await preferences.getPreferences(this.context,this.StoreName)
}
// 存储视频播放状态
async setVideoPlayState(playState:VideoPlayStateType){
const store = await this.getStore()
await store.put(this.PlayStateKey,JSON.stringify(playState))
await store.flush()
}
// 读取视频播放状态
async getVideoPlayState(): Promise<VideoPlayStateType> {
const store = await this.getStore()
return JSON.parse(await store.get(this.VideoPlayStateKey,JSON.stringify(videoDefaultState)) as string) as VideoPlayStateType
}
}
3、video组件实现视频播放
3.1、鸿蒙官网实现代码
// xxx.ets
@Entry
@Component
struct VideoCreateComponent {
@State videoSrc: Resource = $rawfile('video1.mp4')
@State previewUri: Resource = $r('app.media.poster1')
@State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
@State isAutoPlay: boolean = false
@State showControls: boolean = true
controller: VideoController = new VideoController()
build() {
Column() {
Video({
src: this.videoSrc,
previewUri: this.previewUri,
currentProgressRate: this.curRate,
controller: this.controller
})
.width('100%')
.height(600)
.autoPlay(this.isAutoPlay)
.controls(this.showControls)
.onStart(() => {
console.info('onStart')
})
.onPause(() => {
console.info('onPause')
})
.onFinish(() => {
console.info('onFinish')
})
.onError(() => {
console.info('onError')
})
.onStop(() => {
console.info('onStop')
})
.onPrepared((e?: DurationObject) => {
if (e != undefined) {
console.info('onPrepared is ' + e.duration)
}
})
.onSeeking((e?: TimeObject) => {
if (e != undefined) {
console.info('onSeeking is ' + e.time)
}
})
.onSeeked((e?: TimeObject) => {
if (e != undefined) {
console.info('onSeeked is ' + e.time)
}
})
.onUpdate((e?: TimeObject) => {
if (e != undefined) {
console.info('onUpdate is ' + e.time)
}
})
Row() {
Button('src').onClick(() => {
this.videoSrc = $rawfile('video2.mp4') // 切换视频源
}).margin(5)
Button('previewUri').onClick(() => {
this.previewUri = $r('app.media.poster2') // 切换视频预览海报
}).margin(5)
Button('controls').onClick(() => {
this.showControls = !this.showControls // 切换是否显示视频控制栏
}).margin(5)
}
Row() {
Button('start').onClick(() => {
this.controller.start() // 开始播放
}).margin(2)
Button('pause').onClick(() => {
this.controller.pause() // 暂停播放
}).margin(2)
Button('stop').onClick(() => {
this.controller.stop() // 结束播放
}).margin(2)
Button('reset').onClick(() => {
this.controller.reset() // 重置AVPlayer
}).margin(2)
Button('setTime').onClick(() => {
this.controller.setCurrentTime(10, SeekMode.Accurate) // 精准跳转到视频的10s位置
}).margin(2)
}
Row() {
Button('rate 0.75').onClick(() => {
this.curRate = PlaybackSpeed.Speed_Forward_0_75_X // 0.75倍速播放
}).margin(5)
Button('rate 1').onClick(() => {
this.curRate = PlaybackSpeed.Speed_Forward_1_00_X // 原倍速播放
}).margin(5)
Button('rate 2').onClick(() => {
this.curRate = PlaybackSpeed.Speed_Forward_2_00_X // 2倍速播放
}).margin(5)
}
}
}
}
interface DurationObject {
duration: number;
}
interface TimeObject {
time: number;
}
3.2、通过xml布局文件代码实现
实现步骤:
I. 创建项目
打开 DevEco Studio,创建一个新的 HarmonyOS 项目,选择“Empty Ability”模板。
配置权限:
在项目的配置文件 config.json 中,添加存储权限。
{
"module": {
"reqPermissions": [
{
"name": "ohos.permission.READ_STORAGE"
},
{
"name": "ohos.permission.WRITE_STORAGE"
}
]
}
}
II. 定义布局文件
定义布局文件:
在 src/main/resources/base/layout 目录下,创建一个布局文件 ability_main.xml,用于展示音频播放和视频播放的控件。
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:width="match_parent"
ohos:height="match_parent"
ohos:orientation="vertical"
ohos:padding="16vp">
<Video
ohos:id="$+id/video_player"
ohos:width="match_parent"
ohos:height="200vp"
ohos:layout_marginBottom="16vp"
ohos:scale_type="centerCrop"/>
<Button
ohos:id="$+id/button_play_video"
ohos:width="match_content"
ohos:height="wrap_content"
ohos:text="Play Video"/>
<Button
ohos:id="$+id/button_play_audio"
ohos:width="match_content"
ohos:height="wrap_content"
ohos:text="Play Audio"/>
</DirectionalLayout>
III. 实现音频和视频播放功能
编写 MainAbilitySlice.java:
在 src/main/java/com/example/media/slice 目录下,创建一个 MainAbilitySlice.java 文件,实现音频和视频播放功能。
package com.example.media.slice;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.agp.components.Button;
import ohos.agp.components.Video;
import ohos.agp.components.Component;
import ohos.media.audio.AudioPlayer;
import ohos.media.audio.AudioManager;
import ohos.media.audio.AudioStreamType;
import ohos.media.audio.AudioSourceType;
import ohos.media.audio.AudioFormat;
import ohos.media.audio.AudioAttributes;
import ohos.media.audio.AudioAttributes.Builder;
import ohos.media.audio.AudioTrack;
import ohos.media.audio.AudioStream;
import ohos.media.audio.AudioTrackCallback;
import ohos.media.audio.AudioData;
import ohos.media.audio.AudioDataCallback;
import ohos.media.audio.AudioManager;
import ohos.media.audio.AudioStream;
import ohos.media.audio.AudioTrack;
import ohos.media.audio.AudioTrackCallback;
import ohos.media.audio.AudioTrackCallback;
import ohos.media.audio.AudioFormat;
import ohos.media.audio.AudioData;
import ohos.media.audio.AudioStream;
import ohos.media.audio.AudioTrack;
import ohos.media.audio.AudioTrackCallback;
import ohos.media.audio.AudioTrackCallback;
import java.io.IOException;
import java.io.InputStream;
import java.io.File;
import java.io.FileInputStream;
//有的写成public class MyAbility extends Ability也是可行的
public class MainAbilitySlice extends AbilitySlice {
private Video videoPlayer;
private Button buttonPlayVideo;
private Button buttonPlayAudio;
private AudioPlayer audioPlayer;
@Override
public void onStart(Intent intent) {
//这两步关键,鸿蒙无法直接展示xml布局文件,需要通过父类Ability来加载xml布局文件
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
videoPlayer = (Video) findComponentById(ResourceTable.Id_video_player);
buttonPlayVideo = (Button) findComponentById(ResourceTable.Id_button_play_video);
buttonPlayAudio = (Button) findComponentById(ResourceTable.Id_button_play_audio);
buttonPlayVideo.setClickedListener(component -> playVideo());
buttonPlayAudio.setClickedListener(component -> playAudio());
}
private void playVideo() {
String videoPath = "path_to_video.mp4"; // Update with actual video path
videoPlayer.setVideoPath(videoPath);
videoPlayer.start();
}
private void playAudio() {
String audioPath = "path_to_audio.mp3"; // Update with actual audio path
audioPlayer = new AudioPlayer();
audioPlayer.setAudioPath(audioPath);
audioPlayer.setStreamType(AudioStreamType.MUSIC);
audioPlayer.prepare();
audioPlayer.start();
}
@Override
public void onStop() {
super.onStop();
if (audioPlayer != null) {
audioPlayer.stop();
audioPlayer.release();
}
}
}
代码详细解释
IV. 音频播放实现
初始化 AudioPlayer:
在 playAudio() 方法中,我们首先创建一个 AudioPlayer 实例,设置音频文件路径,指定音频流类型为 MUSIC,并准备和开始播放音频。
private void playAudio() {
String audioPath = "path_to_audio.mp3"; // Update with actual audio path
audioPlayer = new AudioPlayer();
audioPlayer.setAudioPath(audioPath);
audioPlayer.setStreamType(AudioStreamType.MUSIC);
audioPlayer.prepare();
audioPlayer.start();
}
资源管理:
在 onStop() 方法中,我们停止并释放 AudioPlayer 实例,确保系统资源被正确释放。
@Override
public void onStop() {
super.onStop();
if (audioPlayer != null) {
audioPlayer.stop();
audioPlayer.release();
}
}
V. 视频播放实现
初始化 Video 组件:
在 playVideo() 方法中,我们设置视频路径,并调用 start() 方法开始播放视频。
private void playVideo() {
String videoPath = "path_to_video.mp4"; // Update with actual video path
videoPlayer.setVideoPath(videoPath);
videoPlayer.start();
}
更新布局:
在布局文件中定义 Video 组件用于展示视频内容,确保视频播放界面能够正确显示。
<Video
ohos:id="$+id/video_player"
ohos:width="match_parent"
ohos:height="200vp"
ohos:layout_marginBottom="16vp"
ohos:scale_type="centerCrop"/>