【FFMpeg视频开发与应用基础】四、调用FFmpeg SDK解析封装格式的视频为音频流和视频流

简介: 《FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK》视频教程已经在“CSDN学院”上线,视频中包含了从0开始逐行代码实现FFMpeg视频开发的过程,欢迎观看!链接地址:FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK工程代码地址:FFmpeg_Tutorial我们平常最常用的音视频文件通常不是单独的音频信号和视频信号,而是一个整体的文件。

《FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK》视频教程已经在“CSDN学院”上线,视频中包含了从0开始逐行代码实现FFMpeg视频开发的过程,欢迎观看!链接地址:FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK

工程代码地址:FFmpeg_Tutorial


我们平常最常用的音视频文件通常不是单独的音频信号和视频信号,而是一个整体的文件。这个文件会在其中包含音频流和视频流,并通过某种方式进行同步播放。通常,文件的音频和视频通过某种标准格式进行复用,生成某种封装格式,而封装的标志就是文件的扩展名,常用的有mp4/avi/flv/mkv等。

从底层考虑,我们可以使用的只有视频解码器、音频解码器,或者再加上一些附加的字幕解码等额外信息,却不存在所谓的mp4解码器或者avi解码器。所以,为了可以正确播放视频文件,必须将封装格式的视频文件分离出视频和音频信息分别进行解码和播放。

事实上,无论是mp4还是avi等文件格式,都有不同的标准格式,对于不同的格式并没有一种通用的解析方法。因此,FFMpeg专门定义了一个库来处理设计文件封装格式的功能,即libavformat。涉及文件的封装、解封装的问题,都可以通过调用libavformat的API实现。这里我们实现一个demo来处理音视频文件的解复用与解码的功能。


1. FFMpeg解复用-解码器所包含的结构

这一过程实际上包括了封装文件的解复用和音频/视频解码两个步骤,因此需要定义的结构体大致包括用于解码和解封装的部分。我们定义下面这样的一个结构体实现这个功能:

/*************************************************
Struct:         DemuxingVideoAudioContex
Description:    保存解复用器和解码器的上下文组件
*************************************************/
typedef struct
{
    AVFormatContext *fmt_ctx;
    AVCodecContext *video_dec_ctx, *audio_dec_ctx;
    AVStream *video_stream, *audio_stream;
    AVFrame *frame;
    AVPacket pkt;

    int video_stream_idx, audio_stream_idx;
    int width, height;

    uint8_t *video_dst_data[4];
    int video_dst_linesize[4];
    int video_dst_bufsize;
    enum AVPixelFormat pix_fmt;
} DemuxingVideoAudioContex;

这个结构体中的大部分数据类型我们在前面做编码/解码等功能时已经见到过,另外几个是涉及到视频文件的复用的,其中有:

  • AVFormatContext:用于处理音视频封装格式的上下文信息。
  • AVStream:表示音频或者视频流的结构。
  • AVPixelFormat:枚举类型,表示图像像素的格式,最常用的是AV_PIX_FMT_YUV420P

2、FFMpeg解复用-解码的过程

(1)、相关结构的初始化

与使用FFMpeg进行其他操作一样,首先需注册FFMpeg组件:

av_register_all();

随后,我们需要打开待处理的音视频文件。然而在此我们不使用打开文件的fopen函数,而是使用avformat_open_input函数。该函数不但会打开输入文件,而且可以根据输入文件读取相应的格式信息。该函数的声明如下:

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

该函数的各个参数的作用为:

  • ps:根据输入文件接收与格式相关的句柄信息;可以指向NULL,那么AVFormatContext类型的实例将由该函数进行分配。
  • url:视频url或者文件路径;
  • fmt:强制输入格式,可设置为NULL以自动检测;
  • options:保存文件格式无法识别的信息;
  • 返回值:成功返回0,失败则返回负的错误码;

该函数的调用方式为:

if (avformat_open_input(&(va_ctx.fmt_ctx), files.src_filename, NULL, NULL) < 0)
{
    fprintf(stderr, "Could not open source file %s\n", files.src_filename);
    return -1;
}

打开文件后,调用avformat_find_stream_info函数获取文件中的流信息。该函数的声明为:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

该函数的第一个参数即前面的文件句柄,第二个参数也是用于保存无法识别的信息的AVDictionary的结构,通常可设为NULL。调用方式如:

/* retrieve stream information */
if (avformat_find_stream_info(va_ctx.fmt_ctx, NULL) < 0) 
{
    fprintf(stderr, "Could not find stream information\n");
    return -1;
}

获取文件中的流信息后,下一步则是获取文件中的音频和视频流,并准备对音频和视频信息进行解码。获取文件中的流使用av_find_best_stream函数,其声明如:

int av_find_best_stream(AVFormatContext *ic,
                    enum AVMediaType type,
                    int wanted_stream_nb,
                    int related_stream,
                    AVCodec **decoder_ret,
                    int flags);

其中各个参数的意义:

  • ic:视频文件句柄;
  • type:表示数据的类型,常用的有AVMEDIA_TYPE_VIDEO表示视频,AVMEDIA_TYPE_AUDIO表示音频等;
  • wanted_stream_nb:我们期望获取到的数据流的数量,设置为-1使用自动获取;
  • related_stream:获取相关的音视频流,如果没有则设为-1;
  • decoder_ret:返回这一路数据流的解码器;
  • flags:未定义;
  • 返回值:函数执行成功返回流的数量,失败则返回负的错误码;

在函数执行成功后,便可调用avcodec_find_decoder和avcodec_open2打开解码器准备解码音视频流。该部分的代码实现如:

static int open_codec_context(IOFileName &files, DemuxingVideoAudioContex &va_ctx, enum AVMediaType type)
{
    int ret, stream_index;
    AVStream *st;
    AVCodecContext *dec_ctx = NULL;
    AVCodec *dec = NULL;
    AVDictionary *opts = NULL;

    ret = av_find_best_stream(va_ctx.fmt_ctx, type, -1, -1, NULL, 0);
    if (ret < 0) 
    {
        fprintf(stderr, "Could not find %s stream in input file '%s'\n", av_get_media_type_string(type), files.src_filename);
        return ret;
    } 
    else 
    {
        stream_index = ret;
        st = va_ctx.fmt_ctx->streams[stream_index];

        /* find decoder for the stream */
        dec_ctx = st->codec;
        dec = avcodec_find_decoder(dec_ctx->codec_id);
        if (!dec) 
        {
            fprintf(stderr, "Failed to find %s codec\n", av_get_media_type_string(type));
            return AVERROR(EINVAL);
        }

        /* Init the decoders, with or without reference counting */
        av_dict_set(&opts, "refcounted_frames", files.refcount ? "1" : "0", 0);
        if ((ret = avcodec_open2(dec_ctx, dec, &opts)) < 0) 
        {
            fprintf(stderr, "Failed to open %s codec\n", av_get_media_type_string(type));
            return ret;
        }

        switch (type)
        {
        case AVMEDIA_TYPE_VIDEO:
            va_ctx.video_stream_idx = stream_index;
            va_ctx.video_stream = va_ctx.fmt_ctx->streams[stream_index];
            va_ctx.video_dec_ctx = va_ctx.video_stream->codec;
            break;
        case AVMEDIA_TYPE_AUDIO:
            va_ctx.audio_stream_idx = stream_index;
            va_ctx.audio_stream = va_ctx.fmt_ctx->streams[stream_index];
            va_ctx.audio_dec_ctx = va_ctx.audio_stream->codec;
            break;
        default:
            fprintf(stderr, "Error: unsupported MediaType: %s\n", av_get_media_type_string(type));
            return -1;
        }
    }

    return 0;
}

整体初始化的函数代码为:

int InitDemuxContext(IOFileName &files, DemuxingVideoAudioContex &va_ctx)
{
    int ret = 0, width, height;

    /* register all formats and codecs */
    av_register_all();

    /* open input file, and allocate format context */
    if (avformat_open_input(&(va_ctx.fmt_ctx), files.src_filename, NULL, NULL) < 0)
    {
        fprintf(stderr, "Could not open source file %s\n", files.src_filename);
        return -1;
    }

    /* retrieve stream information */
    if (avformat_find_stream_info(va_ctx.fmt_ctx, NULL) < 0) 
    {
        fprintf(stderr, "Could not find stream information\n");
        return -1;
    }

    if (open_codec_context(files, va_ctx, AVMEDIA_TYPE_VIDEO) >= 0) 
    {
        files.video_dst_file = fopen(files.video_dst_filename, "wb");
        if (!files.video_dst_file) 
        {
            fprintf(stderr, "Could not open destination file %s\n", files.video_dst_filename);
            return -1;
        }

        /* allocate image where the decoded image will be put */
        va_ctx.width = va_ctx.video_dec_ctx->width;
        va_ctx.height = va_ctx.video_dec_ctx->height;
        va_ctx.pix_fmt = va_ctx.video_dec_ctx->pix_fmt;
        ret = av_image_alloc(va_ctx.video_dst_data, va_ctx.video_dst_linesize, va_ctx.width, va_ctx.height, va_ctx.pix_fmt, 1);
        if (ret < 0) 
        {
            fprintf(stderr, "Could not allocate raw video buffer\n");
            return -1;
        }
        va_ctx.video_dst_bufsize = ret;
    }

    if (open_codec_context(files, va_ctx, AVMEDIA_TYPE_AUDIO) >= 0) 
    {
        files.audio_dst_file = fopen(files.audio_dst_filename, "wb");
        if (!files.audio_dst_file) 
        {
            fprintf(stderr, "Could not open destination file %s\n", files.audio_dst_filename);
            return -1;
        }
    }

    if (va_ctx.video_stream)
    {
        printf("Demuxing video from file '%s' into '%s'\n", files.src_filename, files.video_dst_filename);
    }

    if (va_ctx.audio_stream)
    {
        printf("Demuxing audio from file '%s' into '%s'\n", files.src_filename, files.audio_dst_filename);
    }

    /* dump input information to stderr */
    av_dump_format(va_ctx.fmt_ctx, 0, files.src_filename, 0);

    if (!va_ctx.audio_stream && !va_ctx.video_stream) 
    {
        fprintf(stderr, "Could not find audio or video stream in the input, aborting\n");
        return -1;
    }

    return 0;
}

随后要做的,是分配AVFrame和初始化AVPacket对象:

va_ctx.frame = av_frame_alloc();            //分配AVFrame结构对象
if (!va_ctx.frame)
{
    fprintf(stderr, "Could not allocate frame\n");
    ret = AVERROR(ENOMEM);
    goto end;
}

/* initialize packet, set data to NULL, let the demuxer fill it */
av_init_packet(&va_ctx.pkt);                //初始化AVPacket对象
va_ctx.pkt.data = NULL;
va_ctx.pkt.size = 0;

(2)、循环解析视频文件的包数据

解析视频文件的循环代码段为:

/* read frames from the file */
while (av_read_frame(va_ctx.fmt_ctx, &va_ctx.pkt) >= 0)     //从输入程序中读取一个包的数据
{
    AVPacket orig_pkt = va_ctx.pkt;
    do 
    {
        ret = Decode_packet(files, va_ctx, &got_frame, 0);  //解码这个包
        if (ret < 0)
            break;
        va_ctx.pkt.data += ret;
        va_ctx.pkt.size -= ret;
    } while (va_ctx.pkt.size > 0);
    av_packet_unref(&orig_pkt);
}

这部分代码逻辑上非常简单,首先调用av_read_frame函数,从文件中读取一个packet的数据,并实现了一个Decode_packet对这个packet进行解码。Decode_packet函数的实现如下:

int Decode_packet(IOFileName &files, DemuxingVideoAudioContex &va_ctx, int *got_frame, int cached)
{
    int ret = 0;
    int decoded = va_ctx.pkt.size;
    static int video_frame_count = 0;
    static int audio_frame_count = 0;

    *got_frame = 0;

    if (va_ctx.pkt.stream_index == va_ctx.video_stream_idx)
    {
        /* decode video frame */
        ret = avcodec_decode_video2(va_ctx.video_dec_ctx, va_ctx.frame, got_frame, &va_ctx.pkt);
        if (ret < 0)
        {
            printf("Error decoding video frame (%d)\n", ret);
            return ret;
        }

        if (*got_frame)
        {
            if (va_ctx.frame->width != va_ctx.width || va_ctx.frame->height != va_ctx.height ||
                va_ctx.frame->format != va_ctx.pix_fmt)
            {
                /* To handle this change, one could call av_image_alloc again and
                * decode the following frames into another rawvideo file. */
                printf("Error: Width, height and pixel format have to be "
                    "constant in a rawvideo file, but the width, height or "
                    "pixel format of the input video changed:\n"
                    "old: width = %d, height = %d, format = %s\n"
                    "new: width = %d, height = %d, format = %s\n",
                    va_ctx.width, va_ctx.height, av_get_pix_fmt_name((AVPixelFormat)(va_ctx.pix_fmt)),
                    va_ctx.frame->width, va_ctx.frame->height,
                    av_get_pix_fmt_name((AVPixelFormat)va_ctx.frame->format));
                return -1;
            }

            printf("video_frame%s n:%d coded_n:%d pts:%s\n", cached ? "(cached)" : "", video_frame_count++, va_ctx.frame->coded_picture_number, va_ctx.frame->pts);

            /* copy decoded frame to destination buffer:
            * this is required since rawvideo expects non aligned data */
            av_image_copy(va_ctx.video_dst_data, va_ctx.video_dst_linesize,
                (const uint8_t **)(va_ctx.frame->data), va_ctx.frame->linesize,
                va_ctx.pix_fmt, va_ctx.width, va_ctx.height);

            /* write to rawvideo file */
            fwrite(va_ctx.video_dst_data[0], 1, va_ctx.video_dst_bufsize, files.video_dst_file);
        }
    }
    else if (va_ctx.pkt.stream_index == va_ctx.audio_stream_idx)
    {
        /* decode audio frame */
        ret = avcodec_decode_audio4(va_ctx.audio_dec_ctx, va_ctx.frame, got_frame, &va_ctx.pkt);
        if (ret < 0)
        {
            printf("Error decoding audio frame (%s)\n", ret);
            return ret;
        }
        /* Some audio decoders decode only part of the packet, and have to be
        * called again with the remainder of the packet data.
        * Sample: fate-suite/lossless-audio/luckynight-partial.shn
        * Also, some decoders might over-read the packet. */
        decoded = FFMIN(ret, va_ctx.pkt.size);

        if (*got_frame)
        {
            size_t unpadded_linesize = va_ctx.frame->nb_samples * av_get_bytes_per_sample((AVSampleFormat)va_ctx.frame->format);
            printf("audio_frame%s n:%d nb_samples:%d pts:%s\n",
                cached ? "(cached)" : "",
                audio_frame_count++, va_ctx.frame->nb_samples,
                va_ctx.frame->pts);

            /* Write the raw audio data samples of the first plane. This works
            * fine for packed formats (e.g. AV_SAMPLE_FMT_S16). However,
            * most audio decoders output planar audio, which uses a separate
            * plane of audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P).
            * In other words, this code will write only the first audio channel
            * in these cases.
            * You should use libswresample or libavfilter to convert the frame
            * to packed data. */
            fwrite(va_ctx.frame->extended_data[0], 1, unpadded_linesize, files.audio_dst_file);
        }
    }

        /* If we use frame reference counting, we own the data and need
        * to de-reference it when we don't use it anymore */
        if (*got_frame && files.refcount)
            av_frame_unref(va_ctx.frame);

        return decoded;
}

在该函数中,首先对读取到的packet中的stream_index分别于先前获取的音频和视频的stream_index进行对比来确定是音频还是视频流。而后分别调用相应的解码函数进行解码,以视频流为例,判断当前stream为视频流后,调用avcodec_decode_video2函数将流数据解码为像素数据,并在获取完整的一帧之后,将其写出到输出文件中。


3、总结

相对于前文讲述过的解码H.264格式裸码流,解封装+解码过程看似多了一个步骤,然而在实现起来实际上并无过多差别。这主要是由于FFMpeg中的多个API已经很好地实现了封装文件的解析和读取过程,如打开文件我们使用avformat_open_input代替fopen,读取数据包使用av_read_frame代替fread,其他方面只需要多一步判断封装文件中数据流的类型即可,剩余部分与裸码流的解码并无太多差别。

目录
相关文章
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
2192 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
编解码 监控 网络协议
如何使用FFmpeg实现RTSP推送H.264和H.265(HEVC)编码视频
本文详细介绍了如何使用FFmpeg实现RTSP推送H.264和H.265(HEVC)编码视频。内容涵盖环境搭建、编码配置、服务器端与客户端实现等方面,适合视频监控系统和直播平台等应用场景。通过具体命令和示例代码,帮助读者快速上手并实现目标。
4338 6
|
Java 数据安全/隐私保护
Java ffmpeg 实现视频加文字/图片水印功能
【10月更文挑战第22天】在 Java 中使用 FFmpeg 实现视频加文字或图片水印功能,需先安装 FFmpeg 并添加依赖(如 JavaCV)。通过构建 FFmpeg 命令行参数,使用 `drawtext` 滤镜添加文字水印,或使用 `overlay` 滤镜添加图片水印。示例代码展示了如何使用 JavaCV 实现文字水印。
1820 1
|
编解码 语音技术 内存技术
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
《FFmpeg开发实战:从零基础到短视频上线》一书中的“5.1.2 把音频流保存为PCM文件”章节介绍了将媒体文件中的音频流转换为原始PCM音频的方法。示例代码直接保存解码后的PCM数据,保留了原始音频的采样频率、声道数量和采样位数。但在实际应用中,有时需要特定规格的PCM音频。例如,某些语音识别引擎仅接受16位PCM数据,而标准MP3音频通常采用32位采样,因此需将32位MP3音频转换为16位PCM音频。
693 0
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
|
XML Java Android开发
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
GSYVideoPlayer是一款国产移动端视频播放器,支持弹幕、滤镜、广告等功能,采用IJKPlayer、Media3(EXOPlayer)、MediaPlayer及AliPlayer多种内核。截至2024年8月,其GitHub星标数达2万。集成时需使用新版Android Studio,并按特定步骤配置依赖与权限。提供了NormalGSYVideoPlayer、GSYADVideoPlayer及ListGSYVideoPlayer三种控件,支持HLS、RTMP等多种直播链接。
887 18
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
|
XML 开发工具 Android开发
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
ExoPlayer最初是为了解决Android早期MediaPlayer控件对网络视频兼容性差的问题而推出的。现在,Android官方已将其升级并纳入Jetpack的Media3库,使其成为音视频操作的统一引擎。新版ExoPlayer支持多种协议,解决了设备和系统碎片化问题,可在整个Android生态中一致运行。通过修改`build.gradle`文件、布局文件及Activity代码,并添加必要的权限,即可集成并使用ExoPlayer进行网络视频播放。具体步骤包括引入依赖库、配置播放界面、编写播放逻辑以及添加互联网访问权限。
2144 1
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
509 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
编解码 移动开发 安全
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生
自互联网普及以来,流媒体技术特别是视频直播技术不断进步,出现了多种传输协议。早期的MMS由微软主导,但随WMV格式衰落而减少使用。RTSP由网景和RealNetworks联合提出,支持多种格式,但在某些现代应用中不再受支持。RTMP由Adobe开发,曾广泛用于网络直播,但因HTML5不支持Flash而受影响。HLS由苹果开发,基于HTTP,适用于点播。SRT和RIST均为较新协议,强调安全与可靠性,尤其SRT在电视直播中应用增多。尽管RTMP仍占一定市场,但SRT等新协议正逐渐兴起。
821 8
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生
|
Linux 开发工具 Android开发
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
EasyPusher是一款国产RTSP直播录制推流客户端工具,支持Windows、Linux、Android及iOS等系统。尽管其GitHub仓库(安卓版:https://github.com/EasyDarwin/EasyPusher-Android)已多年未更新,但通过一系列改造,如升级SDK版本、迁移到AndroidX、指定本地NDK版本及更新Gradle版本等,仍可在最新Android Studio上运行。以下是针对Android Studio Dolphin版本的具体改造步骤。
466 3
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher

热门文章

最新文章

推荐镜像

更多
  • DNS