Android

通过阅读本文,您可以了解 Android 输出音视频流裸数据的方法。

输出视频数据

1. 开启视频裸数据回调,调用以下接口:

public int enableVideoSampleObserver(boolean enable, int position);
说明
  • enable : true 表示订阅视频裸数据;false 表示关闭订阅视频裸数据。

  • position:表示获取的视频数据所处的位置

    public static class DingRtcVideoObservePosition {
       /** 采集视频数据,对应输出回调 onCaptureVideoFrame。 */
       public static final int DingRtcPositionPostCapture = 1;
       /** 渲染视频数据,对应输出回调 onRemoteVideoFrame。 */
       public static final int DingRtcPositionPreRender = 1 << 1;
       /** 编码前视频数据,对应输出回调 OnPreEncodeVideoFrame。 */
       public static final int DingRtcPositionPreEncoder = 1 << 2;
    }

调用此接口后,即可开始订阅视频裸数据,裸数据通过回调接口返回。

2. 注册监听获取视频裸数据回调,方法如下:

    public void registerVideoSampleObserver(DingRtcVideoObserver observer);

3. 通过DingRtcVideoObserver回调获取视频裸数据,方法如下:

/**
 * @brief 获取视频数据输出格式。
 * @return 期望视频输出格式。
 * @return 视频数据输出格式。
 */
public DingRtcVideoFormat onGetVideoFormatPreference() {
    return DingRtcVideoFormat.DingRtcVideoFormatI420;
}

/**
 * @brief 订阅的本地采集视频数据回调。
 * @param sourceType  视频流类型。
 * @param videoSample 视频裸数据。
 * @return 
 * - true: 需要写回SDK(默认写回,需要操作DingRtcVideoSample.data时必须要写回);
 * - false: 不需要写回SDK(需要直接操作DingRtcVideoSample.dataFrameY、DingRtcVideoSample.dataFrameU、DingRtcVideoSample.dataFrameV时使用)。
 */
public boolean onLocalVideoSample(DingRtcEngine.DingRtcVideoSourceType sourceType, DingRtcEngine.DingRtcVideoSample videoSample) {
    return false;
}

/**
 * @brief 订阅的远端视频数据回调。
 * @param callId      用户ID。
 * @param sourceType  视频流类型。
 * @param videoSample 视频裸数据。
 * @return 
 * - true:需要写回SDK(默认写回,需要操作DingRtcVideoSample.data时必须要写回);
 * - false:不需要写回SDK(需要直接操作DingRtcVideoSample.dataFrameY、DingRtcVideoSample.dataFrameU、DingRtcVideoSample.dataFrameV时使用)。
 */
public boolean onRemoteVideoSample(String callId, DingRtcEngine.DingRtcVideoSourceType sourceType, DingRtcEngine.DingRtcVideoSample videoSample) {
    return false;
}

/**
 * @brief 本地编码前视频数据。
 * @param sourceType 视频流类型。
 * @param videoSample 视频裸数据。
 * @return 
 * - true:需要写回SDK(默认写回,需要操作DingRtcVideoSample.data时必须要写回);
 * - false:不需要写回SDK(需要直接操作DingRtcVideoSample.dataFrameY、DingRtcVideoSample.dataFrameU、DingRtcVideoSample.dataFrameV时使用)。
 */
public boolean onPreEncodeVideoFrame(DingRtcEngine.DingRtcVideoSourceType sourceType, DingRtcEngine.DingRtcVideoSample videoSample) {
    return false;
}
说明

调用 enableVideoSampleObserver(true)以及registerVideoSampleObserver后:

  • 通过下面回调告诉引擎需要获取的视频格式:

    • onGetVideoFormatPreference

  • 通过以下三个回调获取对应的视频裸数据:

    • onLocalVideoSample为本地预览数据回调,在开始预览之后可收到数据流。

    • onPreEncodeVideoFrame为本地编码前数据回调,在推流之后可收到数据流。

    • onRemoteVideoSample为拉流数据回调,subscribe 拉流成功后可收到数据流。

4. 订阅的远端视频裸数据写入本地YUV文件示例代码:

    // 开启视频数据dump
    private int enableDumpVideo() {
        String pcmPath = "/sdcard/dump_i420.pcm";
        File yuvFile = new File(pcmPath);

        try {
            mYuvBos = new BufferedOutputStream(new FileOutputStream(yuvFile, true));
        } catch (FileNotFoundException e) {
            Log.e(TAG, "open yuv file failed" + e.getMessage());
            return -1;
        }
        mRtcEngine.enableVideoSampleObserver(true, DingRtcPositionPostCapture);
        mRtcEngine.registerVideoSampleObserver(new DingRtcEngine.DingRtcVideoObserver() {
            @Override
            public DingRtcEngine.DingRtcVideoFormat onGetVideoFormatPreference() {
                return DingRtcEngine.DingRtcVideoFormat.DingRtcVideoFormatI420;
            }

            @Override
            public boolean onLocalVideoSample(DingRtcEngine.DingRtcVideoSourceType sourceType, DingRtcEngine.DingRtcVideoSample videoSample) {
                writeToDumpFile(videoSample);
                return false;
            }
        });

        return 0;
    }

    // 关闭视频数据dump
    private int disableDumpVideo() {
        mRtcEngine.enableVideoSampleObserver(false, DingRtcPositionPostCapture);
        mRtcEngine.registerVideoSampleObserver(null);
        try {
            if (mYuvBos != null) {
                mYuvBos.close();
                mYuvBos = null;
            }
        } catch (IOException e) {
            Log.e(TAG, "close output stream error. " + e.getMessage());
            return -1;
        }
        return 0;
    }

    private void writeToDumpFile(DingRtcEngine.DingRtcVideoSample videoSample) {
        try {
            int uvWidth = (videoSample.width + 1) / 2;
            int uvHeight = (videoSample.height + 1) / 2;
            // Write Y plane
            for (int i = 1; i < videoSample.height; i++) {
                mYuvBos.write(videoSample.data, i * videoSample.strideY, videoSample.width);
            }
            // Write U plane
            for (int i = 1; i < uvHeight; i++) {
                mYuvBos.write(videoSample.data, videoSample.height * videoSample.strideY + i * videoSample.strideU, uvWidth);
            }
            // Write V plane
            for (int i = 1; i < uvHeight; i++) {
                mYuvBos.write(videoSample.data, videoSample.height * videoSample.strideY + uvHeight * videoSample.strideU + i * videoSample.strideV, uvWidth);
            }

            mYuvBos.flush();
        } catch (IOException e) {
            Log.e(TAG, "ioexception while write. " + e.getMessage());
        }
    }

输出音频裸数据

1. 启动音频裸数据的回调监听:

public int enableAudioFrameObserver(boolean enable, int position, DingRtcAudioFrameObserverConfig config);
说明
  • enable : true 表示开启;false表示关闭。

  • position:表示获取的音频数据所处的位置

    public static class DintRtcAudioObservePosition {
        /** 采集的音频数据,对应输出回调 OnCapturedAudioFrame。 */
        public static final int RtcEngineAudioPositionCaptured = 1;
        /** 3A后的音频数据,对应输出回调 OnProcessCapturedAudioFrame。 */
        public static final int RtcEngineAudioPositionProcessCaptured = 1 << 1;
        /** 推流的音频数据,对应输出回调 OnPublishAudioFrame。 */
        public static final int RtcEngineAudioPositionPub = 1 << 2;
        /** 播放的音频数据,对应输出回调 OnPlaybackAudioFrame。 */
        public static final int RtcEngineAudioPositionPlayback = 1 << 3;
        /** 拉流的远端音频数据,对应输出回调 OnRemoteUserAudioFrame。 */
        public static final int RtcEngineAudioPositionRemoteUser = 1 << 4;
    }
  • config:表示获取的音频裸数据的规格

    public static class DingRtcAudioFrameObserverConfig {
        /** 回调音频采样率。 */
        public DingRtcAudioSampleRate sampleRate = DingRtcAudioSampleRate.DingRtcAudioSampleRate_48000;
        /** 回调音频声道数。 */
        public DingRtcAudioNumChannel channels = DingRtcAudioNumChannel.DingRtcMonoAudio;
        /** 回调音频读写权限,只读取音频裸数据(仅用于获取音频裸数据场景) 或者 读取并且写入音频裸数据(用于修改音频数据内容场景)。 */
        public DingRtcAudioFramePermission permission = DingRtcAudioFramePermission.DingRtcAudioFramePermissionReadOnly;
        /** 用户自定义参数。 */
        public int userDefinedInfo = DingRtcAudioFrameObserverUserDefinedInfoBitMask.DingRtcAudioFrameObserverUserDefinedInfoBitMaskMixExRender.getValue();
    }

调用此接口后,即可订阅音频裸数据或取消订阅音频裸数据。

2. 注册监听音频裸数据的回调:

public void registerAudioFrameObserver(DingRtcAudioFrameObserver observer);

3. 通过DingRtcAudioFrameObserver回调获取音频裸数据,方法如下:

/**
 * @brief 远端拉流数据回调
 * @param uid 远端用户ID。
 * @param frame 音频数据。
 * @note 默认关闭
 * - 该接口支持设置采样率、声道数。
 * - 该接口支持读写模式。
 * - 请不要在此回调函数中做任何耗时操作,否则可能导致声音异常。
 */
void onRemoteUserAudioFrame(String uid, DingRtcAudioFrame frame);

/**
 * @brief 采集裸数据回调。
 * @param frame 音频数据。
 * @note 默认关闭。
 * - 该接口支持设置采样率、声道数。
 * - 该接口支持读写模式。
 * - 请不要在此回调函数中做任何耗时操作,否则可能导致声音异常。
 */
void onCapturedAudioFrame(DingRtcAudioFrame frame);

/**
 * @since 3.0
 * @param frame 音频数据。
 * @brief 3A后数据回调。
 * @note 默认关闭。
 * - 该接口支持设置采样率、声道数。
 * - 该接口支持读写模式。
 * - 请不要在此回调函数中做任何耗时操作,否则可能导致声音异常。
 */
void onProcessCapturedAudioFrame(DingRtcAudioFrame frame);

/**
 * @since 3.0
 * @param frame 音频数据。
 * @brief 推流数据回调。
 * @note 默认关闭。
 * - 该接口支持设置采样率、声道数。
 * - 该接口只支持只读模式。
 * - 请不要在此回调函数中做任何耗时操作,否则可能导致声音异常。
 */
void onPublishAudioFrame(DingRtcAudioFrame frame);

/**
 * @since 3.0
 * @param frame 音频数据。
 * @brief 播放数据回调。
 * @note 默认关闭开启。
 * - 该接口支持设置采样率、声道数。
 * - 该接口支持读写模式。
 * - 请不要在此回调函数中做任何耗时操作,否则可能导致声音异常。
 */
void onPlaybackAudioFrame(DingRtcAudioFrame frame);

4. 订阅远端音频裸数据,并写入本地 pcm 文件示例代码:

    //开启音频数据dump
    private int enableDumpAudio() {
        String pcmPath = "/sdcard/dump_i420.pcm";
        File yuvFile = new File(pcmPath);

        try {
            mPcmBos = new BufferedOutputStream(new FileOutputStream(yuvFile, true));
        } catch (FileNotFoundException e) {
            Log.e(TAG, "open yuv file failed" + e.getMessage());
            return -1;
        }

        mRtcEngine.enableAudioFrameObserver(true, RtcEngineAudioPositionCaptured);
        mRtcEngine.registerAudioFrameObserver(new DingRtcEngine.DingRtcAudioFrameObserver() {
            @Override
            public void onCapturedAudioFrame(DingRtcEngine.DingRtcAudioFrame frame) {
                writeToPcmFile(frame);
            }

            @Override
            public void onProcessCapturedAudioFrame(DingRtcEngine.DingRtcAudioFrame frame) {

            }

            @Override
            public void onPublishAudioFrame(DingRtcEngine.DingRtcAudioFrame frame) {

            }

            @Override
            public void onPlaybackAudioFrame(DingRtcEngine.DingRtcAudioFrame frame) {

            }
        });
        return 0;
    }

    // 关闭音频数据dump
    private int disableDumpAudio() {
        mRtcEngine.enableAudioFrameObserver(false, RtcEngineAudioPositionCaptured);
        mRtcEngine.registerAudioFrameObserver(null);
        try {
            if (mPcmBos != null) {
                mPcmBos.close();
                mPcmBos = null;
            }
        } catch (IOException e) {
            Log.e(TAG, "close output stream error. " + e.getMessage());
            return -1;
        }
        return 0;
    }

    private int writeToPcmFile(DingRtcEngine.DingRtcAudioFrame audioFrame) {
        int totalBytes = audioFrame.numChannels * audioFrame.numSamples * audioFrame.bytesPerSample;
        byte[] buffer = new byte[totalBytes];
        audioFrame.data.get(buffer);
        try {
            if(mPcmBos!= null) {
                mPcmBos.write(buffer, 0, totalBytes);
                mPcmBos.flush();
            }
        } catch (IOException e) {
            Log.e(TAG, "close output stream error. " + e.getMessage());
            return -1;
        }
        return 0;
    }