考虑到滤镜的性能问题和美颜厂商的多样性,SDK 的自定义视频前处理功能采用面向对象设计,结合线程模型,帮助用户把外部处理视频数据的代码封装成可替换的滤镜组件。
自定义视频前处理一般在以下情况下使用: 当 SDK 自带的美颜无法满足需求,例如需要做挂件、贴纸,或者美颜效果无法达到预期时。
但是对于比较复杂的场景,例如想要用摄像头画面做图层混合,建议开发者使用视频外部采集功能实现,这样性能优化的空间会更大。
请注意:
1.SDK 会在适当的时机创建和销毁 ZegoVideoFilter,开发者无需担心生命周期不一致的问题。
2.开发者在外部调用 SDK 的切换摄像头的方法时,SDK 内部会调用
stopAndDeAllocate
等方法以实现摄像头的切换;如果客户使用了需要初始化多次的美颜资源(比如美颜资源和线程相关或者有上下文的),并在stopAndDeAllocate
中做了释放美颜资源的操作,在有切换摄像头的需求下,需要在allocateAndStart
中重新初始化美颜资源,否则会导致摄像头切换后画面卡顿;像贴纸类只需要初始化一次的美颜资源,建议放到构造滤镜时进行初始化(ZegoVideoFilterFactory 的 create())。
为了实现传输不同数据模型,适配不同线程模型,同时避免实现多余接口,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。
相关功能的 Demo 源码,请联系 ZEGO 技术支持获取。
下面将以 BUFFER_TYPE_MEM
(异步拷贝 RGBA32 图像数据)滤镜类型为例演示外部滤镜的用法。
实现 BUFFER_TYPE_MEM
滤镜类型的时序图如下:
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 先调用 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 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 中找到。
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;
}
}
请注意:
- 大部分情况下,
ZegoVideoFilterFactory
实例会缓存原有ZegoVideoFilter
实例,开发者需避免创建新的实例。- 开发者必须保证
ZegoVideoFilter
在create()
和destroy()
之间是可用的,请勿直接销毁对象。
开发者需要使用外部滤镜功能时,需在使用前调用 setVideoFilterFactory
设置外部滤镜工厂对象。(此例中的对象为步骤 3.2 中所创建的 VideoFilterFactoryDemo)。
请注意,如果用户释放了工厂对象,即不再需要它时,请调用 ZegoExternalVideoFilter.setVideoFilterFactory(null, ZegoConstants.PublishChannelIndex.MAIN) 将其设置为空。
// 外部滤镜
...
VideoFilterFactoryDemo videoFilterFactoryDemo = new VideoFilterFactoryDemo(0); // 此处的 VideoFilterFactoryDemo 是 3.2 节中的类
// 设置外部滤镜工厂
ZegoExternalVideoFilter.setVideoFilterFactory(mFilterFactory, ZegoConstants.PublishChannelIndex.MAIN);
请参考文档:快速开始-初始化
2020-05-12
及之后版本的 SDK,必须在引擎启动之前设置外部滤镜工厂。即 setVideoFilterFactory
需要在 startPreview、startPublishing、startPlaying 之前调用才有效。2020-05-12
之前版本的 SDK,必须在初始化 SDK 前设置外部滤镜工厂。即 setVideoFilterFactory
需要在 “initSDK” 之前调用才有效。请参考文档:快速开始-登录房间
请参考文档:快速开始-推流
若对图像数据进行了前处理并将前处理后的数据回传给了 SDK,拉流时将是带美颜的图像。
方法 | 描述 |
---|---|
setVideoFilterFactory | 设置外部滤镜工厂 |
ZegoVideoFilterFactory | 外部滤镜工厂接口定义 |
ZegoVideoFilter | 外部滤镜接口定义 |
initSDK | 初始化SDK |
setZegoLivePublisherCallback | 设置推流回调监听 |
loginRoom | 登录房间,推拉流之前必须登录房间 |
setPreviewViewMode | 设置预览模式 |
setPreviewView | 设置本地预览视频的视图 |
startPreview | 启动本地预览 |
startPublishing | 开始推流 |
stopPublishing | 停止推流 |
logoutRoom | 退出房间 |
unInitSDK | 释放 SDK 资源,停止各种系统监听 |
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。请注意:
client
的 onProcessCallback
是在外部滤镜的工作线程,同时外部滤镜应确保每次执行 OpenGL ES 绘制时调用 makeCurrent 及 glViewport,否则会产生不可预知的错误。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 对于任何一种方式传递的图像数据都进行了纠正,即正朝向,开发者不需要关心摄像头旋转多少度。
联系我们
文档反馈