自动化多段视频删除:FFmpeg.AutoGen 与 C# 的完整实现
一、功能概述
通过 FFmpeg.AutoGen
库实现视频剪辑功能,支持删除多个指定时间段(如 [8,10]
和 [15,22]
),并重新生成连续时间戳。核心逻辑如下:
ffmpeg -i input.mp4 -vf "select='not(between(t,{8},{10}) + between(t,{15},{22}))',setpts=N/29/TB" -af "aselect='not(between(t,{8},{10}) + between(t,{15},{22}))',asetpts=N/44100/TB" -c:a aac -b:a 128k output.mp4
关键参数说明
setpts
中的29
:视频帧率(通过ffprobe
获取)asetpts
中的44100
:音频采样率(通过ffprobe
获取)
二、环境配置
-
FFmpeg 库
下载 FFmpeg 6.1.1 Shared Build,将 DLL 文件放入项目ffmpeg
目录,并设置为“始终复制”。 -
安装 NuGet 包
Install-Package FFmpeg.AutoGen -Version 6.1.0.1
三、核心处理流程
OpenInputFile(); //初始化输入上下文、解码上下文
OpenOutputFile(); //初始化输出上下文,编码上下文
InitVideoFilterGraph();//构建视频滤镜
InitAudioFilterGraph();//构建音频滤镜
ProcessFrames(); //媒体资源帧处理
WriteTrailer(); //文件结尾信息写入
四、关键函数实现
1. 输入文件初始化 (OpenInputFile
)
private unsafe void OpenInputFile()
{AVCodec* dec;AVCodec* audioCodec;fixed (AVFormatContext** formatContextPtr = &_inputFormatContext){ffmpeg.avformat_open_input(formatContextPtr, _inputPath, null, null).ThrowExceptionIfError();}// 获取资源信息ffmpeg.avformat_find_stream_info(_inputFormatContext, null).ThrowExceptionIfError();// 查找视频流_videoStreamIndex = ffmpeg.av_find_best_stream(_inputFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &dec, 0).ThrowExceptionIfError();// 查找音频流_audioStreamIndex = ffmpeg.av_find_best_stream(_inputFormatContext, AVMediaType.AVMEDIA_TYPE_AUDIO, -1, -1, &audioCodec, 0);if (_videoStreamIndex == -1 && _audioStreamIndex == -1)throw new Exception("未找到视频或音频流");AVStream* stream = _inputFormatContext->streams[_videoStreamIndex];_videoFrameRate = (double)stream->avg_frame_rate.num / stream->avg_frame_rate.den;stream = _inputFormatContext->streams[_audioStreamIndex];_audioSampleRate = stream->codecpar->sample_rate;// 创建视频解码器上下文_videoDeCodecContext = CreateAvCodecContext(dec, _videoStreamIndex);// 创建音频上下文_audioDeCodecContext = CreateAvCodecContext(audioCodec, _audioStreamIndex);}
2. 输出文件初始化 (OpenOutputFile
)
private unsafe void OpenOutputFile(){// 创建输出格式上下文fixed (AVFormatContext** formatContextPtr = &_outputFormatContext){ffmpeg.avformat_alloc_output_context2(formatContextPtr, null, null, _outputPath).ThrowExceptionIfError();}// 添加视频流(如果存在)if (_videoStreamIndex != -1){_videoCodecContext = CreateAVCodecContext(_videoStreamIndex);}// 添加音频流(如果存在)if (_audioStreamIndex != -1){_audioCodecContext = CreateAVCodecContext(_audioStreamIndex);}// 打开输出文件if ((_outputFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0){ffmpeg.avio_open(&_outputFormatContext->pb, _outputPath, ffmpeg.AVIO_FLAG_WRITE).ThrowExceptionIfError();}// 写入文件头ffmpeg.avformat_write_header(_outputFormatContext, null).ThrowExceptionIfError();}
3. 视频滤镜链构建 (InitVideoFilterGraph
)
private unsafe void InitVideoFilterGraph(){int ret = 0;if (_videoStreamIndex == -1) return;// 创建滤镜图_videoFilterGraph = ffmpeg.avfilter_graph_alloc();if (_videoFilterGraph == null) throw new Exception("无法创建视频滤镜图");// 获取输入流AVStream* stream = _inputFormatContext->streams[_videoStreamIndex];AVCodecParameters* codecpar = stream->codecpar;AVRational timeBase = stream->time_base;// 创建 buffer sourcestring bufferSrcArgs = $"video_size={codecpar->width}x{codecpar->height}:pix_fmt={codecpar->format}:time_base={timeBase.num}/{timeBase.den}";AVFilter* bufferSrc = ffmpeg.avfilter_get_by_name("buffer");fixed (AVFilterContext** fc = &_videoBufferSrcContext){ret = ffmpeg.avfilter_graph_create_filter(fc, bufferSrc, "in", bufferSrcArgs, null, _videoFilterGraph).ThrowExceptionIfError();}// 创建 buffer sinkAVFilter* bufferSink = ffmpeg.avfilter_get_by_name("buffersink");fixed (AVFilterContext** fc = &_videoBufferSinkContext){ret = ffmpeg.avfilter_graph_create_filter(fc, bufferSink, "out", null, null, _videoFilterGraph).ThrowExceptionIfError();}// 构建视频滤镜链string filterSpec = BuildVideoFilterSpec();//System.Diagnostics.Debug.WriteLine(filterSpec);AVFilterInOut* outputs = ffmpeg.avfilter_inout_alloc();AVFilterInOut* inputs = ffmpeg.avfilter_inout_alloc();outputs->name = ffmpeg.av_strdup("in");outputs->filter_ctx = _videoBufferSrcContext;outputs->pad_idx = 0;outputs->next = null;inputs->name = ffmpeg.av_strdup("out");inputs->filter_ctx = _videoBufferSinkContext;inputs->pad_idx = 0;inputs->next = null;ret = ffmpeg.avfilter_graph_parse_ptr(_videoFilterGraph, filterSpec, &inputs, &outputs, null);if (ret < 0){ffmpeg.avfilter_inout_free(&inputs);ffmpeg.avfilter_inout_free(&outputs);throw new Exception($"无法解析视频滤镜链: {GetErrorString(ret)}");}ret = ffmpeg.avfilter_graph_config(_videoFilterGraph, null);if (ret < 0){ffmpeg.avfilter_inout_free(&inputs);ffmpeg.avfilter_inout_free(&outputs);throw new Exception($"无法配置视频滤镜图: {GetErrorString(ret)}");}ffmpeg.avfilter_inout_free(&inputs);ffmpeg.avfilter_inout_free(&outputs);}
4. 音频滤镜链构建 (InitAudioFilterGraph
)
private unsafe void InitAudioFilterGraph()
{int ret = 0;if (_audioStreamIndex == -1) return;// 创建滤镜图_audioFilterGraph = ffmpeg.avfilter_graph_alloc();if (_audioFilterGraph == null) throw new Exception("无法创建音频滤镜图");// 获取输入流AVStream* stream = _inputFormatContext->streams[_audioStreamIndex];AVCodecParameters* codecpar = stream->codecpar;AVRational timeBase = stream->time_base;// 获取声道数string channelLayout = FFmpegHelper.GetChannelLayoutString(codecpar->ch_layout);// 创建 buffer sourcestring bufferSrcArgs = $"sample_rate={codecpar->sample_rate}" +$":sample_fmt={FFmpegHelper.GetSampleFormatName((AVSampleFormat)codecpar->format)}" +$":time_base={timeBase.num}/{timeBase.den}" +$":channel_layout={channelLayout}";System.Diagnostics.Debug.WriteLine(bufferSrcArgs);AVFilter* bufferSrc = ffmpeg.avfilter_get_by_name("abuffer");fixed (AVFilterContext** fc = &_audioBufferSrcContext){ret = ffmpeg.avfilter_graph_create_filter(fc, bufferSrc, "in", bufferSrcArgs, null, _audioFilterGraph).ThrowExceptionIfError();}// 创建 buffer sinkAVFilter* bufferSink = ffmpeg.avfilter_get_by_name("abuffersink");fixed (AVFilterContext** fc = &_audioBufferSinkContext){ret = ffmpeg.avfilter_graph_create_filter(fc, bufferSink, "out", null, null, _audioFilterGraph).ThrowExceptionIfError();}// 构建音频滤镜链string filterSpec = BuildAudioFilterSpec(stream);AVFilterInOut* outputs = ffmpeg.avfilter_inout_alloc();AVFilterInOut* inputs = ffmpeg.avfilter_inout_alloc();outputs->name = ffmpeg.av_strdup("in");outputs->filter_ctx = _audioBufferSrcContext;outputs->pad_idx = 0;outputs->next = null;inputs->name = ffmpeg.av_strdup("out");inputs->filter_ctx = _audioBufferSinkContext;inputs->pad_idx = 0;inputs->next = null;ret = ffmpeg.avfilter_graph_parse_ptr(_audioFilterGraph, filterSpec, &inputs, &outputs, null);if (ret < 0){ffmpeg.avfilter_inout_free(&inputs);ffmpeg.avfilter_inout_free(&outputs);throw new Exception($"无法解析音频滤镜链: {GetErrorString(ret)}");}ret = ffmpeg.avfilter_graph_config(_audioFilterGraph, null);if (ret < 0){ffmpeg.avfilter_inout_free(&inputs);ffmpeg.avfilter_inout_free(&outputs);throw new Exception($"无法配置音频滤镜图: {GetErrorString(ret)}");}ffmpeg.avfilter_inout_free(&inputs);ffmpeg.avfilter_inout_free(&outputs);
}
5. 帧处理逻辑 (ProcessFrames
)
private unsafe void ProcessFrames(){AVPacket* packet = ffmpeg.av_packet_alloc();AVFrame* videoFrame = ffmpeg.av_frame_alloc();AVFrame* audioFrame = ffmpeg.av_frame_alloc();AVFrame* filteredVideoFrame = ffmpeg.av_frame_alloc();AVFrame* filteredAudioFrame = ffmpeg.av_frame_alloc();try{while (true){int ret = ffmpeg.av_read_frame(_inputFormatContext, packet);if (ret < 0){if (ret == ffmpeg.AVERROR_EOF) break;throw new Exception($"读取帧错误: {GetErrorString(ret)}");}// 处理视频流if (packet->stream_index == _videoStreamIndex && _videoStreamIndex != -1){ret = ffmpeg.avcodec_send_packet(_videoDeCodecContext, packet).ThrowExceptionIfError();while (ret >= 0){ret = ffmpeg.avcodec_receive_frame(_videoDeCodecContext, videoFrame);if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)break;if (ret < 0) throw new Exception($"接收视频帧错误: {GetErrorString(ret)}");// 应用视频滤镜ret = ffmpeg.av_buffersrc_add_frame(_videoBufferSrcContext, videoFrame);if (ret < 0) throw new Exception($"添加到视频滤镜源错误: {GetErrorString(ret)}");while (true){ret = ffmpeg.av_buffersink_get_frame(_videoBufferSinkContext, filteredVideoFrame);if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)break;if (ret < 0) throw new Exception($"从视频滤镜接收帧错误: {GetErrorString(ret)}");// 编码并写入处理后的视频帧EncodeAndWriteFrame(filteredVideoFrame, _videoStreamIndex, _videoCodecContext);ffmpeg.av_frame_unref(filteredVideoFrame);}ffmpeg.av_frame_unref(videoFrame);}}// 处理音频流else if (packet->stream_index == _audioStreamIndex && _audioStreamIndex != -1){ret = ffmpeg.avcodec_send_packet(_audioDeCodecContext, packet).ThrowExceptionIfError();while (ret >= 0){ret = ffmpeg.avcodec_receive_frame(_audioDeCodecContext, audioFrame);if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)break;if (ret < 0) throw new Exception($"接收音频帧错误: {GetErrorString(ret)}");// 应用音频滤镜ret = ffmpeg.av_buffersrc_add_frame(_audioBufferSrcContext, audioFrame);if (ret < 0) throw new Exception($"添加到音频滤镜源错误: {GetErrorString(ret)}");while (true){ret = ffmpeg.av_buffersink_get_frame(_audioBufferSinkContext, filteredAudioFrame);if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)break;if (ret < 0) throw new Exception($"从音频滤镜接收帧错误: {GetErrorString(ret)}");// 编码并写入处理后的音频帧EncodeAndWriteFrame(filteredAudioFrame, _audioStreamIndex, _audioCodecContext);ffmpeg.av_frame_unref(filteredAudioFrame);}ffmpeg.av_frame_unref(audioFrame);}}ffmpeg.av_packet_unref(packet);}// 刷新视频滤镜图if (_videoStreamIndex != -1){ffmpeg.av_buffersrc_add_frame(_videoBufferSrcContext, null);while (true){int ret = ffmpeg.av_buffersink_get_frame(_videoBufferSinkContext, filteredVideoFrame);if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)break;if (ret < 0) throw new Exception($"从视频滤镜接收帧错误: {GetErrorString(ret)}");EncodeAndWriteFrame(filteredVideoFrame, _videoStreamIndex, _videoCodecContext);ffmpeg.av_frame_unref(filteredVideoFrame);}}// 刷新音频滤镜图if (_audioStreamIndex != -1){ffmpeg.av_buffersrc_add_frame(_audioBufferSrcContext, null);while (true){int ret = ffmpeg.av_buffersink_get_frame(_audioBufferSinkContext, filteredAudioFrame);if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)break;if (ret < 0) throw new Exception($"从音频滤镜接收帧错误: {GetErrorString(ret)}");EncodeAndWriteFrame(filteredAudioFrame, _audioStreamIndex, _audioCodecContext);ffmpeg.av_frame_unref(filteredAudioFrame);}}}finally{ffmpeg.av_frame_free(&videoFrame);ffmpeg.av_frame_free(&audioFrame);ffmpeg.av_frame_free(&filteredVideoFrame);ffmpeg.av_frame_free(&filteredAudioFrame);ffmpeg.av_packet_free(&packet);}}
五、滤镜字符串拼接
1.视频滤镜跟命令行时用的是一样的
private string BuildVideoFilterSpec(){// 格式化帧率(保留4位小数)string frameRateStr = _videoFrameRate.ToString("F4");// 构建完整的视频滤镜链表达式return $"select='not({_excludedTimeRangesString})',setpts=N/{frameRateStr}/TB";}
2.音频滤镜
音频滤镜要注意声道布局处理 : aformat=channel_layouts={channelLayout}
确保输出声道与输入一致。
private string BuildAudioFilterSpec(AVStream* stream){string channelLayout = FFmpegHelper.GetChannelLayoutString(stream->codecpar->ch_layout);return $"aselect='not({_excludedTimeRangesString})',asetpts=N/{_audioSampleRate}/TB,aformat=channel_layouts={channelLayout}";}
六.最后添加项目链接
视频多段删除项目
完整代码要点
- 使用
ffmpeg.avfilter_graph_parse_ptr
解析滤镜表达式- 通过
av_buffersrc_add_frame
和av_buffersink_get_frame
传递帧数据- 调用
avformat_write_header
和av_write_trailer
维护文件结构