美文网首页Mac·iOS开发iOS音频开发
iOS 基于ffmpeg的音视频编、解码以及播放器的制作

iOS 基于ffmpeg的音视频编、解码以及播放器的制作

作者: TMMMMMS | 来源:发表于2021-09-03 21:12 被阅读0次

    最近在学习音视频的相关知识,在接触到ffmpeg库后尝试着使用其编写了一个视频播放器


    demo截图

    音视频解码

    视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文件则不需要解协议,为以下几个步骤:解封装,解码视音频,视音频同步。他们的过程如图所示。


    本文示例使用的是本地视频文件,对解码过程中使用到的api不做过多讲解,具体的api介绍可以参考雷神的博客,或者阅读demo中“FFMpeg解码中”解码音频、解码视频的文件。解码的步骤如下图所示,新版的ffmpeg已经不需要使用av_register_all(),图片来源于网络

    解码后得到的音频数据采用AudioQueue进行播放,视频数据使用OpenGL ES来进行展示,具体可以参照文章末尾处的demo

    关于音视频的同步,有三种方式:

    • 参考一个外部时钟,将音频与视频同步至此时间
    • 以视频为基准,音频去同步视频的时间
    • 以音频为基准,视频去同步音频的时间

    由于某些生物学的原理,人对声音的变化比较敏感,但是对视觉变化不太敏感。所以频繁的去调整声音的播放会有些刺耳或者杂音吧影响用户体验,所以普遍使用第三种方式来做音视频同步

    音视频编码

    音频的录制采用AudioUnit,音频的编码使用AudioConverterRef

    //输入
    AudioBuffer encodeBuffer;
    encodeBuffer.mNumberChannels = inBuffer->mNumberChannels;
    encodeBuffer.mDataByteSize = (UInt32)bufferLengthPerConvert;
    encodeBuffer.mData = current;
    
    
    UInt32 packetPerConvert = PACKET_PER_CONVERT;
    
    //输出
    AudioBufferList outputBuffers;
    outputBuffers.mNumberBuffers = 1;
    outputBuffers.mBuffers[0].mNumberChannels =inBuffer->mNumberChannels;
    outputBuffers.mBuffers[0].mDataByteSize = outPacketLength*packetPerConvert;
    outputBuffers.mBuffers[0].mData = _convertedDataBuf;
    memset(_convertedDataBuf, 0, bufferLengthPerConvert);
    
    OSStatus status = AudioConverterFillComplexBuffer(_audioConverter, convertDataProc, &encodeBuffer, &packetPerConvert, &outputBuffers, NULL);
    if (status != noErr) {
        NSLog(@"转换出错");
    }
    //        TMSCheckStatusUnReturn(status, @"转换出错");
    
    if (current == leftBuf) {
        current = inBuffer->mData + bufferLengthPerConvert - lastLeftLength;
    }else{
        current += bufferLengthPerConvert;
    }
    leftLength -= bufferLengthPerConvert;
    
    //输出数据到下一个环节
    //        NSLog(@"output buffer size:%d",outputBuffers.mBuffers[0].mDataByteSize);
    self.bufferData->bufferList = &outputBuffers;
    self.bufferData->inNumberFrames = packetPerConvert*_outputDesc.mFramesPerPacket;  //包数 * 每个包的帧数(帧数+采样率计算时长)
    [self transportAudioBuffersToNext];
    

    视频的录制采用AVCaptureSession,视频的编码使用ffmpeg

    - (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
    {
        // 1.通过CMSampleBufferRef对象获取CVPixelBufferRef对象
        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        
        // 2.锁定imageBuffer内存地址开始进行编码
        if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
            // 3.从CVPixelBufferRef读取YUV的值
            // NV12和NV21属于YUV格式,是一种two-plane模式,即Y和UV分为两个Plane,但是UV(CbCr)为交错存储,而不是分为三个plane
            // 3.1.获取Y分量的地址
            UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
            // 3.2.获取UV分量的地址
            UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
            
            // 3.3.根据像素获取图片的真实宽度&高度
            size_t width = CVPixelBufferGetWidth(imageBuffer);
            size_t height = CVPixelBufferGetHeight(imageBuffer);
            // 获取Y分量长度
            size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
            size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
            UInt8 *yuv420_data = (UInt8 *)malloc(width * height * 3 / 2);
            
            // 3.4.将NV12数据转成YUV420P(I420)数据
            UInt8 *pY = bufferPtr;
            UInt8 *pUV = bufferPtr1;
            UInt8 *pU = yuv420_data + width * height;
            UInt8 *pV = pU + width * height / 4;
            for(int i =0;i<height;i++)
            {
                memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
            }
            for(int j = 0;j<height/2;j++)
            {
                for(int i =0;i<width/2;i++)
                {
                    *(pU++) = pUV[i<<1];
                    *(pV++) = pUV[(i<<1) + 1];
                }
                pUV += bytesrow1;
            }
            
            // 3.5.分别读取YUV的数据
            picture_buf = yuv420_data;
            pFrame->data[0] = picture_buf;                   // Y
            pFrame->data[1] = picture_buf + y_size;          // U
            pFrame->data[2] = picture_buf + y_size * 5 / 4;  // V
            
            // 4.设置当前帧
            pFrame->pts = framecnt;
            
            // 4.设置宽度高度以及YUV格式
            pFrame->width = encoder_h264_frame_width;
            pFrame->height = encoder_h264_frame_height;
            pFrame->format = AV_PIX_FMT_YUV420P;
            
            // 5.对编码前的原始数据(AVFormat)利用编码器进行编码,将 pFrame 编码后的数据传入pkt 中
            int ret = avcodec_send_frame(pCodecCtx, pFrame);
            if (ret != 0) {
                printf("Failed to encode! \n");
                return;
            }
            
            while (avcodec_receive_packet(pCodecCtx, &pkt) == 0) {
                framecnt++;
                pkt.stream_index = video_st->index;
                //也可以使用C语言函数:fwrite()、fflush()写文件和清空文件写入缓冲区。
    //            ret = av_write_frame(pFormatCtx, &pkt);
                fwrite(pkt.data, 1, pkt.size, file);
                if (ret < 0) {
                    printf("Failed write to file!\n");
                }
                //释放packet
                av_packet_unref(&pkt);
            }
            
            // 7.释放yuv数据
            free(yuv420_data);
        }
        
        CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    }
    
    编码后得到的h264文件通过H264BSAnalyzer解析发现,每个IDR帧之前都含有SPS和PPS,说明此种方式进行的编码可用于网络流的传输

    视频封装

    本文示例将H264和AAC封装成FLV,封装流程示意图如下,具体代码实现请参照文章末尾处demo


    直播推流

    推流:使用的是LFLiveKit三方库
    拉流:可以使用ijkplayer,也可以使用mac端的VLC播放器
    服务器:nginx
    具体的配置及使用可以参考这里

    由于ffmpeg库占用空间过大,需自行引入方可运行
    demo下载

    参考文章:
    雷神博客
    https://github.com/czqasngit/ffmpeg-player
    https://www.jianshu.com/p/ba5045da282c

    相关文章

      网友评论

        本文标题:iOS 基于ffmpeg的音视频编、解码以及播放器的制作

        本文链接:https://www.haomeiwen.com/subject/lrjkwltx.html