文档中心
Old_Live_Room 互动视频
文档中心
体验 App
SDK 中心
API 中心
常见问题
代码市场
进入控制台
立即注册
登录
中文站 English
  • 文档中心
  • 互动视频
  • 视频进阶
  • 视频外部采集

视频外部采集

更新时间:2023-04-25 18:19

1 常用场景

当开发者业务中出现以下情况时,我们推荐使用 SDK 的外部采集功能:

  1. 普通摄像头的采集无法满足需求。例如,包含了大量的原有业务。
  2. 直播过程中,开发者需要使用摄像头完成的额外功能和 SDK 的默认逻辑有冲突,导致摄像头无法正常使用。例如,直播到一半,需要录制短视频。
  3. 直播非摄像头数据。例如视频播放、屏幕分享、游戏直播等。
  4. 开发者可以使用外部采集来做自定义美颜等视频预处理。

2 功能简介

考虑到设备的独占问题,SDK 的视频外部采集采用的是面向对象的设计,帮助用户把原有采集代码封装成外部采集设备。

开发者通过实现 ZegoVideoCaptureFactory 和 ZegoVideoCaptureDevice 协议,可以把外部采集的数据传给 SDK 进行编码推流:

  1. ZegoVideoCaptureFactory 是外部采集的入口,定义了创建、销毁 ZegoVideoCaptureDevice 的接口,向 SDK 提供管理 ZegoVideoCaptureDevice 生命周期的能力。需要调用 setVideoCaptureFactory 的地方必须实现该接口。
  2. ZegoVideoCaptureDevice 定义基本的组件能力,包括 allocateAndStart、stopAndDeAllocate、startCapture、stopCapture、supportBufferType,方便 SDK 在直播流程中进行交互。

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

3 实现步骤

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

简要的实现步骤:

  1. 实现外部采集设备
  2. 实现外部采集工厂
  3. 设置外部采集工厂
  4. 初始化 SDK
  5. 推流

外部采集的接口调用流程如下图所示:

/Pics/iOS/ZegoLiveRoom/ZegoLiveRoom-ExternalCapture/call_flow_Android.png

3.1 通过外部采集工厂创建外部采集设备

在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;
    }

请注意:

  1. 大部分情况下,ZegoVideoCaptureFactory 会缓存 ZegoVideoCaptureDevice 实例,开发者需避免创建新的实例,造成争抢独占设备(例如摄像头)。
  2. 开发者必须保证 ZegoVideoCaptureDevice 在 create 和 destroy 之间是可用的,请勿直接销毁对象。

3.2 外部采集设备实现

下述代码,以创建在显存中绘制图片的采集设备( 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() {
       ...
    }

    ...
}

3.2.1 告知 SDK 当前采集数据的类型

由于 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;
}

3.2.2 初始化资源

开发者初始化资源在 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;
}

3.2.3 启动采集,向SDK传递外部采集视频数据

推流成功后,开发者需要在 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;
}

3.2.4 停止采集

开发者需要在 stopCapture 中停止外部采集设备的采集工作。

这里演示的是清除原有的 EglSurface,保证不会再向 SDK 传输数据。

/**
 * SDK停止推流时,调用 stopCapture 通知外部采集设备停止工作
 */
@Override
protected int stopCapture() {
    setOutputSurfaceTexture(null);
    return 0;
}

3.2.5 释放资源

开发者释放资源在 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;
}

3.3 设置外部采集工厂

开发者需要使用外部采集功能时,请在引擎启动之前(2020-05-12 及之后版本)\ 初始化 SDK前(2020-05-12` 之前版本)设置外部采集工厂对象。

请注意

  1. 调用 setVideoCaptureFactory 接口时,请区分 SDK 版本:
    • 2021-03-09 及之后的版本,在任何时机调用本接口都可直接使用。
    • 2020-05-12 至 2021-03-09 之间的版本,必须在“引擎启动之前”调用本接口。
    • 2020-05-12 之前版本必须在 “InitSDK” 之前调用,且需要使用外部采集功能时,该工厂对象不能为空。
  2. 如果不想再使用视频外部采集,可以在收到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的实例

4 FAQ

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 传递数据。请注意:

  1. SDK会使用该贴图进行绘制,切换 egl 的上下文,开发者必须每次绘制都调用 makeCurrent,否则可能会出现不可预知的情况。
  2. 开发者必须在 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)。解决方案:

  1. 开发者将外部采集的视频分辨率比例修改为 16:9
  2. 开发者调用 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 传视频帧数据的前提下,传输媒体次要信息才能成功。

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

    联系我们

  • 文档反馈