C++ FFmpeg音视频录制实战:从设备采集到MP4封装
2026/7/18 4:51:36 网站建设 项目流程

1. 项目概述:为什么需要自己动手实现音视频录制?

在音视频应用开发中,录制功能是基石。无论是视频会议、在线教育、游戏直播还是安防监控,最终都需要将采集到的音视频数据持久化保存。市面上有很多现成的库和工具,比如直接调用系统API或者使用一些高级框架,但当你需要对录制过程进行精细控制——比如自定义编码参数、实现低延迟的同步、处理复杂的音画同步逻辑,或者将录制模块深度集成到你的C++应用中时,自己动手基于FFmpeg实现一套录制流程就变得至关重要。

FFmpeg作为音视频领域的“瑞士军刀”,其libavcodec、libavformat等库提供了底层、强大且灵活的控制能力。通过C++直接调用这些库,你可以从驱动层拿到原始的音频采样和视频帧数据,然后按照你的意愿进行编码、复用(封装),最终生成一个标准的MP4文件。这个过程听起来复杂,但拆解开来,无非是几个核心环节的串联:设备采集、数据编码、音视频同步、封装写入。本教程的目标,就是带你一步步走通这个流程,让你不仅知道怎么调用API,更理解每一步背后的原理和设计考量,最终实现一个稳定、高效的同步音视频录制器。

2. 环境准备与FFmpeg库集成

在开始写代码之前,搭建一个顺手的开发环境是第一步。对于C++项目,我强烈推荐使用CMake进行项目管理,它能很好地处理库依赖和跨平台编译。

2.1 获取与编译FFmpeg库

首先,你需要FFmpeg的开发库(头文件和链接库)。有两种主流方式:

  1. 使用包管理器(推荐给新手快速上手):在Ubuntu上,你可以通过sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev来安装。在macOS上,使用brew install ffmpeg。这种方式最省心,但库的版本可能不是最新的。
  2. 从源码编译(推荐给需要特定配置或深度定制的开发者):从FFmpeg官网下载源码,编译时可以裁剪掉不需要的组件,减少最终二进制体积。一个典型的配置命令如下:
    ./configure --prefix=/usr/local/ffmpeg --enable-shared --disable-static --enable-gpl --enable-nonfree --enable-libx264 --enable-libfdk-aac make -j$(nproc) sudo make install

    注意:--enable-libx264--enable-libfdk-aac需要你先安装对应的编码器库。编译完成后,库文件通常在/usr/local/ffmpeg/lib,头文件在/usr/local/ffmpeg/include

我个人更倾向于源码编译,因为你可以精确控制支持的编码格式和功能模块。例如,在嵌入式环境或对安装包体积敏感的场景下,裁剪掉不需要的解码器、滤镜和协议支持,可以显著减小依赖库的大小。

2.2 CMake项目配置

假设你的项目目录结构如下:

MyRecorder/ ├── CMakeLists.txt ├── include/ ├── src/ │ └── main.cpp └── third_party/ # 存放FFmpeg库文件(如果使用静态链接或自定义路径)

你的CMakeLists.txt需要正确找到FFmpeg。这里演示如何查找系统安装的FFmpeg:

cmake_minimum_required(VERSION 3.10) project(AVRecorder) set(CMAKE_CXX_STANDARD 11) # 查找FFmpeg的各个组件 find_package(PkgConfig REQUIRED) pkg_check_modules(AVCODEC REQUIRED libavcodec) pkg_check_modules(AVFORMAT REQUIRED libavformat) pkg_check_modules(AVUTIL REQUIRED libavutil) pkg_check_modules(SWSCALE REQUIRED libswscale) # 如果需要重采样音频,可能还需要 libswresample # pkg_check_modules(SWRESAMPLE REQUIRED libswresample) include_directories(${AVCODEC_INCLUDE_DIRS} ${AVFORMAT_INCLUDE_DIRS} ${AVUTIL_INCLUDE_DIRS} ${SWSCALE_INCLUDE_DIRS}) add_executable(av_recorder src/main.cpp) target_link_libraries(av_recorder ${AVCODEC_LIBRARIES} ${AVFORMAT_LIBRARIES} ${AVUTIL_LIBRARIES} ${SWSCALE_LIBRARIES})

如果FFmpeg安装在自定义路径,可以使用find_libraryfind_path手动指定。配置好CMake后,在构建目录执行cmake .. && make,应该就能顺利生成可执行文件了。这一步的常见坑点是库路径不对或版本不匹配,务必确保pkg-config能找到正确的.pc文件。

3. 核心流程设计与数据结构初始化

一个完整的录制流程,可以抽象为一条数据处理管线。我们需要先设计好这条管线的各个节点,并初始化对应的FFmpeg数据结构。

3.1 录制流程总览

整个程序的核心流程可以概括为以下几步:

  1. 初始化:打开音视频输入设备(如摄像头、麦克风),创建输出文件上下文,并为其配置音视频流。
  2. 采集循环:在一个主循环中,分别从音频设备和视频设备读取数据包(AVPacket)。
  3. 编码与写入:将读取到的原始数据包送入对应的编码器(AVCodecContext),将编码后的数据包写入输出文件,并处理好音视频包的时间戳(PTS/DTS)以实现同步。
  4. 收尾工作:跳出循环后,刷新编码器缓冲区中可能残留的数据,写入文件尾,然后释放所有资源。

这个流程的关键在于第三步的“同步”。音视频数据是独立采集的,它们有各自的时间线(采样时间、帧时间)。我们必须将它们映射到同一个时间基准(输出文件的time_base)上,否则播放时就会音画不同步。

3.2 关键数据结构解析

FFmpeg的操作围绕几个核心结构体展开,理解它们是编码的前提:

  • AVFormatContext:格式上下文。对于输入,它代表一个多媒体文件或设备流;对于输出,它代表你要生成的文件。它是整个操作的“总管”。
  • AVCodecContext:编解码器上下文。它持有某个特定音视频流的编解码参数、状态信息。你需要为音频流和视频流分别创建。
  • AVStream:流。一个媒体文件(如MP4)中可以包含多条流(如一条视频流、一条音频流)。AVStream包含了这条流的元信息(编码格式、时间基、帧率等)。
  • AVPacket:数据包。编码前或解码后的数据基本单位。对于原始设备采集,我们通常拿到的是未编码的AVPacket;对于编码后的数据,也是以AVPacket的形式传递。
  • AVFrame:帧。未编码的原始音视频数据单位。一个视频帧(YUV数据),或一段音频采样(PCM数据)。编码器吃进去的是AVFrame,吐出来的是AVPacket

在录制场景中,我们从设备(如v4l2dshow)采集到的数据,FFmpeg通常已经封装成了AVPacket(但内容是未压缩的)。我们的主要工作是将这些AVPacket送入编码器,再将编码后的AVPacket写入输出文件。

4. 输入设备采集与输出文件初始化

4.1 打开音视频输入设备

FFmpeg通过avformat_open_input函数打开输入源,这个输入源可以是文件URL,也可以是设备标识符。在Linux上,视频设备通常是/dev/video0,音频设备可能是hw:0。在Windows上,视频设备可能是video=“Integrated Camera”,音频设备是audio=“Microphone Array”。为了通用性,我们使用FFmpeg的设备抽象格式:

  • 视频:-f dshow -i video=“摄像头名称”(Windows) 或-f v4l2 -i /dev/video0(Linux)
  • 音频:-f dshow -i audio=“麦克风名称”(Windows) 或-f alsa -i hw:0(Linux)

在代码中,我们分别打开两个独立的AVFormatContext

AVFormatContext* video_input_ctx = nullptr; AVFormatContext* audio_input_ctx = nullptr; // 打开视频设备 const char* video_dev_name = “video=Integrated Camera”; // Windows示例 AVDictionary* video_options = nullptr; av_dict_set(&video_options, “framerate”, “30”, 0); // 设置期望帧率 if (avformat_open_input(&video_input_ctx, video_dev_name, av_find_input_format(“dshow”), &video_options) < 0) { // 处理错误 } av_dict_free(&video_options); // 打开音频设备 const char* audio_dev_name = “audio=Microphone Array”; AVDictionary* audio_options = nullptr; av_dict_set(&audio_options, “sample_rate”, “44100”, 0); // 设置采样率 if (avformat_open_input(&audio_input_ctx, audio_dev_name, av_find_input_format(“dshow”), &audio_options) < 0) { // 处理错误 } av_dict_free(&audio_options);

实操心得:设备名称的获取是个小坑。在Windows上,你可以先通过ffmpeg -list_devices true -f dshow -i dummy命令列出所有可用的音视频设备。在代码中处理这些字符串时要注意转义和编码。另外,avformat_open_input的最后一个参数是选项字典,这里可以传递很多设备特有的参数,比如分辨率、像素格式、音频通道数等,如果设备支持,FFmpeg会尝试按此配置打开。

4.2 创建输出文件与配置流

接下来,我们创建输出文件的上下文,并为其添加一条视频流和一条音频流。

AVFormatContext* output_ctx = nullptr; avformat_alloc_output_context2(&output_ctx, nullptr, “mp4”, “output.mp4”); if (!output_ctx) { // 通常是因为不支持mp4格式,可以尝试“mov”或“mkv” } // 创建视频输出流 AVStream* video_stream = avformat_new_stream(output_ctx, nullptr); video_stream->id = output_ctx->nb_streams - 1; // 创建音频输出流 AVStream* audio_stream = avformat_new_stream(output_ctx, nullptr); audio_stream->id = output_ctx->nb_streams - 1;

现在有了空的流,我们需要为它们配置编码器。这里我们选择最通用的H.264编码视频和AAC编码音频。

// 查找视频编码器 const AVCodec* video_codec = avcodec_find_encoder_by_name(“libx264”); if (!video_codec) video_codec = avcodec_find_encoder(AV_CODEC_ID_H264); AVCodecContext* video_codec_ctx = avcodec_alloc_context3(video_codec); // 配置视频编码参数 video_codec_ctx->height = 720; video_codec_ctx->width = 1280; video_codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; // H.264最兼容的格式 video_codec_ctx->time_base = {1, 30}; // 假设帧率30fps,time_base = 1/帧率 video_codec_ctx->framerate = {30, 1}; video_codec_ctx->bit_rate = 2500000; // 2.5 Mbps video_codec_ctx->gop_size = 30; // 关键帧间隔 // 一些针对MP4和快速启动的优化设置 video_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // MP4需要全局头 // 将编码器上下文与输出流关联 if (avcodec_parameters_from_context(video_stream->codecpar, video_codec_ctx) < 0) { // 处理错误 } // 音频编码器配置类似 const AVCodec* audio_codec = avcodec_find_encoder_by_name(“aac”); AVCodecContext* audio_codec_ctx = avcodec_alloc_context3(audio_codec); audio_codec_ctx->sample_rate = 44100; audio_codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO; audio_codec_ctx->channels = 2; audio_codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP; // AAC编码常用格式 audio_codec_ctx->time_base = {1, audio_codec_ctx->sample_rate}; audio_codec_ctx->bit_rate = 128000; audio_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; if (avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_ctx) < 0) { // 处理错误 }

配置好编码器上下文后,需要打开编码器,并打开输出文件进行写入。

// 打开视频编码器 if (avcodec_open2(video_codec_ctx, video_codec, nullptr) < 0) { // 处理错误 } // 打开音频编码器 if (avcodec_open2(audio_codec_ctx, audio_codec, nullptr) < 0) { // 处理错误 } // 写入文件头 if (avformat_write_header(output_ctx, nullptr) < 0) { // 处理错误 }

注意事项:AV_CODEC_FLAG_GLOBAL_HEADER这个标志非常重要。对于MP4、MOV这类容器格式,编码器的配置信息(如SPS/PPS for H.264)需要放在文件头部,而不是每个关键帧里。设置这个标志后,avcodec_open2之后可以通过output_ctx->streams[i]->codecpar->extradata获取到这些全局头信息,并在写文件头时写入。如果不设置,生成的MP4文件可能无法被某些播放器识别。

5. 音视频数据采集、编码与同步写入

这是整个程序的核心循环。我们需要同时从两个设备读取数据,编码,并以正确的时间戳写入同一个文件。这里最大的挑战是同步

5.1 主循环结构与数据读取

我们使用一个while循环,持续从设备读取数据包。由于音视频是两个独立的输入源,我们需要管理两个读取过程。一种简单且有效的方法是使用av_read_frame的非阻塞模式,或者使用多线程。为了简化,我们先采用轮询的方式,并引入一个简单的“时钟”概念来控制写入节奏。

AVPacket* video_pkt = av_packet_alloc(); AVPacket* audio_pkt = av_packet_alloc(); int64_t video_pts = 0; int64_t audio_pts = 0; // 获取系统启动后的相对时间,作为参考时钟 auto start_time = std::chrono::steady_clock::now(); while (!stop_recording) { // 1. 尝试从视频设备读取一帧 if (av_read_frame(video_input_ctx, video_pkt) >= 0) { // 处理视频包... } // 2. 尝试从音频设备读取一包 if (av_read_frame(audio_input_ctx, audio_pkt) >= 0) { // 处理音频包... } // 短暂休眠以避免CPU空转 std::this_thread::sleep_for(std::chrono::milliseconds(1)); }

从设备读取到的AVPacket,其pts(Presentation Time Stamp, 显示时间戳) 和dts(Decoding Time Stamp, 解码时间戳) 通常是以设备时间基为单位的。我们需要将它们转换到编码器的时间基,最后再转换到输出流的时间基。

5.2 时间戳的转换与同步策略

同步的本质是让音频和视频按照一个统一的时间轴来呈现。我们选择音频作为主时钟,因为人对音频的断续更敏感。策略如下:

  1. 计算每个视频帧“应该”被呈现的时刻(基于帧率)。
  2. 计算每个音频包“应该”被呈现的时刻(基于采样率和采样数)。
  3. 在写入时,确保视频帧的pts不落后于音频pts太多,也不超前太多。如果视频快了,就稍作等待或丢弃一些非关键帧;如果视频慢了,则可能需要追赶(在录制场景下,更常见的处理是尽力写入,后期处理时再处理同步问题,或者允许轻微的音频领先)。

时间戳转换是同步的关键:

// 假设 video_pkt 从设备读取,其时间基是 video_input_ctx->streams[0]->time_base // 目标时间基是 video_codec_ctx->time_base (编码器时间基) 和 video_stream->time_base (输出流时间基) // 第一步:将设备pts转换为编码器时间基 AVRational src_time_base = video_input_ctx->streams[video_pkt->stream_index]->time_base; AVRational dst_time_base = video_codec_ctx->time_base; video_pkt->pts = av_rescale_q(video_pkt->pts, src_time_base, dst_time_base); video_pkt->dts = video_pkt->pts; // 对于未编码的原始流,通常dts=pts video_pkt->duration = av_rescale_q(video_pkt->duration, src_time_base, dst_time_base); // 第二步:将编码后的包的pts,从编码器时间基转换到输出流时间基 // 这一步通常在编码后,写入前进行(见下一小节)

对于音频,转换逻辑类似,但要注意音频的duration是以采样数为单位的,转换时间基时需要使用av_rescale_q

5.3 编码与写入文件

读取到的原始AVPacket需要送入编码器。FFmpeg的编码API通常需要配合AVFrame使用,但当我们从某些设备(如dshow,v4l2)以原始格式(如rgb24,yuyv422)读取时,拿到的是未编码的AVPacket。这时,我们需要先将AVPacket转换为AVFrame,然后再编码。不过,更简单的方式是让FFmpeg在打开输入设备时就直接输出编码后的数据(如果设备支持硬件编码),但这会丧失灵活性。本教程假设我们进行软件编码,因此需要转换。

由于篇幅,这里省略了AVPacketAVFrame的转换细节(涉及sws_scale用于视频,swr_convert用于音频重采样)。我们聚焦在编码和写入的逻辑上。

// 假设我们已经有了一个准备好数据的 video_frame (AVFrame*) // 发送帧到编码器 if (avcodec_send_frame(video_codec_ctx, video_frame) < 0) { // 发送失败处理 } // 从编码器接收编码后的包 while (true) { AVPacket encoded_pkt; av_init_packet(&encoded_pkt); encoded_pkt.data = nullptr; encoded_pkt.size = 0; int ret = avcodec_receive_packet(video_codec_ctx, &encoded_pkt); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { // EAGAIN: 编码器需要更多输入帧 // EOF: 编码器已刷新,没有更多输出 av_packet_unref(&encoded_pkt); break; } else if (ret < 0) { // 其他错误 av_packet_unref(&encoded_pkt); break; } // 编码成功,现在处理 encoded_pkt // 关键步骤:将包的时间戳从编码器时间基转换到输出流时间基 encoded_pkt.pts = av_rescale_q(encoded_pkt.pts, video_codec_ctx->time_base, video_stream->time_base); encoded_pkt.dts = av_rescale_q(encoded_pkt.dts, video_codec_ctx->time_base, video_stream->time_base); encoded_pkt.duration = av_rescale_q(encoded_pkt.duration, video_codec_ctx->time_base, video_stream->time_base); encoded_pkt.stream_index = video_stream->index; // 写入文件 if (av_interleaved_write_frame(output_ctx, &encoded_pkt) < 0) { // 写入失败处理 } av_packet_unref(&encoded_pkt); }

音频的编码和写入流程完全类似,只是编码器上下文和流对象不同。av_interleaved_write_frame函数会负责根据包的时间戳,将音频包和视频包交错写入文件,这有助于播放器的缓冲和 seeking。

6. 资源释放与文件收尾

当停止录制时,不能直接关闭文件,因为编码器内部可能还有缓存的帧(尤其是B帧相关的延迟)。我们需要“刷新”编码器。

// 停止主循环后... // 1. 刷新视频编码器 avcodec_send_frame(video_codec_ctx, nullptr); // 发送nullptr表示刷新 while (true) { AVPacket pkt; av_init_packet(&pkt); int ret = avcodec_receive_packet(video_codec_ctx, &pkt); if (ret == AVERROR_EOF) { break; } else if (ret < 0) { break; } // 转换时间戳并写入 pkt.pts = av_rescale_q(pkt.pts, video_codec_ctx->time_base, video_stream->time_base); pkt.dts = av_rescale_q(pkt.dts, video_codec_ctx->time_base, video_stream->time_base); pkt.stream_index = video_stream->index; av_interleaved_write_frame(output_ctx, &pkt); av_packet_unref(&pkt); } // 2. 刷新音频编码器(同上,略) // 3. 写入文件尾 av_write_trailer(output_ctx); // 4. 关闭输出文件 if (output_ctx && !(output_ctx->oformat->flags & AVFMT_NOFILE)) { avio_closep(&output_ctx->pb); } // 5. 释放所有资源 avformat_free_context(output_ctx); avcodec_free_context(&video_codec_ctx); avcodec_free_context(&audio_codec_ctx); avformat_close_input(&video_input_ctx); avformat_close_input(&audio_input_ctx); av_packet_free(&video_pkt); av_packet_free(&audio_pkt); // 释放AVFrame等其它资源...

实操心得:资源泄露是C++音视频编程中最常见的问题之一。务必确保每一个av_allocav_mallocavcodec_alloc_context3avformat_alloc_output_context2等分配函数,都有对应的释放函数(av_freeavcodec_free_contextavformat_free_context)。使用valgrind或 AddressSanitizer 工具进行内存检查是非常好的习惯。另外,av_packet_unrefav_frame_unref对于重复使用的包和帧至关重要,它们会减少内部缓冲区的引用计数,避免内存泄漏。

7. 常见问题排查与性能优化

在实际编码中,你肯定会遇到各种问题。这里记录几个我踩过的坑和解决方案。

7.1 编译与链接问题

  • 问题undefined reference toavcodec_open2‘` 等链接错误。
    • 排查:这通常是链接库顺序不对或库缺失。确保CMake中target_link_libraries包含了所有必要的FFmpeg库(avcodec,avformat,avutil,swscale等),并且顺序正确(一般基础库如avutil放在后面)。使用pkg-config --libs libavcodec命令检查正确的链接参数。
  • 问题:运行时Failed to open ‘/dev/video0’: No such file or directory
    • 排查:检查设备路径是否正确,当前用户是否有权限访问该设备(如video组)。在Windows上,检查设备名称字符串是否准确,是否包含中文或特殊字符需要处理。

7.2 运行时问题

  • 问题:生成的MP4文件无法播放,或播放器显示“编码器不支持”。
    • 排查
      1. 检查是否写入了文件头 (avformat_write_header) 和文件尾 (av_write_trailer)。
      2. 检查编码器全局头 (AV_CODEC_FLAG_GLOBAL_HEADER) 是否设置,并且extradata是否正确写入流参数。
      3. ffprobe output.mp4命令查看文件信息,确认流是否存在,编码格式是否正确。
      4. 尝试用FFplay播放 (ffplay output.mp4),它的错误信息通常更详细。
  • 问题:音画不同步,视频越来越慢或越来越快。
    • 排查:这是时间戳处理错误的最直接表现。
      1. 检查时间基转换:确保每一步的av_rescale_q使用的源时间基和目标时间基是正确的。打印出关键阶段的pts值进行对比。
      2. 检查输入帧率/采样率:确认从设备读取的帧率是否稳定,是否与你设置的编码器帧率匹配。如果设备实际输出15帧,而你按30帧去给pts,视频播放速度就会变快一倍。可以使用av_dict_set在打开设备时指定帧率。
      3. 主时钟选择:在简单的录制程序中,可以以系统运行时间作为主时钟。计算每一帧“应该”出现的时间,如果实际编码写入时间晚于这个时间,可以考虑在下一帧追赶,或者记录一个偏移量。对于高精度同步,需要更复杂的时钟同步算法。

7.3 性能优化点

  1. 采集线程分离:将音频采集和视频采集放在两个独立的线程中,可以避免其中一个设备阻塞导致另一个设备数据丢失。使用线程安全的队列将采集到的数据包传递给主编码/写入线程。
  2. 编码参数调优libx264编码器有丰富的参数。preset(编码速度与质量权衡)和crf(恒定质量因子)是最常用的两个。preset ultrafast编码最快但体积大质量低,preset slower则相反。crf值越小质量越高(通常18-28是合理范围)。根据你的应用场景(实时性要求高还是文件体积要求高)进行调整。
  3. 零拷贝优化:在AVPacketAVFrame的转换中,如果像素格式一致,可以尝试引用数据而不是拷贝数据。使用av_frame_ref或设置AVFramedata指针指向AVPacket的数据区,但这需要你对FFmpeg的内存管理有深刻理解,否则极易导致崩溃。
  4. 硬件加速:如果条件允许,使用硬件编码(如NVIDIA的NVENC,Intel的QSV)可以极大降低CPU占用。这需要FFmpeg编译时支持对应的硬件编码器,并在创建编码器时指定(如h264_nvenc)。代码逻辑与软件编码基本一致。

8. 功能扩展与进阶思路

一个基础的录制器完成后,你可以根据需求添加更多功能:

  • 动态比特率控制:根据网络状况或CPU使用率,动态调整video_codec_ctx->bit_rate。注意,改变比特率后可能需要重新发送编码器参数集。
  • 实时预览:在录制的同时,使用SDL或OpenGL将视频帧渲染到窗口上,将音频通过扬声器播放出来。
  • 分段录制:当文件大小或录制时长达到阈值时,自动关闭当前文件,创建新文件继续录制。需要处理好文件尾和文件头的写入,以及时间戳的连续性(新的文件pts可以从0开始,或累积上一个文件的总时长)。
  • 添加水印或滤镜:使用libavfilter库,可以在编码前轻松地为视频添加文字、图片水印,或者进行色彩转换、缩放等操作。你需要构建一个FilterGraph,将采集到的AVFrame送入滤镜图,再从另一端取出处理后的AVFrame进行编码。
  • 推流:将输出上下文 (AVFormatContext) 的目标从文件改为RTMP/RTSP等流媒体服务器地址,并配置相应的协议参数,你就实现了一个直播推流客户端。

音视频编程就像搭积木,FFmpeg提供了几乎所有你需要的积木块。理解每个核心数据结构的作用,掌握数据流(AVPacket/AVFrame)和时间戳的转换脉络,你就能组合出功能强大的应用。最开始可能会被复杂的API和神秘的时间戳困扰,但多写、多调试、多读FFmpeg示例代码(doc/examples目录下),你会逐渐建立起直觉。遇到问题,别忘了ffmpeg命令行工具本身就是一个最好的参考实现,用-loglevel debug参数运行你的命令,观察它的内部流程,往往能给你带来启发。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询