iOS

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

输出视频数据

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

- (int)enableVideoFrameObserver:(BOOL)enable position:(DingRtcVideoObservePosition)position;
说明
  • enable : true 表示开启订阅视频裸数据;false 表示取消订阅视频裸数据。

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

        /** 采集视频数据,对应输出回调 onCaptureVideoFrame */
        DingRtcVideoObservePositionPostCapture = 1 << 0,
        /** 渲染视频数据,对应输出回调 onRemoteVideoFrame */
        DingRtcVideoObservePositionPreRender   = 1 << 1,
        /** 编码前视频数据,对应输出回调 onPreEncodeVideoFrame */
        DingRtcVideoObservePositionPreEncoder  = 1 << 2,

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

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

- (int)setVideoFrameDelegate:(id<DingRtcVideoFrameDelegate> _Nullable)delegate;

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

/**
 * @brief 告诉引擎需要获取的视频格式。
 */
- (DingRtcVideoFormat)getVideoFormatPreference;

/**
* @brief 订阅的本地采集视频数据回调。
* @param frame video sample
* @return 
* - true: 需要写回SDK(默认写回);
* - false: 不需要写回SDK。
*/
- (BOOL)onCaptureVideoFrame:(DingRtcVideoFrame * _Nonnull)frame;

/**
* @brief 订阅的本地编码前视频数据回调。
* @param frame video sample
* @param videoTrack video track
* @return 
* - true: 需要写回SDK(默认写回);
* - false: 不需要写回SDK。
*/
- (BOOL)onPreEncodeVideoFrame:(DingRtcVideoFrame * _Nonnull)frame videoTrack:(DingRtcVideoTrack)videoTrack;

/**
* @brief 订阅的远端视频数据回调。
* @param frame video sample
* @param userId user id
* @param videoTrack video track
* @return 
* - true: 需要写回SDK(默认写回);
* - false: 不需要写回SDK。
*/- (BOOL)onRemoteVideoFrame:(DingRtcVideoFrame * _Nonnull)frame userId:(NSString *_Nonnull)userId videoTrack:(DingRtcVideoTrack)videoTrack;
说明

调用 enableVideoFrameObserver: true 接口以及setVideoFrameDelegate后:

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

    • getVideoFormatPreference

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

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

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

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

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

@interface SubDataModel : NSObject

@property (nonatomic, copy)NSString *filePath;
@property (nonatomic) FILE *outputFile;

@end

@property (strong, nonatomic) NSMutableDictionary <NSString*, SubDataModel *> *_subDataMutableDic = [NSMutableDictionary dictionary];

- (DingRtcVideoFormat)getVideoFormatPreference {
    return DingRtcVideoFormat_I420;
}

- (BOOL)onCaptureVideoFrame:(DingRtcVideoFrame * _Nonnull)frame {
        return NO;
}

- (BOOL)onPreEncodeVideoFrame:(DingRtcVideoFrame * _Nonnull)frame videoTrack:(DingRtcVideoTrack)videoTrack {
        return NO;
}

- (BOOL)onRemoteVideoFrame:(DingRtcVideoFrame * _Nonnull)frame userId:(NSString *_Nonnull)userId videoTrack:(DingRtcVideoTrack)videoTrack {
       [self dumpVideoYuvData:uid videoSample:videoSample];
       return NO;
}

- (void)dumpVideoYuvData:(NSString *)uid videoSample:(DingRtcVideoFrame *)videoSample {
    //创建串行队列
    dispatch_queue_t testqueue = dispatch_queue_create("subVideo", NULL);

    //同步执行任务
    dispatch_sync(testqueue, ^{
        if(videoSample.frameType != DingRtcVideoFrameRaw || videoSample.frameType != DingRtcVideoI420) {
            return;
        }

        if (!_subDataMutableDic[uid]) {
            NSString *time = [self getCurrentTimes];

            NSString *filePath = [NSString stringWithFormat:@"Documents/yuv/%@/",uid];
            if ([uid isEqualToString:@""]) {
                filePath = [NSString stringWithFormat:@"Documents/yuv/self/"];
            }

            NSString *fileName = [NSString stringWithFormat:@"sub_%@_video_%d*%d_%@.yuv",uid,videoSample.width,videoSample.height,time];
            if ([uid isEqualToString:@""]) {
                fileName = [NSString stringWithFormat:@"pub_%@_video_%d*%d_%@.yuv",uid,videoSample.width,videoSample.height,time];
            }

            NSString * dataPath = [SubFilePath getSubDataFilePath:filePath FileName:fileName];

            FILE * subYUVFile = fopen([dataPath UTF8String], "ab");

            SubDataModel *cache = [[SubDataModel alloc] init];
            cache.filePath = dataPath;
            cache.outputFile = subYUVFile;
            [_subDataMutableDic setObject:cache forKey:uid];
        }

        SubDataModel *cache = _subDataMutableDic[uid];
        FILE * subYUVFile = cache.outputFile;

        uint8_t *outputData = NULL;
        int32_t w = videoSample.width;
        int32_t h = videoSample.height;
        NSArray<NSNumber *> *offsets = videoSample.offset;
        NSArray<NSNumber *> *strides = videoSample.offset;
        uint8_t* yData = videoSample.data + offsets[0].intValue;
        uint8_t* uData = videoSample.data + offsets[1].intValue;
        uint8_t* vData = videoSample.data + offsets[2].intValue;
        int32_t yStride = strides[0].intValue;
        int32_t uStride = strides[1].intValue;
        int32_t vStride = strides[2].intValue;
        int32_t uvWidth = (w + 1) / 2;
        int32_t uvHeight = (h + 1) / 2;

        // Write Y plane
        for (int i = 0; i < h; ++i) {
            fwrite(yData + i * yStride, 1, width, subYUVFile);
        }

        // Write U plane
        for (int i = 0; i < uvHeight; ++i) {
            fwrite(uData + i * uStride, 1, uvWidth, subYUVFile);
        }

        // Write V plane
        for (int i = 0; i < uvHeight; ++i) {
            fwrite(vData + i * vStride, 1, uvWidth, subYUVFile);
        }
        fclose(file);
   });
}

输出音频裸数据

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

- (int)enableAudioFrameObserver:(BOOL)enable position:(DingRtcAudioObservePosition)position config:(DingRtcAudioObserveConfig *_Nonnull)config;
说明
  • enable : true 表示开启;false表示关闭。

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

        /*! DingRtcAudioObservePositionCaptured - 采集的音频数据。 */
        DingRtcAudioObservePositionCaptured = 1 << 0,
        /*! DingRtcAudioObservePositionProcessCaptured - 3A后的音频数据。 */
        DingRtcAudioObservePositionProcessCaptured = 1 << 1,
        /*! DingRtcAudioObservePositionPub - 推流的音频数据。 */
        DingRtcAudioObservePositionPub = 1 << 2,
        /*! DingRtcAudioObservePositionPlayback - 播放的音频数据。 */
        DingRtcAudioObservePositionPlayback = 1 << 3,
        /*!DingRtcAudioObservePositionRemoteUser - 拉流的远端音频数据。 */
        DingRtcAudioObservePositionRemoteUser = 1 << 4,
  • config:表示获取的音频裸数据的规格

    @interface DingRtcAudioObserveConfig : NSObject
    
    /** 回调数据的采样率。 */
    @property (nonatomic, assign) DingRtcAudioSampleRate sampleRate;
    /** 回调数据的通道数。 */
    @property (nonatomic, assign) DingRtcAudioNumChannel channel;
    /** 回调数据的权限:只读取音频裸数据(仅用于获取音频裸数据场景) 或者 读取并且写入音频裸数据(用于修改音频数据内容场景)。 */
    @property (nonatomic, assign) DingRtcAudioFramePermission permission;
    
    @end

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

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

- (int)registerAudioFrameObserver:(id<DingRtcAudioFrameDelegate> _Nullable)observer;

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

@optional
/**
 * @brief 远端拉流数据回调
 * @param uid 远端用户ID。
 * @param frame 音频数据,详见{@link RtcEngineAudioFrame}。
 * @note 请不要在此回调函数中做任何耗时操作,否则可能导致声音异常
 */
- (void)onRemoteUserAudioFrame:(NSString *_Nonnull)uid frame:(DingRtcAudioDataSample * _Nonnull)frame;

/**
 * @brief 本地订阅音频数据回调。
 * @details 远端所有用户混音后待播放的音频数据。
 * @param frame 音频数据。
*/
- (void)onPlaybackAudioFrame:(DingRtcAudioDataSample * _Nonnull)frame;

/**
 * @brief 本地采集音频数据回调。
 * @param frame 音频数据。
 */
- (void)onCapturedAudioFrame:(DingRtcAudioDataSample * _Nonnull)frame;

/**
 * @brief 经过3A处理后的数据回调。
 * @param frame 音频数据。
*/
- (void)onProcessCapturedAudioFrame:(DingRtcAudioDataSample * _Nonnull)frame;

/**
 * @brief 本地推流音频数据回调。
 * @param frame 音频数据。
 */
- (void)onPublishAudioFrame:(DingRtcAudioDataSample * _Nonnull)frame;

@end

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

@property (strong, nonatomic) NSDateFormatter *dateFormatter;
@property (copy, nonatomic) NSString *channelId;
@property (copy, nonatomic) NSString *currentFilePath;
@property (nonatomic) FILE *_inputFile;

// DingRtcAudioFrameDelegate
- (void)onPlaybackAudioFrame:(DingRtcAudioDataSample *)audioSample {
    dispatch_queue_t fileQueue = dispatch_queue_create("saveAudio", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(fileQueue, ^{
        if (!self.currentFilePath) {
            NSString *keyString = [self getAudioFileName:self.channelId numOfChannels:audioSample.numOfChannels sampleRate:audioSample.sampleRate];
            self.currentFilePath = [NSString stringWithFormat:@"%@/%@",self.rootPath, keyString];
            // 停止订阅时,关闭文件
            _inputFile = fopen([self.currentFilePath UTF8String], "ab+");
            NSLog(@"currentFilePath: %@", self.currentFilePath);
        }
        
        int bufferSize = audioSample.numOfSamples * audioSample.bytesPerSample * audioSample.numOfChannels;
        if (bufferSize > 0) {
            int written = (int)fwrite((void *)audioSample.data, 1, bufferSize, _inputFile);
            if (written <= 0) {
                NSLog(@"fwrite error, written : %d", written);
            }
        }
    });
}

- (NSString *)getAudioFileName:(NSString *)channnelId numOfChannels:(int)numOfChannels sampleRate:(int)sampleRate {
    NSString *dateString = [self.dateFormatter stringFromDate:[NSDate date]];
    NSString *keyString = [NSString stringWithFormat:@"%@_%@_%d_%d.pcm", channnelId, dateString, numOfChannels, sampleRate];
    return keyString;
}

- (NSDateFormatter *)dateFormatter {
    if (!_dateFormatter) {
        _dateFormatter = [[NSDateFormatter alloc] init];
        [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
        [_dateFormatter setDateFormat:@"HH:mm:ss:SSS"];
    }
    return _dateFormatter;
}