基于FFmpeg dll的简单录屏
使用FFMediaToolkit能较为容易的实现录屏或者录制组件等功能。
此功能在windows桌面端运行情况良好,但是移动端会出现问题,因此被弃用,大致记录一下录屏思想。
类库准备
FFmpeg的dll准备,FFmpeg.AutoGen可以生成相关的dll,不过后来不能生成了,就需要自己去下载相应的dll
下载地址:https://github.com/BtbN/FFmpeg-Builds
然后在NGET里搜索并且安装FFMediaToolkit
代码编写
DLL加载
加载的源码 FFMediaToolkit的GitHub主页有,我这稍微改了改
我只用了x64的dll所以把dll文件在dll/FFMediaToolkit。加载方式其实就是FFmpeg.Autgen的加载方式。
internal static bool LoadFFmpegDll()
{
var current = Environment.CurrentDirectory;
Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32");
if (!Environment.Is64BitProcess)
{
Console.WriteLine("x86环境不支持录制");
return false;
}
var probe = Path.Combine("dll", "FFMediaToolkit");//相对路径
while (current != null)
{
var ffmpegBinaryPath = Path.Combine(current, probe);
if (Directory.Exists(ffmpegBinaryPath))
{
Console.WriteLine($"FFmpeg binaries found in: {ffmpegBinaryPath}");
FFmpegLoader.FFmpegPath = ffmpegBinaryPath;
return true;
}
current = Directory.GetParent(current)?.FullName;
}
return false;
}
View Code
录屏核心代码
复制屏幕像素转为bitmap
private readonly Rectangle _bounds = System.Windows.Forms.Screen.PrimaryScreen.Bounds;
private Bitmap GetScreenImgByteArray()
{
Bitmap bitmap = new Bitmap(_bounds.Width, _bounds.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
using (Graphics graphics = Graphics.FromImage(bitmap))
{
graphics.CopyFromScreen(System.Drawing.Point.Empty, System.Drawing.Point.Empty, _bounds.Size, CopyPixelOperation.SourceCopy);
return bitmap;
}
}
View Code
创建录制路径
MediaOutput videofile;
int recordfps = 25;
private bool CreatRecordPath(string videotemperpath)
{
VideoEncoderSettings settings = new VideoEncoderSettings(_bounds.Width, _bounds.Height, framerate: recordfps, codec: VideoCodec.H264)
{
EncoderPreset = EncoderPreset.Fast,
CRF = 17
};
videofile = MediaBuilder.CreateContainer(videotemperpath).WithVideo(settings).Create();
}
}
View Code
初始化录制时间类
因为正常写定时器录制总是出现,帧率过快的问题。经过一番查找,使用了别人封装的时间间隔类 TimedIntervalTask,来保证录制帧率正常。
public sealed class TimedIntervalTask
{
///
/// 初始化 时间间隔模式的定时任务类;须给定一些必要参数
///
///
/// 定时任务的方法行为,不可为 null;此方法里须加入 try {} catch {} 异常处理,以保证其稳定运行
///
/// 定时任务的间隔时间,单位:毫秒
///
/// 是否先运行行为;默认为 true,先运行行为,再等待时间间隔;若设为 false,则先等待时间间隔,再运行行为
///
public TimedIntervalTask(Action action, int intervalTime, bool whetherToRunFirst = true)
{
TimedAction = action;
IntervalTime = intervalTime;
WhetherToRunFirst = whetherToRunFirst;
Restore();
}
///
/// 还原
///
private void Restore()
{
IsPause = false;
IsStop = false;
IsRunning = false;
IsStarted = false;
}
///
/// 定时的行为
///
private Action TimedAction { get; set; }
///
/// 行为运行的间隔时间,单位:毫秒
///
public int IntervalTime { get; set; }
///
/// 是否先运行 行为,默认 true
///
/// 若为 true,则先运行行为,再等待时间间隔;若设为 false,则先等待时间间隔,再运行行为
///
///
public bool WhetherToRunFirst { get; set; }
///
/// 是否已经启动,true 已启动,false 未启动;默认 false
///
public bool IsStarted { get; private set; } = false;
///
/// 是否暂停行为,默认 false
///
/// 若为 true,则暂停定时行为的运行;若再设为 false,则继续定时行为的运行
///
///
public bool IsPause { get; private set; } = false;
///
/// 是否终止定时任务,默认 false
///
public bool IsStop { get; private set; } = false;
///
/// 定时任务是否在运行中;true 是,false 否;默认 false
///
public bool IsRunning { get; private set; } = false;
///
/// 启动任务运行
///
///
public void Startup()
{
if (TimedAction == null || IsStarted)
{
return;
}
Task.Run(async () =>
{
IsStarted = true;
while (!IsStop)
{
IntervalTime = IntervalTime < 1 ? 1 : IntervalTime;
if (IsPause)
{
IsRunning = false;
await Task.Delay(IntervalTime);
continue;
}
else
{
IsRunning = true;
}
if (WhetherToRunFirst)
{
_ = Task.Run(() =>
{
try
{
TimedAction();
}
catch (Exception)
{
// 不做处理
}
});
await Task.Delay(IntervalTime);
}
else
{
await Task.Delay(IntervalTime);
_ = Task.Run(() =>
{
try
{
TimedAction();
}
catch (Exception)
{
// 不做处理
}
});
}
}
Restore();
});
}
///
/// 终止定时任务
///
///
///
public void Stop()
{
IsStop = true;
}
///
/// 暂停定时任务
///
public void Pause()
{
IsPause = true;
}
///
/// 继续定时任务
///
public void GoOn()
{
IsPause = false;
}
}
View Code
录制任务
这里开了两个线程,使用图片队列,一个获取图片,一个写入视频
private ConcurrentQueue
private TimedIntervalTask timedIntervalTaskGetScreenImg;
private TimedIntervalTask timedIntervalTaskVideoWriter;
///
/// 录制任务加载
///
private void RecordLoad()
{
bitmaps = new ConcurrentQueue
timedIntervalTaskGetScreenImg = new TimedIntervalTask(() =>
{
if (timedIntervalTaskGetScreenImg.IsRunning)
{
Dispatcher.BeginInvoke(new Action(() =>
{
bitmaps.Enqueue(GetScreenImgByteArray());
framescount++;
}));
}
}, GetFpsTime());
timedIntervalTaskVideoWriter = new TimedIntervalTask(() =>
{
var ts = TimeSpan.FromSeconds(framescount / recordgfps);
Dispatcher.BeginInvoke(new Action(() =>
{
timetip.Text = $"{ts.Hours}:{ts.Minutes}:{ts.Seconds}";
while (bitmaps.Count > 0)
{
Bitmap bitmap = null;
if (bitmaps.TryDequeue(out bitmap))
{
BitmapData bitLock = bitmap.LockBits(new Rectangle(System.Drawing.Point.Empty, gridsize), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
ImageData bitmapData = ImageData.FromPointer(bitLock.Scan0, ImagePixelFormat.Bgr24, gridsize);
videofile.Video.AddFrame(bitmapData);
bitmap.UnlockBits(bitLock);
bitmap.Dispose();
}
GC.Collect();
}
}));
}, 200);
}
View Code
录制开始
在录制开始前需要先创建录制路径,录制路径自己设置
int framescount = 0;
private void StartRecord()
{
framescount = 0;
bitmaps = new ConcurrentQueue
RecordLoad();
timedIntervalTaskGetScreenImg.IntervalTime = GetFpsTime();
timedIntervalTaskGetScreenImg.Startup();
timedIntervalTaskVideoWriter.Startup();
}
View Code
录制结束
因为使用图片队列,所以录制完成需等图片队列清空后再释放资源
private void StopRecord()
{
if (!timedIntervalTaskGetScreenImg.IsStarted)
{
return;
}
timedIntervalTaskGetScreenImg.Stop();
timedIntervalTaskVideoWriter.IntervalTime = 100;
Task.Run(async () =>
{
while (bitmaps.Count >= 0)
{
await Task.Delay(30);
}
});
timedIntervalTaskVideoWriter.Stop();
if (videofile != null)
{
videofile.Video.Dispose();
videofile.Dispose();
}
}
View Code