美文网首页音视频音视频
IOS FFmpeg零到自己的播放器1,解码

IOS FFmpeg零到自己的播放器1,解码

作者: huisedediao | 来源:发表于2018-09-30 14:12 被阅读129次

    前言:

    网上有很多基于FFmpeg写的媒体播放器,都写得很好,但是由于FFmpeg库本身接口多,加上作者们编写代码的时候习惯进行封装,对初学者来说是不友好的,所以打算写一个系列,从最基础开始,编写属于自己的媒体播放器,记录下自己学习的过程,也方便跟我一样的初学者能更快对FFmpeg有所了解。

    基础概念:

    容器/文件:即特定格式的多媒体文件,比如MP4、flv、mov等。
    媒体流:表示时间轴上一段连续数据,如一段声音数据、一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩的,压缩数据需要关联特定的编解码器。
    数据帧/数据包:通常,一个媒体流是由大量的数据帧组成的,对于压缩数据,帧对应这编解码器的最小处理单元,分属于不同媒体流的数据帧交错存储于容器中。
    编解码器:编解码器是以帧为单位实现压缩数据和原始数据之间的相互转换的。

    基本结构体介绍:

    AVFormatContext:对容器或者说媒体文件层次的一个抽象,该文件中(或者说这个容器里),包含了多路流(音频流、视频流、字幕流等)。
    AVStream:对流的抽象,在每一路流中都会描述这路流的编码格式。
    AVCodecContext:对编码格式的抽象。
    AVCodec:对编解码器的抽象。
    AVPacket:对压缩数据的抽象。
    AVFrame:对原始数据的抽象。

    正文

    目标:把一个视频文件,解码成单独的音频PCM文件和视频YUV文件,然后使用命令行工具ffplay去验证播放这两个文件。

    本文Demo:https://github.com/huisedediao/FFmpegDecoder
    ffplay使用:https://www.jianshu.com/p/8cce27b1e294

    新建工程,将编译好的FFmpeg集成到项目中,集成过程可以看这篇文章:https://www.jianshu.com/p/d1ed7b860f1b

    引入头文件:

    #import <libavformat/avformat.h>
    #import <libavcodec/avcodec.h>
    #import <libswscale/swscale.h>
    #import <libswresample/swresample.h>
    #import <libavutil/pixdesc.h>
    


    设置好解码出来的音频数据和视频数据的存储路径,运行代码的时候记得改为自己桌面的路径:

        NSString *audioStorePath = [NSString stringWithFormat:@"/Users/xxb/Desktop/audioData.pcm"];
        XBDataWriter *audioDataWriter = [XBDataWriter new];//用于写音频数据
        
        NSString *videoStorePath = [NSString stringWithFormat:@"/Users/xxb/Desktop/videoData.yuv"];
        XBDataWriter *videoDataWriter = [XBDataWriter new];//用于写视频数据
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:audioStorePath])
        {
            [[NSFileManager defaultManager] removeItemAtPath:audioStorePath error:nil];
        }
    


    注册FFmpeg服务:

      av_register_all();
    


    打开文件,读取文件信息

        NSString *mp4Path = [[NSBundle mainBundle] pathForResource:@"IMG_0427" ofType:@"mp4"];
        AVFormatContext *formatCtx = avformat_alloc_context();
        
        //打开文件
        int status = 0;
        status = avformat_open_input(&formatCtx, [mp4Path UTF8String], NULL, NULL);
        if (status < 0)
        {
            NSLog(@"打开文件失败");
            return;
        }
        
        //读取文件信息
        status = avformat_find_stream_info(formatCtx, NULL);
        if (status < 0)
        {
            NSLog(@"无法获取流信息");
            return;
        }
    


    记录音视频流序号:

        int videoStreamIndex;//记录视频流是第几路
        int audioStreamIndex;//记录音频流是第几路
        
        //寻找音视频流
        for (int i = 0; i < formatCtx->nb_streams; i++)
        {
            AVStream *stream = formatCtx->streams[i];
            
            if (stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)//视频
            {
                videoStreamIndex = i;
            }
            else if (stream->codec->codec_type == AVMEDIA_TYPE_AUDIO)//音频
            {
                audioStreamIndex = i;
            }
        }
    


    打开视频解码器:

        //打开视频解码器
        AVStream *videoStream = formatCtx->streams[videoStreamIndex];
        AVCodecContext *videoCodecCtx = videoStream->codec;
        //寻找解码器
        AVCodec *videoCodec = avcodec_find_decoder(videoCodecCtx->codec_id);
        if (videoCodec == nil)
        {
            NSLog(@"寻找视频解码器失败");
            return;
        }
        //打开解码器
        int openVideoCodecErr = avcodec_open2(videoCodecCtx, videoCodec, NULL);
        if (openVideoCodecErr < 0)
        {
            NSLog(@"打开视频解码器失败");
            return;
        }
    


    打开音频解码器:

        //打开音频解码器
        AVStream *audioStream = formatCtx->streams[audioStreamIndex];
        AVCodecContext *audioCodecCtx = audioStream->codec;
        AVCodec *audioCodec = avcodec_find_decoder(audioCodecCtx->codec_id);
        if (audioCodec == nil)
        {
            NSLog(@"寻找音频解码器失败");
            return;
        }
        int openAudioCodecErr = avcodec_open2(audioCodecCtx, audioCodec, NULL);
        if (openVideoCodecErr < 0)
        {
            NSLog(@"打开音频解码器失败");
            return;
        }
    


    初始化存储解码后视频数据的对象、构建视频的格式转换对象

        //初始化视频数据存储对象
        AVFrame *videoFrame = av_frame_alloc();
        //构建视频格式转换对象
        AVPicture picture;
        int createPic = avpicture_alloc(&picture,
                                            AV_PIX_FMT_YUV420P,
                                            videoCodecCtx->width,
                                            videoCodecCtx->height);
        
        struct SwsContext *swsCtx = NULL;
        if (createPic != 0)
        {
            NSLog(@"创建pic失败");
            return;
        }
        swsCtx = sws_getCachedContext(swsCtx,
                                      videoCodecCtx->width,
                                      videoCodecCtx->height,
                                      videoCodecCtx->pix_fmt,
                                      videoCodecCtx->width,
                                      videoCodecCtx->height,
                                      AV_PIX_FMT_YUV420P,
                                      SWS_FAST_BILINEAR,
                                      NULL,
                                      NULL,
                                      NULL);
    


    初始化存储解码后音频数据的对象、构建音频格式转换对象

        //初始化音频数据存储对象
        AVFrame *audioFrame = av_frame_alloc();
        //构建音频格式转换对象
        SwrContext *swrCtx = NULL;
        if (audioCodecCtx->sample_fmt != AV_SAMPLE_FMT_S16)
        {
            NSLog(@"需要转换音频格式");
            int outputChannel = audioCodecCtx->channels;
            int outputSampleRate = audioCodecCtx->sample_rate;
            NSLog(@"channels:%d,sampleRate:%d,bitRate:%zd",outputChannel,outputSampleRate,audioCodecCtx->bit_rate);
            int64_t in_ch_layout = audioCodecCtx->channel_layout;
            enum AVSampleFormat in_sample_fmt = audioCodecCtx->sample_fmt;
            int in_sample_rate = audioCodecCtx->sample_rate;
            
            swrCtx = swr_alloc_set_opts(NULL,
                                        outputChannel,
                                        AV_SAMPLE_FMT_S16,
                                        outputSampleRate,
                                        in_ch_layout,
                                        in_sample_fmt,
                                        in_sample_rate,
                                        0,
                                        NULL);
            if (!swrCtx || swr_init(swrCtx))
            {
                if (swrCtx)
                {
                    swr_free(&swrCtx);
                }
            }
        }
    


    解码,并且处理解码后的数据:

    
        AVPacket packet;
        int gotFrame = 0;
        NSLog(@"decode start");
        
        int swrBufferSize = 1;
        void *swrBuffer = malloc(sizeof(char) * swrBufferSize);
        
        while (true)
        {
            if (av_read_frame(formatCtx, &packet) < 0)
            {
                NSLog(@"end of file or error");
                break;
            }
            
            int packetStreamIndex = packet.stream_index;
            if (packetStreamIndex == videoStreamIndex)//视频
            {
                int len = avcodec_decode_video2(videoCodecCtx, videoFrame, &gotFrame, &packet);
                if (len < 0)
                {
                    break;
                }
                if (gotFrame)
                {
                    //处理解码后的视频数据
                    @autoreleasepool {//不加内存直接炸了
                        NSData *luma;
                        NSData *chromaB;
                        NSData *chromaR;
                        if (videoCodecCtx->pix_fmt == AV_PIX_FMT_YUV420P || videoCodecCtx->pix_fmt == AV_PIX_FMT_YUVJ420P)
                        {
                            luma = copyFrameData(videoFrame->data[0],
                                                 videoFrame->linesize[0],
                                                 videoCodecCtx->width,
                                                 videoCodecCtx->height);
                            
                            chromaB = copyFrameData(videoFrame->data[1],
                                                    videoFrame->linesize[1],
                                                    videoCodecCtx->width / 2,
                                                    videoCodecCtx->height / 2);
                            
                            chromaR = copyFrameData(videoFrame->data[2],
                                                    videoFrame->linesize[2],
                                                    videoCodecCtx->width / 2,
                                                    videoCodecCtx->height / 2);
                        }
                        else
                        {
                            sws_scale(swsCtx,
                                      (const uint8_t **)videoFrame->data,
                                      videoFrame->linesize,
                                      0,
                                      videoCodecCtx->height,
                                      picture.data,
                                      picture.linesize);
                            luma = copyFrameData(picture.data[0],
                                                 picture.linesize[0],
                                                 videoCodecCtx->width,
                                                 videoCodecCtx->height);
                            
                            chromaB = copyFrameData(picture.data[1],
                                                    picture.linesize[1],
                                                    videoCodecCtx->width / 2,
                                                    videoCodecCtx->height / 2);
                            
                            chromaR = copyFrameData(picture.data[2],
                                                    picture.linesize[2],
                                                    videoCodecCtx->width / 2,
                                                    videoCodecCtx->height / 2);
                        }
                        //写数据到本地,这里建议用模拟器跑,因为出来的文件贼大
                        [videoDataWriter writeData:luma toPath:videoStorePath];
                        [videoDataWriter writeData:chromaB toPath:videoStorePath];
                        [videoDataWriter writeData:chromaR toPath:videoStorePath];
                    }
                }
            }
            else if (packetStreamIndex == audioStreamIndex)//音频
            {
                int len = avcodec_decode_audio4(audioCodecCtx, audioFrame, &gotFrame, &packet);
                if (len < 0)
                {
                    break;
                }
                if (gotFrame)
                {
                    //处理解码后的音频数据
                    void *audioData;
                    int numFrames;
                    int channels = audioCodecCtx->channels;
                    int samplesCount = (int)(audioFrame->nb_samples);
                    if (swrCtx)
                    {
                        int bufSize = av_samples_get_buffer_size(NULL,
                                                                 channels,
                                                                 samplesCount,
                                                                 AV_SAMPLE_FMT_S16,
                                                                 1);
    
                        if (!swrBuffer || swrBufferSize < bufSize)
                        {
                            swrBufferSize = bufSize;
                            swrBuffer = realloc(swrBuffer, swrBufferSize);
                        }
                        Byte *outbuf[2] = {swrBuffer,0};
                        numFrames = swr_convert(swrCtx,
                                                outbuf,
                                                samplesCount,
                                                (const uint8_t **)audioFrame->data,
                                                samplesCount);
                        audioData = swrBuffer;
                    }
                    else
                    {
                        audioData = audioFrame->data[0];
                        numFrames = audioFrame->nb_samples;
                    }
                    //AV_SAMPLE_FMT_S16 16位
                    //channels 为 1
                    //单声道16位,一帧占2个字节
                    int audioDataLen = numFrames * channels * 16 / 8;
                    //pcm数据写到本地
                    [audioDataWriter writeBytes:audioData len:audioDataLen toPath:audioStorePath];
                }
            }
        }
    


    关闭资源占用:

    
        //关闭packet
        av_free_packet(&packet);
        
        //关闭音频资源
        if (swrBuffer)
        {
            free(swrBuffer);
            swrBuffer = NULL;
            swrBufferSize = 0;
        }
        if (swrCtx)
        {
            swr_free(&swrCtx);
            swrCtx = NULL;
        }
        if (audioFrame)
        {
            av_free(audioFrame);
            audioFrame = NULL;
        }
        if (audioCodecCtx)
        {
            avcodec_close(audioCodecCtx);
            audioCodecCtx = NULL;
        }
        
        //关闭视频资源
        if (swsCtx)
        {
            sws_freeContext(swsCtx);
            swsCtx = NULL;
        }
        if (createPic == 0)
        {
            avpicture_free(&picture);
            createPic = -1;
        }
        if (videoFrame)
        {
            av_free(videoFrame);
            videoFrame = NULL;
        }
        if (videoCodecCtx)
        {
            avcodec_close(videoCodecCtx);
            videoCodecCtx = NULL;
        }
        
        //关闭连接资源
        if (formatCtx)
        {
            avformat_close_input(&formatCtx);
            formatCtx = NULL;
        }
        
        NSLog(@"end");
    


    拷贝视频数据的方法:

    static NSData * copyFrameData(UInt8 *src, int linesize, int width, int height)
    {
        width = MIN(linesize, width);
        NSMutableData *md = [NSMutableData dataWithLength: width * height];
        Byte *dst = md.mutableBytes;
        for (NSUInteger i = 0; i < height; ++i) {
            memcpy(dst, src, width);
            dst += width;
            src += linesize;
        }
        return md;
    }
    



    至此,对一个MP4的解码就结束了,代码运行后产生的 audioData.pcm 和 videoData.yuv ,可以用ffplay去验证:

    //播放视频
    ffplay -s 1280*720 /Users/xxb/Desktop/videoData.yuv
    
    //播放音频
    ffplay -f s16le -ar 44100 -ac 1 /Users/xxb/Desktop/audioData.pcm 
    

    觉得有收获的小伙伴,动动小手给个赞哦~

    相关文章

      网友评论

        本文标题:IOS FFmpeg零到自己的播放器1,解码

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