FFmpeg深度剖析:解封装、解码、编码与封装的工作原理
1. FFmpeg 架构概览
2. 解封装(Demuxer):拆解音视频的“包装盒”
2.1 封装格式简介
2.2 解封装器的工作流程
2.3 FFmpeg 中的解封装器
2.4 示例代码
3. 解码(Decoder):将压缩数据还原为原始数据
3.1 编解码器简介
3.2 解码器的工作流程
3.3 FFmpeg 中的解码器
3.4 示例代码
4. 编码(Encoder):将原始数据压缩为特定格式
4.1 编码器的工作流程
4.2 FFmpeg 中的编码器
4.3 示例代码
5. 封装(Muxer):将音视频流打包为文件
5.1 封装器的工作流程
5.2 FFmpeg 中的封装器
5.3 示例代码
6. 总结
作为音视频处理领域的瑞士军刀,FFmpeg 功能强大,应用广泛。但其内部结构复杂,初学者往往难以把握。本文旨在深入剖析 FFmpeg 的核心模块,包括解封装(Demuxer)、解码(Decoder)、编码(Encoder)和封装(Muxer),揭示它们协同工作的原理,助你更好地理解和应用 FFmpeg。
1. FFmpeg 架构概览
FFmpeg 并非一个单一的程序,而是一个包含多个库和工具的集合。其核心架构可以概括为以下几个部分:
- libavformat: 负责处理各种音视频封装格式,提供解封装和封装的功能。
- libavcodec: 包含大量的音视频编解码器,用于实现音视频数据的压缩和解压缩。
- libavutil: 提供一些通用的工具函数,例如内存管理、数据结构等。
- libswscale: 用于图像的缩放和像素格式转换。
- libavfilter: 提供音视频滤镜功能,可以对音视频数据进行各种处理。
- libavdevice: 用于访问各种音视频输入输出设备。
- ffmpeg: 一个命令行工具,基于上述库提供各种音视频处理功能。
本文将重点关注 libavformat 和 libavcodec 这两个核心库,深入探讨解封装、解码、编码和封装的工作原理。
2. 解封装(Demuxer):拆解音视频的“包装盒”
想象一下,你收到一个精心包装的礼物,解封装的过程就像是拆开包装盒,将里面的礼物(音视频数据)取出来。在音视频处理中,解封装器(Demuxer)负责将音视频文件从其封装格式中分离出独立的音视频流。
2.1 封装格式简介
音视频封装格式,也称为容器格式,是一种将音视频数据、字幕、元数据等信息打包在一起的文件格式。常见的封装格式包括:
- MP4: 一种流行的封装格式,广泛应用于各种平台和设备。
- MKV: 一种灵活的封装格式,可以容纳多种音视频编码和字幕格式。
- AVI: 一种较老的封装格式,兼容性较好,但功能相对简单。
- FLV: 一种常见的流媒体封装格式,常用于在线视频网站。
- TS: 一种用于广播电视的封装格式,具有较强的抗干扰能力。
不同的封装格式采用不同的方式组织和存储音视频数据。解封装器的任务就是理解这些不同的格式,并从中提取出有用的信息。
2.2 解封装器的工作流程
解封装器的工作流程大致如下:
- 打开输入文件: 解封装器首先需要打开指定的音视频文件。
- 读取文件头: 解封装器读取文件头,从中获取封装格式的信息,例如音视频流的数量、编码方式等。
- 解析数据包: 解封装器根据封装格式的规范,解析文件中的数据包(Packet)。每个数据包通常包含一部分音视频数据。
- 提取音视频流: 解封装器将数据包中的音视频数据分离出来,形成独立的音视频流。
- 输出音视频流: 解封装器将提取出的音视频流输出给解码器进行处理。
2.3 FFmpeg 中的解封装器
FFmpeg 的 libavformat 库提供了大量的解封装器,支持各种常见的音视频封装格式。例如,mp4 demuxer
用于解封装 MP4 文件,mkv demuxer
用于解封装 MKV 文件。
要使用 FFmpeg 的解封装器,你需要:
- 注册解封装器: 在程序开始时,需要调用
av_register_all()
函数注册所有的解封装器。 - 打开输入文件: 使用
avformat_open_input()
函数打开输入文件,并指定要使用的解封装器(如果需要)。 - 读取文件信息: 使用
avformat_find_stream_info()
函数读取文件信息,包括音视频流的数量、编码方式等。 - 读取数据包: 使用
av_read_frame()
函数读取数据包。 - 处理数据包: 将数据包发送给解码器进行处理。
- 关闭输入文件: 在程序结束时,需要调用
avformat_close_input()
函数关闭输入文件。
2.4 示例代码
以下是一个简单的示例代码,演示如何使用 FFmpeg 的解封装器读取 MP4 文件中的视频流:
#include <libavformat/avformat.h> int main(int argc, char *argv[]) { AVFormatContext *pFormatCtx = NULL; int i, videoStream; AVCodecContext *pCodecCtx = NULL; AVCodec *pCodec = NULL; AVFrame *pFrame = NULL; AVPacket packet; int frameFinished; if(argc < 2) { printf("Usage: tutorial01 <file>\n"); return -1; } // 注册所有格式和编解码器 av_register_all(); // 打开视频文件 if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0) return -1; // Couldn't open file // 检索流信息 if(avformat_find_stream_info(pFormatCtx, NULL)<0) return -1; // Couldn't find stream information // Dump 信息到标准输出 av_dump_format(pFormatCtx, 0, argv[1], 0); // 找到第一个视频流 videoStream=-1; for(i=0; i<pFormatCtx->nb_streams; i++) if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) { videoStream=i; break; } if(videoStream==-1) return -1; // Didn't find a video stream // 获取指向视频流编码上下文的指针 pCodecCtx=pFormatCtx->streams[videoStream]->codec; // 找到视频流的解码器 pCodec=avcodec_find_decoder(pCodecCtx->codec_id); if(pCodec==NULL) { fprintf(stderr, "Unsupported codec!\n"); return -1; // Codec not found } // 用codec上下文参数设置解码器 if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0) return -1; // Could not open codec // 分配帧 pFrame=av_frame_alloc(); // 读取帧并保存到文件 i=0; while(av_read_frame(pFormatCtx, &packet)>=0) { // 判断这个包是否属于视频流 if(packet.stream_index==videoStream) { // 解码视频帧 avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet); // 帧是否解码完成 if(frameFinished) { // 在这里处理解码后的帧 printf("Frame %d decoded\n",i++); } } // 释放包 av_free_packet(&packet); } // 释放帧 av_frame_free(&pFrame); // 关闭解码器 avcodec_close(pCodecCtx); // 关闭视频文件 avformat_close_input(&pFormatCtx); return 0; }
这段代码只是一个简单的示例,并没有对解码后的帧进行任何处理。在实际应用中,你需要根据自己的需求对解码后的帧进行进一步的处理,例如显示、编码或存储。
3. 解码(Decoder):将压缩数据还原为原始数据
解封装器将音视频流从封装格式中提取出来后,下一步就是对这些音视频流进行解码。解码器(Decoder)负责将压缩的音视频数据还原为原始的音视频数据,例如将 H.264 编码的视频流解码为 YUV 格式的图像数据,将 AAC 编码的音频流解码为 PCM 格式的音频数据。
3.1 编解码器简介
编解码器(Codec)是编码器(Encoder)和解码器(Decoder)的统称。编码器负责将原始的音视频数据压缩成特定的格式,以减小文件大小和传输带宽;解码器则负责将压缩后的音视频数据还原为原始的格式,以便播放和处理。常见的音视频编解码器包括:
- 视频编解码器: H.264, H.265 (HEVC), VP9, AV1, MPEG-4
- 音频编解码器: AAC, MP3, Opus, Vorbis, AC-3
不同的编解码器采用不同的压缩算法,具有不同的压缩效率和质量。选择合适的编解码器对于音视频质量和文件大小至关重要。
3.2 解码器的工作流程
解码器的工作流程大致如下:
- 接收数据包: 解码器从解封装器接收包含压缩音视频数据的数据包。
- 解析数据包: 解码器根据编解码器的规范,解析数据包中的数据,提取出编码后的音视频数据。
- 解码音视频数据: 解码器使用相应的解码算法,将编码后的音视频数据还原为原始的音视频数据。
- 输出原始数据: 解码器将解码后的原始音视频数据输出,以便后续的处理,例如显示、编码或存储。
3.3 FFmpeg 中的解码器
FFmpeg 的 libavcodec 库提供了大量的解码器,支持各种常见的音视频编解码器。例如,h264 decoder
用于解码 H.264 编码的视频流,aac decoder
用于解码 AAC 编码的音频流。
要使用 FFmpeg 的解码器,你需要:
- 找到解码器: 使用
avcodec_find_decoder()
函数根据编码器的 ID 找到对应的解码器。 - 创建解码器上下文: 使用
avcodec_alloc_context3()
函数创建解码器上下文。 - 配置解码器上下文: 将解封装器获取到的编码器信息(例如视频的宽度、高度、帧率等)配置到解码器上下文中。
- 打开解码器: 使用
avcodec_open2()
函数打开解码器,并指定要使用的解码器上下文。 - 解码数据包: 使用
avcodec_decode_video2()
或avcodec_decode_audio4()
函数解码数据包。 - 处理解码后的数据: 对解码后的音视频数据进行进一步的处理,例如显示、编码或存储。
- 关闭解码器: 在程序结束时,需要调用
avcodec_close()
函数关闭解码器。
3.4 示例代码
以下是一个简单的示例代码,演示如何使用 FFmpeg 的解码器解码 H.264 编码的视频流:
#include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> int main(int argc, char *argv[]) { AVFormatContext *pFormatCtx = NULL; int i, videoStream; AVCodecContext *pCodecCtxOrig = NULL; AVCodecContext *pCodecCtx = NULL; AVCodec *pCodec = NULL; AVFrame *pFrame = NULL; AVFrame *pFrameRGB = NULL; AVPacket packet; int frameFinished; struct SwsContext *sws_ctx = NULL; if(argc < 2) { printf("Usage: tutorial02 <file>\n"); return -1; } // 注册所有格式和编解码器 av_register_all(); // 打开视频文件 if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0) return -1; // Couldn't open file // 检索流信息 if(avformat_find_stream_info(pFormatCtx, NULL)<0) return -1; // Couldn't find stream information // Dump 信息到标准输出 av_dump_format(pFormatCtx, 0, argv[1], 0); // 找到第一个视频流 videoStream=-1; for(i=0; i<pFormatCtx->nb_streams; i++) if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) { videoStream=i; break; } if(videoStream==-1) return -1; // Didn't find a video stream // 获取指向视频流编码上下文的指针 pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec; // 找到视频流的解码器 pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id); if(pCodec==NULL) { fprintf(stderr, "Unsupported codec!\n"); return -1; // Codec not found } // 复制上下文 pCodecCtx = avcodec_alloc_context3(pCodec); if (!pCodecCtx) { fprintf(stderr, "Failed to allocate codec context\n"); return -1; } if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) { fprintf(stderr, "Couldn't copy codec context"); return -1; // Error copying codec context } // 用codec上下文参数设置解码器 if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0) return -1; // Could not open codec // 分配帧 pFrame=av_frame_alloc(); // 分配RGB帧 pFrameRGB=av_frame_alloc(); if(pFrameRGB==NULL) return -1; // 确定所需缓冲区的大小,并分配缓冲区 int numBytes = avpicture_get_size(AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height); uint8_t *buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t)); avpicture_fill((AVPicture *)pFrameRGB, buffer, AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height); // 初始化SWS上下文以进行转换 sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL); if(sws_ctx == NULL) { fprintf(stderr, "Couldn't initialize swscale\n"); return -1; } // 读取帧并保存到文件 i=0; while(av_read_frame(pFormatCtx, &packet)>=0) { // 判断这个包是否属于视频流 if(packet.stream_index==videoStream) { // 解码视频帧 avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet); // 帧是否解码完成 if(frameFinished) { // 将帧从原始格式转换为RGB sws_scale(sws_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); // 保存帧到PPM文件 char filename[32]; sprintf(filename, "frame%d.ppm", i++); FILE *pFile = fopen(filename, "wb"); if(pFile == NULL) { fprintf(stderr, "Couldnt open file\n"); return -1; } // 写入PPM文件头 fprintf(pFile, "P6\n%d %d\n255\n", pCodecCtx->width, pCodecCtx->height); // 写入像素数据 for(int y=0; y<pCodecCtx->height; y++) fwrite(pFrameRGB->data[0]+y*pFrameRGB->linesize[0], 1, pCodecCtx->width*3, pFile); // 关闭文件 fclose(pFile); } } // 释放包 av_free_packet(&packet); } // 释放帧 av_frame_free(&pFrame); av_frame_free(&pFrameRGB); av_free(buffer); // 关闭编解码器 avcodec_close(pCodecCtx); avcodec_close(pCodecCtxOrig); // 关闭视频文件 avformat_close_input(&pFormatCtx); return 0; }
这段代码将解码后的每一帧视频保存为 PPM 格式的图像文件。在实际应用中,你可以根据自己的需求对解码后的帧进行进一步的处理,例如显示、编码或存储。
4. 编码(Encoder):将原始数据压缩为特定格式
编码是解码的逆过程。编码器(Encoder)负责将原始的音视频数据压缩成特定的格式,以减小文件大小和传输带宽。例如将 YUV 格式的图像数据编码为 H.264 编码的视频流,将 PCM 格式的音频数据编码为 AAC 编码的音频流。
4.1 编码器的工作流程
编码器的工作流程大致如下:
- 接收原始数据: 编码器接收原始的音视频数据,例如 YUV 格式的图像数据或 PCM 格式的音频数据。
- 分析原始数据: 编码器分析原始数据,例如图像的运动矢量、音频的频率等。
- 编码音视频数据: 编码器使用相应的编码算法,将原始的音视频数据压缩成特定的格式。
- 输出编码后的数据: 编码器将编码后的音视频数据输出,以便后续的处理,例如封装或传输。
4.2 FFmpeg 中的编码器
FFmpeg 的 libavcodec 库提供了大量的编码器,支持各种常见的音视频编解码器。例如,libx264 encoder
用于编码 H.264 编码的视频流,libfdk_aac encoder
用于编码 AAC 编码的音频流。
要使用 FFmpeg 的编码器,你需要:
- 找到编码器: 使用
avcodec_find_encoder()
函数根据编码器的 ID 找到对应的编码器。 - 创建编码器上下文: 使用
avcodec_alloc_context3()
函数创建编码器上下文。 - 配置编码器上下文: 设置编码器上下文的参数,例如视频的宽度、高度、帧率、码率等。
- 打开编码器: 使用
avcodec_open2()
函数打开编码器,并指定要使用的编码器上下文。 - 编码数据: 使用
avcodec_encode_video2()
或avcodec_encode_audio2()
函数编码音视频数据。 - 接收编码后的数据: 从编码器接收编码后的数据包。
- 关闭编码器: 在程序结束时,需要调用
avcodec_close()
函数关闭编码器。
4.3 示例代码
以下是一个简单的示例代码,演示如何使用 FFmpeg 的编码器编码 YUV 格式的图像数据为 H.264 编码的视频流:
#include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> int main(int argc, char *argv[]) { AVFormatContext *pFormatCtx = NULL; AVOutputFormat *fmt = NULL; AVStream *video_st = NULL; AVCodecContext *pCodecCtx = NULL; AVCodec *pCodec = NULL; AVFrame *pFrame = NULL; AVPacket packet; uint8_t *picture_buf = NULL; int picture_size; int y, x, i; int frame_idx = 0; // 设置输出视频的参数 int width = 640; int height = 480; int fps = 25; const char *filename = "output.mp4"; // 输出文件名 av_register_all(); // 分配输出格式上下文 avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, filename); if (!pFormatCtx) { printf("Could not deduce output format from file extension: using MPEG.\n"); avformat_alloc_output_context2(&pFormatCtx, NULL, "mpeg", filename); } if (!pFormatCtx) { return -1; } fmt = pFormatCtx->oformat; // 打开输出文件 if (avio_open(&pFormatCtx->pb, filename, AVIO_FLAG_WRITE) < 0) { printf("Could not open output file %s\n", filename); return -1; } // 创建一个新的视频流 video_st = avformat_new_stream(pFormatCtx, NULL); if (!video_st) { printf("Could not allocate stream\n"); return -1; } video_st->id = pFormatCtx->nb_streams - 1; pCodecCtx = video_st->codec; pCodecCtx->codec_id = AV_CODEC_ID_H264; // H.264 编码器 pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO; pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; // 像素格式 pCodecCtx->width = width; pCodecCtx->height = height; pCodecCtx->time_base.num = 1; pCodecCtx->time_base.den = fps; pCodecCtx->bit_rate = 400000; // 码率 pCodecCtx->gop_size = 12; // 关键帧间隔 // 找到编码器 pCodec = avcodec_find_encoder(pCodecCtx->codec_id); if (!pCodec) { printf("Could not find encoder\n"); return -1; } // 设置编码器选项 (例如,使用 libx264 编码器) AVDictionary *opt = NULL; av_dict_set(&opt, "preset", "slow", 0); av_dict_set(&opt, "tune", "film", 0); // 打开编码器 if (avcodec_open2(pCodecCtx, pCodec, &opt) < 0) { printf("Could not open codec\n"); return -1; } // 分配帧 pFrame = av_frame_alloc(); if (!pFrame) { printf("Could not allocate frame\n"); return -1; } pFrame->format = pCodecCtx->pix_fmt; pFrame->width = pCodecCtx->width; pFrame->height = pCodecCtx->height; // 分配帧缓冲区 picture_size = avpicture_get_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); picture_buf = (uint8_t *)av_malloc(picture_size); if (!picture_buf) { printf("Could not allocate picture buffer\n"); return -1; } avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); // 写入文件头 avformat_write_header(pFormatCtx, &opt); av_new_packet(&packet, picture_size); // 编码帧 for (i = 0; i < 100; i++) { // 填充 YUV 数据 (这里只是一个示例,实际应用中需要从图像源获取数据) for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { pFrame->data[0][y * pFrame->linesize[0] + x] = x + y + i * 3; } } // 对 U 和 V 进行类似填充 for (y = 0; y < height / 2; y++) { for (x = 0; x < width / 2; x++) { pFrame->data[1][y * pFrame->linesize[1] + x] = 128 + y + i * 2; pFrame->data[2][y * pFrame->linesize[2] + x] = 64 + x + i * 5; } } pFrame->pts = i; // 编码 int got_packet = 0; int ret = avcodec_encode_video2(pCodecCtx, &packet, pFrame, &got_packet); if (ret < 0) { printf("Error encoding frame\n"); return -1; } if (got_packet) { printf("Writing frame %3d (size=%5d)\n", frame_idx, packet.size); av_packet_rescale_ts(&packet, pCodecCtx->time_base, video_st->time_base); packet.stream_index = video_st->index; // 写入帧 ret = av_interleaved_write_frame(pFormatCtx, &packet); if (ret < 0) { printf("Error while writing video frame\n"); break; } av_free_packet(&packet); frame_idx++; } } // 写入文件尾 av_write_trailer(pFormatCtx); // 释放资源 avcodec_close(pCodecCtx); av_free(pFrame); av_free(picture_buf); avio_close(pFormatCtx->pb); avformat_free_context(pFormatCtx); return 0; }
这段代码将生成一个包含 100 帧视频的 MP4 文件。视频的内容是 YUV 数据,这里只是简单地填充了一些数值,实际应用中需要从图像源获取数据。
5. 封装(Muxer):将音视频流打包为文件
编码器将音视频数据压缩成特定的格式后,最后一步就是将这些编码后的音视频流封装成特定的文件格式。封装器(Muxer)负责将编码后的音视频流、字幕、元数据等信息打包在一起,形成一个完整的音视频文件。
5.1 封装器的工作流程
封装器的工作流程大致如下:
- 接收音视频流: 封装器接收编码器输出的音视频流。
- 创建文件头: 封装器根据封装格式的规范,创建文件头,用于描述音视频流的信息,例如编码方式、分辨率、帧率等。
- 打包数据: 封装器将音视频数据按照封装格式的规范,打包成数据块(Chunk)或数据包(Packet)。
- 写入文件: 封装器将文件头和数据块写入到输出文件中。
- 创建文件尾: 封装器根据封装格式的规范,创建文件尾,用于标识文件的结束。
- 写入文件尾: 封装器将文件尾写入到输出文件中。
5.2 FFmpeg 中的封装器
FFmpeg 的 libavformat 库提供了大量的封装器,支持各种常见的音视频封装格式。例如,mp4 muxer
用于封装 MP4 文件,mkv muxer
用于封装 MKV 文件。
要使用 FFmpeg 的封装器,你需要:
- 创建输出格式上下文: 使用
avformat_alloc_output_context2()
函数创建输出格式上下文,并指定要使用的封装格式。 - 创建音视频流: 使用
avformat_new_stream()
函数创建音视频流,并将编码器的信息(例如编码方式、分辨率、帧率等)配置到音视频流中。 - 打开输出文件: 使用
avio_open()
函数打开输出文件。 - 写入文件头: 使用
avformat_write_header()
函数写入文件头。 - 写入数据包: 使用
av_interleaved_write_frame()
函数写入数据包。 - 写入文件尾: 使用
av_write_trailer()
函数写入文件尾。 - 关闭输出文件: 使用
avio_close()
函数关闭输出文件。
5.3 示例代码
上面的编码示例代码已经包含了封装的过程,这里不再重复。
6. 总结
本文深入剖析了 FFmpeg 的核心模块,包括解封装、解码、编码和封装的工作原理。理解这些原理对于更好地使用 FFmpeg 进行音视频处理至关重要。希望本文能够帮助你更好地理解和应用 FFmpeg,并在音视频处理领域取得更大的成就。
当然,FFmpeg 的内容远不止这些,还有滤镜、设备访问等高级功能等待你去探索。希望你能以此为起点,不断学习和实践,最终成为 FFmpeg 的专家。 记住,实践是检验真理的唯一标准,多动手,多尝试,你一定能掌握 FFmpeg 的强大功能!