自定义视频数据处理

  • Android

更新时间不可用

了解实时互动 RTC 在不同平台上的自定义视频数据处理方式,包括接入流程、关键配置和平台差异。

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

适用场景

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

  • 自定义美颜处理:拉取本地采集的原始视频流,并使用第三方美颜处理工具进行处理。处理后的视频流可以再次推送到频道中,供观看者实时观看。
  • 自定义视频编辑:在视频处理链路的不同阶段对视频流进行编辑处理,如添加水印、裁剪、剪辑、特效等。用户可以根据需求对视频流进行灵活编辑,编辑后的视频流可用于直播或存储。
  • 拉流预览或监控:在视频处理链路的不同阶段拉取视频流进行本地或云端的监控和预览。用户可以实时查看视频流的质量、内容和状态,确保视频流的正常传输和播放。

技术原理

SDK 中视频模块的数据处理流程如下图所示。

根据上图所示,在不同的模式下,观测点对应的回调触发时机也有所不同。SDK 默认采用只读模式,你可以根据自己的需求设置读写模式(详见实现方法中的相应步骤)。

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

信息

  • 视频前处理:位于视频前处理插件(包括 SDK 内置插件以及拓展插件)的默认加载位置、本地预览默认位置和视频帧的抽帧或缩放处理之前。
  • 编码前处理:位于本地预览默认位置和视频帧的抽帧或缩放处理之后。
  • 视频后处理:在视频解码之后,对视频帧进行超分辨率、超级画质处理等处理。

前提条件

在进行操作之前,请确保你已经在项目中实现了基本的实时音视频功能。详见实现音视频互动。

实现方法

根据你的实际业务需求,在以下方案中选择实现视频数据处理的方法。

观测原始数据

参考如下步骤,获取 SDK 采集的原始数据并进行处理。

  1. 调用 registerVideoFrameObserver 方法注册原始视频观测器 IVideoFrameObserver,同时注册相应的回调。

    Java
    engine.registerVideoFrameObserver(iVideoFrameObserver);
  2. (可选)视频观测器默认的观测位置是 VIDEO_MODULE_POSITION_POST_CAPTURER(对应 onCaptureVideoFrame 回调)和 VIDEO_MODULE_POSITION_PRE_RENDERER(对应 onRenderVideoFrame 回调),你可以在 getObservedFramePosition 回调的返回值中设置预期的视频观测位置。

    以下示例代码以观测位置为 VIDEO_MODULE_POSITION_PRE_ENCODER 为例,对应 onPreEncodeVideoFrame 回调。

    Java
    @Override
    public int getObservedFramePosition() {
        return IVideoFrameObserver.VIDEO_MODULE_POSITION_PRE_ENCODER;
    }
  3. (可选)SDK 默认不接收经过处理后的视频数据,如果你要修改此行为,可在 getVideoFrameProcessMode 回调中设置预期的视频处理模式为 1:

    • 0:(默认)只读模式,不需要将处理后的数据发回给 SDK。
    • 1:读写模式,需要将处理后的数据发回给 SDK。
    Java
    @Override
    public int getVideoFrameProcessMode() {
        return IVideoFrameObserver.PROCESS_MODE_READ_WRITE;
    }
  4. (可选)SDK 默认提供的视频数据格式为 I420Buffer 或 TextureBuffer,如果你需要其他格式的视频数据,可在 getVideoFormatPreference 回调的返回值中设置预期的数据格式:

    • 0:(默认)原始视频像素格式
    • 1:I420 格式
    • 4:RGBA 格式
    • 16:I422 格式
    Java
    @Override
    public int getVideoFormatPreference() {
        return IVideoFrameObserver.VIDEO_PIXEL_DEFAULT;
    }
  5. (可选)如果采集到视频帧中设置了旋转信息 rotation,你可以在 getRotationApplied 回调的返回值中设置视频数据按照 rotation 做旋转处理。

    注意

    该回调支持的视频数据格式有:I420、RGBA、Texture。

    Java
    @Override
    public boolean getRotationApplied() {
       return true;
    }
  6. (可选)如果你希望获取镜像处理后的视频数据,可在 getMirrorApplied 回调的返回值中设置视频数据做镜像处理。

    注意

    • 该回调和 setVideoEncoderConfiguration 方法均支持设置镜像效果,声网建议你仅选择一种方法进行设置,同时使用两种方法会导致镜像效果叠加从而造成设置镜像不成功。
      • 该回调支持的视频数据格式有:I420、RGBA、Texture。
    Java
    @Override
    public boolean getMirrorApplied() {
       return true;
    }
  7. 调用 joinChannel[2/2] 方法加入频道,SDK 会在你设置好的观测位置回调相应的视频数据。

    信息

    你不需要加入频道,也可以获取到本地采集的原始视频数据。

    Java
    int res = engine.joinChannel(token, channelId, 0, option);
  8. (可选)如果需要将处理后的视频数据传回给 SDK,则在相应回调中传入处理后的视频数据,并将回调的返回值设置为 true(设置 SDK 接收视频帧)。以下示例代码以 onCaptureVideoFrame 回调为例。

    注意

    建议你在修改 videoFrame 中的参数时,需确保修改后的参数跟视频帧缓冲区中的视频帧实际情况保持一致,否则可能导致本地预览画面和对端的视频画面出现非预期的旋转、失真等问题。

    Java
    @Override
    public boolean onCaptureVideoFrame(int sourceType, VideoFrame videoFrame) {
       ...
       //将修改后的视频数据传回 SDK
       videoFrame.replaceBuffer(new NV21Buffer(nv21, width, height, null), videoFrame.getRotation(), videoFrame.getTimestampNs());
       return true;
    }
  9. 调用 registerVideoFrameObserver 方法,取消注册原始视频观测器 IVideoFrameObserver,即可停止观测视频数据。

    Java
    engine.registerVideoFrameObserver(null);

观测已编码数据

参考如下步骤,获取 SDK 采集的已编码数据并进行处理。

  1. 调用 registerVideoEncodedFrameObserver 方法注册视频编码观测器 IVideoEncodedFrameObserver,同时注册回调 onEncodedVideoFrameReceived。

    Java
    engine.registerVideoEncodedFrameObserver(iVideoEncodedFrameObserver);
  2. 调用 joinChannel[2/2] 方法加入频道。

    Java
    int res = engine.joinChannel(token, channelId, 0, option);
  3. 通过 onEncodedVideoFrameReceived 回调获取编码后的视频数据。

    Java
     @Override
     public boolean onEncodedVideoFrameReceived(ByteBuffer buffer, EncodedVideoFrameInfo info) {
         if (isWrite && info.frameType == VIDEO_FRAME_TYPE_KEY_FRAME) {
             Log.i(TAG, "OnEncodedVideoImageReceived  uid:" + info.uid + "   widht:" + info.width + "  height:" + info.height +"  codecType:" + info.codecType + "  frameType:" + info.frameType);
             isWrite = false;
             File cacheDir = getContext().getExternalCacheDir();
             if (cacheDir == null) {
                 cacheDir = getContext().getCacheDir();
             }
             // 导出一个关键帧数据到本地存储,可以通过`ffplay -i videodata`来查看画面
             File file = new File(cacheDir, "videodata");
             try (FileOutputStream fos = new FileOutputStream(file)) {
                 byte[] bytes = new byte[buffer.remaining()];
                 buffer.get(bytes);
                 fos.write(bytes);
                 Log.i(TAG, "write success");
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
         return false;
     }
  4. 调用 registerVideoEncodedFrameObserver 方法,取消注册视频观测器 IVideoEncodedFrameObserver,即可停止观测视频数据。

    Java
    engine.registerVideoEncodedFrameObserver(null);

同时观测已编码数据和渲染前数据

如果你想同时获取远端用户的已编码数据(简称为 A 组)和渲染前视频数据(简称为 B 组),可参考如下步骤。

  1. 调用 registerVideoEncodedFrameObserver 方法注册视频编码观测器 IVideoEncodedFrameObserver,同时注册回调 onEncodedVideoFrameReceived。

    Java
    engine.registerVideoEncodedFrameObserver(iVideoEncodedFrameObserver);
  2. 调用 registerVideoFrameObserver 方法注册原始视频观测器 IVideoFrameObserver,同时注册相应的回调。

    Java
    engine.registerVideoFrameObserver(iVideoFrameObserver);
  3. 调用 joinChannel[2/2] 方法加入频道,并设置 autoSubscribeVideo 为 false(不自动订阅任何视频流)。

    Java
    ChannelMediaOptions option = new ChannelMediaOptions();
    option.autoSubscribeVideo = false;
    int res = engine.joinChannel(token, channelId, 0, option);
  4. (可选)SDK 默认订阅所有远端原始数据和编码后的数据(即 encodedFrameOnly 为 false)和视频大流,如果你想针对不同 uid 设置不同的订阅选项,可调用 setRemoteVideoSubscriptionOptions 方法设置。

    信息

    SDK 对远端视频流的默认订阅行为取决于注册的视频观测器类型:

    • 如果注册的是 IVideoFrameObserver 观测器,则默认订阅原始数据和编码后的数据。
    • 如果注册的是 IVideoEncodedFrameObserver 观测器,则默认仅订阅编码后的数据。
    • 如果注册了两种观测器,则默认跟随后注册的视频观测器。在本场景中,由于后注册的是 IVideoFrameObserver 观测器,所以默认订阅原始数据和编码后的数据,且 A 组和 B 组数据都可以按照预期被观测到。
    Java
    @Override
    public void onUserJoined(int uid, int elapsed) {
        VideoSubscriptionOptions options = new VideoSubscriptionOptions();
        options.streamType = REMOTE_VIDEO_STREAM_LOW;
        options.encodedFrameOnly = true;
        engine.setRemoteVideoSubscriptionOptions(uid, options);
    }
  5. 调用 muteRemoteVideoStream 方法,并将 muted 设置为 false(订阅远端用户的视频流),开始接收远端用户的视频流。此时:

    Java
    engine.muteRemoteVideoStream(uid, false);
  6. 通过相应的回调获取预期的数据:

    Java
     @Override
     public boolean onEncodedVideoFrameReceived(ByteBuffer buffer, EncodedVideoFrameInfo info) {
         if (isWrite && info.frameType == VIDEO_FRAME_TYPE_KEY_FRAME) {
             Log.i(TAG, "OnEncodedVideoImageReceived  uid:" + info.uid + "   widht:" + info.width + "  height:" + info.height +"  codecType:" + info.codecType + "  frameType:" + info.frameType);
             isWrite = false;
             File cacheDir = getContext().getExternalCacheDir();
             if (cacheDir == null) {
                 cacheDir = getContext().getCacheDir();
             }
             // 导出一个关键帧数据到本地存储,可以通过`ffplay -i videodata`来查看画面
             File file = new File(cacheDir, "videodata");
             try (FileOutputStream fos = new FileOutputStream(file)) {
                 byte[] bytes = new byte[buffer.remaining()];
                 buffer.get(bytes);
                 fos.write(bytes);
                 Log.i(TAG, "write success");
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
         return false;
     }
    
     @Override
     public boolean onRenderVideoFrame(String s, int i, VideoFrame videoFrame) {
         // 自定义渲染远端用户的画面
         if (mSurfaceView != null && videoFrame != lastI420Frame) {
             Log.d(TAG, "onRenderVideoFrame: " + i + "   connection: " + s + "  buffer: " + videoFrame.getBuffer());
             lastI420Frame = videoFrame;
             textureBufferHelper.invoke(new Callable<Void>() {
                 @Override
                 public Void call() throws Exception {
                     if (lastI420Frame.getBuffer() instanceof VideoFrame.I420Buffer) {
                         final VideoFrame.I420Buffer i420Buffer = (VideoFrame.I420Buffer) lastI420Frame.getBuffer();
                         GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
                         yuvUploader.uploadFromBuffer(i420Buffer);
                     }
                     return null;
                 }
             });
             mSurfaceView.requestRender();
         }
         return false;
     }
  7. 分别调用 registerVideoEncodedFrameObserver 和 registerVideoFrameObserver 方法,取消注册视频观测器,即可停止观测视频数据。

    Java
    // 取消注册已编码视频观测器
    engine.registerVideoEncodedFrameObserver(null);
    // 取消注册原始视频观测器
    engine.registerVideoFrameObserver(null);
    

示例项目

声网提供了开源的原始视频数据示例项目供你参考,你可以前往下载或查看其中的源代码。