c#螢幕錄製(經典)(含原始碼和AForge.Video.FFMPEG.DLL)及填坑辦法

語言: CN / TW / HK

一直覺得.net在多媒體處理方面渣得不行。最近需要做一個攝像頭的程式,為了方便,用了AForge這個開源專案。AForge專案中有AForge.Video和AForge.Video. DirectShow這兩個子專案,可以方便的呼叫攝像頭。但是這兩個專案最終只能取得影片幀,並不能儲存為影片檔案。經高人指點,AForge還有一個子專案AForge.Video.FFMPEG,它可以將圖片壓制成Avi影片格式。不過這個AForge.Video.FFMPEG在實際使用的時候會遇到不少坑,下面我將我在這次使用中遇到的坑分享給大家。

AForge.NET是一個專門為開發者和研究者基於C#框架設計的,該庫是一個開源專案,他包括計算機視覺與人工智慧,影象處理,神經網路,遺傳演算法,機器學習,模糊系統,機器人控制等領域,提供很多影象的處理,和影片處理功能

這個框架由一系列的類庫組成。主要包括有:

AForge.Imaging —— 一些日常的影象處理和過濾器

AForge.Vision —— 計算機視覺應用類庫

AForge.Neuro —— 神經網路計算庫AForge.Genetic -進化演算法程式設計庫

AForge.MachineLearning —— 機器學習類庫

AForge.Robotics —— 提供一些機器人的工具類庫

AForge.Video —— 一系列的影片處理類庫

AForge.Fuzzy —— 模糊推理系統類庫

AForge.Controls—— 影象,三維,圖表顯示控制元件

官網:http://www.aforgenet.com/

Aforge.Net子專案有個AForge.Video.VFW提供了對Avi檔案的操作,AForge後面加入了子專案 AForge.Video.FFMPEG 通過FFmpeg庫,提供了對大量影片格式的支援,我們都知道,FFmpeg是一個非常強大的影片處理類庫,同樣也是開源的,不過 AForge.Video.FFMPEG 還處於實驗階段,目標是用 FFmpeg 取代 AForge.Video.VFW 提供一個更好的對影片檔案操作的庫,但是該庫值目前提供了對影片資料的讀寫,不支援對音訊檔案的讀寫,可能以後會支援

第一坑:引用

你要用AForge.Video.FFMPEG,當然第一步是引用啦。但這個AForge.Video.FFMPEG並不能像AForge其他專案一樣可以用Visual Studio自帶的NuGet去獲得,你會發現NuGet上根本找不到這個專案。

找不到麼,那我就去官網找好了,咱們可以去AForge專案官網下載AForge專案的原始碼和已編譯檔案。不過這裡有倆問題:

  1. AForge專案官網開啟速度非常非常非常慢,你可以點連結開啟官網,然後開啟遊戲玩一會兒。(這裡我就給各位放個AForge下載頁直鏈:http://www.aforgenet.com/framework/downloads.html)

  2. AForge專案的原始碼和生成檔案最終都是放在GoogleCode上的,國內你懂得。不過這邊我們就可以用的小花招就是用迅雷之類的下載器下載,他們的離線下載是可以翻牆的。

我是選擇了“Download Installer”,右鍵選擇“複製連結地址”,然後放進迅雷下載。

下載下來之後是一個壓縮包,AForge.Video.FFMPEG.dll就放在壓縮包的Release資料夾中。

第二坑:呼叫

剛剛我們從官網下載下來了AForge.Video.FFMPEG.dll,接下來呼叫就好了對吧。

然而並不是,你只是一腳踏進了一個深坑罷了,為什麼說是深坑呢?因為這個dll呼叫非常非常的噁心。

我們來看一下有多噁心,首先我們假設我們已經在專案中已經添加了AForge.Video和AForge.Video.FFMPEG這二個類庫。

然後修改Main函式:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


namespace ConsoleRecoderTest
{
class Program
{
static void Main(string[] args)
{
ScreenRecorderTemplate tmp = new ScreenRecorderTemplate()
{
IsStartUp = false,
StartDateTime = DateTime.Now,
StopDateTime = DateTime.Now.AddMinutes(2)
};
new ScreenRecorderTool(tmp).StartRecording();
Console.WriteLine("complete");
Console.Read();
}
}
}


按F5除錯,瞬間爆炸:

發生這個問題的原因比較簡單,因為這個AForge.Video.FFMPEG使用VC++寫的,編譯的時候已經被編譯成原生代碼,而我們現在C#一般目標平臺都是“Any CPU”,所以會發生這個問題。

解決方案就是不再選擇使用“Any CPU”作為目標平臺,改成“x86”或者“x64”。因為x86可以跑在x64上,而x64不能在x86上跑,所以我選擇了x86。

現在再按F5啟動除錯,再一次瞬間爆炸【冷漠臉】。

怎麼說呢,起碼出錯提示換了對吧【冷漠臉】。

那麼這次又是出什麼問題了呢。

咱們現在用的是AForge.Video.FFMPEG對吧。我們都知道FFMPEG是一個著名的開源多媒體處理專案對吧,這個AForge.Video.FFMPEG其實是在內部呼叫FFMPEG來工作的。所以這個FileNotFoundException其實是AForge.Video.FFMPEG找不到FFMPEG的檔案所以丟擲來的。AForge.Video.FFMPEG依賴的FFMPEG元件其實已經放在了剛剛下載下來的壓縮包的\Externals\ffmpeg\bin目錄下:

我們把這個8個檔案複製到程式目錄下,注意我們剛剛改過目標平臺了,現在程式編譯輸出的目錄已經是\bin\x86\Debug,不要複製錯了。

複製好之後我們繼續按F5除錯程式。

嗯,爆炸了,我已經習慣了【冷漠臉】

這次問題的原因是什麼呢……

其實是因為我的專案目標框架是.net Framework 4.0,而AForge官方在編譯AForge.Video.FFMPEG.dll的時候,目標框架選的是.net Framework 2.0……

在.net Framework 4.0以前,由於程式執行環境本質還是.net Framework 2.0,並且.net Framework 2.0相容.net Framework 1.0和1.1,但在升級到.net Framework 4.0時,.NET的核心作了重大調整,以前在.net Framework 2.0或.net3.5中生成的程式集,如果要在.net Framework 4.0下執行,需要在配置檔案中指定此應用程式支援的公共語言執行時版本和啟用.net Framework 2.0執行時啟用策略。

解決方案有三種:

  1. 降低自己專案的目標.net Framework版本;

  2. 修改Config檔案;

  3. 重新編譯Video.FFMPEG。

這裡我就講一下方法二,

在Visual Studio中按Ctrl+Shift+A,開啟“新增新項”視窗,選擇“應用程式配置檔案”,再點選“新增”(vs2017建立的時候已經自帶了App.config無需再次新增)

開啟新建的App.Config檔案,在<configuration>和</configuration>標籤中加入以下內容:

  <startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/>
</startup>

新增完成後,按F5啟動除錯。

終於一切正常。

錄製的影片在檔案執行目錄下:

專案的引用項:

使用的開源的影片處理元件AForge,當然它所包含的功能遠不止於此,想了解更多到官網上去看吧。一下程式碼主要是錄製桌面螢幕,每20秒存入一個影片檔案,可以為有類似需要的通知提供一點幫助。

ScreenRecorderTool.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
//using Accord.Video;
//using Accord.Video.FFMPEG;
using AForge.Video;
using AForge.Video.FFMPEG;


namespace ConsoleRecoderTest
{
/// <summary>
/// 位元率
/// </summary>
public enum BitRate : int
{
_50kbit = 5000,
_100kbit = 10000,
_500kbit = 50000,
_1000kbit = 1000000,
_2000kbit = 2000000,
_3000kbit = 3000000
}
/// <summary>
/// 螢幕錄製模板
/// </summary>
public class ScreenRecorderTemplate
{
/// <summary>
/// 模板名稱
/// </summary>
public string TmpName { get; set; }
/// <summary>
/// 錄屏開始時間
/// </summary>
public DateTime? StartDateTime { get; set; }
/// <summary>
/// 錄屏結束時間
/// </summary>
public DateTime? StopDateTime { get; set; }
/// <summary>
/// 是否為開機啟動
/// </summary>
public bool IsStartUp { get; set; }


}
/// <summary>
/// 螢幕錄製工具類
/// </summary>
public class ScreenRecorderTool
{
#region Fields
private int screenWidth;
private int screenHight;
private int bitRate = (int)BitRate._500kbit;
private int frameRate = 5;//預設幀率為5
private bool isRecording;




private string saveFolderPath;
private string fileName;
private Stopwatch stopWatch;
private Rectangle screenArea;
private VideoFileWriter videoWriter;
private ScreenCaptureStream videoStreamer;
private VideoCodec videoCodec = VideoCodec.MSMPEG4v2;
private ScreenRecorderTemplate recorderTmp;
private static object key = new object();
#endregion
/// <summary>
/// 是否正在錄製
/// </summary>
private bool IsRecording
{
get
{
lock (key)
{
return isRecording;
}
}
set
{
lock (key)
{
isRecording = value;
}
}
}
public ScreenRecorderTool(ScreenRecorderTemplate recorderTmp)
{
this.recorderTmp = recorderTmp;
this.screenWidth = SystemInformation.VirtualScreen.Width;
this.screenHight = SystemInformation.VirtualScreen.Height;
this.IsRecording = false;
this.SaveFolderPath = AppDomain.CurrentDomain.BaseDirectory;
this.stopWatch = new Stopwatch();
this.screenArea = Rectangle.Empty;
SetScreenArea();
}
/// <summary>
/// 影片儲存位置
/// </summary>
private string SaveFolderPath
{
get { return this.saveFolderPath; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentNullException("saveFolderpath", "儲存路徑不能為空");
}
this.saveFolderPath = value;
}
}
/// <summary>
/// 影片檔名稱
/// </summary>
private string FileName
{
get { return this.fileName; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentNullException("fileName", "File name can not be empty or null");
}
this.fileName = value;
}
}
/// <summary>
/// 完成一幀錄製的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void video_NewFrame(object sender, NewFrameEventArgs e)
{
if (this.IsRecording)
{
if (videoWriter != null)
{
this.videoWriter.WriteVideoFrame(e.Frame);
}
if (this.stopWatch.Elapsed.Seconds >= 20)
{
Console.WriteLine("超過指定時間,寫入檔案");
StopRecording();
}
}
else
{
videoStreamer.SignalToStop();
videoWriter.Close();
videoWriter.Dispose();
//GC.Collect();
Console.WriteLine("停止錄製");
if (recorderTmp.IsStartUp)//開機錄製
{
StartRecording();
}
else
{
if (DateTime.Now <= recorderTmp.StopDateTime.Value)
{
Console.WriteLine("記錄重啟錄製");
StartRecording();
}
else
{
Console.WriteLine("時間到不再錄製");
}
}
}
}


/// <summary>
/// 設定必要引數開啟影片寫入工具
/// </summary>
private void InitializeRecordingParameters()
{
if (!this.IsRecording)
{
this.IsRecording = true;
CreateCatalog();
this.FileName = saveFolderPath + string.Format
(@"{0}-{1}.avi",
"MSR",
DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss"));
this.videoWriter.Open(this.FileName, this.screenWidth, this.screenHight, this.frameRate, this.videoCodec, this.bitRate);
}
}


/// <summary>
/// 建立目錄
/// </summary>
private void CreateCatalog()
{
if (saveFolderPath == AppDomain.CurrentDomain.BaseDirectory)
{
var catalog = SaveFolderPath + DateTime.Now.ToString("yyyy-MM-dd") + "\\";
if (!System.IO.Directory.Exists(catalog))
{
System.IO.Directory.CreateDirectory(catalog);
}
SaveFolderPath = catalog;
}
}


/// <summary>
/// 設定螢幕錄製區域為全屏
/// </summary>
private void SetScreenArea()
{
foreach (Screen screen in Screen.AllScreens)
{
this.screenArea = Rectangle.Union(this.screenArea, screen.Bounds);
}


if (this.screenArea == Rectangle.Empty)
{
//logger.Error("沒有獲取到螢幕資訊");
throw new InvalidOperationException("Screan area can not be set");
}
}
/// <summary>
/// 舊檔案清理(避免檔案大小超標)
/// </summary>
private void ClearOldVideo()
{


}


#region public method


/// <summary>
/// 開啟影片流開始錄製
/// </summary>
public void StartRecording()
{
if (recorderTmp == null)
{
Console.WriteLine("模板不能為空");
return;
}
if (!recorderTmp.IsStartUp)
{
if (!recorderTmp.StartDateTime.HasValue
|| !recorderTmp.StopDateTime.HasValue
|| recorderTmp.StartDateTime.Value > recorderTmp.StopDateTime.Value)
{
Console.WriteLine("模板不正確");
return;
}
}
this.videoWriter = new VideoFileWriter();
InitializeRecordingParameters();
this.videoStreamer = new ScreenCaptureStream(this.screenArea);
this.videoStreamer.NewFrame += new NewFrameEventHandler(video_NewFrame);
this.videoStreamer.Start();
this.stopWatch.Start();
this.IsRecording = true;
Console.WriteLine("開始錄製...");
}
/// <summary>
/// 停止錄製
/// </summary>
public void StopRecording()
{
this.stopWatch.Reset();
this.IsRecording = false;
}


#endregion
}
}

示例呼叫:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


namespace ConsoleRecoderTest
{
class Program
{
static void Main(string[] args)
{
ScreenRecorderTemplate tmp = new ScreenRecorderTemplate()
{
IsStartUp = false,
StartDateTime = DateTime.Now,
StopDateTime = DateTime.Now.AddMinutes(2)
};
new ScreenRecorderTool(tmp).StartRecording();
Console.WriteLine("complete");
Console.Read();
}
}
}


補充:

直接執行的話有問題,frameRate = 5;//預設幀率為5,實際上寫在影片裡是30秒!
問題出在幀率與截圖間隔不對。


`


//this.videoStreamer = new ScreenCaptureStream(this.screenArea);
//要加錄製間隔時間
this.videoStreamer = new ScreenCaptureStream(this.screenArea, 1000 / frameRate);
`


另外 this.stopWatch.Elapsed.Seconds >= 20要改成21,因為大於等於20的話就停止的話實際上就只錄了19秒,所有要改為21
`


if (this.stopWatch.Elapsed.Seconds >= 21)

原始碼、播放器、AForge.NET Framework-2.2.5.exe下載地址:

連結:https://pan.baidu.com/s/11O8z8Fj4JyMqgQ3ybxZ3ZQ

提取碼:5fxo

參考連結:

  1. http://www.diqisoft.com/technical/20087.htm

  2. https://www.cnblogs.com/yyq745201/p/5334294.html

喜歡就來個三連,讓更多人因你而受益