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


前言:

网上有很多基于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 


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容