鸿蒙5.0 APP开发案例分析:HDRVivid视频录制、播放与转码开发实践

往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)

✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✏️ 记录一场鸿蒙开发岗位面试经历~

✏️ 持续更新中……


概述

随着5G、光纤等网络技术的普及,终端显示能力进一步提升,“真实”、“沉浸”的超高清音视频体验时代已到来。高动态范围(High-Dynamic Range,简称HDR)作为超高清音视频产业的关键技术之一,比传统的标准动态范围(Standard Dynamic Range,简称SDR)拥有更广的色域和更高的动态范围(动态范围指的是亮度最大值和最小值的比值),为图像保留更多细节。超高清HDR加上宽色域能让视频亮度层次和色彩的呈现更真实、更自然。

HDRVivid是由华为主导开发的高动态范围视频技术标准,中文名为“菁彩影像”。它受亮度、对比度、色深、色域等因素影响,是一种提高画面亮度及对比度的画面处理技术。 其技术特点与优势为:

  1. 高动态范围:HDRVivid通过更广的色域和更高的动态范围,使得画面能够容纳更多的细节和色彩层次。与传统的SDR相比,HDRVivid的高光亮度是SDR的40倍,它能够同时呈现更深的黑色和更亮的白色,让画面的亮部和暗部细节更加清晰。
  2. 色彩丰富:HDRVivid支持10bit/12bit的色深,使得色彩过渡更加平滑,色彩表现更加细腻。色域面积相对BT.709标准增加了70%**,**色域越大证明能显示的颜色越多,HDRVivid能够呈现更加真实的色彩,让用户感受到更加丰富的视觉体验。
  3. 智能优化:作为一种动态HDR标准,HDRVivid能够根据显示硬件和视频场景,逐帧动态优化画面的亮度、对比度和色彩。HDRVivid使用动态元数据和智能映射引擎,将母版颜色容积映射到显示设备上,这种映射使得创作者的意图在不同显示设备上都得以保留,并且呈现最优画质效果,解决了传统制作和显示过程中色彩信息和亮度细节丢失的问题。

目前涉及到的HDR场景包括:HDR视频录制、HDR视频播放、视频转码(HDR2SDR)、HDR图片拍照等。本文旨在向开发者介绍,如何使用HarmonyOS的系统能力进行常用场景的开发:

  • HDRVivid视频录制:主要使用场景为有HDRVivid视频录制需求的应用。
  • HDRVivid视频播放:主要使用场景为有播放HDRVivid视频片源需求的应用。
  • HDRVivid视频转码成SDR视频:主要使用场景为不支持HDRVivid视频播放的场景或其他特殊情况。

HDRVivid视频录制

HDRVivid视频录制是一种先进的视频录制技术,它基于动态元数据对亮度、对比度和颜色进行逐个场景优化,为显示终端提供更加准确的动态映射方式。

HDRVivid视频录制的开发和 主流格式视频录制 的开发基本一致,差异点体现在部分参数的配置上。

HarmonyOS上开发录制视频的功能一般有两个方案:

  • 使用Camera配合系统级音视频录制类AVRecorder进行录制。
  • 使用Camera配合系统编码器进行录制。

使用Camera+AVRecorder录制

实现原理

应用通过调用 AVRecorder 实现视频录制时,先通过Camera接口调用相机服务,通过视频HDI捕获图像数据送显至应用,同时送至AVRecorder的录制服务,录制服务将图像数据编码后封装至文件中,实现视频录制功能。流程图如下:

使用 Camera + AVRecorder 录制HDRVivid视频,与录制普通视频的区别主要在于:

  • AVRecorder

    1. AVRecoder需要配置isHdr参数为true,对应的编码格式必须为video/hevc。
  • Camera

    1. 相机创建video output实例时,选择yuv 10bit profile(CAMERA_FORMAT_YCRCB_P010)。
    2. HDR录像需要相机支持视频防抖功能。
    3. 相机会话配置颜色空间为BT2020_HLG_LIMIT。

开发步骤

针对以上四点不同,开发视频录制功能时,可参考以下步骤(详细开发步骤可参考 HDR Vivid相机录像(ArkTS) ):

  1. 调用 createAVRecorder() 创建AVRecorder实例。
// 创建AVRecorder实例
try {
  this.avRecorder = await media.createAVRecorder();
} catch (error) {
  let err = error as BusinessError;
  Logger.error(TAG, `createAVRecorder call failed. error code: ${err.code}`);
}
  1. 配置预览流与录像输出流的分辨率为16:9, AVRecorderProfile 参数中的变量isHdr为True,videoCodec为VIDEO_HEVC格式,表示要录制的是HDRVivid视频。
let videoSize: camera.Size = {
  width: 1280,
  height: 720
}
// 设置配置参数AVRecorderProfile中的变量isHdr和videoCodec
let avRecorderProfile: media.AVRecorderProfile = {
  // ...
  videoCodec: media.CodecMimeType.VIDEO_HEVC,
  // ...
  isHdr: true
};
  1. 调用 createVideoOutput() 创建video output实例,选择yuv 10bit profile。
let videoProfile: undefined | camera.VideoProfile = videoProfilesArray.find((profile: camera.VideoProfile) => {
  return profile.size.width === videoSize.width && profile.size.height === videoSize.height &&
    profile.format === camera.CameraFormat.CAMERA_FORMAT_YCRCB_P010;
});

let previewProfile: undefined | camera.Profile = previewProfilesArray.find((profile: camera.Profile) => {
  return profile.format === camera.CameraFormat.CAMERA_FORMAT_YCRCB_P010 &&
    profile.size.width === videoSize.width && profile.size.height == videoSize.height
});

// ...

let avRecorderConfig: media.AVRecorderConfig = {
  audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
  videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
  profile: avRecorderProfile,
  url: this.url,
  rotation: 0,
  location: {
    latitude: 30,
    longitude: 130
  }
};

// ...

try {
  await this.avRecorder.prepare(avRecorderConfig);
} catch (error) {
  let err = error as BusinessError;
  Logger.error(TAG, `prepare call failed. error code: ${err.code}`);
}

let videoSurfaceId: string | undefined = undefined;
try {
  videoSurfaceId = await this.avRecorder.getInputSurface();
} catch (error) {
  let err = error as BusinessError;
  Logger.error(TAG, `getInputSurface call failed. error code: ${err.code}`);
}
if (videoSurfaceId === undefined) {
  return;
}

try {
  this.videoOutput = this.cameraManager.createVideoOutput(videoProfile, videoSurfaceId);
} catch (error) {
  let err = error as BusinessError;
  Logger.error(TAG, `Failed to create the videoOutput instance. error: ${JSON.stringify(err)}`);
}
  1. 创建并配置普通录像模式( VideoSession )的相机会话,同时设置视频防抖及色彩空间属性。
// 创建并配置相机会话
try {
  this.captureSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession;
} catch (error) {
  // ...
}
// ...

try {
  this.captureSession.beginConfig();
} catch (error) {
  // ...
}

// ...

try {
  await this.captureSession.commitConfig();
} catch (error) {
  // ...
}

let mode: camera.VideoStabilizationMode = camera.VideoStabilizationMode.AUTO;
// 查询是否支持视频防抖
let isSupported: boolean = false;
try {
  isSupported = this.captureSession.isVideoStabilizationModeSupported(mode);
  Logger.info(TAG, `isVideoStabilizationModeSupported: ${JSON.stringify(isSupported)}`);
} catch (error) {
  // 失败返回错误码error.code并处理
  let err = error as BusinessError;
  Logger.error(`The isVideoStabilizationModeSupported call failed. error code: ${err.code}`);
}
if (isSupported) {
  // 设置视频防抖
  this.captureSession.setVideoStabilizationMode(mode);
  let activeVideoStabilizationMode = this.captureSession.getActiveVideoStabilizationMode();
  Logger.info(`activeVideoStabilizationMode: ${activeVideoStabilizationMode}`);
} else {
  Logger.error(`videoStabilizationMode: ${mode} is not support`);
}

// Camera Session配置颜色空间为BT2020_HLG_LIMIT
if (isSupported) {
  let colorSpace: colorSpaceManager.ColorSpace = colorSpaceManager.ColorSpace.BT2020_HLG_LIMIT;
  let colorSpaces: Array<colorSpaceManager.ColorSpace> = [];
  try {
    colorSpaces = this.captureSession.getSupportedColorSpaces();
  } catch (error) {
    let err = error as BusinessError;
    Logger.error(`The getSupportedColorSpaces call failed. error code: ${err.code}`);
  }
  let isSupportedColorSpaces = colorSpaces.indexOf(colorSpace) >= 0;
  if (isSupportedColorSpaces) {
    Logger.info(`setColorSpace: ${colorSpace}`);
    try {
      this.captureSession.setColorSpace(colorSpace);
    } catch (error) {
      let err = error as BusinessError;
      Logger.error(`The setColorSpace call failed, error code: ${err.code}`);
    }
    let activeColorSpace: colorSpaceManager.ColorSpace = this.captureSession.getActiveColorSpace();
    Logger.info(`activeColorSpace: ${activeColorSpace}`);
  } else {
    Logger.error(`colorSpace: ${colorSpace} is not support`);
  }
}

try {
  await this.captureSession.start();
} catch (error) {
  let err = error as BusinessError;
  Logger.error(TAG, `captureSession start error: ${JSON.stringify(err)}`);
}
  1. 启动video流。
this.videoOutput.start((err: BusinessError) => {
  if (err) {
    Logger.error(TAG, `Failed to start the video output. error: ${JSON.stringify(err)}`);
    return;
  }
  Logger.info(TAG, 'Callback invoked to indicate the video output start success.');
});
  1. 启动recorder进行录制。
async startRecord() {
  if (this.avRecorder) {
    try {
      await this.avRecorder.start();
    } catch (error) {
      let err = error as BusinessError;
      Logger.error(TAG, `avRecorder start error: ${JSON.stringify(err)}`);
    }
  }
}

使用Camera+AVCodec录制

实现原理

应用通过调用 AVCodec 实现视频录制时,先通过Camera接口调用相机服务,通过视频HDI捕获图像数据送显至应用,同时送至AVCodec的编码模块将图像数据编码后封装至文件中,实现视频录制功能。流程图如下:

使用Camera+ AVCodec 录制HDRVivid视频,与录制普通视频的区别主要在于:

  • AVCodec
    1. 视频编码器AVCodec需要选择HEVC格式,并配置profile为HEVC_PROFILE_MAIN_10的相机底层。
    2. 编码器AVCodec配置颜色相关参数为COLOR_PRIMARY_BT2020。
  • Camera
    1. 相机在创建video output实例时,选择yuv 10bit profile。
    2. HDR录像需要相机支持视频防抖功能,并配置颜色空间为BT2020_HLG_LIMIT。

开发步骤

针对以上四点不同,使用Camera+AVCodec开发HDRVivid视频录制功能时,可参考以下步骤(详细开发步骤可参考 HDRVivid视频录制 ):

  1. 调用 OH_VideoEncoder_CreateByMime() 根据MIME类型创建创建HEVC格式视频编码器并初始化。
// 创建视频编码器并初始化
int32_t VideoEncoder::Create(const std::string &videoCodecMime)
{
    encoder_ = OH_VideoEncoder_CreateByMime(OH_AVCODEC_MIMETYPE_VIDEO_HEVC);
    CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed");
    return AVCODEC_SAMPLE_ERR_OK;
}
  1. 配置HDRVivid相关参数,包括可选配置视频帧宽度、频帧高度、视频颜色格式,以及必须配置为HEVC_PROFILE_MAIN_10的 OH_HEVCProfile ,表示HEVC编码档次为10bit主档次。
int32_t VideoEncoder::Configure(const SampleInfo &sampleInfo)
{
    // ...
    // 配置HDRVivid相关参数
    if (sampleInfo.isHDRVivid) {
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_PROFILE, sampleInfo.hevcProfile);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_I_FRAME_INTERVAL, sampleInfo.iFrameInterval);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_RANGE_FLAG, sampleInfo.rangFlag);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_COLOR_PRIMARIES, sampleInfo.primary);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_TRANSFER_CHARACTERISTICS, sampleInfo.transfer);
        OH_AVFormat_SetIntValue(format, OH_MD_KEY_MATRIX_COEFFICIENTS, sampleInfo.matrix);
    }
    // ...
}
  1. 使用 OH_VideoEncoder_Configure() 配置编码器。
// 配置编码器
int ret = OH_VideoEncoder_Configure(encoder_, format);
  1. ArkTs侧调用createVideoOutput创建video output实例,选择yuv 10bit profile。
export function videoProfileCheck(cameraManager: camera.CameraManager,
  cameraData: CameraDataModel): undefined | camera.VideoProfile {
  let cameraDevices = cameraManager.getSupportedCameras();
  // ...

  let profiles: camera.CameraOutputCapability =
    cameraManager.getSupportedOutputCapability(cameraDevices[0], camera.SceneMode.NORMAL_VIDEO);
  // ...

  let videoProfiles: Array<camera.VideoProfile> = profiles.videoProfiles;
  // ...

  let videoProfile: undefined | camera.VideoProfile = videoProfiles.find((profile: camera.VideoProfile) => {
    if (cameraData.isHDRVivid) {
      // ...
        return profile.size.width === cameraData.cameraWidth &&
          profile.size.height === cameraData.cameraHeight &&
          profile.format === camera.CameraFormat.CAMERA_FORMAT_YCRCB_P010 &&
          profile.frameRateRange.min === 1 &&
          profile.frameRateRange.max === 30;
      // ...
    } else {
      // ...
    }
  });
  return videoProfile;
}

let videoProfile: undefined | camera.VideoProfile = videoProfileCheck(cameraManager, params);
if (!videoProfile) {
  Logger.error(TAG, 'videoProfile is not found!');
  return;
}

// ...

// 创建video output实例
encoderVideoOutput = cameraManager.createVideoOutput(videoProfile, params.surfaceId);
if (encoderVideoOutput === undefined) {
  Logger.error(TAG, 'encoderVideoOutput is undefined');
  return;
}
Logger.info(TAG, 'encoderVideoOutput  success');
  1. 设置视频防抖及色彩空间。
function isVideoStabilizationModeSupported(session: camera.VideoSession, mode: camera.VideoStabilizationMode): boolean {
  let isSupported: boolean = false;
  try {
    isSupported = session.isVideoStabilizationModeSupported(mode);
  } catch (error) {
    // 失败返回错误码error.code并处理
    let err = error as BusinessError;
    console.error(`The isVideoStabilizationModeSupported call failed. error code: ${err.code}`);
  }
  return isSupported;
}

function setVideoStabilizationMode(session: camera.VideoSession): boolean {
  let mode: camera.VideoStabilizationMode = camera.VideoStabilizationMode.AUTO;
  // 查询是否支持视频防抖
  let isSupported: boolean = isVideoStabilizationModeSupported(session, mode);
  if (isSupported) {
    console.info(`setVideoStabilizationMode: ${mode}`);
    // 设置视频防抖
    session.setVideoStabilizationMode(mode);
    let activeVideoStabilizationMode = session.getActiveVideoStabilizationMode();
    console.info(`activeVideoStabilizationMode: ${activeVideoStabilizationMode}`);
  } else {
    console.info(`videoStabilizationMode: ${mode} is not support`);
  }
  return isSupported;
}

function getSupportedColorSpaces(session: camera.VideoSession): Array<colorSpaceManager.ColorSpace> {
  let colorSpaces: Array<colorSpaceManager.ColorSpace> = [];
  try {
    colorSpaces = session.getSupportedColorSpaces();
  } catch (error) {
    let err = error as BusinessError;
    console.error(`The getSupportedColorSpaces call failed. error code: ${err.code}`);
  }
  return colorSpaces;
}

// 设置色彩空间
function setColorSpaceBeforeCommitConfig(session: camera.VideoSession, isHdr: boolean): void {
  let colorSpace: colorSpaceManager.ColorSpace = isHdr? colorSpaceManager.ColorSpace.BT2020_HLG_LIMIT : colorSpaceManager.ColorSpace.BT709_LIMIT;
  let colorSpaces: Array<colorSpaceManager.ColorSpace> = getSupportedColorSpaces(session);
  let isSupportedColorSpaces = colorSpaces.indexOf(colorSpace) >= 0;
  if (isSupportedColorSpaces) {
    console.info(`setColorSpace: ${colorSpace}`);
    session.setColorSpace(colorSpace);
    let activeColorSpace:colorSpaceManager.ColorSpace = session.getActiveColorSpace();
    console.info(`activeColorSpace: ${activeColorSpace}`);
  } else {
    console.info(`colorSpace: ${colorSpace} is not support`);
  }
}
  1. 创建并配置相机会话。
let XComponentPreviewProfile: camera.Profile | undefined = previewProfileCameraCheck(cameraManager, params)
if (XComponentPreviewProfile === undefined) {
  Logger.error(TAG, 'XComponentPreviewProfile is not found');
  return;
}
// ...

// 创建会话流
try {
  videoSession = cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession;
} catch (error) {
  let err = error as BusinessError;
  Logger.error(TAG, `Failed to create the session instance. error: ${JSON.stringify(err)}`);
}
// ...

// 开始配置会话
try {
  videoSession.beginConfig();
} catch (error) {
  // ...
}

try {
  videoSession.addInput(cameraInput);
} catch (error) {
  // ...
}

// 将XComponent预览流添加到会话中
try {
  videoSession.addOutput(XComponentPreviewOutput);
} catch (error) {
  // ...
}

try {
  videoSession.addOutput(encoderVideoOutput);
} catch (error) {
  // ...
}

try {
  await videoSession.commitConfig();
} catch (error) {
  // ...
}

// 设置视频防抖
if (setVideoStabilizationMode(videoSession)) {
  // 设置色彩空间
  setColorSpaceBeforeCommitConfig(videoSession, true);
}

try {
  await videoSession.start();
} catch (error) {
  // ...
}

// 启动视频输出流
encoderVideoOutput.start((err: BusinessError) => {
  // ...
});
  1. 调用OH_AVMuxer_Create()创建AVMuxer封装器实例对象,设置封装格式及封装路径,配置HDRVivid相关参数。
// 创建封装器实例对象,设置封装格式为mp4
int32_t Muxer::Create(int32_t fd)
{
    muxer_ = OH_AVMuxer_Create(fd, AV_OUTPUT_FORMAT_MPEG_4);
    CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer create failed, fd: %{public}d", fd);
    return AVCODEC_SAMPLE_ERR_OK;
}

int32_t Muxer::Config(SampleInfo &sampleInfo)
{
    CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer is null");

    OH_AVFormat *formatVideo = OH_AVFormat_CreateVideoFormat(sampleInfo.videoCodecMime.data(),
        sampleInfo.videoWidth, sampleInfo.videoHeight);
    CHECK_AND_RETURN_RET_LOG(formatVideo != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create video format failed");

    OH_AVFormat_SetDoubleValue(formatVideo, OH_MD_KEY_FRAME_RATE, sampleInfo.frameRate);
    OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_WIDTH, sampleInfo.videoWidth);
    OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_HEIGHT, sampleInfo.videoHeight);
    OH_AVFormat_SetStringValue(formatVideo, OH_MD_KEY_CODEC_MIME, sampleInfo.videoCodecMime.data());
    if (sampleInfo.isHDRVivid) {
        OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_VIDEO_IS_HDR_VIVID, 1);
        OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_RANGE_FLAG, sampleInfo.rangFlag);
        OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_COLOR_PRIMARIES, sampleInfo.primary);
        OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_TRANSFER_CHARACTERISTICS, sampleInfo.transfer);
        OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_MATRIX_COEFFICIENTS, sampleInfo.matrix);
    }
    
    int32_t ret = OH_AVMuxer_AddTrack(muxer_, &videoTrackId_, formatVideo);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "AddTrack failed");
    OH_AVFormat_Destroy(formatVideo);
    OH_AVMuxer_SetRotation(muxer_, sampleInfo.videoHeight > sampleInfo.videoWidth ? VERTICAL_ANGLE : HORIZONTAL_ANGLE);
    return AVCODEC_SAMPLE_ERR_OK;
}
  1. 调用 OH_VideoEncoder_Start() 启动编码器。
// 启动编码器
int32_t VideoEncoder::Start()
{
    CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null");

    int ret = OH_VideoEncoder_Start(encoder_);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret);
    return AVCODEC_SAMPLE_ERR_OK;
}

HDRVivid视频播放

要实现HDRVivid视频播放,通常需要在视频播放流程中进行特定的处理,这包括:视频文件解析、解码器配置和渲染处理。

相对于主流格式视频播放的开发,HDRVivid视频播放的开发与其流程基本一致,只在解码格式上有所区别。下面将从解码格式上来讲解在HarmonyOS上进行视频开发的主要两种方案:

  • 使用系统播放器(已集成系统解码和渲染能力)进行开发。
  • 基于系统解码和渲染能力开发应用级播放器。

使用系统播放器AVPlayer开发

实现原理

AVPlayer提供功能完善一体化播放能力,应用只需要提供流媒体来源,不负责数据解析和解码就可达成播放效果。

开发步骤

使用系统播放器 AVPlayer 开发视频播放功能,主要开发步骤为(详细开发步骤可参考使用AVPlayer播放视频(ArkTS)):

  1. 调用createAVPlayer() 创建AVPlayer实例,初始化进入idle状态并设置资源属性url。
// 创建AVPlayer实例
public initAVPlayer() {
  media.createAVPlayer().then((player: media.AVPlayer) => {
    if (player !== null) {
      this.avPlayer = player;
      // this.curSource.video为本地视频路径,this.curSource.url为网络视频路径
      if (this.curSource.video) {
        let fileDescriptor = this.context?.resourceManager.getRawFdSync(this.curSource.video);
        if (fileDescriptor) {
          let avFileDescriptor: media.AVFileDescriptor =
            { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };
          this.avPlayer.fdSrc = avFileDescriptor;
        }
      } else {
        this.avPlayer.url = this.curSource.url;
      }
      // ...
    } else {
      Logger.error(TAG, 'createAVPlayer fail');
    }
  }).catch((error: BusinessError) => {
    Logger.error(TAG, `AVPlayer catchCallback, error message:${error.message}`);
  });
}
  1. 设置业务需要的监听事件,从XComponent组件获取surfaceID并设置该属性,接着调用 prepare() ,AVPlayer进入prepared状态,可设置缩放模式、音量等。
private setStateChangeCallback(avPlayer: media.AVPlayer) {
  avPlayer.on('stateChange', async (state: string) => {
    switch (state) {
      // ...
      case 'initialized':
        Logger.info(TAG, 'AVPlayer state initialized called.' + ` this.curIndex:${this.curIndex}`);
        avPlayer.surfaceId = this.surfaceID;
        avPlayer.prepare();
        break;
      case 'prepared':
        // ...
        break;
      // ...
    }
  })
}
  1. 设置视频播控功能,包括播放 play() ,暂停 pause() ,跳转 seek() ,停止 stop() 、重置 reset() 等操作。
public playVideo() {
  // ...
}

public pauseVideo() {
  // ...
}

public stopVideo() {
  // ...
}

public seek(seekTime: number) {
  // ...
}
  1. 最后调用 release() 销毁实例,退出播放。
public releaseVideo(index: number) {
  if (this.avPlayer) {
    Logger.info(TAG,
      `releaseVideo: state:${this.avPlayer.state} this.curIndex:${this.curIndex} this.index:${index}`);
    this.avPlayer.off('timeUpdate');
    this.avPlayer.off('seekDone');
    this.avPlayer.off('speedDone');
    this.avPlayer.off('error');
    this.avPlayer.off('stateChange');
    this.avPlayer.release();
    // ...
  }
}

使用系统解码器AVCodec开发

实现原理

AVCodec模块 的Native API接口,可以完成视频解码功能,即将媒体数据在系统侧解码成YUV文件或送显至应用上。

开发步骤

使用系统解码器AVCodec开发HDRVivid视频播放功能,主要开发步骤为(详细开发步骤可参考HDR Vivid视频播放):

  1. 调用OH_VideoDecoder_CreateByMime()通过HEVC格式创建解码器实例对象。如果需要对HDRVivid视频进行解码,需要配置MimeType为H265 (即OH_AVCODEC_MIMETYPE_VIDEO_HEVC)。
// 创建解码器实例对象
int32_t VideoDecoder::Create(const std::string &videoCodecMime) {
    decoder_ = OH_VideoDecoder_CreateByMime(OH_AVCODEC_MIMETYPE_VIDEO_HEVC);
    CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed");
    return AVCODEC_SAMPLE_ERR_OK;
}
  1. 调用 OH_VideoDecoder_RegisterCallback() 设置回调函数。
// 设置回调函数
int32_t VideoDecoder::SetCallback(CodecUserData *codecUserData) {
    int32_t ret = AV_ERR_OK;
    ret = OH_VideoDecoder_RegisterCallback(decoder_,
                                           {SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange,
                                            SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer},
                                           codecUserData);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set callback failed, ret: %{public}d", ret);

    return AVCODEC_SAMPLE_ERR_OK;
}
  1. 调用 OH_VideoDecoder_Configure() 配置解码器。
// 配置解码器
int32_t VideoDecoder::Configure(const SampleInfo &sampleInfo) {
    // ...
    int ret = OH_VideoDecoder_Configure(decoder_, format);
    // ...
}
  1. 从XComponent组件获取window参数,设置Surface,并调用 OH_VideoDecoder_Prepare() 使解码器就绪。
int32_t VideoDecoder::Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData) {
    // ...
    // 设置Surface。本例中的nativeWindow,需要从XComponent组件获取
    if (sampleInfo.window != nullptr) {
        int ret = OH_VideoDecoder_SetSurface(decoder_, sampleInfo.window);
        CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK && sampleInfo.window, AVCODEC_SAMPLE_ERR_ERROR,
                                 "Set surface failed, ret: %{public}d", ret);
    }

    // ...
    
    // 解码器就绪
    {
        int ret = OH_VideoDecoder_Prepare(decoder_);
        CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Prepare failed, ret: %{public}d", ret);
    }

    return AVCODEC_SAMPLE_ERR_OK;
}
  1. 调用 OH_VideoDecoder_Start() 启动解码器。
// 启动解码器
int32_t VideoDecoder::Start() {
    CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null");

    int ret = OH_VideoDecoder_Start(decoder_);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret);
    return AVCODEC_SAMPLE_ERR_OK;
}
  1. 调用 OH_VideoDecoder_PushInputBuffer() 写入解码码流。
// 写入解码码流
int32_t VideoDecoder::PushInputBuffer(CodecBufferInfo &info) {
    CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null");
    int32_t ret = OH_VideoDecoder_PushInputBuffer(decoder_, info.bufferIndex);
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Push input data failed");
    return AVCODEC_SAMPLE_ERR_OK;
}
  1. 调用 OH_VideoDecoder_RenderOutputBuffer() 渲染并释放解码帧。
// 渲染并释放解码帧
int32_t VideoDecoder::FreeOutputBuffer(uint32_t bufferIndex, bool render) {
    CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null");

    int32_t ret = AVCODEC_SAMPLE_ERR_OK;
    if (render) {
        ret = OH_VideoDecoder_RenderOutputBuffer(decoder_, bufferIndex);
    } else {
        ret = OH_VideoDecoder_FreeOutputBuffer(decoder_, bufferIndex);
    }
    CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Free output data failed");
    return AVCODEC_SAMPLE_ERR_OK;
}
  1. 最后调用 OH_VideoDecoder_Destroy() 销毁解码器实例,释放资源。
// 销毁解码器实例,释放资源
int32_t VideoDecoder::Release() {
    if (decoder_ != nullptr) {
        OH_VideoDecoder_Flush(decoder_);
        OH_VideoDecoder_Stop(decoder_);
        OH_VideoDecoder_Destroy(decoder_);
        decoder_ = nullptr;
    }
    return AVCODEC_SAMPLE_ERR_OK;
}

HDRVivid视频转码成SDR视频

实现原理

将HDRVivid视频转码成SDR视频是一个涉及多个技术要点的复杂过程。通过合理的转码处理,可以确保视频内容在不同设备上都能呈现出更好的效果,不仅优化了视频的播放体验,还可以满足更广泛受众的需求,提高市场影响力。

开发步骤

HarmonyOS提供了Native侧的 VideoProcessing 函数,可以将HDRVivid视频转码成SDR视频,主要的开发步骤为(详细开发步骤可参考 视频色彩空间转换 ):

  1. 调用 OH_VideoProcessing_Create() 创建色彩空间转换模块。
void PluginManager::PrepareSurface(){
    VideoProcessing_ErrorCode ret = OH_VideoProcessing_InitializeEnvironment();
    if (ret != VIDEO_PROCESSING_SUCCESS) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessing_InitializeEnvironment %d",ret);
        return;
    }
    
    // 创建色彩空间转换模块
    ret = OH_VideoProcessing_Create(&processor, VIDEO_PROCESSING_TYPE_COLOR_SPACE_CONVERSION);
    if (ret != VIDEO_PROCESSING_SUCCESS) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessing_Create %d",ret);
        return;
    }
    
    // ...
}
  1. 从XComponent获取OHNativeWindow实例,调用 OH_VideoProcessing_SetSurface() 设置surface。
void PluginManager::InitProcessing(SampleInfo &sampleInfo){
    int32_t err = 0;
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","target colorspace %{public}d",sampleInfo.colorSpace);
    // 为OHNativeWindow设置元数据属性值、宽高、内容格式等
    if (sampleInfo.colorSpace == OH_COLORSPACE_BT709_LIMIT) {
        err = OH_NativeWindow_NativeWindowHandleOpt(PluginManager::GetInstance()->windowOut, SET_FORMAT, NATIVEBUFFER_PIXEL_FMT_YCBCR_420_SP);
        // ...
    } else if (sampleInfo.colorSpace == OH_COLORSPACE_BT2020_HLG_LIMIT) {
        err = OH_NativeWindow_SetMetadataValue(PluginManager::GetInstance()->windowOut, 
            OH_HDR_METADATA_TYPE, sizeof(uint8_t), (uint8_t *)&sampleInfo.metaData);
        // ...
        err = OH_NativeWindow_NativeWindowHandleOpt(PluginManager::GetInstance()->windowOut, SET_FORMAT, NATIVEBUFFER_PIXEL_FMT_RGBA_1010102);
        // ...
    } else if (sampleInfo.colorSpace == OH_COLORSPACE_BT2020_PQ_LIMIT) {
        err = OH_NativeWindow_SetMetadataValue(PluginManager::GetInstance()->windowOut, 
            OH_HDR_METADATA_TYPE, sizeof(uint8_t), (uint8_t *)&sampleInfo.metaData);
        // ...
        err = OH_NativeWindow_NativeWindowHandleOpt(PluginManager::GetInstance()->windowOut, SET_FORMAT, NATIVEBUFFER_PIXEL_FMT_RGBA_1010102);
        // ...
    }
    
    // 为OHNativeWindow设置颜色空间属性
    err = OH_NativeWindow_SetColorSpace(PluginManager::GetInstance()->windowOut, sampleInfo.colorSpace);
    // ...
    // 设置输出surface
    VideoProcessing_ErrorCode ret = OH_VideoProcessing_SetSurface(processor, PluginManager::GetInstance()->windowOut);
    // ...
}
  1. 调用 OH_VideoProcessing_RegisterCallback() 等函数创建并绑定回调函数。
// 创建并绑定回调函数
ret = OH_VideoProcessingCallback_Create(&callback);
if (ret != VIDEO_PROCESSING_SUCCESS) {
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessingCallback_Create %d",ret);
    return;
}

ret = OH_VideoProcessingCallback_BindOnError(callback, OnError);
if (ret != VIDEO_PROCESSING_SUCCESS) {
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessingCallback_BindOnError %d",ret);
    return;
}
ret = OH_VideoProcessingCallback_BindOnState(callback, OnState);
if (ret != VIDEO_PROCESSING_SUCCESS) {
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessingCallback_BindOnState %d",ret);
    return;
}
ret = OH_VideoProcessingCallback_BindOnNewOutputBuffer(callback, OnNewOutputBuffer);
if (ret != VIDEO_PROCESSING_SUCCESS) {
    OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessingCallback_BindOnNewOutputBuffer %d",ret);
    return;
}
OH_VideoProcessing_RegisterCallback(processor, callback, nullptr);
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "PluginManager","InitProcessing end");
  1. 调用 OH_VideoProcessing_Start() 启动色彩空间转换处理。
// 启动色彩空间转换处理
void PluginManager::StartProcessing(){
    OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "PluginManager","StartProcessing ");
    VideoProcessing_ErrorCode ret = OH_VideoProcessing_Start(processor);
    if (ret != VIDEO_PROCESSING_SUCCESS) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","StartProcessing %d",ret);
        return;
    }
}
  1. 调用 OH_VideoProcessing_Stop() 停止色彩空间转换处理。
// 停止色彩空间转换处理
void PluginManager::StopProcessing(){
    OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "PluginManager","StopProcessing ");
    VideoProcessing_ErrorCode ret = OH_VideoProcessing_Stop(processor);
    if (ret != VIDEO_PROCESSING_SUCCESS) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessing_Stop %d",ret);
        return;
    }
}
  1. 调用 OH_VideoProcessing_Destroy() 及 OH_VideoProcessing_DeinitializeEnvironment() 释放处理实例和资源。
// 释放处理实例和资源
void PluginManager::DestroyProcessing(){
    OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "PluginManager","DestroyProcessing ");
    VideoProcessing_ErrorCode ret = OH_VideoProcessing_Destroy(processor);
    if (ret != VIDEO_PROCESSING_SUCCESS) {
       OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessing_Destroy %d",ret);
       return;
    }
    ret = OH_VideoProcessingCallback_Destroy(callback);
    if (ret != VIDEO_PROCESSING_SUCCESS) {
       OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessingCallback_Destroy %d",ret);
       return;
    }
    ret = OH_VideoProcessing_DeinitializeEnvironment();
    if (ret != VIDEO_PROCESSING_SUCCESS) {
       OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager","OH_VideoProcessing_DeinitializeEnvironment %d",ret);
       return;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值