鸿蒙Next开发日记 Day02 - 使用AVPlayer制作一个简易播放器

avplayer.gif

AVPlayer:功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。

1、创建播放器的相关回调

// 注册avplayer回调函数
setAVPlayerCallback(avPlayer: media.AVPlayer) {
  // startRenderFrame首帧渲染回调函数
  avPlayer.on('startRenderFrame', () => {
    console.info(`AVPlayer start render frame`);
  })
  // seek操作结果回调函数
  avPlayer.on('seekDone', (seekDoneTime: number) => {
    console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
  })
  // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
  avPlayer.on('error', (err: BusinessError) => {
    console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
    avPlayer.reset(); // 调用reset重置资源,触发idle状态
  })
  avPlayer.on('videoSizeChange', (width: number, height: number) => {
    // 获取当前屏幕尺寸
    const whData = ScreenUtil.getScreenSize() as ScreenWidthHeightData
    if (whData.error) {
      UiUtils.toast(whData.error)
    } else {
      // 计算视频展示区域
      const screenWidth = whData.width
      const screenHeight = whData.height
      const videoWidth = width
      const videoHeight = height
      // 计算视频的宽高比
      const videoAspectRatio = videoWidth / videoHeight;

      // 根据屏幕宽高比和视频宽高比,确定展示组件的宽高
      let displayWidth: number, displayHeight: number;

      // 屏幕宽高比大于视频宽高比时,以屏幕高度为基准计算宽度
      if (screenWidth / screenHeight > videoAspectRatio) {
        displayHeight = screenHeight;
        displayWidth = screenHeight * videoAspectRatio;
      } else {
        // 屏幕宽高比小于视频宽高比时,以屏幕宽度为基准计算高度
        displayWidth = screenWidth;
        displayHeight = screenWidth / videoAspectRatio;
      }
      console.log(screenWidth + '', screenHeight + '', videoWidth + '', videoHeight + '', displayWidth + '', displayHeight + '')
      this.xComController.setXComponentSurfaceRect({ surfaceWidth: displayWidth, surfaceHeight: displayHeight });
    }
  });
  // 状态机变化回调函数
  avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
    switch (state) {
      case 'idle': // 成功调用reset接口后触发该状态机上报
        console.info('AVPlayer state idle called.');
        avPlayer.release(); // 调用release接口销毁实例对象
        break;
      case 'initialized': // avplayer 设置播放源后触发该状态上报
        console.info('AVPlayer state initialized called.');
        avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置
        avPlayer.prepare();
        break;
      case 'prepared': // prepare调用成功后上报该状态机
        console.info('AVPlayer state prepared called.');
        avPlayer.play(); // 调用播放接口开始播放
        break;
      case 'playing': // play成功调用后触发该状态机上报
        console.info('AVPlayer state playing called.');
        break;
      case 'paused': // pause成功调用后触发该状态机上报
        console.info('AVPlayer state paused called.');
        avPlayer.play(); // 再次播放接口开始播放
        break;
      case 'completed': // 播放结束后触发该状态机上报
        console.info('AVPlayer state completed called.');
        avPlayer.stop(); //调用播放结束接口
        break;
      case 'stopped': // stop接口成功调用后触发该状态机上报
        console.info('AVPlayer state stopped called.');
        avPlayer.reset(); // 调用reset接口初始化avplayer状态
        break;
      case 'released':
        console.info('AVPlayer state released called.');
        break;
      default:
        console.info('AVPlayer state unknown called:' + state);
        break;
    }
  })
}

2、渲染方法里面使用XComponent组件承载播放器画面。

XComponent组件作为一种绘制组件,通常用于满足开发者较为复杂的自定义绘制需求,例如相机预览流的显示和游戏画面的绘制。

setXComponentSurfaceRect设置视频绘制组件的大小不是必须的,在videoSizeChange回调中,我们可以获取到当前的视频尺寸,再根据当前手机尺寸,计算并设置视频绘制组件的宽高。

build() {
  Column() {
    XComponent({
      id: 'videoXComponent',
      type: 'surface',
      controller: this.xComController
    })
      .onLoad(async () => {
        this.xComController.setXComponentSurfaceRect({ surfaceWidth: 0, surfaceHeight: 0 }); // 设置的长宽不是最终的
        this.surfaceID = this.xComController.getXComponentSurfaceId()
        await this.preDownloadDemo()
      })
  }
  .width("100%")
}

3、加载视频并播放

async preDownloadDemo() {
  // 创建avPlayer实例对象
  this.avPlayer = await media.createAVPlayer();
  this.setAVPlayerCallback(this.avPlayer)
  // 设置媒体来源和播放策略
  this.avPlayer.url = this.mediaUrl
}

上面是一个加载网络视频的方法,通过avPlayer.url设置过视频地址后,会回调initialized,继续后续操作。

4、添加页面销毁时的播放器释放

aboutToDisappear(): void {
  // 销毁播放器
  if (this.avPlayer) {
    this.avPlayer.release()
  }
}

完整代码如下

import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { router } from '@kit.ArkUI';
import { ScreenUtil } from '../../utils/ScreenUtil';
import { ScreenWidthHeightData } from '../../viewmodel/common/ScreenWidthHeightData';
import UiUtils from '../../utils/UiUtil';

@Entry
@Component
struct Player {
  xComController: XComponentController = new XComponentController()
  private surfaceID: string = ''; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法

  @State isPlaying: boolean = false;

  avPlayer: media.AVPlayer | undefined = undefined

  routerParams: Record<string, Object> = router.getParams() as Record<string, Object>; // 获取传递过来的参数对象

  @Prop mediaUrl: string = '你的视频地址';
  // 注册avplayer回调函数
  setAVPlayerCallback(avPlayer: media.AVPlayer) {
    // startRenderFrame首帧渲染回调函数
    avPlayer.on('startRenderFrame', () => {
      console.info(`AVPlayer start render frame`);
    })
    // seek操作结果回调函数
    avPlayer.on('seekDone', (seekDoneTime: number) => {
      console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    })
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
    avPlayer.on('error', (err: BusinessError) => {
      console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
      avPlayer.reset(); // 调用reset重置资源,触发idle状态
    })
    avPlayer.on('videoSizeChange', (width: number, height: number) => {
      // 获取当前屏幕尺寸
      const whData = ScreenUtil.getScreenSize() as ScreenWidthHeightData
      if (whData.error) {
        UiUtils.toast(whData.error)
      } else {
        // 计算视频展示区域
        const screenWidth = whData.width
        const screenHeight = whData.height
        const videoWidth = width
        const videoHeight = height
        // 计算视频的宽高比
        const videoAspectRatio = videoWidth / videoHeight;

        // 根据屏幕宽高比和视频宽高比,确定展示组件的宽高
        let displayWidth: number, displayHeight: number;

        // 屏幕宽高比大于视频宽高比时,以屏幕高度为基准计算宽度
        if (screenWidth / screenHeight > videoAspectRatio) {
          displayHeight = screenHeight;
          displayWidth = screenHeight * videoAspectRatio;
        } else {
          // 屏幕宽高比小于视频宽高比时,以屏幕宽度为基准计算高度
          displayWidth = screenWidth;
          displayHeight = screenWidth / videoAspectRatio;
        }
        console.log(screenWidth + '', screenHeight + '', videoWidth + '', videoHeight + '', displayWidth + '', displayHeight + '')
        this.xComController.setXComponentSurfaceRect({ surfaceWidth: displayWidth, surfaceHeight: displayHeight });
      }
    });
    // 状态机变化回调函数
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      switch (state) {
        case 'idle': // 成功调用reset接口后触发该状态机上报
          console.info('AVPlayer state idle called.');
          avPlayer.release(); // 调用release接口销毁实例对象
          break;
        case 'initialized': // avplayer 设置播放源后触发该状态上报
          console.info('AVPlayer state initialized called.');
          avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置
          avPlayer.prepare();
          break;
        case 'prepared': // prepare调用成功后上报该状态机
          console.info('AVPlayer state prepared called.');
          avPlayer.play(); // 调用播放接口开始播放
          break;
        case 'playing': // play成功调用后触发该状态机上报
          console.info('AVPlayer state playing called.');
          break;
        case 'paused': // pause成功调用后触发该状态机上报
          console.info('AVPlayer state paused called.');
          avPlayer.play(); // 再次播放接口开始播放
          break;
        case 'completed': // 播放结束后触发该状态机上报
          console.info('AVPlayer state completed called.');
          avPlayer.stop(); //调用播放结束接口
          break;
        case 'stopped': // stop接口成功调用后触发该状态机上报
          console.info('AVPlayer state stopped called.');
          avPlayer.reset(); // 调用reset接口初始化avplayer状态
          break;
        case 'released':
          console.info('AVPlayer state released called.');
          break;
        default:
          console.info('AVPlayer state unknown called:' + state);
          break;
      }
    })
  }

  pause() {
    if (this.avPlayer) {
      this.avPlayer.pause()
      this.isPlaying = false
    }
  }

  play() {
    if (this.avPlayer && !this.isPlaying ) {
      this.avPlayer.play()
      this.isPlaying = true;
    }
  }

  aboutToDisappear(): void {
    // 销毁播放器
    if (this.avPlayer) {
      this.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')
  }

  // 以下demo为通过setMediaSource设置自定义头域及媒体播放优选参数实现初始播放参数设置
  async preDownloadDemo() {
    // 创建avPlayer实例对象
    this.avPlayer = await media.createAVPlayer();
    this.setAVPlayerCallback(this.avPlayer)
    // 设置媒体来源和播放策略
    this.avPlayer.url = this.mediaUrl
  }

  build() {
    Column() {
      XComponent({
        id: 'videoXComponent',
        type: 'surface',
        controller: this.xComController
      })
        .onLoad(async () => {
          this.xComController.setXComponentSurfaceRect({ surfaceWidth: 0, surfaceHeight: 0 }); // 设置的长宽不是最终的
          this.surfaceID = this.xComController.getXComponentSurfaceId()
          await this.preDownloadDemo()
        })
    }
    .width("100%")
  }
}

export default Player
export class ScreenWidthHeightData {
  width: number = 0
  height: number = 0
  error: string | undefined = undefined
}
import { display } from '@kit.ArkUI';
import { ScreenWidthHeightData } from '../viewmodel/common/ScreenWidthHeightData';

export class ScreenUtil {
  /**
   * 获取屏幕尺寸
   * @returns
   */
  static getScreenSize(): ScreenWidthHeightData | string {
    let displayClass: display.Display | null = null;
    try {
      displayClass = display.getDefaultDisplaySync()
      const data: ScreenWidthHeightData = {
        width: displayClass.width,
        height: displayClass.height,
        error: undefined
      }
      return data
    } catch (exception) {
      console.error('error:' + JSON.stringify(exception))
      return {
        width: 0,
        height: 0,
        error: '[异常]' + JSON.stringify(exception)
      }
    }
  }
}


关键词:鸿蒙NEXT

网友留言(0 条)

发表评论

验证码