WEBKT

FFmpeg深度剖析:解封装、解码、编码与封装的工作原理

86 0 0 0

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 解封装器的工作流程

解封装器的工作流程大致如下:

  1. 打开输入文件: 解封装器首先需要打开指定的音视频文件。
  2. 读取文件头: 解封装器读取文件头,从中获取封装格式的信息,例如音视频流的数量、编码方式等。
  3. 解析数据包: 解封装器根据封装格式的规范,解析文件中的数据包(Packet)。每个数据包通常包含一部分音视频数据。
  4. 提取音视频流: 解封装器将数据包中的音视频数据分离出来,形成独立的音视频流。
  5. 输出音视频流: 解封装器将提取出的音视频流输出给解码器进行处理。

2.3 FFmpeg 中的解封装器

FFmpeg 的 libavformat 库提供了大量的解封装器,支持各种常见的音视频封装格式。例如,mp4 demuxer 用于解封装 MP4 文件,mkv demuxer 用于解封装 MKV 文件。

要使用 FFmpeg 的解封装器,你需要:

  1. 注册解封装器: 在程序开始时,需要调用 av_register_all() 函数注册所有的解封装器。
  2. 打开输入文件: 使用 avformat_open_input() 函数打开输入文件,并指定要使用的解封装器(如果需要)。
  3. 读取文件信息: 使用 avformat_find_stream_info() 函数读取文件信息,包括音视频流的数量、编码方式等。
  4. 读取数据包: 使用 av_read_frame() 函数读取数据包。
  5. 处理数据包: 将数据包发送给解码器进行处理。
  6. 关闭输入文件: 在程序结束时,需要调用 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 解码器的工作流程

解码器的工作流程大致如下:

  1. 接收数据包: 解码器从解封装器接收包含压缩音视频数据的数据包。
  2. 解析数据包: 解码器根据编解码器的规范,解析数据包中的数据,提取出编码后的音视频数据。
  3. 解码音视频数据: 解码器使用相应的解码算法,将编码后的音视频数据还原为原始的音视频数据。
  4. 输出原始数据: 解码器将解码后的原始音视频数据输出,以便后续的处理,例如显示、编码或存储。

3.3 FFmpeg 中的解码器

FFmpeg 的 libavcodec 库提供了大量的解码器,支持各种常见的音视频编解码器。例如,h264 decoder 用于解码 H.264 编码的视频流,aac decoder 用于解码 AAC 编码的音频流。

要使用 FFmpeg 的解码器,你需要:

  1. 找到解码器: 使用 avcodec_find_decoder() 函数根据编码器的 ID 找到对应的解码器。
  2. 创建解码器上下文: 使用 avcodec_alloc_context3() 函数创建解码器上下文。
  3. 配置解码器上下文: 将解封装器获取到的编码器信息(例如视频的宽度、高度、帧率等)配置到解码器上下文中。
  4. 打开解码器: 使用 avcodec_open2() 函数打开解码器,并指定要使用的解码器上下文。
  5. 解码数据包: 使用 avcodec_decode_video2()avcodec_decode_audio4() 函数解码数据包。
  6. 处理解码后的数据: 对解码后的音视频数据进行进一步的处理,例如显示、编码或存储。
  7. 关闭解码器: 在程序结束时,需要调用 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 编码器的工作流程

编码器的工作流程大致如下:

  1. 接收原始数据: 编码器接收原始的音视频数据,例如 YUV 格式的图像数据或 PCM 格式的音频数据。
  2. 分析原始数据: 编码器分析原始数据,例如图像的运动矢量、音频的频率等。
  3. 编码音视频数据: 编码器使用相应的编码算法,将原始的音视频数据压缩成特定的格式。
  4. 输出编码后的数据: 编码器将编码后的音视频数据输出,以便后续的处理,例如封装或传输。

4.2 FFmpeg 中的编码器

FFmpeg 的 libavcodec 库提供了大量的编码器,支持各种常见的音视频编解码器。例如,libx264 encoder 用于编码 H.264 编码的视频流,libfdk_aac encoder 用于编码 AAC 编码的音频流。

要使用 FFmpeg 的编码器,你需要:

  1. 找到编码器: 使用 avcodec_find_encoder() 函数根据编码器的 ID 找到对应的编码器。
  2. 创建编码器上下文: 使用 avcodec_alloc_context3() 函数创建编码器上下文。
  3. 配置编码器上下文: 设置编码器上下文的参数,例如视频的宽度、高度、帧率、码率等。
  4. 打开编码器: 使用 avcodec_open2() 函数打开编码器,并指定要使用的编码器上下文。
  5. 编码数据: 使用 avcodec_encode_video2()avcodec_encode_audio2() 函数编码音视频数据。
  6. 接收编码后的数据: 从编码器接收编码后的数据包。
  7. 关闭编码器: 在程序结束时,需要调用 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 封装器的工作流程

封装器的工作流程大致如下:

  1. 接收音视频流: 封装器接收编码器输出的音视频流。
  2. 创建文件头: 封装器根据封装格式的规范,创建文件头,用于描述音视频流的信息,例如编码方式、分辨率、帧率等。
  3. 打包数据: 封装器将音视频数据按照封装格式的规范,打包成数据块(Chunk)或数据包(Packet)。
  4. 写入文件: 封装器将文件头和数据块写入到输出文件中。
  5. 创建文件尾: 封装器根据封装格式的规范,创建文件尾,用于标识文件的结束。
  6. 写入文件尾: 封装器将文件尾写入到输出文件中。

5.2 FFmpeg 中的封装器

FFmpeg 的 libavformat 库提供了大量的封装器,支持各种常见的音视频封装格式。例如,mp4 muxer 用于封装 MP4 文件,mkv muxer 用于封装 MKV 文件。

要使用 FFmpeg 的封装器,你需要:

  1. 创建输出格式上下文: 使用 avformat_alloc_output_context2() 函数创建输出格式上下文,并指定要使用的封装格式。
  2. 创建音视频流: 使用 avformat_new_stream() 函数创建音视频流,并将编码器的信息(例如编码方式、分辨率、帧率等)配置到音视频流中。
  3. 打开输出文件: 使用 avio_open() 函数打开输出文件。
  4. 写入文件头: 使用 avformat_write_header() 函数写入文件头。
  5. 写入数据包: 使用 av_interleaved_write_frame() 函数写入数据包。
  6. 写入文件尾: 使用 av_write_trailer() 函数写入文件尾。
  7. 关闭输出文件: 使用 avio_close() 函数关闭输出文件。

5.3 示例代码

上面的编码示例代码已经包含了封装的过程,这里不再重复。

6. 总结

本文深入剖析了 FFmpeg 的核心模块,包括解封装、解码、编码和封装的工作原理。理解这些原理对于更好地使用 FFmpeg 进行音视频处理至关重要。希望本文能够帮助你更好地理解和应用 FFmpeg,并在音视频处理领域取得更大的成就。

当然,FFmpeg 的内容远不止这些,还有滤镜、设备访问等高级功能等待你去探索。希望你能以此为起点,不断学习和实践,最终成为 FFmpeg 的专家。 记住,实践是检验真理的唯一标准,多动手,多尝试,你一定能掌握 FFmpeg 的强大功能!

音视频探索者 FFmpeg音视频处理编解码

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9518