当开发者业务中出现以下情况时,我们推荐使用 SDK 的外部采集功能:
考虑到设备的独占问题,SDK 的视频外部采集采用的是面向对象的设计,帮助用户把原有采集代码封装成外部采集设备。
开发者通过实现 ZegoVideoCaptureFactory
和 ZegoVideoCaptureDevice
协议,可以把外部采集的数据传给 SDK 进行编码推流:
ZegoVideoCaptureFactory
是外部采集的入口,定义了创建、销毁 ZegoVideoCaptureDevice
的接口,向 SDK 提供管理 ZegoVideoCaptureDevice
生命周期的能力。需要调用 setVideoCaptureFactory
的地方必须实现该接口。ZegoVideoCaptureDevice
定义基本的组件能力,包括 allocateAndStart
、stopAndDeAllocate
、startCapture
、stopCapture
、supportBufferType
,方便 SDK 在直播流程中进行交互。请注意,SDK 会在适当的时机创建和销毁
ZegoVideoCaptureDevice
,开发者无需担心生命周期不一致的问题。
相关功能的 Demo 源码,请联系 ZEGO 技术支持获取。
简要的实现步骤:
外部采集的接口调用流程如下图所示:
在SDK 回调create
时,创建并返回ZegoVideoCaptureDevice
实例。
下述代码展示了如何创建外部采集工厂。工厂保存了 ZegoVideoCaptureDevice
的实例,不会反复创建。
public class VideoCaptureFactoryDemo extends ZegoVideoCaptureFactory {
private CaptureOrigin origin = CaptureOrigin.CaptureOrigin_Image;
private Context context = null;
private ZegoVideoCaptureDevice mDevice = null;
// 采集来源类型
public enum CaptureOrigin{
CaptureOrigin_Image, //当前采集设备使用的数据传递类型是Surface_Texture
CaptureOrigin_ImageV2, //当前采集设备使用的数据传递类型是GL_Texture_2D
CaptureOrigin_Screen,
CaptureOrigin_Camera, //当前采集设备使用的数据传递类型是YUV格式(内存拷贝)
CaptureOrigin_CameraV2 //当前采集设备使用的数据传递类型是Surface_Texture
}
public VideoCaptureFactoryDemo(CaptureOrigin origin){
this.origin = origin;
}
public void setContext(Context context) {
this.context = context;
}
public ZegoVideoCaptureDevice create(String device_id) {
if (origin == CaptureOrigin.CaptureOrigin_Camera) {
mDevice = new VideoCaptureFromCamera();
} else if (origin == CaptureOrigin.CaptureOrigin_Image) {
mDevice = new VideoCaptureFromImage(mContext);
} else if (origin == CaptureOrigin.CaptureOrigin_ImageV2) {
mDevice = new VideoCaptureFromImage2(mContext);
} else if (origin == CaptureOrigin.CaptureOrigin_CameraV2) {
mDevice = new VideoCaptureFromCamera2();
}
return mDevice;
}
public void destroy(ZegoVideoCaptureDevice device) {
mDevice = null;
}
请注意:
- 大部分情况下,
ZegoVideoCaptureFactory
会缓存ZegoVideoCaptureDevice
实例,开发者需避免创建新的实例,造成争抢独占设备(例如摄像头)。- 开发者必须保证
ZegoVideoCaptureDevice
在create
和destroy
之间是可用的,请勿直接销毁对象。
下述代码,以创建在显存中绘制图片的采集设备( VideoCaptureFromImage
)为例,开发者可按各自的需求,参看实现步骤。
VideoCaptureFromImage
的类定义如下:
public class VideoCaptureFromImage extends ZegoVideoCaptureDevice implements Choreographer.FrameCallback, TextureView.SurfaceTextureListener, SurfaceHolder.Callback {
@Override
protected void allocateAndStart(ZegoVideoCaptureDevice.Client client) {
...
}
@Override
protected void stopAndDeAllocate() {
...
}
@Override
protected int startCapture() {
...
}
@Override
protected int stopCapture() {
...
}
@Override
protected int supportBufferType() {
...
}
...
}
由于 Android 采集的多样性,SDK 支持多种外部采集数据传递格式,所以开发者必须显示告知 SDK 当前采集设备使用何种数据传递类型。目前 SDK 支持的类型有:
类型定义 | 类型说明 |
---|---|
PIXEL_BUFFER_TYPE_MEM |
内存,YUV格式,后续通过 client 的 onByteBufferFrameCaptured 传递采集数据 |
PIXEL_BUFFER_TYPE_SURFACE_TEXTURE |
SurfaceTexture,当开发者使用该种类型传输采集数据时,可通过 client 获取 SurfaceTexture 对象 |
PIXEL_BUFFER_TYPE_GL_TEXTURE_2D |
OpenGL ES的2d贴图,后续通过 client 的 onTextureCaptured 传递采集数据 |
PIXEL_BUFFER_TYPE_ENCODED_FRAME |
码流,传递编码后的采集数据,后续通过 client 的 onEncodedFrameCaptured 传递采集数据。同时使用音频外部采集与渲染时,需要在 VideoCodecConifg 里面将 is_external_clock 设置为 true,否则会出现音画不同步的问题。 |
后续示例代码都将以官方推荐的 SurfaceTexture
类型来传输采集数据,用于演示 ZegoVideoCaptureDevice
的使用:
@Override
protected int supportBufferType() {
return PIXEL_BUFFER_TYPE_SURFACE_TEXTURE;
}
开发者初始化资源在 allocateAndStart
中进行。
开发者在 allocateAndStart
中获取到 client (SDK 内部实现的、同样实现 ZegoVideoCaptureDevice.Client 协议的客户端),用于通知 SDK 采集结果。
SDK 会在 App 第一次预览 / 推流 / 拉流时调用 allocateAndStart
。除非 App 中途调用过 stopAndDeAllocate
,否则 SDK 不会再调用 allocateAndStart
。
请注意,
client
实例在stopAndDeAllocate
被调用前必须一直保存。
部分示例代码如下:
protected void allocateAndStart(ZegoVideoCaptureDevice.Client client) {
mClient = client;
init();
setBitmap(createBitmapFromAsset());
}
此处,init()负责初始化 OpenGL ES 的资源,为保证后续调用都是合法的,建议使用同步方式初始化。下面代码演示通过注册 Choreographer 的刷新回调,保证后续绘制图片时不会出现画面撕裂的问题。
private int init() {
mThread = new HandlerThread("VideoCaptureFromImage" + hashCode());
mThread.start();
mHandler = new Handler(mThread.getLooper());
final CountDownLatch barrier = new CountDownLatch(1);
mHandler.post(new Runnable() {
@Override
public void run() {
captureEglBase = EglBase.create(null, EglBase.CONFIG_RECORDABLE);
previewEglBase = EglBase.create(captureEglBase.getEglBaseContext(), EglBase.CONFIG_RGBA);
captureDrawer = new GlRectDrawer();
previewDrawer = new GlRectDrawer();
mIsEgl14 = EglBase14.isEGL14Supported();
Choreographer.getInstance().postFrameCallback(VideoCaptureFromImage.this);
mIsRunning = true;
barrier.countDown();
}
});
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return 0;
}
推流成功后,开发者需要在 startCapture
中,采集数据并传递给 SDK 的 client 对象。
以下示例代码演示把 client
返回的 SurfaceTexture
转换成 EglSurface
,用于 OpenGL ES 绘制。同时,借助 SurfaceTexture
的 setDefaultBufferSize
方法设置图像宽高(SDK 内部通过系统API获取后续的图像宽高)。
/**
* SDK开始推流时,调用 startCapture 通知外部采集设备开始工作
*/
@Override
protected int startCapture() {
setOutputSurfaceTexture(mClient.getSurfaceTexture());
return 0;
}
private int setOutputSurfaceTexture(SurfaceTexture surface_texture) {
final SurfaceTexture temp = surface_texture;
mHandler.post(new Runnable() {
@Override
public void run() {
releaseCaptureSurface();
if (temp != null) {
temp.setDefaultBufferSize(mImageWidth, mImageHeight);
try {
captureEglBase.createSurface(temp);
captureEglBase.makeCurrent();
mIsCapture = true;
} catch (RuntimeException e) {
// Clean up before rethrowing the exception.
captureEglBase.releaseSurface();
throw e;
}
} else {
mIsCapture = false;
}
}
});
return 0;
}
注意: 这里的宽高必须和后续的 glViewPort 的宽高保持一致,避免图像变形;另外,如果是采用
PIXEL_BUFFER_TYPE_MEM
或者PIXEL_BUFFER_TYPE_GL_TEXTURE_2D
类型传递采集数据时,需要显示传递图像宽高和采集时间戳。
更新采集数据
示例代码通过 Choreographer.FrameCallback
实现,当 Choreographer
刷新回调触发时,绘制图像数据到屏幕和 SDK 提供的 EglSurface
上 。
@Override
public void doFrame(long frameTimeNanos) {
if (!mIsRunning) {
return;
}
Choreographer.getInstance().postFrameCallback(this);
if (mBitmap == null) {
return;
}
if (mIsPreview) {
if (mTextureView != null) {
attachTextureView();
} else if (mSurfaceView != null) {
attachSurfaceView();
}
if (previewEglBase.hasSurface()) {
drawToPreview(mBitmap);
}
}
if (mIsCapture && captureEglBase.hasSurface()) {
drawToCapture(mBitmap);
}
if (mDrawCounter == 0) {
mX = (mX + 1) % 4;
if (mX == 0) {
mY = (mY + 1) % 4;
}
}
mDrawCounter = (mDrawCounter + 1) % 60;
}
开发者需要在 stopCapture
中停止外部采集设备的采集工作。
这里演示的是清除原有的 EglSurface
,保证不会再向 SDK 传输数据。
/**
* SDK停止推流时,调用 stopCapture 通知外部采集设备停止工作
*/
@Override
protected int stopCapture() {
setOutputSurfaceTexture(null);
return 0;
}
开发者释放资源在 stopAndDeAllocate
中进行。
建议同步停止采集任务后再清理 client 对象,保证 SDK 调用 stopAndDeAllocate
后,没有残留的异步任务导致野指针 crash。
请注意,开发者必须在
stopAndDeAllocate
方法中调用 client 的destroy
方法,否则会造成内存泄漏。
部分示例代码如下:
protected void stopAndDeAllocate {
uninit();
mClient.destroy(); // 必须调用 mClient.destroy() 以确保资源被完全释放
mClient = null;
}
此处,uninit()负责反初始化释放 OpenGL ES 资源,同步停止保证后续没有更多的异步回调产生。
private int uninit() {
final CountDownLatch barrier = new CountDownLatch(1);
mHandler.post(new Runnable() {
@Override
public void run() {
mIsRunning = false;
release();
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;
return 0;
}
开发者需要使用外部采集功能时,请在引擎启动之前(2020-05-12
及之后版本)\ 初始化 SDK前(2020-05-12` 之前版本)设置外部采集工厂对象。
请注意
- 调用
setVideoCaptureFactory
接口时,请区分 SDK 版本:
2021-03-09
及之后的版本,在任何时机调用本接口都可直接使用。2020-05-12
至2021-03-09
之间的版本,必须在“引擎启动之前”调用本接口。2020-05-12
之前版本必须在 “InitSDK” 之前调用,且需要使用外部采集功能时,该工厂对象不能为空。- 如果不想再使用视频外部采集,可以在收到onAVEngineStop通知引擎关闭时,调用本接口
setVideoCaptureFactory
设置为 null,否则下次引擎启动后(2020-05-12
及之后版本)或者 Init SDK 后(2020-05-12
之前版本)仍然还会使用视频外部采集
引擎启动的时机有:1、未登录房间启动预览功能;2、未登录房间使用媒体播放器;3、未登录房间使用音效播放器。 引擎关闭的时机有:1、退出房间;2、未登录房间并且预览、媒体播放器和引擎播放器都停止工作。
2020-05-12
之前版本,由于外部采集的设置时机需要在初始化 SDK 之前,若客户端使用开关控制是否使用外部采集,在其改变useVideoCapture
的布尔值之后需要先**反初始化 SDK(unInitSDK)**,再重新执行外部采集的设置。
if (useVideoCapture) {
ZegoVideoCaptureFactory factory = new VideoCaptureFactoryDemo();
factory.setContext(ZegoApplication.sApplicationContext);
factory.setCaptureOrigin(captureOrigin); //设置采集来源,图片或者摄像头
//设置外部采集工厂对象
videoCapture.setVideoCaptureFactory(factory, ZegoConstants.PublishChannelIndex.MAIN); // videoCapture 是 ZegoExternalVideoCapture实例
} else {
videoCapture.setVideoCaptureFactory(null, ZegoConstants.PublishChannelIndex.MAIN); //videoCapture 是 ZegoExternalVideoCapture实例
}
...
//初始化SDK
zegoliveRoom.initSDK(appId, signKey); //zegoliveRoom是ZegoliveRoom的实例
Q1: 如何使用
PIXEL_BUFFER_TYPE_MEM
方式的传递采集数据?
答:选择 PIXEL_BUFFER_TYPE_MEM
方式时,开发者可以调用 client 的 onByteBufferFrameCaptured
传递数据。
对于图像内存,要求所有平面的数据是连续的,比如 NV21 有两个平面,一个是 Y 平面,一个是 UV 混合交错的平面,那么 Y 平面和 UV 平面在内存里面的排布必须是连续的。
具体的支持的颜色空间请参考 ZegoVideoCaptureDevice
的 PIXEL_FORMAT
定义。
Q2:如何使用
PIXEL_BUFFER_TYPE_GL_TEXTURE_2D
方式传递采集数据?
答:选择 PIXEL_BUFFER_TYPE_GL_TEXTURE_2D
方式时,开发者可以调用 client
的 onTextureCaptured
传递数据。请注意:
stopAndDeAllocate
方法中,切换到对应的工作线程,再调用client
的 destroy
方法。因为采用这种方式,SDK会共享线程的上下文,销毁时,如果缺少对应的上下文,可能会出现不可预知的情况。Q3:
ZegoVideoCaptureDevice
子类的哪些方法是必须要实现的?
答:除 allocateAndStart
、stopAndDeAllocate
、startCapture
、stopCapture
、supportBufferType
以外的方法都不要求实现。
其他接口仅仅只是为了方便开发者在自己的采集实现和 SDK 默认的采集实现间切换,避免业务层需要写两套逻辑代码。
每个方法都有对应的 SDK 接口,调用 SDK 接口时,会透传调用对应的方法。比如调用 SDK 的 startPreview
时,会透传调用 ZegoVideoCaptureDevice
子类的 startPreview
方法。
Q4:
VideoCaptureFormat
的 strides 表示什么?
答:stride 表示每一行多少字节,也可以叫 pitch,在某些平台上也叫 BytesPerRow,SDK 需要知道每个平面存储的长度,才能正确的进行颜色空间转换。
Q5:如何在
Java
层进行OpenGL ES
的绘制?
答:请参考 https://developer.android.com/guide/topics/graphics/opengl.html 和 https://developer.android.com/training/graphics/opengl/index.html。
Q6:
ZegoVideoCaptureFactory
的子类什么时候释放?
答:我们推荐把工厂的实例保存为单例,仅作为 SDK 管理外部采集设备生命周期的通道,开发者可以为工厂子类添加 setter 和 getter,一起管理采集类的生命周期。
Q7:使用外部采集,本地预览的画面正常,推出去观众端看到的画面变形了?
答:外部采集进来的图像比例和 SDK 默认设置的分辨率的比例不一致(比如外部采集进来的是 4:3 ,SDK 默认推流分辨率 360 * 640 是 16:9)。解决方案:
setAvConfig
,将 SDK 的推流分辨率自定义为 4:3(比如480 * 640)Q8:开启外部采集后,如何使用 SDK 的预览功能?
答:开启外部采集后,默认情况下,外部采集的数据,需要用户自己渲染。如果想让 SDK 支持在开启外部视频采集的同时也能预览,需在初始化 SDK 前通过 ZegoLiveRoom.setConfig("vcap_external_support_preview=true")
开启,然后正常调用 setPreviewView
、startPreview
接口即可。
Q9:开启外部采集后,外部采集的帧率和拉流播放帧率不一致?
答:使用 PIXEL_BUFFER_TYPE_MEM
方式传递数据时,通过设置SDK的帧率 setVideoFPS
和外部采集调用 onByteBufferFrameCaptured
的频率一致。
使用 PIXEL_BUFFER_TYPE_GL_TEXTURE_2D
方式传递数据时,通过设置SDK的帧率 setVideoFPS
和外部采集调用 onTextureCaptured
的频率一致。
使用 PIXEL_BUFFER_TYPE_ENCODED_FRAME
方式传递数据时,通过设置SDK的帧率 setVideoFPS
和外部采集调用 onEncodedFrameCaptured
的频率一致。
Q10:SDK 接收视频帧数据方法内部对传入的数据是同步处理还是异步处理?
答:SDK 接收视频帧数据后,会先同步拷贝数据,然后再异步执行编码等操作,所以在将数据传入 SDK 后即可立即释放。
Q11:开启外部采集后,使用媒体次要信息功能时需要注意什么?
答:媒体次要信息的数据是跟随视频数据传输的,所以必须是在调用 onByteBufferFrameCaptured
、onTextureCaptured
或者 onEncodedFrameCaptured
方法向 SDK 传视频帧数据的前提下,传输媒体次要信息才能成功。
联系我们
文档反馈