开天辟地 HarmonyOS(鸿蒙) - 媒体: AVTranscoder(视频转码)

源码 https://github.com/webabcd/HarmonyDemo
作者 webabcd

开天辟地 HarmonyOS(鸿蒙) - 媒体: AVTranscoder(视频转码)

示例如下:

pages\media\AVTranscoderDemo.ets

/*
 * AVTranscoder - 视频转码
 *
 * 先创建 worker 线程文件
 * 1、在 /src/main/ets/ 目录层级之下创建一个 .ets 文件,本例创建的文件是 /src/main/ets/workers/transcodeworker.ets
 * 2、编辑 build-profile.json5 文件,并添加类似如下的内容
 * {
 *   "apiType": "stageMode",
 *   "buildOption": {
 *     "sourceOption": {
 *       "workers": [
 *         './src/main/ets/workers/transcodeworker.ets'
 *       ]
 *     }
 *   }
 * }
 */

import { TitleBar } from '../TitleBar'
import { sendableContextManager } from '@kit.AbilityKit';
import { ErrorEvent, MessageEvents, worker } from '@kit.ArkTS';

@Entry
@Component
struct AVTranscoderDemo {

  build() {
    Column({ space: 10 }) {
      TitleBar()
      Tabs() {
        TabContent() { MySample1() }.tabBar('基础').align(Alignment.Top)
      }
      .scrollable(true)
      .barMode(BarMode.Scrollable)
      .layoutWeight(1)
    }
  }
}

@Component
struct MySample1 {

  @State message:string = ""

  // 创建 worker 线程做转码
  async startWorker() {

    // 通过指定的 worker 线程文件创建工作线程对象(注:此处的文件路径要省略 src/main/)
    let threadWorker = new worker.ThreadWorker('entry/ets/workers/transcodeworker.ets');

    // 接收到工作线程传递过来的消息后的回调
    threadWorker.onmessage = (event: MessageEvents) => {
      // e.data 就是工作线程传递过来的消息
      this.message += `onmessage: ${event.data}\n`

      if (event.data == 'finish') {
        // 如果 worker 线程传来了 'finish' 则终止 worker 线程
        threadWorker.terminate();
      }
    }

    // 接收到工作线程传递过来的无法被反序列化的消息后的回调
    threadWorker.onmessageerror = (event: MessageEvents) => {
      this.message += `onmessageerror: ${event.data}\n`
    }

    // 工作线程运行中出现异常后的回调
    threadWorker.onerror = (err: ErrorEvent) => {
      this.message += `onerror: ${err.message}\n`
    }

    // 工作线程被销毁后的回调(code 为 0 代表正常退出,code 为 1 代表异常退出)
    threadWorker.onexit = (code: number) => {
      this.message += `onexit: ${code}\n`
    }

    // 向 worker 线程发送消息
    let context = this.getUIContext().getHostContext();
    if (context != undefined) {
      const sendableContext: sendableContextManager.SendableContext = sendableContextManager.convertFromContext(context);
      // 需要转码的文件的 rawfile 地址
      let sourceFilePath = "video/mp4.mp4"
      // 转码后的文件的沙箱地址
      let transcodedFilePath = context.getApplicationContext().filesDir + "/" + "mp4_transcoded.mp4";
      const sendableObject: MySendableObject = new MySendableObject(sendableContext, sourceFilePath, transcodedFilePath);
      threadWorker.postMessageWithSharedSendable(sendableObject);
    }
  }

  build() {
    Column({space:10}) {
      Text(this.message).fontSize(16)

      Button("click me").fontSize(16).onClick(async () => {
        await this.startWorker();
      })
    }
  }
}

// 用于在线程之间传递的 @Sendable 对象
@Sendable
export class MySendableObject {
  constructor(sendableContext: sendableContextManager.SendableContext, rawfilePath: string, transcodedFilePath: string) {
    this.sendableContext = sendableContext;
    this.rawfilePath = rawfilePath;
    this.transcodedFilePath = transcodedFilePath;
  }

  // 用于封装 context 对象
  private sendableContext: sendableContextManager.SendableContext;
  // 需要转码的文件的 rawfile 地址
  private rawfilePath: string;
  // 转码后的文件的沙箱地址
  private transcodedFilePath: string;

  public getSendableContext() {
    return this.sendableContext;
  }

  public getRawfilePath() {
    return this.rawfilePath;
  }

  public getTranscodedFilePath() {
    return this.transcodedFilePath;
  }
}

\entry\src\main\ets\workers\transcodeworker.ets

/*
 * 此 worker 线程用于演示视频转码
 */

import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { common, sendableContextManager } from '@kit.AbilityKit';
import { MySendableObject } from '../pages/media/AVTranscoderDemo';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = async (event: MessageEvents) => {

  console.info('workerPort onmessage');

  const sendableObject: MySendableObject = event.data
  const sendableContext: sendableContextManager.SendableContext = sendableObject.getSendableContext()
  const context: common.Context = sendableContextManager.convertToContext(sendableContext)

  // 需要转码的文件的 rawfile 地址
  let rawfilePath = sendableObject.getRawfilePath()
  // 转码后的文件的沙箱地址
  let transcodedFilePath = sendableObject.getTranscodedFilePath()

  await transcode(context, rawfilePath, transcodedFilePath);

  workerPort.postMessage('finish');
};

workerPort.onmessageerror = (event: MessageEvents) => {
  console.info('workerPort onmessageerror');
};

workerPort.onerror = (event: ErrorEvent) => {
  console.info('workerPort onerror: ', event.message);
};


let transcoder: media.AVTranscoder | undefined
async function transcode(context: common.Context, rawfilePath: string, transcodedFilePath: string) {
  try {
    /*
     * AVTranscoder - 视频转码器
     *   media.createAVTranscoder() - 创建 AVTranscoder 对象
     *   on('complete')/off('complete') - 转码完成时的回调
     *   on('error')/off('error') - 转码异常时的回调
     *   on('progressUpdate')/off('progressUpdate') - 转码进度发生变化时的回调
     *   fdSrc - 需要转码的文件的 AVFileDescriptor 对象
     *   fdDst - 转码后的文件的 fd number
     *   prepare() - 准备转码(传入一个 AVTranscoderConfig 对象)
     *     audioBitrate - 输出的音频码率,单位:bps,默认值:48kbps
     *     audioCodec - 输出的音频的编码格式(CodecMimeType 枚举),默认值为 media.CodecMimeType.AUDIO_AAC
     *     fileFormat - 输出的文件的封装格式(ContainerFormatType 枚举),目前只支持 media.ContainerFormatType.CFT_MPEG_4
     *     videoBitrate - 输出的视频码率,单位:bps
     *     videoCodec - 输出的视频的编码格式(CodecMimeType 枚举),默认值为 media.CodecMimeType.VIDEO_AVC
     *     videoFrameWidth - 输出的视频的宽,单位:px
     *     videoFrameHeight - 输出的视频的高,单位:px
     *   start() - 开始转码,需要 prepare() 成功之后才能开始转码
     *   pause() - 暂停转码
     *   resume() - 继续转码
     *   cancel() - 取消转码
     *   release() - 清理资源
     */
    transcoder = await media.createAVTranscoder();

    transcoder.on('complete', async () => {
      console.info(`mytranscoder: complete`);
      workerPort.postMessage('mytranscoder: complete');
      await release()
    })

    transcoder.on('error', async (err: BusinessError) => {
      console.info(`mytranscoder: error`, err.code, err.message);
      workerPort.postMessage(`mytranscoder: error, ${err.code}, ${err.message}`);
      await release()
    })

    transcoder.on('progressUpdate', (progress: number) => {
      console.info(`mytranscoder: progressUpdate`, progress);
    })

    // 将需要转码的文件转为 AVFileDescriptor 对象,并赋值给 transcoder 的 fdSrc 属性
    // 将 rawfile 中的视频文件转为 AVFileDescriptor 对象
    let fileDescriptor: media.AVFileDescriptor = await context.resourceManager.getRawFd(rawfilePath);
    // 将沙箱路径中的视频文件转为 AVFileDescriptor 对象
    // let fileDescriptor: media.AVFileDescriptor = fs.openSync(filePath);
    transcoder.fdSrc = fileDescriptor;

    // 拿到转码后的文件的文件描述符,并赋值给 transcoder 的 fdDst 属性
    let file = fs.openSync(transcodedFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
    transcoder.fdDst = file.fd;

    let avConfig: media.AVTranscoderConfig = {
      audioBitrate: 48_000,
      audioCodec: media.CodecMimeType.AUDIO_AAC,
      fileFormat: media.ContainerFormatType.CFT_MPEG_4,
      videoBitrate: 1_000_000,
      videoCodec: media.CodecMimeType.VIDEO_AVC,
      videoFrameWidth: 960,
      videoFrameHeight: 400,
    };
    await transcoder?.prepare(avConfig);
    await transcoder?.start();
  } catch (error) {
    console.info(`mytranscoder: ${error}`);
    workerPort.postMessage(`mytranscoder: ${error}`);
    await release()
  }
}

// 清理资源
async function release() {
  transcoder?.off('complete')
  transcoder?.off('error')
  transcoder?.off('progressUpdate')
  transcoder?.release()
}

源码 https://github.com/webabcd/HarmonyDemo
作者 webabcd

posted @ 2025-05-27 14:47  webabcd  阅读(20)  评论(0)    收藏  举报