IOS-FFmpeg解封装与封装

作者: Johnny_Wu | 来源:发表于2019-04-16 09:41 被阅读0次

    解封装跟封装一块讨论,更容易理解,也更好调试。
    解封装:一个视频文件,比如mp4,解封装后,可以得到具体的流AVStream(视频流,音频流,文字流等)
    封装:把各种流组成一个视频文件
    所以你可以看到demo代码总体逻辑为:先解封装,得到各种流后,再根据这些流封装成视频文件。
    代码参考:http://ffmpeg.org/doxygen/trunk/remuxing_8c-example.html

    头文件:

    #include <libavutil/avassert.h>
    #include <libavutil/channel_layout.h>
    #include <libavutil/opt.h>
    #include <libavutil/mathematics.h>
    #include <libavutil/timestamp.h>
    #include <libavformat/avformat.h>
    #include <libswscale/swscale.h>
    #include <libswresample/swresample.h>
    

    比较懒,全部的头文件都加进来了

    主要代码:

    - (int)mainFunc{
        av_register_all();
        avcodec_register_all();
        
        AVOutputFormat *ofmt = NULL;
        AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
        AVPacket pkt;
        const char *in_filename, *out_filename;
        int ret, i;
        int stream_index = 0;
        int *stream_mapping = NULL;
        int stream_mapping_size = 0;
        
        //input
        NSString *filePath = [CommonFunc getDocumentWithFile:@"movie.mp4"];
        in_filename = [filePath cStringUsingEncoding:NSASCIIStringEncoding];
        //output
        filePath = [CommonFunc getDefaultPath:@"movieOut.mp4"];
        out_filename = [filePath cStringUsingEncoding:NSASCIIStringEncoding];
    
        if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
            fprintf(stderr, "Could not open input file '%s'", in_filename);
            goto end;
        }
        if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
            fprintf(stderr, "Failed to retrieve input stream information");
            goto end;
        }
        av_dump_format(ifmt_ctx, 0, in_filename, 0);
        avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
        if (!ofmt_ctx) {
            fprintf(stderr, "Could not create output context\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }
        stream_mapping_size = ifmt_ctx->nb_streams;
        stream_mapping = av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));
        if (!stream_mapping) {
            ret = AVERROR(ENOMEM);
            goto end;
        }
        ofmt = ofmt_ctx->oformat;
        for (i = 0; i < ifmt_ctx->nb_streams; i++) {
            AVStream *out_stream;
            AVStream *in_stream = ifmt_ctx->streams[I];
            AVCodecParameters *in_codecpar = in_stream->codecpar;
            if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
                in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
                in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
                stream_mapping[i] = -1;
                continue;
            }
            stream_mapping[i] = stream_index++;
            //这里创建的流,默认是从0开始累加的,所以不一定跟ifmt_ctx流的位置对应
            out_stream = avformat_new_stream(ofmt_ctx, NULL);
            if (!out_stream) {
                fprintf(stderr, "Failed allocating output stream\n");
                ret = AVERROR_UNKNOWN;
                goto end;
            }
            ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
            if (ret < 0) {
                fprintf(stderr, "Failed to copy codec parameters\n");
                goto end;
            }
    //        out_stream->codecpar->codec_tag = 0;
            //可以在avformat_write_header配置好time_base,要不然执行完avformat_write_header后,就会有默认的time_base,因为两个流的time_base不一样,所以后面的转换设置pkt.pts pkt.dts pkt.duration。time_base一样就不需要设置
            out_stream->time_base = in_stream->time_base;
        }
        av_dump_format(ofmt_ctx, 0, out_filename, 1);
        if (!(ofmt->flags & AVFMT_NOFILE)) {
            ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
            if (ret < 0) {
                fprintf(stderr, "Could not open output file '%s'", out_filename);
                goto end;
            }
        }
        //执行这个后,out_stream->time_base修改为对应格式的默认值,比如mp4,默认为90k
        ret = avformat_write_header(ofmt_ctx, NULL);
        if (ret < 0) {
            fprintf(stderr, "Error occurred when opening output file\n");
            goto end;
        }
        while (1) {
            AVStream *in_stream, *out_stream;
            ret = av_read_frame(ifmt_ctx, &pkt);
            if (ret < 0)
                break;
            in_stream  = ifmt_ctx->streams[pkt.stream_index];
            if (pkt.stream_index >= stream_mapping_size ||
                stream_mapping[pkt.stream_index] < 0) {
                av_packet_unref(&pkt);
                continue;
            }
            pkt.stream_index = stream_mapping[pkt.stream_index];
            out_stream = ofmt_ctx->streams[pkt.stream_index];
    //        out_stream->time_base = in_stream->time_base;
            log_packet(ifmt_ctx, &pkt, "in");
            /* copy packet */
    //        pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
    //        pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
    //        pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
    //        pkt.pos = -1;
            log_packet(ofmt_ctx, &pkt, "out");
            ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
            if (ret < 0) {
                fprintf(stderr, "Error muxing packet\n");
                break;
            }
            av_packet_unref(&pkt);
        }
        av_write_trailer(ofmt_ctx);
    end:
        avformat_close_input(&ifmt_ctx);
        /* close output */
        if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
            avio_closep(&ofmt_ctx->pb);
        avformat_free_context(ofmt_ctx);
        av_freep(&stream_mapping);
        if (ret < 0 && ret != AVERROR_EOF) {
            fprintf(stderr, "Error occurred: %s\n", av_err2str(ret));
            return 1;
        }
        return 0;
    }
    
    static void log_packet(const AVFormatContext *fmt_ctx, const AVPacket *pkt, const char *tag)
    {
        AVRational *time_base = &fmt_ctx->streams[pkt->stream_index]->time_base;
        printf("%s: pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s stream_index:%d\n",
               tag,
               av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, time_base),
               av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, time_base),
               av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, time_base),
               pkt->stream_index);
    }
    

    解封装部分:

    AVFormatContext *ifmt_ctx = NULL;
    avformat_open_input(&ifmt_ctx, in_filename, 0, 0)
    avformat_find_stream_info(ifmt_ctx, 0)
    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
            AVStream *out_stream;
          //得到了各个流
            AVStream *in_stream = ifmt_ctx->streams[I];
    }
    

    解封装就这么简单。
    可以根据in_stream->codecpar->codec_type判断具体的流的类型,之前版本使用in_stream->codec,最新的提示弃用了这个结构,改为in_stream->codecpar,当然现在你也还是可以用in_stream->codec的。其实in_stream->codec就是之前说的AVCodecContext,记录编解码的一些信息。没有它,就算得到了流,也没意义,因为无法具体去解码。

    封装部分:

    //创建输出的封装上下文
    AVFormatContext *ofmt_ctx = NULL
    //根据输出文件名,得到具体的ofmt_ctx内容
    //当然这里得到的只是很少的内容
    avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
    //从ofmt_ctx创建流,流的index是从0开始累加的
    out_stream = avformat_new_stream(ofmt_ctx, NULL);
    //从对应的输入流中拷贝一份codecpar给对应的输出流
    //这就确定了,这是什么类型的流(视频流,音频流等)
    avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
    //time_base也用输入流的,每个流都有它对应的time_base,之前有讨论过time_base的意思
    out_stream->time_base = in_stream->time_base;
    //下面都是具体的输出操作
    ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE)
    avformat_write_header(ofmt_ctx, NULL)
    av_interleaved_write_frame(ofmt_ctx, &pkt)
    av_write_trailer(ofmt_ctx)
    

    存在于流中的,代表具体的编码的数据内容的是AVPacket结构体
    AVPacket存储的是编码后的数据,如果是mp4,就有可能存的是h264编码数据

    所以你看代码:
    AVPacket pkt;
    av_read_frame(ifmt_ctx, &pkt)
    从输入流读出来的是pkt。这里也好理解。
    然后就是把pkt写入到输出ofmt_ctx 的对应的流中:
    av_interleaved_write_frame(ofmt_ctx, &pkt)
    那么,我怎么知道写入到哪个流?
    根据pkt.stream_index值,就知道是写入哪个流了

    代码还用到了stream_mapping,这个不是主要代码,就是为了用输入流的index找到对应的新创的输出流的index,因为他们的index不一定相等。

    还有很有意思的,无关要紧的一段输出log代码:

    AVRational *time_base = &fmt_ctx->streams[pkt->stream_index]->time_base;
        printf("%s: pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s stream_index:%d\n",
               tag,
               av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, time_base),
               av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, time_base),
               av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, time_base),
               pkt->stream_index);
    

    这里面还是包含了不少信息,我简单讲讲:
    pkt->pts:可以认为是帧的展示时间,就是在显示器的显示的时间
    pkt->dts:帧的解码时间,为什么要加入这个呢,不是根据pts时间,然后同步解码就可以啦。其实有些帧是要根据前后的帧才可以解码的,如B帧,那么得到B帧后,是不是得先去解B帧后面的帧。
    pkt->duration:帧的时长
    time_base:pts、dts、duration的时间单位,之前有讲到过
    截取了段输出内容:


    屏幕快照 2019-04-15 下午8.24.37.png

    pts:1024 然后转化为秒就是0.0666667,具体算法是:pts*timebase = 1024*(1/15360)= 0.0666667
    就是说着一帧在0.0666667的时候播放出来,那么后面我们就可以通过控制timebase来决定pts了,那么就相当于控制了视频的播放速度,总时长等。
    最后别忘了进行资源的释放与文件的关闭。

    相关文章

      网友评论

        本文标题:IOS-FFmpeg解封装与封装

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