创建异步线程执行AVTranscoder视频转码(ArkTS)

在开发过程中,应用经常会创建异步线程执行视频转码任务以满足不同诉求,主要包括:

基础概念

视频的码率(Bitrate)和分辨率(宽×高)是影响视频画质和文件大小的两个关键因素。它们之间的关系并非简单的线性对应,而是受到编码效率、内容复杂度等多种因素的共同影响。

选择合适的码率和分辨率

视频转码压缩开发样例

import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
export class AVTranscoderDemo {
    private avTranscoder: media.AVTranscoder | undefined = undefined;
    private context: Context | undefined;
    constructor(context: Context) {
        this.context = context;
    }
    private avConfig: media.AVTranscoderConfig = {
        // audioBitrate: 100000, // 音频比特率。
        // audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式。
        fileFormat: media.ContainerFormatType.CFT_MPEG_4, // 封装格式。
        videoBitrate: 1000000, // 视频比特率。
        videoCodec: media.CodecMimeType.VIDEO_AVC, // 视频编码格式。
        videoFrameWidth: 1280, // 视频分辨率的宽。
        videoFrameHeight: 720  // 视频分辨率的高。
    };
    // 注册avTranscoder回调函数。
    setAVTranscoderCallback() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder")) {
            if (this.avTranscoder != undefined) {
                // 转码完成回调函数。
                this.avTranscoder.on('complete', async () => {
                    console.info(`AVTranscoder is completed`);
                    await this.releaseTranscoderingProcess();
                });
                // 错误上报回调函数。
                this.avTranscoder.on('error', (err: BusinessError) => {
                    console.error(`AVTranscoder failed, code is ${err.code}, message is ${err.message}`);
                });
            }
        }
    }
    // 开始转码对应的流程。
    async startTranscoderingProcess() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder") && this.context != undefined) {
            if (this.avTranscoder != undefined) {
                await this.avTranscoder.release();
                this.avTranscoder = undefined;
            }
            // 1.创建转码实例。
            this.avTranscoder = await media.createAVTranscoder();
            this.setAVTranscoderCallback();
            // 2.获取转码源文件fd和目标文件fd赋予avTranscoder;参考FilePicker文档。
            let fileDescriptor = await this.context.resourceManager.getRawFd('H264_AAC.mp4');
            this.avTranscoder.fdSrc = fileDescriptor;
            let outputFilePath = this.context.filesDir + "/output.mp4";
            let file = fileIo.openSync(outputFilePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
            this.avTranscoder.fdDst = file.fd;
            // 3.配置转码参数完成准备工作。
            await this.avTranscoder.prepare(this.avConfig);
            // 4.开始转码。
            await this.avTranscoder.start();
        }
    }
    // 暂停转码对应的流程。
    async pauseTranscoderingProcess() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder")) {
            if (this.avTranscoder != undefined) { // 仅在调用start返回后调用pause为合理调用。
                await this.avTranscoder.pause();
            }
        }
    }
    // 恢复对应的转码流程。
    async resumeTranscoderingProcess() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder")) {
            if (this.avTranscoder != undefined) { // 仅在调用pause返回后调用resume为合理调用。
                await this.avTranscoder.resume();
            }
        }
    }
    // 释放转码流程。
    async releaseTranscoderingProcess() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder")) {
            if (this.avTranscoder != undefined) {
                // 1.释放转码实例。
                await this.avTranscoder.release();
                this.avTranscoder = undefined;
                // 2.关闭转码目标文件fd。
            }
        }
    }
    // 一个完整的【开始转码-暂停转码-恢复转码-转码完成】示例。
    async avTranscoderDemo() {
        await this.startTranscoderingProcess(); // 开始转码。
        await this.pauseTranscoderingProcess(); // 暂停转码。
        await this.resumeTranscoderingProcess(); // 恢复转码。
        await this.releaseTranscoderingProcess(); // 释放转码。
    }
}

具体如何使用转码能力对视频进行转码,可以参见文档:用AVTranscoder实现视频转码。

使用异步线程的方式进行转码

本示例使用的是worker线程的方式来实现异步线程进行转码,worker线程的详细使用方式,可以参见文档:

[h2]开发步骤

  1. 引入头文件,创建worker线程,并注册回调。

    import { ErrorEvent, MessageEvents, worker } from '@kit.ArkTS'
    import { SendableObject } from '../util/SendableObject';
    import { common, sendableContextManager } from '@kit.AbilityKit';
    // 创建Worker对象。
    private workerInstance?: worker.ThreadWorker;
    @State currentProgress: number = 0;
    
    this.workerInstance = new worker.ThreadWorker('entry/ets/workers/task.ets');
    
    // 注册onmessage回调,当宿主线程接收到来自其创建的Worker通过workerPort.postMessage接口发送的消息时被调用,在宿主线程执行。
    this.workerInstance.onmessage = (e: MessageEvents) => {
       let data: string | number = e.data;
       if (typeof data === 'string') { // complete事件。
         console.info('workerInstance onmessage is: ', data);
         if (data === 'complete') {
           console.info('complete: ', data);
           this.workerInstance?.terminate();
         }
       } else if (typeof data === 'number') {
         this.currentProgress = data; // 当前进度。
       }
     }
    
    // 注册onErrors回调,可以捕获Worker线程的onmessage回调、timer回调以及文件执行等流程产生的全局异常,在宿主线程执行。
    this.workerInstance.onerror = (err: ErrorEvent) => {
        console.info("workerInstance onerror message is: " + err.message);
    }
    
    // 注册onmessageerror回调,当Worker对象接收到一条无法被序列化的消息时被调用,在宿主线程执行。
    this.workerInstance.onmessageerror = () => {
        console.info('workerInstance onmessageerror');
    }
    
    // 注册onexit回调,当Worker销毁时被调用,在宿主线程执行。
    this.workerInstance.onexit = (e: number) => {
        // 当Worker正常退出时code为0,异常退出时code为1。
        console.info("workerInstance onexit code is: ", e);
    }
  2. 创建参数对象,向worker线程发送参数对象。

    如下是参数对象模型:

    import { sendableContextManager } from '@kit.AbilityKit';
    
    // 发送的参数必须加上@Sendable标注。
    @Sendable
    export class SendableObject {
      constructor(sendableContext: sendableContextManager.SendableContext, data: string = '') {
        this.sendableContext = sendableContext;
        this.data = data;
      }
    
      private sendableContext: sendableContextManager.SendableContext;
      private data: string;
    
    
      public getSendableContext() {
        return this.sendableContext;
      }
    
      public getData() {
        return this.data;
      }
    }

    如下是发送参数的逻辑:

    private context: Context | undefined;
    // 向Worker线程发送消息。
    this.context = this.getUIContext().getHostContext(); // 获取当前组件所在Ability的Context。
    if (this.context != undefined) {
      const sendableContext: sendableContextManager.SendableContext = sendableContextManager.convertFromContext(
        this.context);
      const sendableObject: SendableObject = new SendableObject(sendableContext, 'some information');
      this.workerInstance.postMessageWithSharedSendable(sendableObject);
    }
  3. worker线程接收参数,并且执行转码的逻辑。

    worker线程接收参数:

    //worker线程接收参数。
    const sendableObject: SendableObject = event.data;
    const sendableContext: sendableContextManager.SendableContext =
      sendableObject.getSendableContext() as sendableContextManager.SendableContext;
    const context: common.Context =
      sendableContextManager.convertToContext(sendableContext) as common.Context;
    // 执行转码逻辑。
    await doSome(context);
    // 向主线程发送消息。
    workerPort.postMessage('start end');

    执行转码逻辑:

    async function doSome(context: common.Context) {
      console.info(`doSome in`);
      try {
        let transcoder = await media.createAVTranscoder();
        // 转码完成回调函数。
        transcoder.on('complete', async () => {
          console.info(`transcode complete`);
          await transcoder?.release()
          if (transcoder.fdDst != undefined) {
            fs.closeSync(transcoder.fdDst);
          }
          if (transcoder.fdSrc != undefined) {
            fs.closeSync(transcoder.fdSrc.fd);
          }
          workerPort.postMessage('complete');
        })
        // 转码错误回调函数。
        transcoder.on('error', async (err: BusinessError) => {
          await transcoder?.release();
          if (transcoder.fdDst != undefined) {
            fs.closeSync(transcoder.fdDst);
          }
          if (transcoder.fdSrc != undefined) {
            fs.closeSync(transcoder.fdSrc.fd);
          }
        })
        // 转码进度更新回调函数。
        transcoder.on('progressUpdate', (progress: number) => {
          console.info(`AVTranscoder progressUpdate = ${progress}`);
          workerPort.postMessage(progress);
        })
    
        try {
          // 获取输入文件fd,H264_AAC.mp4为rawfile目录下的预置资源,需要开发者根据实际情况进行替换。
          let fileDescriptor = await context.resourceManager.getRawFd('H264_AAC.mp4');
          transcoder.fdSrc = fileDescriptor; // 设置fdSrc。
        } catch (error) {
          console.error('Failed to get the file descriptor, please check the resource and path.');
        }
        // 设置输出文件路径,context.filesDir为应用的沙箱路径。
        let fdPath = context.filesDir + "/" + "VID_" + Date.parse(new Date().toString()) + ".mp4";
        let file = fileIo.openSync(fdPath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
        let fd = file.fd;
        console.info(`file fd ${fd}`);
        transcoder.fdDst = file.fd;
    
        let config: media.AVTranscoderConfig = {
          fileFormat: media.ContainerFormatType.CFT_MPEG_4, // 封装格式。
          audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式。
          videoCodec: media.CodecMimeType.VIDEO_AVC, // 视频编码格式。
          videoBitrate: 200000, // 视频比特率。
        }
        await transcoder?.prepare(config);
        await transcoder?.start();
      } catch (e) {
        console.error(`transcode error: code = ` + e.code.toString() + `, message = ${JSON.stringify(e.message)}`);
      }
    }
  4. 监听转码的Complete回调,在转码结束的时候向主线程发送消息。

    // 转码完成回调函数。
    transcoder.on('complete', async () => {
      console.info(`transcode complete`);
      fileIo.closeSync(transcoder.fdDst); // 关闭fdDst。
      await transcoder?.release()
      workerPort.postMessage('complete');
    })
  5. 主线程接收到worker线程转码结束的信息,销毁worker线程。

    // 注册onmessage回调,当宿主线程接收到来自其创建的Worker通过workerPort.postMessage接口发送的消息时被调用,在宿主线程执行。
    this.workerInstance.onmessage = (e: MessageEvents) => {
      let data: string | number = e.data;
      if (typeof data === 'string') {
        console.info('workerInstance onmessage is: ', data);
        if (data === 'complete') {
          console.info('complete: ', data);
          this.workerInstance?.terminate();
        }
      } else if (typeof data === 'number') {
        this.currentProgress = data;
      }
    }

运行示例工程

参考以下示例,使用worker线程的方式来实现异步线程进行转码。

  1. 新建工程,下载完整示例工程,并将示例工程的资源复制到对应目录。
    AsyncTranscoder
    entry/build-profile.json5 (配置字段信息将Worker线程文件打包到应用)
    entry/src/main/ets/
    ├── pages
    │    └── Index.ets (转码界面)
    ├── util
    │    └── SendableObject.ets (Sendable对象)
    │
    └── workers
        └── task.ets (转码任务)
    
    entry/src/main/resources/
    ├── base
    │   ├── element
    │   │   ├── color.json
    │   │   ├── float.json
    │   │   └── string.json
    │   └── media
    │
    └── rawfile
        └── H264_AAC.mp4 (视频资源)
  2. 编译新建工程并运行。