HarmonyOS6 - XComponent与AVPlayer实现视频播放功能

HarmonyOS6 - XComponent与AVPlayer实现视频播放功能

1. 自定义渲染 (XComponent)

1. 概述

XComponent组件作为一种渲染组件,可用于EGL/OpenGLES和媒体数据写入,通过使用XComponent持有的"NativeWindow"来渲染画面,通常用于满足开发者较为复杂的自定义渲染需求,例如相机预览流的显示和游戏画面的渲染。其可通过指定type字段来实现不同的渲染方式,分别为XComponentType.SURFACE和XComponentType.TEXTURE。对于SURFACE类型,开发者将定制的绘制内容单独展示到屏幕上。对于TEXTURE类型,开发者将定制的绘制内容和XComponent组件的内容合成后展示到屏幕上。

XComponent持有一个Surface,开发者能通过调用NativeWindow等接口,申请并提交Buffer至图形队列,以此方式将自绘制内容传送至该Surface。XComponent负责将此Surface整合进UI界面,其中展示的内容正是开发者传送的自绘制内容。Surface的默认位置与大小与XComponent组件一致,开发者可利用setXComponentSurfaceRect接口自定义调整Surface的位置和大小。XComponent组件负责创建Surface,并通过回调将Surface的相关信息告知应用。应用可以通过一系列接口设定Surface的属性。该组件本身不对所绘制的内容进行感知,亦不提供渲染绘制的接口。

目前XComponent组件主要有三个应用场景:

XComponent组件应用场景 场景简介 场景特点
使用XComponentController管理Surface生命周期场景 该场景在ArkTS侧的XComponentController获取SurfaceId,生命周期回调、触摸、鼠标、按键等事件回调等均在ArkTS侧触发。 适用于视频播放、相机预览等媒体播放类场景,该场景需要在ArkTS侧获取SurfaceId,并将SurfaceId传入对应接口。
使用OH_ArkUI_SurfaceHolder管理Surface生命周期场景 该场景根据XComponent组件对应的ArkUI_NodeHandle创建OH_ArkUI_SurfaceHolder,生命周期回调、触摸等事件回调、无障碍和可变帧率回调等均在Native侧触发。 适用于如下场景:1.有较复杂的交互逻辑、对频繁跨语言调用导致性能损耗敏感的场景。2.希望能控制Surface生命周期触发时机的场景。
使用NativeXComponent管理Surface生命周期场景 该场景在native层获取Native XComponent实例,在Native侧注册XComponent的生命周期回调,以及触摸、鼠标、按键等事件回调。 使用OH_ArkUI_SurfaceHolder管理Surface生命周期场景类似,但交互事件接口不够丰富,且使用不当容易出现稳定性问题,建议使用OH_ArkUI_SurfaceHolder的接口。

2. 约束与限制

当开发者传输的绘制内容包含透明元素时,Surface区域的显示效果会与下方内容进行合成展示。例如,若传输的内容完全透明,且XComponent的背景色被设置为黑色,同时Surface保持默认的大小与位置,则最终显示的将是一片黑色区域。

2. AVPlayer

1. 概述

当前提供两种视频播放开发的方案:

  • AVPlayer:功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。
  • Video组件:封装了视频播放的基础能力,需要设置数据源及基础信息即可播放视频,但相对扩展能力较弱。Video组件由ArkUI提供能力,相关指导请参考UI开发文档-Video组件

本开发指导将介绍如何使用AVPlayer开发视频播放功能,以完整播放一个视频作为示例,实现端到端播放原始媒体资源。

播放的全流程包含:创建AVPlayer,设置播放资源和窗口,设置播放参数(音量/倍速/缩放模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在视频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。

播放状态变化示意图

状态的详细说明请参考AVPlayerState。当播放处于prepared / playing / paused / completed状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。

2. 开发建议

当前指导仅介绍如何实现媒体资源播放,在应用开发过程中可能会涉及后台播放、播放冲突等情况,请根据实际需要参考以下说明。

  • 如果要实现后台播放或熄屏播放,需要接入AVSession(媒体会话)申请长时任务,避免播放被系统强制中断。
  • 应用在播放过程中,若播放的媒体数据涉及音频,根据系统音频管理策略(参考处理音频焦点事件),可能会被其他应用打断,建议应用主动监听音频打断事件,根据其内容提示,做出相应的处理,避免出现应用状态与预期效果不一致的问题。
  • 面对设备同时连接多个音频输出设备的情况,应用可以通过on('audioOutputDeviceChangeWithInfo')监听音频输出设备的变化,从而做出相应处理。
  • 如果需要访问在线媒体资源,需要申请 ohos.permission.INTERNET 权限。

3. 开发步骤及注意事项

(1)调用createAVPlayer()创建AVPlayer实例,初始化进入idle状态。

js 复制代码
import { media } from '@kit.MediaKit';

// 创建avPlayer实例对象。
let avPlayer = await media.createAVPlayer();

(2)设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:

事件类型 说明
stateChange 必要事件,监听播放器的state属性改变。需要播放器在idle状态下、未调用设置资源接口前完成设置监听,若在调用设置资源接口后再设置监听,可能导致无法收到资源设置过程中上报的stateChange事件。
error 必要事件,监听播放器的错误信息。需要播放器在idle状态下、未调用设置资源接口前完成设置监听,若在调用设置资源接口后再设置监听,可能导致无法收到资源设置过程中上报的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 用于视频播放,监听视频播放首帧渲染时间。当AVPlayer首次起播进入playing状态后,等到首帧视频画面被渲染到显示画面时,将上报该事件。应用通常可以利用此事件上报,进行视频封面移除,达成封面与视频画面的顺利衔接。
videoSizeChange 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。
audioInterrupt 监听音频焦点切换信息,搭配属性audioInterruptMode使用。如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。
js 复制代码
// 此处仅为示例,开发者根据需要设置合适的监听事件。
import { BusinessError } from '@kit.BasicServicesKit';
import { audio } from '@kit.AudioKit';

avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('error', (error: BusinessError) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('durationUpdate', (duration: number) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('timeUpdate', (time:number) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('seekDone', (seekDoneTime:number) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('speedDone', (speed:number) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('volumeChange', (vol: number) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('bitrateDone', (bitrate:number) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('availableBitrates', (bitrates: Array<number>) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('startRenderFrame', () => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('videoSizeChange', (width: number, height: number) => {
    // 开发者根据需要写入业务逻辑。
});
avPlayer.on('audioInterrupt', (info: audio.InterruptEvent) => {
    // 开发者根据需要写入业务逻辑。
});

(3)设置资源:设置属性url,AVPlayer进入initialized状态。

说明:

下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:

js 复制代码
let url = 'https://xxx.xxx.xxx.mp4';
if (avPlayer == null) {
    return;
}
avPlayer.url = url;

(4)设置窗口:获取并设置属性SurfaceID,用于设置显示画面。

应用需要从XComponent组件获取surfaceID

js 复制代码
// 通过接口getXComponentSurfaceId获取surfaceId。
let surfaceId = '';
if (avPlayer == null) {
    return;
}
if (surfaceId === '') {
    return;
}
avPlayer.surfaceId = surfaceId;

(5)准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置缩放模式、音量等。

js 复制代码
import { BusinessError } from '@kit.BasicServicesKit';

avPlayer.prepare((err: BusinessError) => {
    if (err) {
        console.error('Failed to prepare,error message is :' + err.message);
    } else {
        console.info('Succeeded in preparing');
    }
});

(6)视频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。

js 复制代码
import { BusinessError } from '@kit.BasicServicesKit';

// 播放操作。
avPlayer.play().then(() => {
    console.info('Succeeded in playing');
}, (err: BusinessError) => {
    console.error('Failed to play,error message is :' + err.message);
});
// 暂停操作。
avPlayer.pause((err: BusinessError) => {
    if (err) {
        console.error('Failed to pause,error message is :' + err.message);
    } else {
        console.info('Succeeded in pausing');
    }
});
// 跳转操作。
let seekTime: number = 1000;
avPlayer.seek(seekTime, media.SeekMode.SEEK_PREV_SYNC);
// 停止操作。
avPlayer.stop((err: BusinessError) => {
    if (err) {
        console.error('Failed to stop,error message is :' + err.message);
    } else {
        console.info('Succeeded in stopping');
    }
});

(7)(可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。

js 复制代码
import { BusinessError } from '@kit.BasicServicesKit';

avPlayer.reset((err: BusinessError) => {
    avPlayer.url = url;
    if (err) {
        console.error('Failed to reset,error message is :' + err.message);
    } else {
        console.info('Succeeded in resetting');
    }
});
// 更换url。
let url = 'https://xxx.xxx.xxx.mp4';
if (avPlayer == null) {
    return;
}
avPlayer.url = url;

(8)退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。

js 复制代码
import { BusinessError } from '@kit.BasicServicesKit';

avPlayer.release((err: BusinessError) => {
    if (err) {
        console.error('Failed to release,error message is :' + err.message);
    } else {
        console.info('Succeeded in releasing');
    }
});

更详细的API参考文档地址:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-media-avplayer

3. 实战案例

基于以上知识点,我们使用视频播放开发方案一,使用AVPlayer结合XComponent做一个实战页面

1. 效果

AVPlayer实现视频播放功能

2. 代码

js 复制代码
import display from '@ohos.display';
import emitter from '@ohos.events.emitter';
import { common } from '@kit.AbilityKit';
import media from '@ohos.multimedia.media';

const PROPORTION = 0.99; // 占屏幕比例
const SURFACE_W = 0.9; // 表面宽比例
const SURFACE_H = 1.78; // 表面高比例
const SET_INTERVAL = 100; // interval间隔时间
const TIME_ONE = 60000;
const TIME_TWO = 1000;
const SPEED_ZERO = 0;
const SPEED_ONE = 1;
const SPEED_TWO = 2;
const SPEED_THREE = 3;
const SPEED_COUNT = 4;
let innerEventFalse: emitter.InnerEvent = {
  eventId: 1,
  priority: emitter.EventPriority.HIGH
};
let innerEventTrue: emitter.InnerEvent = {
  eventId: 2,
  priority: emitter.EventPriority.HIGH
};
let innerEventWH: emitter.InnerEvent = {
  eventId: 3,
  priority: emitter.EventPriority.HIGH
};

/**
 * 视频播放案例
 */
@Entry
@Component
struct Page05 {
  tag: string = 'AVPlayManager';
  private xComponentController: XComponentController = new XComponentController();
  private avPlayer: media.AVPlayer | null = null;
  private surfaceId: string = '';
  private intervalID: number = -1;
  private seekTime: number = -1;
  private context: common.UIAbilityContext | undefined = undefined;
  @State title: Resource = $r('app.string.EntryAbility_label');
  @State fileName: string = 'test.mp4'; //rawfile目录下的视频文件
  @State isSwiping: boolean = false; // 用户滑动过程中
  @State isPaused: boolean = true; // 暂停播放
  @State XComponentFlag: boolean = false;
  @State speedSelect: number = 0; // 倍速选择
  @State speedList: Resource[] =
    [$r('app.string.video_speed_1_0X'), $r('app.string.video_speed_1_25X'), $r('app.string.video_speed_1_75X'),
      $r('app.string.video_speed_2_0X')];
  @StorageLink('durationTime') durationTime: number = 0; // 视频总时长
  @StorageLink('currentTime') currentTime: number = 0; // 视频当前时间
  @StorageLink('speedName') speedName: Resource = $r('app.string.video_speed_1_0X');
  @StorageLink('speedIndex') speedIndex: number = 0; // 倍速索引
  @State surfaceW: number | null = null;
  @State surfaceH: number | null = null;
  @State percent: number = 0;
  @State windowWidth: number = 300;
  @State windowHeight: number = 200;

  getDurationTime(): number {
    return this.durationTime;
  }

  getCurrentTime(): number {
    return this.currentTime;
  }

  timeConvert(time: number): string {
    let min: number = Math.floor(time / TIME_ONE);
    let second: string = ((time % TIME_ONE) / TIME_TWO).toFixed(0);
    // return `${min}:${(+second < TIME_THREE ? '0' : '') + second}`;
    second = second.padStart(2, '0');
    return `${min}:${second}`;
  }

  async msleepAsync(ms: number): Promise<boolean> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(true)
      }, ms)
    })
  }

  async avSetupVideo() {
    // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址。
    // 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度。
    if (this.context == undefined) {
      return;
    }
    let fileDescriptor = await this.context.resourceManager.getRawFd(this.fileName);
    let avFileDescriptor: media.AVFileDescriptor =
      { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };

    if (this.avPlayer) {
      console.info(`${this.tag}: init avPlayer release2createNew`);
      this.avPlayer.release();
      await this.msleepAsync(1500);
    }
    // 创建avPlayer实例对象
    this.avPlayer = await media.createAVPlayer();

    // 创建状态机变化回调函数
    await this.setAVPlayerCallback((avPlayer: media.AVPlayer) => {
      this.percent = avPlayer.width / avPlayer.height;
      this.setVideoWH();
      this.durationTime = this.getDurationTime();
      setInterval(() => { // 更新当前时间
        if (!this.isSwiping) {
          this.currentTime = this.getCurrentTime();
        }
      }, SET_INTERVAL);
    });

    // 为fdSrc赋值触发initialized状态机上报
    this.avPlayer.fdSrc = avFileDescriptor;
  }

  avPlay(): void {
    if (this.avPlayer) {
      try {
        this.avPlayer.play();
      } catch (e) {
        console.error(`${this.tag}: avPlay = ${JSON.stringify(e)}`);
      }
    }
  }

  avPause(): void {
    if (this.avPlayer) {
      try {
        this.avPlayer.pause();
        console.info(`${this.tag}: avPause==`);
      } catch (e) {
        console.error(`${this.tag}: avPause== ${JSON.stringify(e)}`);
      }
    }
  }

  async avSeek(seekTime: number, mode: SliderChangeMode): Promise<void> {
    if (this.avPlayer) {
      try {
        console.info(`${this.tag}: videoSeek  seekTime== ${seekTime}`);
        this.avPlayer.seek(seekTime, 2);
        this.currentTime = seekTime;
      } catch (e) {
        console.error(`${this.tag}: videoSeek== ${JSON.stringify(e)}`);
      }
    }
  }

  avSetSpeed(speed: number): void {
    if (this.avPlayer) {
      try {
        this.avPlayer.setSpeed(speed);
        console.info(`${this.tag}: avSetSpeed enum ${speed}`);
      } catch (e) {
        console.error(`${this.tag}: avSetSpeed == ${JSON.stringify(e)}`);
      }
    }
  }

  // 注册avplayer回调函数
  async setAVPlayerCallback(callback: (avPlayer: media.AVPlayer) => void, vType?: number): Promise<void> {
    // seek操作结果回调函数
    if (this.avPlayer == null) {
      console.error(`${this.tag}: avPlayer has not init!`);
      return;
    }
    this.avPlayer.on('seekDone', (seekDoneTime) => {
      console.info(`${this.tag}: setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    });
    this.avPlayer.on('speedDone', (speed) => {
      console.info(`${this.tag}: setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);
    });
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
    this.avPlayer.on('error', (err) => {
      console.error(`${this.tag}: setAVPlayerCallback Invoke avPlayer failed ${JSON.stringify(err)}`);
      if (this.avPlayer == null) {
        console.error(`${this.tag}: avPlayer has not init on error`);
        return;
      }
      this.avPlayer.reset();
    });
    // 状态机变化回调函数
    this.avPlayer.on('stateChange', async (state, reason) => {
      if (this.avPlayer == null) {
        console.info(`${this.tag}: avPlayer has not init on state change`);
        return;
      }
      switch (state) {
        case 'idle': // 成功调用reset接口后触发该状态机上报
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state idle called.`);
          break;
        case 'initialized': // avplayer 设置播放源后触发该状态上报
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state initialized called.`);
          if (this.surfaceId) {
            this.avPlayer.surfaceId = this.surfaceId; // 设置显示画面,当播放的资源为纯音频时无需设置
            console.info(`${this.tag}: setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);
            this.avPlayer.prepare();
          }
          break;
        case 'prepared': // prepare调用成功后上报该状态机
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state prepared called.`);
          this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
            console.info(`${this.tag}: bufferingUpdate called, infoType value: ${infoType}, value:${value}}`);
          })
          this.durationTime = this.avPlayer.duration;
          this.currentTime = this.avPlayer.currentTime;
          this.avPlayer.play(); // 调用播放接口开始播放
          console.info(`${this.tag}:
            setAVPlayerCallback speedSelect: ${this.speedSelect}, duration: ${this.durationTime}`);
          if (this.speedSelect != -1) {
            switch (this.speedSelect) {
              case SPEED_ZERO:
                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
                break;
              case SPEED_ONE:
                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
                break;
              case SPEED_TWO:
                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
                break;
              case SPEED_THREE:
                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
                break;
            }
          }
          callback(this.avPlayer);
          break;
        case 'playing': // play成功调用后触发该状态机上报
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state playing called.`);
          if (this.intervalID != -1) {
            clearInterval(this.intervalID)
          }
          this.intervalID = setInterval(() => { // 更新当前时间
            AppStorage.setOrCreate('durationTime', this.durationTime);
            AppStorage.setOrCreate('currentTime', this.currentTime);
          }, 100);
          let eventDataTrue: emitter.EventData = {
            data: {
              'flag': true
            }
          };
          let innerEventTrue: emitter.InnerEvent = {
            eventId: 2,
            priority: emitter.EventPriority.HIGH
          };
          emitter.emit(innerEventTrue, eventDataTrue);
          break;
        case 'completed': // 播放结束后触发该状态机上报
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state completed called.`);
          let eventDataFalse: emitter.EventData = {
            data: {
              'flag': false
            }
          };
          let innerEvent: emitter.InnerEvent = {
            eventId: 1,
            priority: emitter.EventPriority.HIGH
          };
          emitter.emit(innerEvent, eventDataFalse);
          if (this.intervalID != -1) {
            clearInterval(this.intervalID)
          }
          this.avPlayer.off('bufferingUpdate')
          AppStorage.setOrCreate('currentTime', this.durationTime);
          break;
        case 'released':
          console.info(`${this.tag}: setAVPlayerCallback released called.`);
          break
        case 'stopped':
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state stopped called.`);
          break
        case 'error':
          console.error(`${this.tag}: setAVPlayerCallback AVPlayer state error called.`);
          break
        case 'paused':
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state paused called.`);
          break
        default:
          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state unknown called.`);
          break;
      }
    });
    // 时间上报监听函数
    this.avPlayer.on('timeUpdate', (time: number) => {
      this.currentTime = time;
    });
  }

  aboutToAppear() {
    this.windowWidth = display.getDefaultDisplaySync().width;
    this.windowHeight = display.getDefaultDisplaySync().height;
    this.surfaceW = this.windowWidth * SURFACE_W;
    this.surfaceH = this.surfaceW / SURFACE_H;
    this.isPaused = true;
    this.context = getContext(this) as common.UIAbilityContext;
  }

  aboutToDisappear() {
    if (this.avPlayer == null) {
      console.info(`${this.tag}: avPlayer has not init aboutToDisappear`);
      return;
    }
    this.avPlayer.release((err) => {
      if (err == null) {
        console.info(`${this.tag}: videoRelease release success`);
      } else {
        console.error(`${this.tag}: videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);
      }
    });
    emitter.off(innerEventFalse.eventId);
  }

  onPageHide() {
    this.avPause();
    this.isPaused = false;
  }

  onPageShow() {
    emitter.on(innerEventTrue, (res: emitter.EventData) => {
      if (res.data) {
        this.isPaused = res.data.flag;
        this.XComponentFlag = res.data.flag;
      }
    });
    emitter.on(innerEventFalse, (res: emitter.EventData) => {
      if (res.data) {
        this.isPaused = res.data.flag;
      }
    });
    emitter.on(innerEventWH, (res: emitter.EventData) => {
      if (res.data) {
        this.windowWidth = res.data.width;
        this.windowHeight = res.data.height;
        this.setVideoWH();
      }
    });
  }

  setVideoWH(): void {
    if (this.percent >= 1) { // 横向视频
      this.surfaceW = Math.round(this.windowWidth * PROPORTION);
      this.surfaceH = Math.round(this.surfaceW / this.percent);
    } else { // 纵向视频
      this.surfaceH = Math.round(this.windowHeight * PROPORTION);
      this.surfaceW = Math.round(this.surfaceH * this.percent);
    }
  }

  @Builder
  CoverXComponent() {
    XComponent({
      // 装载视频容器
      id: 'xComponent',
      type: XComponentType.SURFACE,
      controller: this.xComponentController
    })
      .id('VideoView')
      .visibility(this.XComponentFlag ? Visibility.Visible : Visibility.Hidden)
      .onLoad(() => {
        this.surfaceId = this.xComponentController.getXComponentSurfaceId();
        this.avSetupVideo();
      })
      .height(`${this.surfaceH}px`)
      .width(`${this.surfaceW}px`)
  }

  build() {
    Column() {
      Stack() {
        Column() {
          this.CoverXComponent()
        }
        .align(Alignment.TopStart)
        .margin({ top: 80 })
        .id('VideoView')
        .justifyContent(FlexAlign.Center)

        Text()
          .height(`${this.surfaceH}px`)
          .width(`${this.surfaceW}px`)
          .margin({ top: 80 })
          .backgroundColor(Color.Black)
          .opacity($r('app.float.size_zero_five'))
          .visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden)

        Row() {
          Text(this.timeConvert(this.currentTime))
            .id("currentTime")
            .fontSize($r('app.float.size_24'))
            .opacity($r('app.float.size_1'))
            .fontColor($r("app.color.slider_selected"))
          Text("/" + this.timeConvert(this.durationTime))
            .id("durationTime")
            .fontSize($r('app.float.size_24'))
            .opacity($r('app.float.size_1'))
            .fontColor(Color.White)
        }
        .margin({ top: 80 })
        .visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden)

        Column() {

          Blank()

          Column() {
            // 进度条
            Row() {
              Row() {
                // 播放、暂停键
                Image(this.isPaused ? $r("app.media.ic_video_play") : $r("app.media.ic_video_pause"))// 暂停/播放
                  .id(this.isPaused ? 'pause' : 'play')
                  .width($r('app.float.size_40'))
                  .height($r('app.float.size_40'))
                  .onClick(() => {
                    if (this.isPaused) {
                      this.avPause();
                      this.isPaused = false;
                    } else {
                      this.avPlay();
                      this.isPaused = true;
                    }
                  })
                // 左侧时间
                Text(this.timeConvert(this.currentTime))
                  .id("currentTimeText")
                  .fontColor(Color.White)
                  .textAlign(TextAlign.End)
                  .fontWeight(FontWeight.Regular)
                  .margin({ left: $r('app.float.size_10') })
              }

              // 进度条
              Row() {
                Slider({
                  value: this.currentTime,
                  min: 0,
                  max: this.durationTime,
                  style: SliderStyle.OutSet
                })
                  .id('Slider')
                  .blockColor(Color.White)
                  .trackColor(Color.Gray)
                  .selectedColor($r("app.color.slider_selected"))
                  .showTips(false)
                  .onChange((value: number, mode: SliderChangeMode) => {
                    if (this.seekTime !== value) {
                      this.seekTime = value;
                      this.avSeek(Number.parseInt(value.toFixed(0)), mode);
                    }
                  })
              }
              .layoutWeight(1)

              Row() {
                // 右侧时间
                Text(this.timeConvert(this.durationTime))
                  .id("durationTimeText")
                  .fontColor(Color.White)
                  .fontWeight(FontWeight.Regular)

                // 倍速按钮
                Button(this.speedName, { type: ButtonType.Normal })
                  .border({ width: $r('app.float.size_1'), color: Color.White })
                  .width(75)
                  .height($r('app.float.size_40'))
                  .fontSize($r('app.float.size_15'))
                  .borderRadius($r('app.float.size_24'))
                  .fontColor(Color.White)
                  .backgroundColor(Color.Black)
                  .opacity($r('app.float.size_1'))
                  .margin({ left: $r('app.float.size_10') })
                  .id('Speed')
                  .onClick(() => {
                    this.speedIndex = (this.speedIndex + 1) % SPEED_COUNT;
                    this.speedSelect = this.speedIndex;
                    this.speedName = this.speedList[this.speedIndex];
                    if (!this.avPlayer) {
                      return;
                    }
                    switch (this.speedSelect) {
                      case 0:
                        this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);
                        break;
                      case 1:
                        this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);
                        break;
                      case 2:
                        this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);
                        break;
                      case 3:
                        this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
                        break;
                    }
                  })
              }
            }
            .justifyContent(FlexAlign.Center)
            .padding({ left: $r('app.float.size_25'), right: $r('app.float.size_30') })
            .width('100%')
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
        }
        .width('100%')
        .height('100%')
      }
      .backgroundColor(Color.Black)
      .height('90%')
      .width('100%')

      Row() {
        Text(this.title)
          .fontSize($r('app.float.size_20'))
          .fontColor(Color.White)
          .opacity($r('app.float.size_zero_six'))
          .fontWeight(FontWeight.Regular)
          .textAlign(TextAlign.Center)
      }
    }.backgroundColor(Color.Black)
    .height('100%')
    .width('100%')
  }
}
相关推荐
奋斗的小青年!!4 小时前
Flutter在OpenHarmony上实现渐变文字动画的深度优化实践
前端·flutter·harmonyos·鸿蒙
kirk_wang5 小时前
Flutter `share_plus` 库在鸿蒙 OHOS 平台的分享功能适配实践
flutter·移动开发·跨平台·arkts·鸿蒙
小学生波波5 小时前
HarmonyOS6 - ArkUI学习-下
鸿蒙·鸿蒙系统·arkui·鸿蒙开发·harmonyos6
世人万千丶5 小时前
鸿蒙跨端框架Flutter学习day 2、常用UI组件-折行布局 Wrap & Chip
学习·flutter·ui·华为·harmonyos·鸿蒙
小学生波波6 小时前
HarmonyOS6 - ArkUI学习-上
arkts·鸿蒙·arkui·鸿蒙开发·harmonyos6
奋斗的小青年!!6 小时前
Flutter跨平台开发适配鸿蒙:骨架屏,让加载不那么“煎熬“
flutter·harmonyos·鸿蒙
小雨下雨的雨18 小时前
Flutter 框架跨平台鸿蒙开发 —— SingleChildScrollView 控件之长内容滚动艺术
flutter·ui·华为·harmonyos·鸿蒙
小学生波波20 小时前
HarmonyOS6 - 调用第三方接口实现新闻APP
鸿蒙·鸿蒙系统·鸿蒙开发·harmonyos6·鸿蒙app
奋斗的小青年!!21 小时前
Flutter跨平台开发适配OpenHarmony:文件系统操作深度实践
flutter·harmonyos·鸿蒙