目录

Creating an Asynchronous Thread for AVTranscoder Video Transcoding (ArkTS)

During development, applications often create asynchronous threads to perform video transcoding tasks to meet various requirements, including:

Basic Concepts

The bit rate and resolution (width × height) of a video are two key factors that affect video quality and file size. Their relationship is not a simple linear correspondence but is influenced by multiple factors, including encoding efficiency and content complexity.

Choosing the Right Bit Rate and Resolution

Video Transcoding Development Example

import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } 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, // Audio bit rate.
        // audioCodec: media.CodecMimeType.AUDIO_AAC, // Audio encoding format.
        fileFormat: media.ContainerFormatType.CFT_MPEG_4, // Container format.
        videoBitrate: 1000000, // Video bit rate.
        videoCodec: media.CodecMimeType.VIDEO_AVC, // Video encoding format.
        videoFrameWidth: 1280, // Video frame width.
        videoFrameHeight: 720  // Video frame height.
    };
    // Set AVTranscoder callback functions.
    setAVTranscoderCallback() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder")) {
            if (this.avTranscoder != undefined) {
                // Callback function for the completion of transcoding.
                this.avTranscoder.on('complete', async () => {
                    console.log(`AVTranscoder is completed`);
                    await this.releaseTranscoderingProcess();
                });
                // Callback function for errors.
                this.avTranscoder.on('error', (err: BusinessError) => {
                    console.error(`AVTranscoder failed, code is ${err.code}, message is ${err.message}`);
                });
            }
        }
    }
    // Process of starting transcoding.
    async startTranscoderingProcess() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder") && this.context != undefined) {
            if (this.avTranscoder != undefined) {
                await this.avTranscoder.release();
                this.avTranscoder = undefined;
            }
            // 1. Create an avTranscoder instance.
            this.avTranscoder = await media.createAVTranscoder();
            this.setAVTranscoderCallback();
            // 2. Obtain the source file FD and output file FD and assign them to avTranscoder. For details, see the FilePicker document.
            let fileDescriptor = await this.context.resourceManager.getRawFd('H264_AAC.mp4');
            this.avTranscoder.fdSrc = fileDescriptor;
            let outputFilePath = this.context.filesDir + "/output.mp4";
            let file = fs.openSync(outputFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
            this.avTranscoder.fdDst = file.fd;
           // 3. Set transcoding parameters to complete the preparations.
            await this.avTranscoder.prepare(this.avConfig);
            // 4. Start transcoding.
            await this.avTranscoder.start();
        }
    }
    // Process of pausing transcoding.
    async pauseTranscoderingProcess() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder")) {
            if (this.avTranscoder != undefined) { // It is reasonable to call pause only after start is called and returns a value.
                await this.avTranscoder.pause();
            }
        }
    }
    // Resume the transcoding process.
    async resumeTranscoderingProcess() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder")) {
            if (this.avTranscoder != undefined) { // It is reasonable to call resume only after pause is called and returns a value.
                await this.avTranscoder.resume();
            }
        }
    }
    // Release the transcoding process.
    async releaseTranscoderingProcess() {
        if (canIUse("SystemCapability.Multimedia.Media.AVTranscoder")) {
            if (this.avTranscoder != undefined) {
                // 1. Release the avTranscoder instance.
                await this.avTranscoder.release();
                this.avTranscoder = undefined;
                // 2. Close the FD of the output file.
            }
        }
    }
    // A complete example of starting, pausing, resuming, and exiting transcoding.
    async avTranscoderDemo() {
        await this.startTranscoderingProcess(); // Start transcoding.
        await this.pauseTranscoderingProcess(); // Pause transcoding.
        await this.resumeTranscoderingProcess(); // Resume transcoding.
        await this.releaseTranscoderingProcess(); // Exit transcoding.
    }
}

For details about how to use the transcoding capability to transcode videos, see Using AVTranscoder to Transcode Videos.

Using Asynchronous Threads for Transcoding

This example uses a Worker thread to perform asynchronous transcoding. For details about how to use Worker threads, refer to the following documents:

[h2]How to Develop

  1. Import header files, create a Worker thread, and register callback functions.

    import { ErrorEvent, MessageEvents, worker } from '@kit.ArkTS'
    import { SendableObject } from '../util/SendableObject';
    import { common, sendableContextManager } from '@kit.AbilityKit';
    // Create a Worker object.
    private workerInstance?: worker.ThreadWorker;
    @State currentProgress: number = 0;
    
    this.workerInstance = new worker.ThreadWorker('entry/ets/workers/task.ets');
    
    // Register the onmessage callback. When the host thread receives a message from the Worker thread through the workerPort.postMessage interface, this callback is invoked and executed in the host thread.
    this.workerInstance.onmessage = (e: MessageEvents) => {
       let data: string | number = e.data;
       if (typeof data === 'string') { // Complete event.
         console.info('workerInstance onmessage is: ', data);
         if (data === 'complete') {
           console.info('complete: ', data);
           this.workerInstance?.terminate();
         }
       } else if (typeof data === 'number') {
         this.currentProgress = data; // Current progress.
       }
     }
    
    // Register the onErrors callback to capture global exceptions generated during the onmessage callback, timer callback, and file execution of the Worker thread. This callback is executed in the host thread.
    this.workerInstance.onerror = (err: ErrorEvent) => {
        console.info("workerInstance onerror message is: " + err.message);
    }
    
    // Register the onmessageerror callback. When the Worker object receives a message that cannot be serialized, this callback is invoked and executed in the host thread.
    this.workerInstance.onmessageerror = () => {
        console.info('workerInstance onmessageerror');
    }
    
    // Register the onexit callback. When the Worker object is destroyed, this callback is invoked and executed in the host thread.
    this.workerInstance.onexit = (e: number) => {
        // When the Worker object exits normally, the code is 0. When the Worker object exits abnormally, the code is 1.
        console.info("workerInstance onexit code is: ", e);
    }
  2. Create a parameter object and send it to the Worker thread.

    The parameter object model is as follows:

    import { sendableContextManager } from '@kit.AbilityKit';
    
    // Decorate the parameters with @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;
      }
    }

    The logic for sending parameters is as follows:

    private context: Context | undefined;
    // Send a message to the Worker thread.
    this.context = this.getUIContext().getHostContext(); // Obtain the context of the ability to which the current component belongs.
    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. The Worker thread receives the parameters and executes the transcoding logic.

    Receiving parameters:

    // Receive the parameters.
    const sendableObject: SendableObject = event.data;
    const sendableContext: sendableContextManager.SendableContext =
      sendableObject.getSendableContext() as sendableContextManager.SendableContext;
    const context: common.Context =
      sendableContextManager.convertToContext(sendableContext) as common.Context;
    // Execute the transcoding logic.
    await doSome(context);
    // Send a message to the main thread.
    workerPort.postMessage('start end');

    Executing transcoding logic:

    async function doSome(context: common.Context) {
      console.info(`doSome in`);
      try {
        let transcoder = await media.createAVTranscoder();
        // Callback function for the completion of transcoding.
        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');
        })
        // Callback function for transcoding errors.
        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);
          }
        })
        // Callback function for updating the transcoding progress.
        transcoder.on('progressUpdate', (progress: number) => {
          console.info(`AVTranscoder progressUpdate = ${progress}`);
          workerPort.postMessage(progress);
        })
    
        try {
          // Obtain the file descriptor of the input file. H264_AAC.mp4 is a preset resource in the rawfile directory. Replace it with the actual one.
          let fileDescriptor = await context.resourceManager.getRawFd('H264_AAC.mp4');
          transcoder.fdSrc = fileDescriptor; // Set fdSrc.
        } catch (error) {
          console.error('Failed to get the file descriptor, please check the resource and path.');
        }
        // Set the output file path. context.filesDir is the sandbox path of the application.
        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, // Container format.
          audioCodec: media.CodecMimeType.AUDIO_AAC, // Audio encoding format.
          videoCodec: media.CodecMimeType.VIDEO_AVC, // Video encoding format.
          videoBitrate: 200000, // Video bit rate.
        }
        await transcoder?.prepare(config);
        await transcoder?.start();
      } catch (e) {
        console.error(`transcode error: code = ` + e.code.toString() + `, message = ${JSON.stringify(e.message)}`);
      }
    }
  4. Listen for transcoding completion events and send a message to the main thread when transcoding is complete.

    // Callback function for the completion of transcoding.
    transcoder.on('complete', async () => {
      console.info(`transcode complete`);
      fileIo.closeSync(transcoder.fdDst); // Close fdDst.
      await transcoder?.release()
      workerPort.postMessage('complete');
    })
  5. The main thread receives the message from the Worker thread indicating transcoding completion and destroys the Worker thread.

    // Register the onmessage callback. When the host thread receives a message from the Worker thread through the workerPort.postMessage interface, this callback is invoked and executed in the host thread.
    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;
      }
    }

Running the Sample Project

Refer to the sample project to use a Worker thread to implement asynchronous transcoding.

  1. Create a project, download the sample project, and copy its resources to the corresponding directories.
    AsyncTranscoder
    entry/build-profile.json5 (Configure fields to package Worker thread files into the application.)
    entry/src/main/ets/
    ├── pages
    │    └── Index.ets (Transcoding UI)
    ├── util
    │    └── SendableObject.ets (Sendable object)
    │
    └── workers
        └── task.ets (Transcoding task)
    
    entry/src/main/resources/
    ├── base
    │   ├── element
    │   │   ├── color.json
    │   │   ├── float.json
    │   │   └── string.json
    │   └── media
    │
    └── rawfile
        └── H264_AAC.mp4 (Video resource)
  2. Compile and run the project.