文档中心
Old_Live_Room 互动视频
文档中心
体验 App
SDK 中心
API 中心
常见问题
代码市场
进入控制台
立即注册
登录
中文站 English
  • 文档中心
  • 互动视频
  • 视频进阶
  • 自定义视频前处理

自定义视频前处理

更新时间:2023-05-08 11:23

1 功能简介

考虑到滤镜的性能问题和美颜厂商的多样性,SDK 的自定义视频前处理功能采用面向对象设计,结合线程模型,帮助用户把外部处理视频数据的代码封装成可替换的滤镜组件。

自定义视频前处理一般在以下情况下使用: 当 SDK 自带的美颜无法满足需求,例如需要做挂件、贴纸,或者美颜效果无法达到预期时。

但是对于比较复杂的场景,例如想要用摄像头画面做图层混合,建议开发者使用视频外部采集功能实现,这样性能优化的空间会更大。

请注意:

1.SDK 会在适当的时机创建和销毁 ZegoVideoFilter,开发者无需担心生命周期不一致的问题。

2.开发者在外部调用 SDK 的切换摄像头的方法时,SDK 内部会调用 stopAndDeAllocate 等方法以实现摄像头的切换;如果客户使用了需要初始化多次的美颜资源(比如美颜资源和线程相关或者有上下文的),并在 stopAndDeAllocate 中做了释放美颜资源的操作,在有切换摄像头的需求下,需要在 allocateAndStart 中重新初始化美颜资源,否则会导致摄像头切换后画面卡顿;像贴纸类只需要初始化一次的美颜资源,建议放到构造滤镜时进行初始化(ZegoVideoFilterFactory 的 create())。

2 支持的滤镜类型

为了实现传输不同数据模型,适配不同线程模型,同时避免实现多余接口,SDK 采用伪 COM 的设计方式。

开发者需要在 ZegoVideoFilter 子类中显式指定一种数据传递类型,以实现对应的滤镜处理,SDK 目前支持的类型有:

滤镜类型 说明
BUFFER_TYPE_MEM 异步 RGBA32 滤镜,异步传递 RGBA32 的图像数据给 SDK
BUFFER_TYPE_SURFACE_TEXTURE SurfaceTexture 滤镜,SDK 会调用此滤镜的 getSurfaceTexture 方法获取 SurfaceTexture 对象
BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D 异步传递纹理 ID,SDK 会抛出 RGBA32 的图像数据,可通过 onProcessCallback 方法传递前处理后的纹理 ID 给 SDK
BUFFER_TYPE_SYNC_GL_TEXTURE_2D 同步传递纹理 ID,SDK 会调用此滤镜的 onProcessCallback 方法来获取前处理后的纹理 ID
BUFFER_TYPE_ASYNC_I420_MEM 异步 I420 滤镜,异步传递 I420 的图像数据给 SDK
BUFFER_TYPE_ASYNC_PIXEL_BUFFER Android 不支持
BUFFER_TYPE_SYNC_PIXEL_BUFFER Android 不支持

SDK 会根据数据类型,实例化不同类型的 client,并在调用 allocateAndStart 时将client 传给外部滤镜。

SDK 不推荐采用同步滤镜(BUFFER_TYPE_SYNC_GL_TEXTURE_2D)实现外部滤镜,因为在同一线程中,OpenGL ES 的上下文、设置、uniform、attribute 是共用的,倘若对 OpenGL ES 不是很熟悉,极易在细节上出现不可预知的 Bug。

3 使用步骤

相关功能的 Demo 源码,请联系 ZEGO 技术支持获取。

下面将以 BUFFER_TYPE_MEM (异步拷贝 RGBA32 图像数据)滤镜类型为例演示外部滤镜的用法。

实现 BUFFER_TYPE_MEM 滤镜类型的时序图如下:

3.1 创建外部滤镜

ZegoVideoFilter 定义最基本的组件功能,包括 allocateAndStart、stopAndDeAllocate,方便 SDK 在直播流程中进行交互。

类定义

VideoFilterMemDemo 的类定义如下:

/**
 * 异步滤镜设备需要实现 ZegoVideoFilter 协议
 */
public class VideoFilterMemDemo extends ZegoVideoFilter {
    // 初始化资源
     @Override
    protected void allocateAndStart(Client client) {
        ...
    }
    // 释放资源
    @Override
    protected void stopAndDeAllocate() {
        ...
    }
    // 指定滤镜类型
    @Override
    protected int supportBufferType() {
        ...
    }
    // 滤镜类型若是会传递图像数据的(比如 BUFFER_TYPE_MEM、BUFFER_TYPE_ASYNC_I420_MEM),SDK 会请求内存下标
    @Override
    protected synchronized int dequeueInputBuffer(int width, int height, int stride) {
        ...
    }
    // 滤镜类型若是会传递图像数据的,SDK 会请求对应内存下标的 ByteBuffer
    @Override
    protected synchronized ByteBuffer getInputBuffer(int index) {
        ...
    }
    // 滤镜类型若是会传递图像数据的,SDK 会抛出图像数据,外部滤镜进行处理
    @Override
    protected synchronized void queueInputBuffer(int bufferIndex, final int width, int height, int stride, long timestamp_100n) {
        ...
    }
    ...
}

指定滤镜类型

SDK 需要根据外部滤镜 supportBufferType 返回的类型值创建不同的 client 对象。在本示例中,返回 BUFFER_TYPE_MEM:

@Override
protected int supportBufferType() {
    return BUFFER_TYPE_MEM;
}

初始化资源

开发者初始化资源在 allocateAndStart 中进行。

开发者在 allocateAndStart 中获取到 client(SDK 内部实现 ZegoVideoFilter.Client 协议的对象),用于后续通知 SDK 视频前处理结果。

SDK 会在 App 第一次预览/推流/拉流时调用 allocateAndStart。除非 App 中途调用过 stopAndDeAllocate,否则 SDK 不会再调用 allocateAndStart。

@Override
protected void allocateAndStart(Client client) {
    mClient = client; // mClient 在此文件中是用来保存 SDK 返回的 client 对象,供后续使用
    mThread = new HandlerThread("video-filter");
    mThread.start();
    mHandler = new Handler(mThread.getLooper());
    mIsRunning = true;

    mProduceQueue.clear();
    mConsumeQueue.clear();
    mWriteIndex = 0;
    mWriteRemain = 0;
    mMaxBufferSize = 0;
}

请注意,client 必须保存为强引用对象,在 stopAndDeAllocate 被调用前必须一直被保存。SDK 不负责管理 client 的生命周期。

释放资源

开发者释放资源在 stopAndDeAllocate 中进行。

建议同步停止滤镜任务后再清理 client 对象,保证 SDK 调用 stopAndDeAllocate 后,没有残留的异步任务导致野指针 crash。

@Override
protected void stopAndDeAllocate() {
    mIsRunning = false;

    final CountDownLatch barrier = new CountDownLatch(1);
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            barrier.countDown();
        }
    });
    try {
        barrier.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    mHandler = null;

    if (Build.VERSION.SDK_INT >= 18) {
        mThread.quitSafely();
    } else {
        mThread.quit();
    }
    mThread = null;

    mClient.destroy(); // 必须调用
    mClient = null;
}

请注意,开发者必须在 stopAndDeAllocate 方法中调用 client 的 destroy 方法,否则会造成内存泄漏。

SDK 通知外部滤镜当前采集图像的宽高并请求内存池下标

SDK 先调用 ZegoVideoFilter 子类的 int dequeueInputBuffer(int, int, int) 方法,通知外部滤镜当前采集图像的宽高,并请求外部滤镜返回内存池的下标。

@Override
protected synchronized int dequeueInputBuffer(int width, int height, int stride) {
    if (stride * height > mMaxBufferSize) {
        if (mMaxBufferSize != 0) {
            mProduceQueue.clear();
        }
        // 每帧图像数据的大小
        mMaxBufferSize = stride * height;
        // 此处是通过 ByteBuffer.allocateDirect 方式创建了4个存放图像数据的 ByteBuffer,buffer 大小是 mMaxBufferSize
        createPixelBufferPool(4);
    }
    if (mWriteRemain == 0) {
        return -1;
    }

    mWriteRemain--;
    return (mWriteIndex + 1) % mProduceQueue.size();
}

SDK 请求外部滤镜返回 ByteBuffer

当 SDK 获得外部滤镜内存池下标后,会调用 ByteBuffer getInputBuffer(int) 获取 通过 ByteBuffer.allocateDirect 方式创建的 ByteBuffer,然后在 C++ 底层执行内存拷贝。

@Override
protected synchronized ByteBuffer getInputBuffer(int index) {
    if (mProduceQueue.isEmpty()) {
        return null;
    }
    // 此处的 mProduceQueue 是 PixelBuffer 类型的 ArrayList
    ByteBuffer buffer = mProduceQueue.get(index).buffer;
    buffer.position(0);
    return buffer;
}

外部滤镜处理数据

当 SDK 拷贝完数据后,会调用 void queueInputBuffer(int, int, int, int, long) 方法通知外部滤镜取图像数据。外部滤镜应当按照约定的数据传递类型,切换线程,异步进行前处理。

正常情况下,如果 SDK 是异步调用外部滤镜,外部滤镜完成前处理后,也按照SDK 调用外部滤镜的步骤将图像数据回传给 SDK。

下面的演示代码没有做前处理,只是在另一个线程进行数据拷贝。拷贝流程和 SDK 调用外部滤镜的步骤类似。开发者应该按照各自需求,实现该方法。

@Override
protected synchronized void queueInputBuffer(int bufferIndex, final int width, int height, int stride, long timestamp_100n) {
    if (bufferIndex == -1) {
        return ;
    }
    // 根据 SDK 返回的 bufferIndex 索引取出相应的图像数据,加到消费队列中
    PixelBuffer pixelBuffer = mProduceQueue.get(bufferIndex);
    pixelBuffer.width = width;
    pixelBuffer.height = height;
    pixelBuffer.stride = stride;
    pixelBuffer.timestamp_100n = timestamp_100n;
    pixelBuffer.buffer.limit(height * stride);
    mConsumeQueue.add(pixelBuffer);

    mWriteIndex = (mWriteIndex + 1) % mProduceQueue.size();

    // 切换线程,异步进行前处理
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            if (!mIsRunning) {
                Log.e(TAG, "already stopped");
                return ;
            }
            // 取出图像数据
            PixelBuffer pixelBuffer = getConsumerPixelBuffer();

            // 以下将按照 SDK 调用外部滤镜的步骤进行调用,将前处理后的数据回传给 SDK
            // 外部滤镜通知 SDK 图像数据的宽高并请求内存池下标
            int index = mClient.dequeueInputBuffer(pixelBuffer.width, pixelBuffer.height, pixelBuffer.stride);
            if (index >= 0) {
                // 此处应进行前处理,未做示例,若需参考示例请查看互动视频示例 Demo
                ...
                // 外部滤镜请求 SDK 返回 ByteBuffer
                ByteBuffer dst = mClient.getInputBuffer(index);
                dst.position(0);
                // 此处应将前处理后的数据写入 dst,示例未做前处理,仅将 SDK 抛出的图像数据又回传给 SDK
                pixelBuffer.buffer.position(0);
                dst.put(pixelBuffer.buffer);
                // 通知 SDK 取图像数据
                mClient.queueInputBuffer(index, pixelBuffer.width, pixelBuffer.height, pixelBuffer.stride, pixelBuffer.timestamp_100n);
            }

            returnProducerPixelBuffer(pixelBuffer);
        }
    });
}

上述步骤带 faceunity 美颜的示例代码可以在目录 /LiveRoomPlayground/videoFilter/src/main/java/com/zego/videofilter/videoFilter 下的 VideoFilterMemDemo.java 与 VideoFilterI420MemDemo.java 中找到。

3.2 创建外部滤镜工厂

ZegoVideoFilterFactory 是外部滤镜的入口,定义了创建、销毁 ZegoVideoFilter 接口,向 SDK 提供管理 ZegoVideoFilter 生命周期的能力;需要调用 setVideoFilterFactory 的地方必须实现该接口。

下述代码演示了如何创建外部滤镜工厂。工厂保存了 ZegoVideoFilter 的实例,不会反复创建。

public class VideoFilterFactoryDemo extends ZegoVideoFilterFactory {
    private int mode = 0;
    private ZegoVideoFilter mFilter = null;

    public VideoFilterFactoryDemo(int mode) {
        this.mode = mode;
    }

    // 创建外部滤镜实例
    public ZegoVideoFilter create() {
         if (mFilter == null) {
             // 此处的 mode 对应 Android 支持的滤镜类型,以创建不同的滤镜
            switch (mode) {
               case 0:
                mFilter = new VideoFilterMemDemo();
                break;
            case 1:
                mFilter = new VideoFilterSurfaceTextureDemo();
                break;
            case 2:
                mFilter = new VideoFilterHybridDemo();
                break;
            case 3:
                mFilter = new VideoFilterSurfaceTextureDemo2();
                break;
            case 4:
                mFilter = new VideoFilterI420MemDemo();
                break;
            case 5:
                mFilter = new VideoFilterGlTexture2dDemo();
                break;
            }
        }
        return mFilter;
    }
    // 销毁外部滤镜实例
    public void destroy(ZegoVideoFilter vf) {
        mFilter = null;
    }
}

请注意:

  1. 大部分情况下,ZegoVideoFilterFactory 实例会缓存原有 ZegoVideoFilter 实例,开发者需避免创建新的实例。
  2. 开发者必须保证 ZegoVideoFilter 在 create() 和 destroy() 之间是可用的,请勿直接销毁对象。

3.3 设置外部滤镜工厂

开发者需要使用外部滤镜功能时,需在使用前调用 setVideoFilterFactory 设置外部滤镜工厂对象。(此例中的对象为步骤 3.2 中所创建的 VideoFilterFactoryDemo)。

请注意,如果用户释放了工厂对象,即不再需要它时,请调用 ZegoExternalVideoFilter.setVideoFilterFactory(null, ZegoConstants.PublishChannelIndex.MAIN) 将其设置为空。

// 外部滤镜
...
VideoFilterFactoryDemo videoFilterFactoryDemo = new VideoFilterFactoryDemo(0); // 此处的 VideoFilterFactoryDemo 是 3.2 节中的类
// 设置外部滤镜工厂
ZegoExternalVideoFilter.setVideoFilterFactory(mFilterFactory, ZegoConstants.PublishChannelIndex.MAIN);

3.4 初始化 SDK

请参考文档:快速开始-初始化

  • 2020-05-12 及之后版本的 SDK,必须在引擎启动之前设置外部滤镜工厂。即 setVideoFilterFactory 需要在 startPreview、startPublishing、startPlaying 之前调用才有效。
  • 2020-05-12 之前版本的 SDK,必须在初始化 SDK 前设置外部滤镜工厂。即 setVideoFilterFactory 需要在 “initSDK” 之前调用才有效。

3.5 登录房间

请参考文档:快速开始-登录房间

3.6 推流

请参考文档:快速开始-推流

若对图像数据进行了前处理并将前处理后的数据回传给了 SDK,拉流时将是带美颜的图像。

4 API 参考列表

方法 描述
setVideoFilterFactory 设置外部滤镜工厂
ZegoVideoFilterFactory 外部滤镜工厂接口定义
ZegoVideoFilter 外部滤镜接口定义
initSDK 初始化SDK
setZegoLivePublisherCallback 设置推流回调监听
loginRoom 登录房间,推拉流之前必须登录房间
setPreviewViewMode 设置预览模式
setPreviewView 设置本地预览视频的视图
startPreview 启动本地预览
startPublishing 开始推流
stopPublishing 停止推流
logoutRoom 退出房间
unInitSDK 释放 SDK 资源,停止各种系统监听

5 相关文档

  • 进阶功能-视频外部采集

6 FAQ

Q1:ZegoVideoFilterFactory 的子类什么时候释放?

  答:我们推荐把工厂的实例保存为单例,仅作为 SDK 管理外部滤镜生命周期的通道,开发者可以为工厂子类添加 setter 和 getter,一起管理滤镜的生命周期。

Q2:如何使用 BUFFER_TYPE_ASYNC_I420_MEM 方式传递数据?

  答:BUFFER_TYPE_ASYNC_I420_MEM 和 BUFFER_TYPE_MEM 并无本质区别,只是 BUFFER_TYPE_ASYNC_I420_MEM 颜色空间是 I420,而 BUFFER_TYPE_MEM 是 RGBA32,两者同样都是需要异步实现。

Q3:如何使用 BUFFER_TYPE_SURFACE_TEXTURE 方式传递数据?

  答:选择 BUFFER_TYPE_SURFACE_TEXTURE 滤镜类型时,开发者可以通过client 获取 SurfaceTexture 对象,转换成 EglSurface,用于 OpenGL ES 绘制,同时 SDK 也要求外部滤镜显式实现 ZegoVideoFilter 的 getSurfaceTexture 作为滤镜的输入。

  如果对于这个机制不太熟悉,开发者可以参考 TextureView.getSurfaceTexture 或者 MediaCode.createInputSurface。SurfaceTexture 作为官方推荐的一种传输数据的管道,在许多系统 API 中都有使用,包括 android.hardware.camera、android.media.MediaCodec 等。

  因为没有显式传递图像宽高和时间戳的接口,需要借助 SurfaceTexture 的setDefaultBufferSize 方法设置图像宽高,然后 SDK 可以通过系统 API 获取后续的图像宽高。注意这里的宽高必须和后续的 glViewport 的宽高保持一致,避免图像变形。

Q4:如何使用 BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D 方式传递数据?

  答:选择 BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D 方式时,SDK 会按顺序依次调用外部滤镜的 dequeueInputBuffer、getInputBuffer、queueInputBuffer 方法向外部滤镜传递数据,而外部滤镜调用 client 的onProcessCallback 向 SDK 传递包含前处理结果的 texture。请注意:

  1. 这里的线程模型是异步的,即 SDK 调用 dequeueInputBuffer、getInputBuffer、queueInputBuffer 是在 SDK 线程,外部滤镜前处理调用client 的 onProcessCallback 是在外部滤镜的工作线程,同时外部滤镜应确保每次执行 OpenGL ES 绘制时调用 makeCurrent 及 glViewport,否则会产生不可预知的错误。
  2. 开发者必须在 stopAndDeAllocate 方法中,切换到对应的工作线程,再调用 client 的 destroy 方法。因为采用这种方式,SDK 会共享线程的上下文,销毁时,如果缺少对应的上下文,可能会出现不可预知的情况。

Q5:几种外部滤镜类型在实现上的主要区别是什么?

  答:当数据类型为 BUFFER_TYPE_MEM、BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D、BUFFER_TYPE_ASYNC_I420_MEM 类型的外部滤镜时,SDK 会按照 dequeueInputBuffer、getInputBuffer、queueInputBuffer的顺序调用外部滤镜的接口。

  BUFFER_TYPE_MEM 与 BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D 类型数据均为RGBA32 图像数据;BUFFER_TYPE_ASYNC_I420_MEM 类型数据为I420图像数据。

  当数据类型为 BUFFER_TYPE_SURFACE_TEXTURE 类型的外部滤镜时,SDK 会调用外部滤镜的getSurfaceTexture 方法。

  当数据类型为 BUFFER_TYPE_SYNC_GL_TEXTURE_2D 类型的外部滤镜时, SDK 会调用外部滤镜的 onProcessCallback 方法。

Q6:为什么 BUFFER_TYPE_MEM 滤镜类型的实现这么复杂?

  答:BUFFER_TYPE_MEM 完全参考 MediaCodec 的 dequeueInputBuffer 、getInputBuffer、queueInputBuffer 的流程实现。因为 Android 平台没有一个类似于 iOS 的 CVPixelBufferRef 的结构体,dequeueInputBuffer 和 getInputBuffer 是为了方便开发者保存每一帧数据故意设计成两个接口,SDK 只关心核心的内存拷贝,但是外部滤镜还需要保存图像的宽高。

Q7: 为什么使用 ByteBuffer 而不使用 byte[] 访问内存?

  答:ByteBuffer 是 Java 提供的直接访问 C++ 内存的方法,SDK 的核心逻辑是跨平台 C++ 实现的,外部滤镜的实际内存是通过 C++ 管理。为了避免 C++ 堆到 Java 堆上多余的拷贝,所以选择 ByteBuffer。ByteBuffer 在 Java 层可以通过ByteBuffer.allocateDirect 方法指定分配内存到 C++ 堆上,在 C++ 层可以通过jenv->NewDirectByteBuffer 方法包裹成 Java 对象,使用起来并不会比 byte[] 麻烦。同时 Android API 对 ByteBuffer 的支持也很友好,比如 OpenGL ES 里面上传贴图glTexImage2D 明确指定需要 java.nio.Buffer,Bitmap.copyPixelsFromBuffer 也支持 java.nio.Buffer。

Q8:如何创建 SurfaceTexture?

  答:首先当前线程需要 attach OpenGL ES 上下文,即调用eglMakeCurrent,然后生成类型为 GLES11Ext.GL_TEXTURE_EXTERNAL_OES 的 texture,最后通过 SurfaceTexture 的构造函数实例化。注意这里的 attach OpenGL ES 上下文,并没有要求必须是 TextureView 的回调或者是 SurfaceView 的回调,开发者完全可以自己创建线程,构造EglContext、EglSurface,和系统控件没有任何联系。

Q9:RGBA32 和 I420 的内存是如何排布的?

  答:SDK 传递的图像数据所有平面都是连续,没有特别的要求。

Q10:如何获取摄像头的旋转角度?

  答:当开发者使用美颜厂商提供的 SDK 时,绝大部分需要指定摄像头的旋转角度。考虑到美颜厂商 SDK 的兼容性问题, SDK 对于任何一种方式传递的图像数据都进行了纠正,即正朝向,开发者不需要关心摄像头旋转多少度。

本篇目录
  • 免费试用
  • 提交工单
    咨询集成、功能及报价等问题
    电话咨询
    400 1006 604
    咨询客服
    微信扫码,24h在线

    联系我们

  • 文档反馈