自定义视频数据处理

在实时视频互动过程中,您可能有对 SDK 采集到的原始视频数据进行处理的需求,也可能有获取 SDK 采集并编码的视频数据的需求。本文介绍如何通过不同类型的视频观测器获取 SDK 采集到的视频数据,并对其进行处理。

使用场景

自定义视频数据处理适用于以下场景。

  1. 自定义美颜处理:拉取本地采集的原始视频流,并使用第三方美颜处理SDK进行处理。处理后的视频流可以再次推送到频道中,供观看者实时观看。

  2. 自定义视频编辑:在视频处理链路的不同阶段对视频流进行编辑处理,如添加特效、水印、裁剪、剪辑等。用户可以根据需求对视频流进行灵活编辑,编辑后的视频流可用于直播或存储。

  3. 拉流预览或监控:在视频处理链路的不同阶段拉取视频流进行本地或云端的监控和预览。用户可以实时查看视频流的内容、质量和状态,确保视频流的正常传输和播放。

技术原理

因为iOSAndroid端设备的视频采集API存在差异,接下来我们分两部分介绍iOS端和Android端采集的数据处理流程。

iOSSDK中视频模块采集的数据处理流程

视频采集:

image

解码渲染:

image

观测点对应的回调如下所示:

  • 观测点 1:通过 onCaptureVideoSample 回调获取未经过缩放的数据。

  • 观测点 2:通过 onPreEncodeVideoSample 回调获取编码前的数据。

  • 观测点 3: 通过 onRemoteVideoSample 回调获取解码后的数据。

  • 观测点 4:通过 onTextureCreate 回调获取纹理的glcontext,绑定到美颜模块。

  • 观测点 5:通过 onTextureUpdate 回调获取采集到的纹理数据。

  • 观测点 6:通过 onTextureDestroy 回调获取释放glcontext时机。

观测点1、2、3返回值返回True表示数据更新,否则表示不更新数据;

观测点4,返回值>0表示SDK需要使用新的textureId进行编码和处理,其他返回值表示未更新textureId。

AndroidSDK中视频模块采集的数据处理流程

视频采集:

image

解码渲染:

image

观测点对应的回调如下所示:

  • 观测点 1:通过 onLocalVideoSample 回调获取未经过缩放的数据。

  • 观测点 2:通过 onPreEncodeVideoSample 回调获取编码前的数据。

  • 观测点 3: 通过 onRemoteVideoSample 回调获取解码后的数据。

  • 观测点 4:通过 onTextureCreate 回调获取纹理的glcontext。

  • 观测点 5:通过 onTextureUpdate 回调获取采集到的纹理数据。

  • 观测点 6:通过 onTextureDestroy 回调获取释放glcontext时机。

观测点1、2、3返回值返回True表示数据更新,否则表示不更新数据;

观测点4,返回值>0表示SDK需要使用新的textureId进行编码和处理,其他返回值表示未更新textureId。

前提条件

在进行实现相关功能之前,请确保您已经在项目中实现了1V1视频通话功能且可正常使用,包括视频采集、推流、远端渲染均正常。

iOS实现方案

一、视频帧回调

1.注册回调

回调通过AliRtcEngineDelegate回调。

registerVideoSampleObserver

2.返回关注的位置

SDK根据该回调返回值确认回调位置。

/**
 * @brief 视频数据输出位置
 */
typedef NS_ENUM(NSInteger, AliRtcVideoObserPosition) {
    /** 采集视频数据,对应输出回调onCaptureVideoSample */
    AliRtcPositionPostCapture = 1 << 0,
    /** 渲染视频数据,对应输出回调onRemoteVideoSample */
    AliRtcPositionPreRender = 1 << 1,
    /** 编码前视频数据,对应输出回调onPreEncodeVideoSample */
    AliRtcPositionPreEncoder = 1 << 2,
};

/*
* 在AliRtcEngine::registerVideoSampleObserver之后触发
*/

- (NSInteger)onGetVideoObservedFramePosition;

3.返回需要输出格式

iOS 平台如果需要返回cvPixelBuffer需要创建引擎设置extra字段,"capture_callback_cvpixelbuffer_to_raw"=1

/**
 * @brief 视频数据格式
 */
typedef NS_ENUM(NSInteger, AliRtcVideoFormat) {
    AliRtcVideoFormat_UNKNOW = -1,
    AliRtcVideoFormat_BGRA = 0,
    AliRtcVideoFormat_I420,
    AliRtcVideoFormat_NV21,
    AliRtcVideoFormat_NV12,
    AliRtcVideoFormat_RGBA,
    AliRtcVideoFormat_I422,
    AliRtcVideoFormat_ARGB,
    AliRtcVideoFormat_ABGR,
    AliRtcVideoFormat_RGB24,
    AliRtcVideoFormat_BGR24,
    AliRtcVideoFormat_RGB565,
    AliRtcVideoFormat_TextureOES,
    AliRtcVideoFormat_Texture2D,
    AliRtcVideoFormat_H264,
    AliRtcVideoFormat_H265,
    AliRtcVideoFormat_File,
    AliRtcVideoFormat_cvPixelBuffer,
};

/*
* 在AliRtcEngine::registerVideoSampleObserver之后触发
*/

- (AliRtcVideoFormat)onGetVideoFormatPreference {
    return AliRtcVideoFormat_I420;
}

4.返回需要的对齐方式

/**
 * @brief 视频输出宽度对齐方式
 */
typedef enum {
    /** 保持原有视频宽度(默认值) */
    AliRtcAlignmentDefault = 0,
    /** 宽度偶数对齐 */
    AliRtcAlignmentEven = 1,
    /** 宽度是4的倍数 */
    AliRtcAlignment4 = 2,
    /** 宽度是8的倍数 */
    AliRtcAlignment8 = 3,
    /** 宽度是16的倍数 */
    AliRtcAlignment16 = 4,
} AliRtcVideoObserAlignment;

/*
* 在AliRtcEngine::registerVideoSampleObserver之后触发
*/

- (AliRtcVideoObserAlignment)onGetVideoAlignment {
  return AliRtcAlignmentDefault;
}

5.获取的视频帧是否需要镜像

/**
 * @brief 视频输出数据是否需要镜像
 * @return
 * - YES: 镜像
 * - NO: 不镜像(默认)
 */
 
 /*
* 在AliRtcEngine::registerVideoSampleObserver之后触发
*/
- (BOOL)onGetObserverDataMirrorApplied {
  return FLASE ;
}

6.执行回调

/**
 * @brief 订阅的本地采集视频数据回调
 * @param videoSource 视频流类型
 * @param videoSample 视频裸数据
 * @return
 * - YES: 需要写回SDK(只对I420和CVPixelBuffer(ios/mac)有效)
 * - NO: 不需要写回SDK
*/
- (BOOL)onCaptureVideoSample:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *_Nonnull)videoSample {
  /*
   * do....
   */
   return FALSE ;
}

/**
 * @brief 订阅的本地编码前视频数据回调
 * @param videoSource 视频流类型
 * @param videoSample 视频裸数据
 * @return
 * - YES: 需要写回SDK(只对I420和CVPixelBuffer(ios/mac)有效)
 * - NO: 不需要写回SDK
*/
- (BOOL)onPreEncodeVideoSample:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *_Nonnull)videoSample {
  /*
   * do....
   */
   return FALSE ;
}

/**
 * @brief 订阅的远端视频数据回调
 * @param uid 用户ID
 * @param videoSource 视频流类型
 * @param videoSample 视频裸数据
 * @return
 * - YES: 需要写回SDK(只对I420和CVPixelBuffer(ios/mac)有效)
 * - NO: 不需要写回SDK
*/
- (BOOL)onRemoteVideoSample:(NSString *_Nonnull)uid videoSource:(AliRtcVideoSource)videoSource videoSample:(AliRtcVideoDataSample *_Nonnull)videoSample {

  /*
   * do....
   */
  return TRUE ;
}

7.取消回调注册

unregisterVideoSampleObserver

二、纹理处理

因为openGL线程模型的原因,所有的纹理回调都是在同一个线程callback。

1.注册纹理回调

registerLocalVideoTexture

2.执行回调

openGL context创建后回调。
/**
 * @brief OpenGL上下文创建回调
 * @param context OpenGL上下文
 * @note 该回调是在SDK内部OpenGL上下文创建的时候触发
 */
- (void)onTextureCreate:(void *_Nullable)context {
    [[beautifyMoudle_ shared] create];
}
openGL 纹理变更回调
/**
 * @brief OpenGL纹理更新回调
 * @param textureId OpenGL纹理ID
 * @param width OpenGL纹理宽
 * @param height OpenGL纹理高
 * @param videoSample 视频帧数据,详见 {@link AliRtcVideoDataSample}
 * @return OpenGL纹理ID
 * @note
 * - 该回调会在每一帧视频数据上传到OpenGL纹理之后触发,当外部注册了OpenGL纹理数据观测器,在该回调中可以对纹理进行处理,并返回处理后的纹理ID
 * - 注意该回调返回值必须为有效的纹理ID,如果不做任何处理必须返回参数textureId
 * 回调的TextureID是 AliRtcVideoFormat_Texture2D 格式
 */
- (int)onTextureUpdate:(int)textureId width:(int)width height:(int)height videoSample:(AliRtcVideoDataSample *_Nonnull)videoSample {
    
    if ( [[beautifyMoudle_ shared] enabled] == NO) {
        return textureId;
    }
    
    int texId = [[beautifyMoudle_ shared] processTextureToTexture:textureId Width:width Height:height];
    
    if(texId<0) {
       texId = textureId;
    }
    return texId;
}
openGL 上下文删除回调
- (void)onTextureDestory
{
    if (self.settingModel.chnnelType == ChannelTypePrimary) {
        [[beautifyMoudle_ shared] destroy];
    }
    
}

3.取消纹理回调

unregisterLocalVideoTexture

Android实现方案

一、视频帧回调

1.注册回调

回调通过AliRtcEngine.AliRtcVideoObserver。

public abstract void registerVideoSampleObserver(AliRtcVideoObserver observer);

2.返回关注的位置

SDK根据该回调返回值确认回调位置。

 public enum AliRtcVideoObserPosition{
        /*! 采集视频数据,对应输出回调 onLocalVideoSample */
        AliRtcPositionPostCapture(1),
        /*! 渲染视频数据,对应输出回调 onRemoteVideoSample */
        AliRtcPositionPreRender(2),
        /*! 编码前视频数据,对应输出回调 onPreEncodeVideoSample */
        AliRtcPositionPreEncoder(4);
    }

public int onGetObservedFramePosition(){
    return AliRtcVideoObserPosition.AliRtcPositionPostCapture.getValue() | AliRtcVideoObserPosition.AliRtcPositionPreRender.getValue();
}

3.返回需要输出格式

/**
 * @brief 视频数据格式
 */
public enum AliRtcVideoFormat{
        AliRtcVideoFormatUNKNOW(-1),
        AliRtcVideoFormatBGRA(0),
        AliRtcVideoFormatI420(1),
        AliRtcVideoFormatNV21(2),
        AliRtcVideoFormatNV12 (3),
        AliRtcVideoFormatRGBA(4),
        AliRtcVideoFormatI422 (5),
        AliRtcVideoFormatARGB(6),
        AliRtcVideoFormatABGR (7),
        AliRtcVideoFormatRGB24(8),
        AliRtcVideoFormatBGR24(9),
        AliRtcVideoFormatRGB565(10),
        AliRtcVideoFormatTextureOES(11),
        AliRtcVideoFormatTexture2D(12),
        AliRtcVideoFormatH264(13),
        AliRtcVideoFormatH265(14),
        AliRtcVideoFormatFile(15);
};

/*
* 在AliRtcEngine::registerVideoSampleObserver之后触发
*/

/**
* @brief 视频数据输出格式
* @return 期望视频输出格式,参考 {@link AliRtcVideoFormat}
*/
public AliRtcVideoFormat onGetVideoFormatPreference(){
    return AliRtcVideoFormat.AliRtcVideoFormatI420;
}

4.返回需要的对齐方式

public enum AliRtcVideoObserAlignment{
        /*! 保持原有视频宽度(默认值) */
        AliRtcAlignmentDefault(0),
        /*! 宽度偶数对齐 */
        AliRtcAlignmentEven(1),
        /*! 宽度是4的倍数 */
        AliRtcAlignment4(2),
        /*! 宽度是8的倍数 */
        AliRtcAlignment8(3),
        /*! 宽度是16的倍数 */
        AliRtcAlignment16(4);
};

/*
* 在AliRtcEngine::registerVideoSampleObserver之后触发
*/

public int onGetVideoAlignment(){
  return AliRtcVideoObserAlignment.AliRtcAlignmentDefault.getValue();
}

5.获取的视频帧是否需要镜像

public boolean onGetObserverDataMirrorApplied(){
  return false;
}

6.执行回调

/*
* 返回true表示写回
* 默认写回,需要操作AliRtcVideoSample.data时必须要写回
*/

@Override
public boolean onLocalVideoSample(AliRtcEngine.AliRtcVideoSourceType sourceType, AliRtcEngine.AliRtcVideoSample videoSample) {
  boolean ret = false;
  /*
  * 处理本地采集的数据
  */
  return ret;
}

@Override
public boolean onRemoteVideoSample(String userId, AliRtcEngine.AliRtcVideoSourceType sourceType, AliRtcEngine.AliRtcVideoSample videoSample) {
  /*
  * 处理远端数据
  */
  return false;
}

@Override
public boolean onPreEncodeVideoSample(AliRtcEngine.AliRtcVideoSourceType aliVideoSourceType, AliRtcEngine.AliRtcVideoSample videoSample) {
  boolean ret = false;
  /*
  * 处理编码前数据
  */
  return false ;
}

7.取消回调注册

unregisterVideoSampleObserver

二、纹理处理

因为openGL线程模型的原因,所有的纹理回调都是在同一个线程callback。所有的纹理回调都是通过对象AliRtcTextureObserver实现。

1.注册纹理回调

public abstract void registerLocalVideoTextureObserver(AliRtcTextureObserver observer);

2.执行回调

openGL context创建后回调
@Override
public void onTextureCreate(long context) {
      context_ = context ;
      Log.d(TAG, "texture context: "+context_+" create!") ;
}
openGL 纹理变更回调
@Override
public int onTextureUpdate(int textureId, int width, int height, AliRtcEngine.AliRtcVideoSample videoSample) {
    /*
    *  进行 textureid处理
    */
     ++log_ref ;
     return textureId;
}       
openGL 上下文删除回调
@Override
public void onTextureDestroy() {
     Log.d(TAG, "texture context: "+context_+" destory!") ;
}

3.取消纹理回调

public abstract void  unRegisterLocalVideoTextureObserver();