视频外部滤镜

1 使用场景

当 SDK 自带的美颜无法满足需求,例如需要做挂件、贴纸,或者美颜效果无法达到预期时,建议开发者使用外部滤镜功能。

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

2 功能简介

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

主要结构如下:

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

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

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

3 选择合适的外部滤镜

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

开发者需要在子类中显式指定一种数据传递类型,SDK 目前支持的类型有:

滤镜类型

说明

BUFFER_TYPE_MEM

异步滤镜(异步传递 RGBA32 的图像数据)

BUFFER_TYPE_ASYNC_PIXEL_BUFFER

Android 不支持

BUFFER_TYPE_SYNC_PIXEL_BUFFER

Android 不支持

BUFFER_TYPE_SURFACE_TEXTURE

当开发者使用该种类型滤镜时,SDK 会调用此滤镜的 getSurfaceTexture 获取 SurfaceTexture 对象

BUFFER_TYPE_HYBRID_MEM_GL_TEXTURE_2D

异步 RGBA32 的图像数据

BUFFER_TYPE_SYNC_GL_TEXTURE_2D

当开发者使用该种滤镜时,SDK 会调用此滤镜的 onProcessCallback 方法

BUFFER_TYPE_ASYNC_I420_MEM

异步 I420 滤镜(异步传递 I420 的图像数据)

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

下面将以 BUFFER_TYPE_MEM 类型滤镜为例演示外部滤镜的用法。

4 创建外部滤镜

4.1 创建外部滤镜工厂

下述代码演示了如何创建外部滤镜工厂。工厂保存了 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) {
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;
}
}

请注意:

大部分情况下,ZegoVideoFilterFactory 实例会缓存原有 ZegoVideoFilter 实例,开发者需避免创建新的实例。

开发者必须保证 ZegoVideoFilter 在 create 和 destroy 之间是可用的,请勿直接销毁对象。

4.2 设置外部滤镜工厂

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

请注意,如果用户释放了工厂对象,不再需要它时,请调用本接口将其设置为空。

if (useVideoFilter) {
// 外部滤镜
if (mFilterFactory == null) {
VideoFilterFactoryDemo videoFilterFactoryDemo = new VideoFilterFactoryDemo(0);
mFilterFactory = videoFilterFactoryDemo;
}
ZegoLiveRoom.setVideoFilterFactory(mFilterFactory);
} else {
ZegoLiveRoom.setVideoFilterFactory(null);
}

4.3 创建外部滤镜

下述代码,以创建BUFFER_TYPE_MEM(异步拷贝图像)类型滤镜为例,开发者可根据需求,参考如下实现步骤。

类定义

ZegoVideoFilterDemo 的类定义如下:

/**
* 异步滤镜设备需要实现 ZegoVideoFilter 协议
*/
public class VideoFilterMemDemo extends ZegoVideoFilter {
@Override
protected void allocateAndStart(Client client) {
...
}
@Override
protected void stopAndDeAllocate() {
...
}
@Override
protected int supportBufferType() {
...
}
@Override
protected synchronized int dequeueInputBuffer(int width, int height, int stride) {
...
}
@Override
protected synchronized ByteBuffer getInputBuffer(int index) {
...
}
@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;
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。正常情况下,如果 SDK 是异步调用外部滤镜,外部滤镜完成前处理后,也按照同样的步骤回调 SDK。

@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;
createPixelBufferPool(4);
}
if (mWriteRemain == 0) {
return -1;
}
mWriteRemain--;
return (mWriteIndex + 1) % mProduceQueue.size();
}

SDK 请求外部滤镜返回 Direct 的 ByteBuffer

当 SDK 获得外部滤镜内存池下标后,会调用 ByteBuffer getInputBuffer(int) 获取 Direct 的 ByteBuffer,在 C++ 底层执行内存拷贝。

@Override
protected synchronized ByteBuffer getInputBuffer(int index) {
if (mProduceQueue.isEmpty()) {
return null;
}
ByteBuffer buffer = mProduceQueue.get(index).buffer;
buffer.position(0);
return buffer;
}

外部滤镜处理数据

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

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

@Override
protected synchronized void queueInputBuffer(int bufferIndex, final int width, int height, int stride, long timestamp_100n) {
if (bufferIndex == -1) {
return ;
}
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();
int index = mClient.dequeueInputBuffer(pixelBuffer.width, pixelBuffer.height, pixelBuffer.stride);
if (index >= 0) {
ByteBuffer dst = mClient.getInputBuffer(index);
dst.position(0);
pixelBuffer.buffer.position(0);
dst.put(pixelBuffer.buffer);
mClient.queueInputBuffer(index, pixelBuffer.width, pixelBuffer.height, pixelBuffer.stride, pixelBuffer.timestamp_100n);
}
returnProducerPixelBuffer(pixelBuffer);
}
});
}

上述步骤的示例代码可以在目录 app/src/main/java/com/zego/livedemo5/videofilter 下的 VideoFilterFactoryDemo.java 与 VideoFilterMemDemo.java 中找到,具体细节不再赘述。

5 同步滤镜

5.1 创建外部滤镜工厂

下述代码演示了如何创建外部滤镜工厂。工厂保存了 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) {
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;
break;
case 4:
mFilter = new VideoFilterI420MemDemo();
break;
case 5:
mFilter = new VideoFilterGlTexture2dDemo();
break;
}
}
return mFilter;
}
public void destroy(ZegoVideoFilter vf) {
mFilter = null;
}
}

请注意:

大部分情况下,ZegoVideoFilterFactory 实例会缓存原有 ZegoVideoFilter 实例,开发者需避免创建新的实例。

开发者必须保证 ZegoVideoFilter 在 create 和 destroy 之间是可用的,请勿直接销毁对象。

5.2 设置外部滤镜工厂

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

请注意,如果用户释放了工厂对象,不再需要它时,请调用本接口将其设置为空。

if (useVideoFilter) {
// 外部滤镜
if (mFilterFactory == null) {
VideoFilterFactoryDemo videoFilterFactoryDemo = new VideoFilterFactoryDemo(5);
mFilterFactory = videoFilterFactoryDemo;
}
ZegoLiveRoom.setVideoFilterFactory(mFilterFactory);
} else {
ZegoLiveRoom.setVideoFilterFactory(null);
}
5.3

创建外部滤镜

下述代码,以创建BUFFER_TYPE_SYNC_GL_TEXTURE_2D(同步拷贝图像)类型滤镜为例,开发者可根据需求,参考如下实现步骤。

类定义

VideoFilterGlTexture2dDemo 的类定义如下:
/**
* 外部滤镜设备需要实现 ZegoVideoFilter 协议
*/
public class VideoFilterMemDemo extends ZegoVideoFilter {
@Override
protected void allocateAndStart(Client client) {
...
}
@Override
protected void stopAndDeAllocate() {
...
}
@Override
protected int supportBufferType() {
...
}
@Override
protected void onProcessCallback(int textureId, int width, int height, long timestamp_100n) {
...
}
...
}

指定滤镜类型

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

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

初始化资源

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

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

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

@Override
protected void allocateAndStart(Client client) {
mClient = client;
mWidth = mHeight = 0;
if (mDrawer == null) {
mDrawer = new GlRectDrawer();
}
}

请注意,client 在 stopAndDeAllocate 被调用前必须一直被保存。

释放资源

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

建议同步停止滤镜任务后再清理 client 对象,保证 SDK 调用 stopAndDeAllocate 后,没有残留的异步任务导致野指针 crash。正常情况下,如果 SDK 是异步调用外部滤镜,外部滤镜完成前处理后,也按照同样的步骤回调 SDK。

@Override
protected void stopAndDeAllocate() {
if (mTextureId != 0) {
int[] textures = new int[]{mTextureId};
GLES20.glDeleteTextures(1, textures, 0);
mTextureId = 0;
}
if (mFrameBufferId != 0) {
int[] frameBuffers = new int[]{mFrameBufferId};
GLES20.glDeleteFramebuffers(1, frameBuffers, 0);
mFrameBufferId = 0;
}
if (mDrawer != null) {
mDrawer.release();
mDrawer = null;
}
mClient.destroy();
mClient = null;
}

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

绘制图像数据

SDK 会通过调用外部滤镜的 void onProcessCallback(int, int, int height, long) 方法通知外部滤镜采集的结果,然后外部滤镜在同一线程进行前处理,再通过调用 client 的 onProgressCallback 通知 SDK 前处理的结果。

@Override
protected void onProcessCallback(int textureId, int width, int height, long timestamp_100n) {
if (mWidth != width || mHeight != height) {
if (mTextureId != 0) {
int[] textures = new int[]{mTextureId};
GLES20.glDeleteTextures(1, textures, 0);
mTextureId = 0;
}
if (mFrameBufferId != 0) {
int[] frameBuffers = new int[]{mFrameBufferId};
GLES20.glDeleteFramebuffers(1, frameBuffers, 0);
mFrameBufferId = 0;
}
mWidth = width;
mHeight = height;
}
if (mTextureId == 0) {
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
mTextureId = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
mFrameBufferId = GlUtil.generateFrameBuffer(mTextureId);
} else {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBufferId);
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
mDrawer.drawRgb(textureId, transformationMatrix,
width, height, 0, 0, width, height);
mClient.onProcessCallback(mTextureId, width, height, timestamp_100n);
}

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

上述步骤的示例代码可在 app/src/main/java/com/zego/livedemo5/videofilter 目录下的 VideoFilterFactoryDemo.java 和 VideoFilterGlTexture2dDemo.java 中找到。其它类型外部滤镜的示例代码亦可在 app/src/main/java/com/zego/livedemo5/videofilter 下找到,具体细节不再赘述。

6 Q&A

ZegoVideoFilterFactory的子类什么时候释放?

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

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

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

如何使用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的宽高保持一致,避免图像变形。具体请参考VideoCaptureFromImage.java。

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

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

请注意:

这里的线程模型是异步的,即SDK调用dequeueInputBuffer、getInputBuffer、queueInputBuffer在SDK线程,外部滤镜前处理调用client的onProcessCallback在外部滤镜的工作线程,同时外部滤镜应确保每次执行OpenGL ES绘制时调用makeCurrent及glViewport,否则会产生不可预知的错误。

开发者必须在 stopAndDeAllocate 方法中,切换到对应的工作线程,再调用 client 的 destroy 方法。因为采用这种方式,SDK会共享线程的上下文,销毁时,如果缺少对应的上下文,可能会出现不可预知的情况。

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

当数据类型为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方法。

为什么BUFFER_TYPE_MEM这么复杂?

BUFFER_TYPE_MEM完全参考MediaCodec的dequeueInputBuffer

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

为什么使用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。

如何创建SurfaceTexture?

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

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

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

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