鸿蒙实现视频播放功能

发布于:2024-12-06 ⋅ 阅读:(28) ⋅ 点赞:(0)

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"/>

网站公告

今日签到

点亮在社区的每一天
去签到