技术背景

好多开发者都知道我们有Windows、Android、Linux平台的RTSP转RTMP推送模块,实际上,iOS平台我们也有,并在2016年就已发布。我们都知道,一个好的RTSP转RTMP推送模块,需要足够稳定的前提下,还要低延迟、灵活、有状态反馈机制、资源占用低,方便进行多路转发。

技术设计

iOS平台如何实现低延迟RTSP转RTMP推送?_音视频

1. 拉流:通过RTSP直播播放SDK的数据回调接口,拿到音视频数据;

2. 转推:通过RTMP直播推送SDK的编码后数据输入接口,把回调上来的数据,传给RTMP直播推送模块,实现RTSP数据流到RTMP服务器的转发;

3. 录像:如果需要录像,借助RTSP直播播放SDK,拉到音视频数据后,直接存储MP4文件即可;

4. 快照:如果需要实时快照,拉流后,解码调用播放端快照接口,生成快照,因为快照涉及到video数据解码,如无必要,可不必开启,不然会额外消耗性能。

5. 拉流预览:如需预览拉流数据,只要调用播放端的播放接口,即可实现拉流数据预览;

6. 数据转AAC后转发:考虑到好多监控设备出来的音频可能是PCMA/PCMU的,如需要更通用的音频格式,可以转AAC后,在通过RTMP推送;

7. 转推RTMP实时静音:只需要在传audio数据的地方,加个判断即可;

8. 拉流速度反馈:通过RTSP播放端的实时码率反馈event,拿到实时带宽占用即可;

9. 整体网络状态反馈:考虑到有些摄像头可能会临时或异常关闭,RTMP服务器亦是,可以通过推拉流的event回调状态,查看那整体网络情况,如此界定:是拉不到流,还是推不到RTMP服务器。

接口实现

再说iOS平台RTSP转RTMP技术实现细节,先说拉流:

调用InitPlayer的时候,我们可以设置audio转AAC在推送:

//  ViewController.m
//  SmartiOSRelayDemo
//
//  Author: daniulive.com
-(bool)InitPlayer
{
    NSLog(@"InitPlayer++");
    
    if(is_inited_player_)
    {
        NSLog(@"InitPlayer: has inited before..");
        return true;
    }
    
    _smart_player_sdk = [[SmartPlayerSDK alloc] init];
    
    if (_smart_player_sdk ==nil ) {
        NSLog(@"SmartPlayerSDK init failed..");
        return false;
    }
    
    if (playback_url_.length == 0) {
        NSLog(@"_streamUrl with nil..");
        return false;
    }
    
    if (_smart_player_sdk.delegate == nil)
    {
        _smart_player_sdk.delegate = self;
        NSLog(@"SmartPlayerSDK _player.delegate:%@", _smart_player_sdk);
    }
    
    NSInteger initRet = [_smart_player_sdk SmartPlayerInitPlayer];
    if ( initRet != DANIULIVE_RETURN_OK )
    {
        NSLog(@"SmartPlayerSDK call SmartPlayerInitPlayer failed, ret=%ld", (long)initRet);
        return false;
    }
    
    [_smart_player_sdk SmartPlayerSetPlayURL:playback_url_];
    
    //超低延迟模式
    is_low_latency_mode_ = YES;
    [_smart_player_sdk SmartPlayerSetLowLatencyMode:(NSInteger)is_low_latency_mode_];
    
    buffer_time_ = 0;
    if(buffer_time_ >= 0)
    {
        [_smart_player_sdk SmartPlayerSetBuffer:buffer_time_];
    }
    
    is_fast_startup_ = YES;
    [_smart_player_sdk SmartPlayerSetFastStartup:(NSInteger)is_fast_startup_];
    
    NSLog(@"[relayDemo]is_fast_startup_:%d, buffer_time_:%ld", is_fast_startup_, (long)buffer_time_);
    
    [_smart_player_sdk SmartPlayerSetRTSPTcpMode:is_rtsp_tcp_mode_];
    
    NSInteger rtsp_timeout = 10;    //RTSP超时时间设置
    [_smart_player_sdk SmartPlayerSetRTSPTimeout:rtsp_timeout];
    
    NSInteger is_auto_switch_tcp_udp = 1;       //RTSP TCP/UDP模式自动切换设置
    [_smart_player_sdk SmartPlayerSetRTSPAutoSwitchTcpUdp:is_auto_switch_tcp_udp];
    
    NSInteger image_flag = 1;
    [_smart_player_sdk SmartPlayerSaveImageFlag:image_flag];
    
    //如需查看实时流量信息,可打开以下接口
    //NSInteger is_report = 1;
    //NSInteger report_interval = 1;
    //[_player SmartPlayerSetReportDownloadSpeed:is_report report_interval:report_interval];
    
    NSInteger is_rec_trans_code = 1;
    [_smart_player_sdk SmartPlayerSetRecorderAudioTranscodeAAC:is_rec_trans_code];
    
    NSInteger is_pull_trans_code = 1;
    [_smart_player_sdk SmartPlayerSetPullStreamAudioTranscodeAAC:is_pull_trans_code];
    
    is_inited_player_ = YES;
    
    NSLog(@"InitPlayer--");
    return true;
}

对应的设置接口设计如下:

NSInteger is_pull_trans_code = 1;
[_smart_player_sdk SmartPlayerSetPullStreamAudioTranscodeAAC:is_pull_trans_code];

设置音视频数据callback如下,音视频数据回调设置,需要在调用SmartPlayerStartPullStream()接口之前完成:

-(void)StartStreamDataCallback
{
    //_smart_player_sdk.pullStreamVideoDataBlock = nil; //如不需要回调视频数据
   
    if(is_stream_data_callback_started)
    {
        NSLog(@"StartStreamDataCallback: has inited before..");
        return;
    }
    
    if(_smart_player_sdk == nil)
    {
        NSLog(@"StartStreamDataCallback failed, _smart_player_sdk is null..");
        return;
    }
    
    __weak __typeof(self) weakSelf = self;
    
    _smart_player_sdk.pullStreamVideoDataBlock = ^(int video_codec_id, unsigned char *data, int size, int is_key_frame, unsigned long long timestamp, int width, int height, unsigned char *parameter_info, int parameter_info_size, unsigned long long presentation_timestamp)
    {
        //NSLog(@"[pullStreamVideoDataBlock]videoCodecID:%d, is_key_frame:%d, size:%d, width:%d, height:%d, ts:%lld",
        //      video_codec_id, is_key_frame, size, width, height, timestamp);
        
        [weakSelf OnPostVideoEncodedData:video_codec_id data:data size:size is_key_frame:is_key_frame timestamp:timestamp pts:presentation_timestamp];
    };
    
    //_smart_player_sdk.pullStreamAudioDataBlock = nil; //如不需要回调音频数据
    
    _smart_player_sdk.pullStreamAudioDataBlock = ^(int audio_codec_id, unsigned char *data, int size, int is_key_frame, unsigned long long timestamp, int sample_rate, int channel, unsigned char *parameter_info, int parameter_info_size, unsigned long long reserve)
    {
        //NSLog(@"[pullStreamAudioDataBlock]audioCodecID:%x, is_key_frame:%d, size:%d, parameter_info_size:%d",
        //      audio_codec_id, is_key_frame, size, parameter_info_size);
        
        [weakSelf OnPostAudioEncodedData:audio_codec_id data:data size:size is_key_frame:is_key_frame timestamp:timestamp parameter_info:parameter_info parameter_info_size:parameter_info_size];
    };
    
    //设置拉流视频数据回调
    bool isEnablePSVideoDataBlock = true;
    [_smart_player_sdk SmartPlayerSetPullStreamVideoDataBlock:isEnablePSVideoDataBlock];
    
    //设置拉流音频数据回调
    bool isEnablePSAudioDataBlock = true;
    [_smart_player_sdk SmartPlayerSetPullStreamAudioDataBlock:isEnablePSAudioDataBlock];
    
    if([_smart_player_sdk SmartPlayerStartPullStream] != DANIULIVE_RETURN_OK)
    {
        NSLog(@"Call SmartPlayerStartPullStream failed..");
    }
    
    is_stream_data_callback_started = YES;
}

获取到的RTSP编码后的音视频数据投递到推送模块:

//如需转发video数据
- (void)OnPostVideoEncodedData:(NSInteger)codec_id data:(unsigned char*)data size:(NSInteger)size is_key_frame:(NSInteger)is_key_frame timestamp:(unsigned long long)timestamp pts:(unsigned long long)pts
{
    if((is_pulling_ || isRTSPPublisherRunning) && _smart_publisher_sdk != nil )
    {
        [_smart_publisher_sdk SmartPublisherPostVideoEncodedData:codec_id data:data size:size is_key_frame:is_key_frame timestamp:timestamp pts:pts];
    }
}

//如需转发audio数据
- (void)OnPostAudioEncodedData:(NSInteger)codec_id data:(unsigned char*)data size:(NSInteger)size is_key_frame:(NSInteger)is_key_frame timestamp:(unsigned long long)timestamp parameter_info:(unsigned char*)parameter_info parameter_info_size:(NSInteger)parameter_info_size
{
    if((is_pulling_ || isRTSPPublisherRunning) && _smart_publisher_sdk != nil )
    {
        [_smart_publisher_sdk SmartPublisherPostAudioEncodedData:codec_id data:data size:size is_key_frame:is_key_frame timestamp:timestamp parameter_info:parameter_info parameter_info_size:parameter_info_size];
    }
}

如果转推RTMP,转推的部分实现如下,需要注意的是,转推的时候,audio_opt和video_opt设置2即可:

//推送端接口封装
-(bool)InitPublisher
{
    NSLog(@"InitPublisher++");
    
    if(is_inited_publisher_)
    {
        NSLog(@"InitPublisher: has inited before..");
        return true;
    }
    
    if(_smart_publisher_sdk != nil)
    {
        NSLog(@"InitPublisher, publisher() has inited before..");
        return true;
    }
    
    _smart_publisher_sdk = [[SmartPublisherSDK alloc] init];
    
    if (_smart_publisher_sdk == nil )
    {
        NSLog(@"_smart_publisher_sdk with nil..");
        return false;
    }
    
    if(_smart_publisher_sdk.delegate == nil)
    {
        _smart_publisher_sdk.delegate = self;
    }
    
    NSInteger audio_opt = 2;
    NSInteger video_opt = 2;
    
    if([_smart_publisher_sdk SmartPublisherInit:audio_opt video_opt:video_opt] != DANIULIVE_RETURN_OK)
    {
        NSLog(@"Call SmartPublisherInit failed..");
        
        _smart_publisher_sdk = nil;
        return false;
    }
    
    is_inited_publisher_ = YES;
    
    NSLog(@"InitPublisher--");
    return true;
}

-(bool)StartPushRTMP
{
    NSLog(@"StartPushRTMP++");
    if ( _smart_publisher_sdk == nil )
    {
        NSLog(@"StartPushRTMP, publisher SDK with nil");
        return false;
    }
    
    NSInteger errorCode = [_smart_publisher_sdk SmartPublisherStartPublisher:relay_url_];
    
    NSLog(@"rtmp pusher url: %@", relay_url_);
    
    if(errorCode != DANIULIVE_RETURN_OK)
    {
        NSLog(@"Call SmartPublisherStartPublisher failed..ret:%ld", (long)errorCode);
        return false;
    }
    
    NSLog(@"StartPushRTMP--");
    return true;
}

-(bool)StopPushRTMP
{
    NSLog(@"StopPushRTMP++");
    if ( _smart_publisher_sdk == nil )
    {
        NSLog(@"StopPushRTMP, publiher SDK with nil");
        return false;
    }
    
    [_smart_publisher_sdk SmartPublisherStopPublisher];
    
    NSLog(@"StopPushRTMP--");
    return true;
}

总结

以上是iOS平台RTSP转RTMP推送模块大概设计思路,如果需要录像,可以调用录像接口,也可以实现实时快照,或者转推轻量级RTSP服务,感兴趣的开发者,可以单独跟我交流。