采集

本章节主要介绍音视频数据采集相关的功能。默认是SDK接管采集,开发者也可以选择自己控制采集也就是自定义音视频数据输入。 清晰度档位支持 清晰度设置默认是480P 支持的清晰度设置包括:720P540P480P高分辨率中等分辨率低分辨率

帧率档位支持 帧率设置默认是15fps 支持的帧率设置包括:5fps10fps15fps20fps25fps

开始和停止视频采集

开启预览前需要打开视频模块,停止预览后需要关闭视频模块。

    /**
     * 开启本地视频预览
     * 开启预览前需要 {@link AVChatManager#enableVideo()} 打开视频模块,{@link AVChatManager#setupLocalVideoRender(IVideoRender, boolean, int)} 设置本地预览画布
     *
     * @return {@code true} 方法调用成功,{@code false} 方法调用失败
     */
    public abstract boolean startVideoPreview();

    /**
     * 停止本地视频预览
     *
     * @return {@code true} 方法调用成功,{@code false} 方法调用失败
     */
    public abstract boolean stopVideoPreview();
        //开始视频采集
        AVChatManager.getInstance().enableRtc();
        if (mVideoCapturer == null) {
            mVideoCapturer = AVChatVideoCapturerFactory.createCameraCapturer();
            AVChatManager.getInstance().setupVideoCapturer(mVideoCapturer);
        }
        this.callingState = (callTypeEnum == AVChatType.VIDEO ? CallStateEnum.VIDEO : CallStateEnum.AUDIO);
        AVChatManager.getInstance().setParameters(avChatParameters);
        if (callTypeEnum == AVChatType.VIDEO) {
            AVChatManager.getInstance().enableVideo();
            AVChatManager.getInstance().startVideoPreview();
        }

       //关闭视频采集
        if (callingState == CallStateEnum.OUTGOING_VIDEO_CALLING || callingState == CallStateEnum.VIDEO) {
            AVChatManager.getInstance().stopVideoPreview();
            AVChatManager.getInstance().disableVideo();
        }
       AVChatManager.getInstance().disableRtc();

视频采集参数

视频采集参数是通过setParameters或者setParameter接口进行设置的,具体的参数名称在AVChatParameters中。

    /**
     * 默认情况下设备顺时针旋转角度, 某些特殊设备在默认情况下无法获取到设备方向,可指定默认的设备角度。
     *
     * 当一个设备水平放置时,它当前的角度是未定义的。 如果一个设备的工作环境永远是水平放置的,那么需要为它制定一个默认的旋转角度。
    public static final Key<Integer> KEY_DEVICE_DEFAULT_ROTATION = new Key<>(RtcParameters.KEY_DEVICE_DEFAULT_ROTATION, Integer.class);

    /**
     * 设备旋转角度修正偏移量。
     *
     * 如果存在SDK内部获取的角度和设备实际角度总是存在同样的误差,那么可以这只一个修正偏移量。主要存在一个定制设备有此类问题,大部分手机平板不需要关注此参数。
    public static final Key<Integer> KEY_DEVICE_ROTATION_FIXED_OFFSET = new Key<>(RtcParameters.KEY_DEVICE_ROTATION_FIXED_OFFSET, Integer.class);

    /**
     * 视频硬件编码模式。
     *
     * 目前仅支持高通CPU系列的硬件编码,如果设备不支持硬件编码,设置硬件编码后会自动回滚到软件编码模式。
     *
     */
    public static final Key<String> KEY_VIDEO_ENCODER_MODE = new Key<>(RtcParameters.KEY_VIDEO_ENCODER_MODE, String.class);

    /**
     * 视频清晰度。
     *
     * 在视频通话时,可以设置你期望的视频发送分辨率。
     * 实际采集分辨率和你设置的会有一定的区别, 对于{@code 720P} 可能存在 {@code 960x720} 或者 {@code 1280x720}。
     * 更极端的情况下,设备摄像头不支持采集某一档次的清晰度,会自动降低到下一档次。实际采用的清晰度级别可以通过此参数获取。
    public static final Key<Integer> KEY_VIDEO_QUALITY = new Key<>(RtcParameters.KEY_VIDEO_QUALITY, Integer.class);

    /**
     * 视频绘制时自动旋转。
     *
     * 视频绘制时会结合对方发送的画面角度,自己设备当前的角度以及当前视图布局的角度来旋转画面,让画面在任何情况下相对于自己都是正方向。
    public static final Key<Boolean> KEY_VIDEO_ROTATE_IN_RENDING = new Key<>(RtcParameters.KEY_VIDEO_ROTATE_IN_RENDING, Boolean.class);

    /**
     * 默认前置摄像头采集。
     *
     * 当使用摄像头为视频源时,存在多个摄像头可以选择优先使用前置摄像头。
     *
    public static final Key<Boolean> KEY_VIDEO_DEFAULT_FRONT_CAMERA = new Key<>(RtcParameters.KEY_VIDEO_DEFAULT_FRONT_CAMERA, Boolean.class);

    /**
     * 视频最大码率。
     *
     * 设置视频流的最大码率,码率需要结合分辨率来设置,不能设置过小。
     *
    public static final Key<Integer> KEY_VIDEO_MAX_BITRATE = new Key<>(RtcParameters.KEY_VIDEO_MAX_BITRATE, Integer.class);

    /**
     * 视频帧率。
     *
     * 设置视频画面期望帧率,由于设备限制,性能等问题可能存在实际帧率和期望帧率不一致。
    public static final Key<Integer> KEY_VIDEO_FRAME_RATE = new Key<>(RtcParameters.KEY_VIDEO_FRAME_RATE, Integer.class);

    /**
     * 视频采集方向。
     *
     * 默认情况下都是竖屏采集,用户可以设置不同的采集方向来调整拉流观众观看的角度。
     *
    public static final Key<Integer> KEY_VIDEO_CAPTURE_ORIENTATION = new Key<>(RtcParameters.KEY_VIDEO_CAPTURE_ORIENTATION, Integer.class);

    /**
     * 视频画面裁剪比例。
     *
     * 默认情况是视频采集画面不裁剪。 在不同的设备上采集处理的分辨率可能不一样。 比如 {@code 720P} 可能存在 {@code 960x720} 和 {@code 1280x720}。
     * 不同的分辨率会导致在预览绘制时画面存在画面显示不全或者黑边等问题。
    public static final Key<Integer> KEY_VIDEO_FIXED_CROP_RATIO = new Key<>(RtcParameters.KEY_VIDEO_FIXED_CROP_RATIO, Integer.class);
 AVChatParameters avChatParameters = new AVChatParameters();
 //对avChatParameters设置上述参数
 AVChatManager.getInstance().setParameters(avChatParameters);

切换视频质量

切换视频质量是通过setParameters或者setParameter接口设置的。

    /**
     * 视频清晰度。
     *
     * 在视频通话时,可以设置你期望的视频发送分辨率。
     * 实际采集分辨率和你设置的会有一定的区别, 对于{@code 720P} 可能存在 {@code 960x720} 或者 {@code 1280x720}。
     * 更极端的情况下,设备摄像头不支持采集某一档次的清晰度,会自动降低到下一档次。实际采用的清晰度级别可以通过此参数获取。
    public static final Key<Integer> KEY_VIDEO_QUALITY = new Key<>(RtcParameters.KEY_VIDEO_QUALITY, Integer.class);
AVChatVideoQuality 视频清晰度
QUALITY_DEFAULT 默认。
QUALITY_LOW 低分辨率。
QUALITY_MEDIUM 中等分辨率。
QUALITY_HIGH 高分辨率。
QUALITY_480P 480P。
QUALITY_540P 540P。
QUALITY_720P 720P。
   //设置高分辨率
   AVChatManager.getInstance().setParameter(AVChatParameters.KEY_VIDEO_QUALITY, AVChatVideoQuality.QUALITY_HIGH);

视频采集开始以后的动态控制

目前我们提供了独立的视频采集模块, 使用 AVChatCameraCapturer 来控制Camera视频源。 Camera 视频源提供了切换摄像头,对焦设置,焦距调整等功能。视频采集开始以后的动态控制是通过调用 AVChatCameraCapturer中的控制接口实现的。

通过AVChatVideoCapturerFactory来创建对应的视频采集模块, 支持Camera和Camera2,之后调用AVChatManager#setupVideoCapturer把视频源设置到SDK内部,然后调用AVChatCameraCapturer 的控制接口来动态控制Camera。

//切换摄像头
public abstract int switchCamera();
//多摄像头判断
public static boolean hasMultipleCameras();
//焦距调整
public abstract void setZoom(int zoomValue);
//获取当前焦距
public abstract int getCurrentZoom();
//获取最大焦距
public abstract int getMaxZoom();
//前置摄像头闪光灯控制
public abstract int setFlash(boolean flash);
//设置测光区域
public abstract void setMeteringAreas(float x, float y);
private AVChatCameraCapturer mVideoCapturer;
if (mVideoCapturer == null) {
    mVideoCapturer = AVChatVideoCapturerFactory.createCameraCapturer();
    AVChatManager.getInstance().setupVideoCapturer(mVideoCapturer);
}
mVideoCapturer.setZoom(zoomValue);
mVideoCapturer.switchCamera();
int currentZoom = mVideoCapturer.getCurrentZoom();
mVideoCapturer.setFlash(flash);
mVideoCapturer.switchCamera();
boolean hasMultipleCameras = mVideoCapturer.hasMultipleCameras();
mVideoCapturer.setMeteringAreas(event.getRawX(), event.getRawY());

自定义视频数据输入

SDK支持自定义外部视频数据输入。开发者根据自己的需求,只需从文件或相机等外部视频源读入或采集视频数据,并通过SDK提供的对应接口提交每一帧的视频数据即可,SDK会负责完成余下的工作。具体的,开发者需要继承于AVChatExternalVideoCapturer,重写类中相关方法并使用类中提供的Observe接口提交视频帧数据。开发者首先需要将AVChatExternalVideoCapturer的自定义实例通过AVChatManager#setupVideoCapturer方法进行设置,以便SDK能向其中注册Observer接口供开发者使用。

参考AVChatExternalVideoCapturer类,简要介绍如下:

内部类Observer接口的方法说明:
`void onCapturerStarted(boolean success)`:可选调用,用于通知SDK外部是否成功开启了视频提交流程。
`void onCapturerStopped()`:可选调用,用于通知SDK外部已经停止了视频提交流程。
`int onByteFrameCaptured(byte[] data, int dataLen, int width, int height, int rotation, int frameRate, int format, long timeStamp, boolean mirror)`:通过该方法向SDK提交每一帧的视频数据,成功提交返回`0`,其他为失败。

      data: 每一帧的视频数据;
      dataLen: 数据长度;
      width: 视频帧宽度;
      height: 视频帧高度;
      rotation: 旋转角度,顺时针为正方向,90的倍数;
      frameRate: 视频帧率;
      format: 视频数据格式 ,支持`AVChatImageFormat#I420`和`AVChatImageFormat#NV21`;
      timeStamp: 视频帧时间戳,以`AVChatManager#enableRtc`时刻为开始的基准时间,使用`SystemClock#elapsedRealtime()`即可;
      mirror: 是否需要镜像,该操作在旋转之后进行;

`AVChatExternalVideoCapturer`作为自定义外部视频输入的父类使用,开发者在具体实现时只需要关注以下这些方法:

`onStart()`:在`AVChatManager#startVideoPreview`调用时会同步回调该方法,可重写该方法,开启视频输入。
`onStop()`:在`AVChatManager#stopVideoPreview`调用时会同步回调该方法,可重写该方法,关闭视频输入。
`getObserver()`:返回SDK向类中注册的数据提交接口,通过该接口中的`onByteFrameCaptured`方法可向SDK提交视频数据。

内部类Observer接口的onByteFrameCaptured方法参数说明:

onByteFrameCaptured参数 说明
data 每一帧的视频数据。
dataLen 数据长度。
width 视频帧宽度。
height 视频帧高度。
rotation 旋转角度,顺时针为正方向,90的倍数。
frameRate 视频帧率。
format 视频数据格式 ,支持AVChatImageFormat#I420AVChatImageFormat#NV21
timeStamp 视频帧时间戳,以AVChatManager#enableRtc时刻为开始的基准时间,使用SystemClock#elapsedRealtime()即可。
mirror 是否需要镜像,该操作在旋转之后进行。
public class AVChatYUVFileVideoCapturer extends AVChatExternalVideoCapturer {

    private static final String TAG = "YUVFileVideoCapturer";

    private final int mWidth, mHeight;
    private FileInputStream mYUVInputStream;
    private FileChannel mYUVFileChannel;
    private final AtomicBoolean mRunning = new AtomicBoolean(false);
    private Handler mFrameHandler;
    private int mFrameCount = 0;
    private ByteBuffer mByteBuffer;
    private long mFileLen, mPosition;
    private final int mRotation;
    private final boolean mMirror;
    private int mFrameRate = 15, mFrameDelay = 1000 / 15;

    public AVChatYUVFileVideoCapturer(String yuvFilePath, int width, int height, int frameRate, int rotation, boolean mirror){
        if (TextUtils.isEmpty(yuvFilePath)){
            throw new IllegalArgumentException("Invalid yuv file path!");
        }
        File yuvFile = new File(yuvFilePath);
        if (yuvFile.exists() && yuvFile.isFile() && yuvFile.canRead()){
            mWidth = width;
            mHeight = height;
            mRotation = rotation;
            mMirror = mirror;
            try {
                mYUVInputStream = new FileInputStream(yuvFile);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            if (frameRate > 0){
                mFrameRate = frameRate;
                mFrameDelay = 1000 / mFrameRate;
            }
            check();
        }else {
            throw new IllegalArgumentException("Illegal yuv file!");
        }
    }

    private void check(){
        mYUVFileChannel = mYUVInputStream.getChannel();
        try {
            long length = mYUVFileChannel.size();
            if (length * 8 % 12 != 0){ //i420格式,每个像素为12bit
                Log.e(TAG, "check I420 format failed, please check file!");
                mYUVInputStream.close();
                mYUVInputStream = null;
                mYUVFileChannel = null;
            }else {
                mByteBuffer = ByteBuffer.allocateDirect(mWidth * mHeight * 3 / 2);
                mFileLen = length;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onStart(){
        super.onStart();
        if (mYUVFileChannel == null){
            Log.e(TAG, "cannot start");
            return;
        }

        if (mFrameHandler == null) {

            HandlerThread thread = new HandlerThread("yuv-file-reader");
            thread.start();
            mFrameHandler = new Handler(thread.getLooper());

            mFrameHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (mRunning.get()) {
                        getAndPushFrame();
                    }
                    synchronized (AVChatYUVFileVideoCapturer.this) {
                        if (mFrameHandler != null) {
                            mFrameHandler.postDelayed(this, mFrameDelay);
                        }
                    }
                }
            });

            resume();
        }
    }

    public void pause(){
        if (mRunning.compareAndSet(true, false)){
            Log.i(TAG, "paused");
        }
    }

    public void resume(){
        if (mRunning.compareAndSet(false, true)){
            Log.i(TAG, "resumed");
        }
    }

    private synchronized void quit(){
        if (mFrameHandler != null) {
            mFrameHandler.getLooper().quit();
            mFrameHandler = null;
        }
    }

    void getAndPushFrame(){
        int bytes = 0;
        mByteBuffer.clear();
        try {
            bytes = mYUVFileChannel.read(mByteBuffer, mPosition);
        } catch (IOException e) {
            e.printStackTrace();
        }
        mByteBuffer.flip();
        mPosition += bytes;
        if (mPosition >= mFileLen){
            mPosition = 0;
        }

        AVChatExternalVideoCapturer.Observer observer = getObserver();
        if (observer == null) return;
        if (bytes > 0){
            mFrameCount++;
            if (mFrameCount == 1){
                observer.onCapturerStarted(true);
            }
            observer.onByteBufferFrameCaptured(mByteBuffer.array(), bytes, mWidth, mHeight, mRotation, mFrameRate, AVChatImageFormat.I420, SystemClock.elapsedRealtime(), mMirror);
        }
    }

    private void closeFile(){
        if (mYUVFileChannel != null){
            try {
                mYUVFileChannel.close();
                mYUVFileChannel = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void onStop(){
        super.onStop();
        pause();
        quit();
        closeFile();
        Log.i(TAG, "stopCapture, frame count: " + mFrameCount);
    }
}

自定义音频数据输入

SDK支持外部自定义音频数据输入,用户可以从文件或者其他设备读取语音PCM数据。

仅支持PCM数据, 通过参数 AVChatParameters#KEY_AUDIO_EXTERNAL_CAPTURE 来启用外部自定义语音数据输入, 然后使用 AVChatManager#pushExternalAudioData 输入外部语音数据。语音数据长度尽量不要超过 40ms, 数据时间戳需要精确到毫秒, 如果需要内部自动生成时间戳 timeMs 可以设置为 -1.

    /**
     * 外部输入音频数据, 每次最多允许传递 40ms 数据
     *
     * @param data           音频数据
     * @param samples        样本数量
     * @param sampleRate     采样率,仅支持 8000,16000,32000,44100,48000
     * @param channel        频道数量
     * @param bytesPerSample 每个样本的字节数, 仅支持PCM 16
     * @param blocking       是否阻塞写入数据。
     *                       如果非阻塞写入数据, 内部缓冲不足时会立即返回失败<code>-100</code>,
     *                       如果阻塞写入数据, 当内部缓冲不足时,会等待一定时间后再返回(待写入数据时长), 内部缓冲不足返回失败<code>-100</code>。
     * @return {@code 0} 方法调用成功,其他失败
     * @see AVChatParameters#KEY_AUDIO_EXTERNAL_CAPTURE
     */
    public abstract int pushExternalAudioData(byte[] data,
                                              int samples,
                                              int sampleRate,
                                              int channel,
                                              int bytesPerSample,
                                              boolean blocking) throws NullPointerException, IllegalArgumentException;
参数 说明
data 音频数据。
samples 样本数量。
sampleRate 采样率,仅支持 8000,16000,32000,44100,48000。
channel 频道数量。
bytesPerSample 每个样本的字节数, 仅支持PCM 16
blocking 是否阻塞写入数据。。

     /**
       * 数据长度为Samples
       * 数据采样率仅支持 8000, 16000, 32000, 44100, 48000
       * 数据声道仅支持 1, 2
       * 数据仅支持PCM16 => 2
       **/
       AVChatManager.getInstance().pushExternalAudioData(data, dataLen / 2, sampleRateHz, channels, 2, true);