Skip to content

withsalt/WithSalt.FFmpeg.Recorder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

63 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WithSalt.FFMpeg.Recorder

基于FFmpeg实现的高性能视频录制框架,支持从多种输入源(本地视频、摄像头、网络流、桌面等)中提取连续的图像帧。

View English

核心功能

受支持的输入源

  • 本地视频文件,支持多文件
  • 摄像头实时捕获
  • 流媒体(RTSP、RTMP、HLS等)
  • 桌面屏幕录制(Windows、Linux XOrg)

受支持的平台和操作系统

OS Runtime  x86 x64 ARM ARM64 LoongArch64
Windows .NET Core 3.1+
Linux .NET Core 3.1+

快速开始

安装Nuget包

通过Nuget包管理器安装WithSalt.FFmpeg.Recorder NuGet Version

通过Terminal安装:

dotnet add package WithSalt.FFmpeg.Recorder

安装FFmpeg

Windows系统,可前往 https://github.com/BtbN/FFmpeg-Builds/releases 下载编译好的FFmpeg。然后将ffmpeg.exe放入应用程序根目录,或者放入下一小节中支持自动搜索的目录。
建议在项目配置中新增条件编译参数,在编译Windows环境运行的引用程序时,自动复制ffmpeg。

<ItemGroup Condition="'$(OS)' == 'Windows_NT' OR '$(RuntimeIdentifier)' == 'win-x64'">
	<None Update="runtimes\win-x64\bin\ffmpeg.exe">
		<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
	</None>
</ItemGroup>

Linux系统(例如Debian;Ubuntu),通过命令:sudo apt install ffmpeg 安装。
可以参考项目中Demo示例中ffmpeg路径。

加载FFmpeg

工欲善其事必先利其器。在调用任何库提供的API之前,让我们先告诉程序ffmpeg所在目录,以及进行一些基础配置。

//使用默认的ffmpeg加载器
FFmpegHelper.SetDefaultFFmpegLoador();

调用SetDefaultFFmpegLoador之后,会进行以下初始化操作:

  1. 程序会从以下路径开始搜索ffmpeg应用程序所在位置

    • 优先查找与当前进程架构匹配的运行时目录,如:.\runtimes\win-x64\bin\ffmpeg.exe
    • 应用程序所在根目录,如:<应用程序目录>\ffmpeg.exe
    • 在应用程序目录下的bin目录,如:<应用程序目录>\bin\ffmpeg.exe
    • [Windows]系统变量Path下面的所有目录
    • [Linux]ffmpeg安装目录,包括:/usr/bin;/usr/local/bin;/usr/share

    当以上路径均找不到时,抛出异常。

  2. 设置ffmpeg工作目录
    工作目录默认设置为应用程序根目录,且创建tmp目录作为ffmpeg临时文件存放目录

构建FFmpeg执行参数

如果进行桌面录制

FFMpegArgumentProcessor ffmpegCmd = new FFmpegArgumentsBuilder()
    .WithDesktopInput()
    .WithRectangle(new SKRect(0, 0, 1920, 1080))
    .WithFramerate(60)
    .WithImageHandle((frameIndex, bitmap) =>
    {
        if (!frameChannel.Writer.TryWrite((frameIndex, bitmap)))
        {
            bitmap.Dispose();
        }
    })
    .WithOutputQuality(OutputQuality.Medium)
    .Build()
    //.NotifyOnProgress(frame => Console.WriteLine($"Frame {frame} captured."), TimeSpan.FromSeconds(1))
    ;

上述代码解释:使用输入源为桌面,录制的区域从左上角0,0坐标开始,录制屏幕1920x1080的区域。当图片产生之后,通过WithImageHandle回调,放入frameChannel中。
使用其他的输入源,调用对应的API即可,比如:WithCameraInput()。

完整Demo

以下是录制屏幕的完整demo

using System.Diagnostics;
using System.Threading.Channels;
using FFMpegCore;
using SkiaSharp;
using WithSalt.FFmpeg.Recorder;
using WithSalt.FFmpeg.Recorder.Models;

namespace ConsoleAppDemo
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            if (Directory.Exists("output"))
            {
                Directory.Delete("output", true);
            }
            Directory.CreateDirectory("output");

            //使用默认的ffmpeg加载器
            FFmpegHelper.SetDefaultFFmpegLoador();

            Channel<(long frameIndex, SKBitmap data)> frameChannel = Channel.CreateBounded<(long frameIndex, SKBitmap data)>(
                new BoundedChannelOptions(10)
                {
                    FullMode = BoundedChannelFullMode.Wait
                });

            //计算FPS
            int uiFrameCount = 0;
            Stopwatch lastUiFpsUpdate = Stopwatch.StartNew();
            Stopwatch totalUiFpsUpdate = Stopwatch.StartNew();
            int currentUiFps = 0;
            long totalUiFps = 0;

            // 启动写入任务
            var cts = new CancellationTokenSource();
            var writeTask = Task.Run(async () =>
            {
                totalUiFpsUpdate.Restart();

                while (!cts.IsCancellationRequested && await frameChannel.Reader.WaitToReadAsync(cts.Token))
                {
                    SKBitmap? latestBitmap = null;
                    long frameIndex = 0;

                    // 取出所有可用帧,只保留最后一帧
                    while (frameChannel.Reader.TryRead(out (long frameIndex, SKBitmap bitmap) data))
                    {
                        latestBitmap?.Dispose();
                        latestBitmap = data.bitmap;
                        frameIndex = data.frameIndex;
                    }

                    try
                    {
                        if (latestBitmap != null)
                        {
                            // 更新FPS计数器
                            uiFrameCount++;
                            if (lastUiFpsUpdate.ElapsedMilliseconds >= 1000)
                            {
                                currentUiFps = uiFrameCount;
                                totalUiFps += uiFrameCount;
                                uiFrameCount = 0;
                                lastUiFpsUpdate.Restart();

                                TimeSpan totalElapsed = totalUiFpsUpdate.Elapsed;
                                int avgFps = (int)(totalUiFps / Math.Max(1, totalElapsed.TotalSeconds));

                                Console.Write($"\r{(int)totalElapsed.TotalHours:00}:{totalElapsed.Minutes:00}:{totalElapsed.Seconds:00} | Current FPS: {currentUiFps} | AVG FPS: {avgFps}   ");
                            }

                            //Console.WriteLine("收到图片帧");
                            SaveBitmapAsImage(latestBitmap, $"output/{frameIndex}.jpg", SKEncodedImageFormat.Jpeg, 100);
                        }
                    }
                    finally
                    {
                        latestBitmap?.Dispose();
                    }
                }
            });

            await DesktopTest(frameChannel);
            Console.WriteLine("Done.");
        }

        private static Action? _cancel = null;

        static async Task DesktopTest(Channel<(long frameIndex, SKBitmap data)> frameChannel)
        {
            FFMpegArgumentProcessor ffmpegCmd = new FFmpegArgumentsBuilder()
                .WithDesktopInput()
                .WithRectangle(new SKRect(0, 0, 0, 0))
                .WithFramerate(60)
                .WithImageHandle((frameIndex, bitmap) =>
                {
                    if (!frameChannel.Writer.TryWrite((frameIndex, bitmap)))
                    {
                        bitmap.Dispose();
                    }
                })
                .WithOutputQuality(OutputQuality.Medium)
                .Build()
                .CancellableThrough(out _cancel)
                //.NotifyOnProgress(frame => Console.WriteLine($"Frame {frame} captured."), TimeSpan.FromSeconds(1))
                ;

            var cmd = ffmpegCmd.Arguments;
            Console.WriteLine($"FFMpeg命令:{Environment.NewLine}ffmpeg {cmd}");

            await ffmpegCmd.ProcessAsynchronously();
        }

        /// <summary>
        /// 将 SKBitmap 保存为指定格式的图片文件
        /// </summary>
        /// <param name="bitmap">要保存的 SKBitmap 实例</param>
        /// <param name="filePath">保存的文件路径</param>
        /// <param name="imageFormat">图像格式(PNG、JPEG 等)</param>
        /// <param name="quality">编码质量(针对有损格式,如 JPEG)</param>
        static void SaveBitmapAsImage(SKBitmap bitmap, string filePath, SKEncodedImageFormat imageFormat, int quality)
        {
            if (bitmap == null)
                throw new ArgumentNullException(nameof(bitmap));

            using (SKImage image = SKImage.FromBitmap(bitmap))
            using (SKData data = image.Encode(imageFormat, quality))
            using (FileStream stream = File.OpenWrite(filePath))
            {
                data.SaveTo(stream);
            }
        }
    }
}

开发建议

  1. 强烈建议使用异步队列处理图像帧
    通常情况下,比如说使用CPU进行图像别,是比较慢的,处理输入远远跟不上ffmpeg获取视频帧的速度。使用异步队列可保证管道畅通,且配合合理丢帧策略,可以让开发的引用程序非常丝滑。可以参考示例项目。
  2. 随应用程序携带ffmpeg应用程序时,建议放到运行时目录中,支持自动搜索,应用程序目录也不会乱糟糟的
    比如:runtimes\win-x64\bin\ffmpeg.exe

更多完整示例

https://github.com/withsalt/BemfaCloud/tree/main/src/Examples

许可

  • 本软件使用MIT开源协议。
  • 本软件使用了 FFmpeg( https://ffmpeg.org ),FFmpeg 受 LGPL/GPL 许可证保护。

致谢

感谢这些伟大的开源项目。

About

基于FFmpeg实现的高性能视频录制框架,支持从多种输入源(本地视频、摄像头、网络流、桌面等)中提取连续的图像帧。 A high-performance video recording framework based on FFmpeg, which supports extracting continuous image frames from various input sources (local videos, cameras, network streams, desktops, etc.).

Resources

License

Stars

Watchers

Forks

Packages