陪玩平台源码中完整文件拉流解析,解码同步渲染音视频流

本文详细介绍了陪玩平台源码中音视频流的解析、解码、同步及渲染过程。使用FFmpeg进行文件解析和解码,通过OpenGL进行视频渲染,AudioQueue播放音频。同时,针对不同格式的音视频数据,如H.264和AAC,使用了不同的解码策略,并讨论了解码同步和时间戳校正的重要性。
摘要由CSDN通过智能技术生成

需求
在陪玩平台源码的解析文件中,音视频流以解码同步并将视频渲染到屏幕上,音频通过扬声器输出.对于仅仅需要单纯播放一个视频文件可直接使用AVFoundation中上层播放器,这里是用最底层的方式实现,可获取原始音视频帧数据.

实现原理

本文主要分为三大块,陪玩平台源码的解析模块使用FFmpeg parse文件中的音视频流,陪玩平台源码的解码模块使用FFmpeg或苹果原生解码器解码音视频,陪玩平台源码的渲染模块使用OpenGL将视频流渲染到屏幕,使用Audio Queue Player将音频以扬声器形式输出.

总体架构
在这里插入图片描述
本文以解码一个陪玩平台源码中的.MOV媒体文件为例, 该文件中包含H.264编码的视频数据, AAC编码的音频数据,首先要通过FFmpeg去parse文件中的音视频流信息,parse出来的结果保存在AVPacket结构体中,然后分别提取音视频帧数据,音频帧通过FFmpeg解码器或苹果原生框架中的Audio Converter进行解码,视频通过FFmpeg或苹果原生框架VideoToolbox中的解码器可将数据解码,解码后的音频数据格式为PCM,解码后的视频数据格式为YUV原始数据,根据陪玩平台源码时间戳对音视频数据进行同步,最后将PCM数据音频传给Audio Queue以实现音频的播放,将YUV视频原始数据封装为CMSampleBufferRef数据结构并传给OpenGL以将视频渲染到陪玩平台源码屏幕上,至此一个完整拉取文件视频流的操作完成.

注意: 通过网址拉取一个RTMP流进行解码播放的流程与拉取文件流基本相同, 只是需要通过socket接收音视频数据后再完成解码及后续流程.

简易流程
Parse

  • 创建AVFormatContext上下文对象: AVFormatContext
    *avformat_alloc_context(void);
  • 从陪玩平台源码文件中获取上下文对象并赋值给指定对象: int avformat_open_input(AVFormatContext **ps,
    const char *url, AVInputFormat *fmt, AVDictionary **options)
  • 读取文件中的流信息: int avformat_find_stream_info(AVFormatContext *ic,
    AVDictionary **options);
  • 获取陪玩平台源码文件中音视频流: m_formatContext->streams[audio/video index]e
  • 开始parse以获取文件中视频帧帧: int av_read_frame(AVFormatContext *s, AVPacket
    *pkt);
  • 如果是视频帧通过av_bitstream_filter_filter生成sps,pps等关键信息.
  • 读取到的AVPacket即包含文件中所有的音视频压缩数据.

解码
通过FFmpeg解码

  • 获取文件流的解码器上下文: formatContext->streams[a/v index]->codec;
  • 通过陪玩平台源码解码器上下文找到解码器: AVCodec *avcodec_find_decoder(enum AVCodecID id);
  • 打开解码器: int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec,
    AVDictionary **options);
  • 将陪玩平台源码文件中音视频数据发送给解码器: int avcodec_send_packet(AVCodecContext *avctx, const
    AVPacket *avpkt);
  • 循环接收解码后的音视频数据: int avcodec_receive_frame(AVCodecContext *avctx,
    AVFrame *frame);
  • 如果是陪玩平台源码音频数据可能需要重新采样以便转成设备支持的格式播放.(借助SwrContext)

通过VideoToolbox解码视频

  • 将从FFmpeg中parse到的extra data中分离提取中NALU头关键信息sps,pps等
  • 通过上面提取的关键信息创建视频描述信息:CMVideoFormatDescriptionRef,
    CMVideoFormatDescriptionCreateFromH264ParameterSets /
    CMVideoFormatDescriptionCreateFromHEVCParameterSets
  • 创建解码器:VTDecompressionSessionCreate,并指定一系列相关参数.
  • 将压缩数据放入CMBlockBufferRef中:CMBlockBufferCreateWithMemoryBlock
  • 开始解码: VTDecompressionSessionDecodeFrame
  • 在陪玩平台源码回调中接收解码后的视频数据

通过AudioConvert解码音频

  • 通过陪玩平台源码原始数据与解码后数据格式的ASBD结构体创建解码器: AudioConverterNewSpecific
  • 指定解码器类型AudioClassDescription
  • 开始解码: AudioConverterFillComplexBuffer
  • 注意: 陪玩平台源码解码的前提是每次需要有1024个采样点才能完成一次解码操作.

同步

因为这里解码的是陪玩平台源码文件中的音视频, 也就是说只要陪玩平台源码文件中音视频的时间戳打的完全正确,我们解码出来的数据是可以直接播放以实现同步的效果.而我们要做的仅仅是保证音视频解码后同时渲染.

注意: 比如通过一个RTMP地址拉取的流因为存在网络原因可能造成某个时间段数据丢失,造成音视频不同步,所以需要有一套机制来纠正时间戳.大体机制即为视频追赶音频,后面会有文件专门介绍,这里不作过多说明.

渲染
通过上面的步骤获取到的陪玩平台源码视频原始数据即可通过封装好的OpenGL ES直接渲染到屏幕上,苹果原生框架中也有GLKViewController可以完成屏幕渲染.音频这里通过Audio Queue接收音频帧数据以完成播放.

文件结构
在这里插入图片描述
快速使用
使用FFmpeg解码

首先根据陪玩平台源码文件地址初始化FFmpeg以实现parse音视频流.然后利用FFmpeg中的解码器解码音视频数据,这里需要注意的是,我们将从读取到的第一个I帧开始作为起点,以实现音视频同步.解码后的音频要先装入传输队列中,因为audio queue player设计模式是不断从传输队列中取数据以实现播放.视频数据即可直接进行渲染.

- (void)startRenderAVByFFmpegWithFileName:(NSString *)fileName {
    NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"];
    
    XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
    
    XDXFFmpegVideoDecoder *videoDecoder = [[XDXFFmpegVideoDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] videoStreamIndex:[parseHandler getVideoStreamIndex]];
    videoDecoder.delegate = self;
    
    XDXFFmpegAudioDecoder *audioDecoder = [[XDXFFmpegAudioDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] audioStreamIndex:[parseHandler getAudioStreamIndex]];
    audioDecoder.delegate = self;
    
    static BOOL isFindIDR = NO;
    
    [parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
        if (isFinish) {
            isFindIDR = NO;
            [videoDecoder stopDecoder];
            [audioDecoder stopDecoder];
            dispatch_async(dispatch_get_main_queue(), ^{
                self.startWorkBtn.hidden = NO;
            });
            return;
        }
        
        if (isVideoFrame) { // Video
            if (packet.flags == 1 && isFindIDR == NO) {
                isFindIDR = YES;
            }
            
            if (!isFindIDR) {
                return;
            }
            
            [videoDecoder startDecodeVideoDataWithAVPacket:packet];
        }else {             // Audio
            [audioDecoder startDecodeAudioDataWithAVPacket:packet];
        }
    }];
}

-(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer {
    CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
    [self.previewView displayPixelBuffer:pix];
}

- (void)getDecodeAudioDataByFFmpeg:(void *)data size:(int)size pts:(int64_t)pts isFirstFrame:(BOOL)isFirstFrame {
//    NSLog(@"demon test - %d",size);
    // Put audio data from audio file into audio data queue
    [self addBufferToWorkQueueWithAudioData:data size:size pts:pts];

    // control rate
    usleep(14.5*1000);
}

使用原生框架解码

首先根据陪玩平台源码文件地址初始化FFmpeg以实现parse音视频流.这里首先根据陪玩平台源码文件中实际的音频流数据构造ASBD结构体以初始化音频解码器,然后将解码后的音视频数据分别渲染即可.这里需要注意的是,如果要拉取的文件视频是H.265编码格式的,解码出来的数据的因为含有B帧所以时间戳是乱序的,我们需要借助一个链表对其排序,然后再将排序后的数据渲染到屏幕上.

- (void)startRenderAVByOriginWithFileName:(NSString *)fileName {
    NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"];
    XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
    
    XDXVideoDecoder *videoDecoder = [[XDXVideoDecoder alloc] init];
    videoDecoder.delegate = self;

    // Origin file aac format
    AudioStreamBasicDescription audioFormat = {
        .mSampleRate         = 48000,
        .mFormatID           = kAudioFormatMPEG4AAC,
        .mChannelsPerFrame   = 2,
        .mFramesPerPacket    = 1024,
    };
    
    XDXAduioDecoder *audioDecoder = [[XDXAduioDecoder alloc] initWithSourceFormat:audioFormat
                                                                     destFormatID:kAudioFormatLinearPCM
                                                                       sampleRate:48000
                                                              isUseHardwareDecode:YES];
    
    [parseHandler startParseWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, struct XDXParseVideoDataInfo *videoInfo, struct XDXParseAudioDataInfo *audioInfo) {
        if (isFinish) {
            [videoDecoder stopDecoder];
            [audioDecoder freeDecoder];
            
            dispatch_async(dispatch_get_main_queue(), ^{
                self.startWorkBtn.hidden = NO;
            });
            return;
        }
        
        if (isVideoFrame) {
            [videoDecoder startDecodeVideoData:videoInfo];
        }else {
            [audioDecoder decodeAudioWithSourceBuffer:audioInfo->data
                                     sourceBufferSize:audioInfo->dataSize
                                      completeHandler:^(AudioBufferList * _Nonnull destBufferList, UInt32 outputPackets, AudioStreamPacketDescription * _Nonnull outputPacketDescriptions) {
                                          // Put audio data from audio file into audio data queue
                                          [self addBufferToWorkQueueWithAudioData:destBufferList->mBuffers->mData size:destBufferList->mBuffers->mDataByteSize pts:audioInfo->pts];

                                          // control rate
                                          usleep(16.8*1000);
                                      }];
        }
    }];
}

- (void)getVideoDecodeDataCallback:(CMSampleBufferRef)sampleBuffer isFirstFrame:(BOOL)isFirstFrame {
    if (self.hasBFrame) {
        // Note : the first frame not need to sort.
        if (isFirstFrame) {
            CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
            [self.previewView displayPixelBuffer:pix];
            return;
        }
        
        [self.sortHandler addDataToLinkList:sampleBuffer];
    }else {
        CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
        [self.previewView displayPixelBuffer:pix];
    }
}

#pragma mark - Sort Callback
- (void)getSortedVideoNode:(CMSampleBufferRef)sampleBuffer {
    int64_t pts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000);
    static int64_t lastpts = 0;
//    NSLog(@"Test marigin - %lld",pts - lastpts);
    lastpts = pts;
    
    [self.previewView displayPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
}

注意

因为不同陪玩平台源码文件中压缩的音视频数据格式不同,这里仅仅兼容部分格式,可自定义进行扩展.以上就是“陪玩平台源码中完整文件拉流解析,解码同步渲染音视频流”的全部内容,希望对大家有帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值