用C#寫(xiě)了個(gè)錄屏軟件 最后音視頻合并這塊直接難住了 看了一圈 簡(jiǎn)單的是安裝ffmpeg 實(shí)現(xiàn) 關(guān)鍵是折騰了幾天 裝了fmpeg 也沒(méi)實(shí)現(xiàn) 代碼我直接發(fā)出來(lái) 留給那個(gè)大神來(lái)解決吧。現(xiàn)在的問(wèn)題是點(diǎn)擊合成后 進(jìn)度一直卡在0% 能不用ffmpeg 更好,可以大大減少問(wèn)價(jià)大小。
using System;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Diagnostics;
using System.Windows.Forms;
using Accord.Video.FFMPEG;
using NAudio.CoreAudioApi;
using NAudio.Wave;
using System.Collections.Generic;
using MediaToolkit;
using MediaToolkit.Model;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
using System.Threading.Tasks;
using System.Linq;
namespace ScreenRecorder
{
public partial class Form1 : Form
{
private VideoFileWriter writer;
private Thread captureThread;
private Thread timerThread;
private Thread frameInsertThread;
private Thread recordingThread;
private Thread audioRecordingThread;
private bool isRecording = false;
private string outputFilePath;
private Stopwatch stopwatch;
private bool isWriterClosed = true;
private CancellationTokenSource cancellationTokenSource;
private readonly object writerLock = new object();
private int targetFrameRate = 30;
private int actualFrameCount = 0;
private Bitmap lastFrame;
private int framesToAdd = 0;
private long lastFrameTimestamp = 0;
private WasapiLoopbackCapture loopbackCapture; // 用于捕獲電腦聲音
private WasapiCapture microphoneCapture; // 用于捕獲麥克風(fēng)聲音
private WaveFileWriter loopbackWriter;
private WaveFileWriter microphoneWriter;
private List<MMDevice> outputDevices = new List<MMDevice>();
private List<MMDevice> inputDevices = new List<MMDevice>();
public Form1()
{
InitializeComponent();
string folderPath = Path.Combine(Application.StartupPath, "錄像");
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
comboQuality.Items.AddRange(new string[] { "低", "中", "高" });
comboQuality.SelectedIndex = 1;
comboFrameRate.Items.AddRange(new string[] { "15", "20", "25", "30", "60" });
comboFrameRate.SelectedIndex = 3;
// 添加音頻品質(zhì)選項(xiàng)
comboAudioQuality.Items.AddRange(new string[] { "低", "中", "高" });
comboAudioQuality.SelectedIndex = 0;
lblRecordingTime.Text = "錄制時(shí)間: 00:00:00";
this.Text = "屏幕錄制器";
// 枚舉音頻設(shè)備
EnumerateAudioDevices();
}
private void EnumerateAudioDevices()
{
MMDeviceEnumerator enumerator = new MMDeviceEnumerator();
// 枚舉音頻輸出設(shè)備
foreach (var device in enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active))
{
outputDevices.Add(device);
cmbOutputDevices.Items.Add(device.FriendlyName);
}
if (outputDevices.Count == 0)
{
cmbOutputDevices.Items.Add("沒(méi)有找到相應(yīng)的設(shè)備");
cmbOutputDevices.Enabled = false;
cmbOutputDevices.SelectedIndex = 0; // 設(shè)置默認(rèn)選中提示項(xiàng)
chkEnableOutput.Enabled = false;
}
else
{
cmbOutputDevices.SelectedIndex = 0;
chkEnableOutput.Checked = true;
}
// 枚舉音頻輸入設(shè)備
foreach (var device in enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active))
{
inputDevices.Add(device);
cmbInputDevices.Items.Add(device.FriendlyName);
}
if (inputDevices.Count == 0)
{
cmbInputDevices.Items.Add("沒(méi)有找到相應(yīng)的設(shè)備");
cmbInputDevices.Enabled = false;
cmbInputDevices.SelectedIndex = 0; // 設(shè)置默認(rèn)選中提示項(xiàng)
chkEnableInput.Enabled = false;
}
else
{
cmbInputDevices.SelectedIndex = 0;
chkEnableInput.Checked = true;
}
}
private void btnStart_Click(object sender, EventArgs e)
{
if (!isRecording)
{
try
{
cancellationTokenSource = new CancellationTokenSource();
targetFrameRate = int.Parse(comboFrameRate.SelectedItem.ToString());
int bitrate = GetBitrate(comboQuality.SelectedItem.ToString());
int captureWidth = Screen.PrimaryScreen.Bounds.Width;
int captureHeight = Screen.PrimaryScreen.Bounds.Height;
string fileName = $"Loopback_{DateTime.Now:yyyyMMdd_HHmmss}.mp4";
outputFilePath = Path.Combine(Application.StartupPath, "錄像", fileName);
writer = new VideoFileWriter();
writer.Open(outputFilePath, captureWidth, captureHeight, targetFrameRate, VideoCodec.MPEG4, bitrate);
isWriterClosed = false;
isRecording = true;
stopwatch = Stopwatch.StartNew();
captureThread = new Thread(() => CaptureScreen(captureWidth, captureHeight));
timerThread = new Thread(UpdateTimer);
frameInsertThread = new Thread(InsertFrames);
recordingThread = new Thread(RecordVideo);
audioRecordingThread = new Thread(StartAudioRecording); // 創(chuàng)建音頻錄制線程
captureThread.Start();
timerThread.Start();
frameInsertThread.Start();
recordingThread.Start();
audioRecordingThread.Start(); // 啟動(dòng)音頻錄制線程
}
catch (Exception ex)
{
MessageBox.Show($"開(kāi)始錄屏?xí)r出現(xiàn)錯(cuò)誤: {ex.Message}");
isRecording = false;
isWriterClosed = true;
writer = null;
}
}
}
private void btnStop_Click(object sender, EventArgs e)
{
if (isRecording)
{
try
{
cancellationTokenSource.Cancel();
captureThread.Join();
timerThread.Join();
frameInsertThread.Join();
recordingThread.Join();
audioRecordingThread.Join(); // 等待音頻錄制線程結(jié)束
if (writer != null && !isWriterClosed)
{
CompensateFrames();
writer.Close();
writer.Dispose();
writer = null;
isWriterClosed = true;
}
// 停止音頻錄制
StopAudioRecording();
MessageBox.Show("錄屏已停止,文件保存為 " + outputFilePath);
}
catch (Exception ex)
{
MessageBox.Show($"關(guān)閉視頻文件寫(xiě)入器時(shí)出現(xiàn)錯(cuò)誤: {ex.Message}");
}
finally
{
isRecording = false;
lastFrame?.Dispose();
lastFrame = null;
actualFrameCount = 0;
}
}
}
private void StartAudioRecording()
{
// 確保在主線程上訪問(wèn)控件
if (comboAudioQuality.InvokeRequired)
{
comboAudioQuality.Invoke(new Action(StartAudioRecording));
return; // 退出當(dāng)前方法,等待主線程完成
}
var audioQuality = comboAudioQuality.SelectedItem.ToString();
var waveFormat = GetWaveFormat(audioQuality);
if (chkEnableOutput.Checked)
{
// 確保在主線程上訪問(wèn)控件
if (cmbOutputDevices.InvokeRequired)
{
cmbOutputDevices.Invoke(new Action(() => StartAudioRecording()));
return; // 退出當(dāng)前方法,等待主線程完成
}
// 處理音頻輸出錄制
var loopbackDevice = outputDevices[cmbOutputDevices.SelectedIndex];
loopbackCapture = new WasapiLoopbackCapture(loopbackDevice);
loopbackCapture.WaveFormat = waveFormat;
string loopbackFileName = $"Loopback_{DateTime.Now:yyyyMMdd_HHmmss}.wav";
string loopbackOutputPath = Path.Combine(Application.StartupPath, "錄像", loopbackFileName);
loopbackWriter = new WaveFileWriter(loopbackOutputPath, loopbackCapture.WaveFormat);
loopbackCapture.DataAvailable += LoopbackCapture_DataAvailable;
loopbackCapture.RecordingStopped += LoopbackCapture_RecordingStopped;
loopbackCapture.StartRecording();
}
if (chkEnableInput.Checked)
{
// 確保在主線程上訪問(wèn)控件
if (cmbInputDevices.InvokeRequired)
{
cmbInputDevices.Invoke(new Action(() => StartAudioRecording()));
return; // 退出當(dāng)前方法,等待主線程完成
}
// 處理音頻輸入錄制
var microphoneDevice = inputDevices[cmbInputDevices.SelectedIndex];
microphoneCapture = new WasapiCapture(microphoneDevice);
microphoneCapture.WaveFormat = waveFormat;
string microphoneOutputPath = Path.Combine(Application.StartupPath, "錄像", $"Microphone_{DateTime.Now:yyyyMMdd_HHmmss}.wav");
microphoneWriter = new WaveFileWriter(microphoneOutputPath, microphoneCapture.WaveFormat);
microphoneCapture.DataAvailable += MicrophoneCapture_DataAvailable;
microphoneCapture.RecordingStopped += MicrophoneCapture_RecordingStopped;
microphoneCapture.StartRecording();
}
}
private void StopAudioRecording()
{
if (loopbackCapture != null)
{
loopbackCapture.StopRecording();
loopbackWriter?.Dispose();
loopbackWriter = null;
loopbackCapture?.Dispose();
loopbackCapture = null;
}
if (microphoneCapture != null)
{
microphoneCapture.StopRecording();
microphoneWriter?.Dispose();
microphoneWriter = null;
microphoneCapture?.Dispose();
microphoneCapture = null;
}
}
private void CaptureScreen(int width, int height)
{
while (!cancellationTokenSource.IsCancellationRequested)
{
using (Bitmap bitmap = new Bitmap(width, height))
{
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(0, 0, 0, 0, bitmap.Size);
}
lock (writerLock)
{
lastFrame?.Dispose();
lastFrame = (Bitmap)bitmap.Clone();
lastFrameTimestamp = stopwatch.ElapsedMilliseconds; // 記錄當(dāng)前幀的時(shí)間戳
}
}
Thread.Sleep(1000 / targetFrameRate);
}
}
private void UpdateTimer()
{
while (!cancellationTokenSource.IsCancellationRequested)
{
TimeSpan elapsedTime = stopwatch.Elapsed;
lblRecordingTime.Invoke(new Action(() =>
{
lblRecordingTime.Text = $"錄制時(shí)間: {elapsedTime:hh\\:mm\\:ss}";
}));
Thread.Sleep(1000);
}
}
private void InsertFrames()
{
while (!cancellationTokenSource.IsCancellationRequested)
{
lock (writerLock)
{
if (lastFrame != null && !lastFrame.IsDisposed())
{
framesToAdd = targetFrameRate - actualFrameCount;
}
}
Thread.Sleep(1000);
}
}
private void RecordVideo()
{
long frameDuration = 1000 / targetFrameRate; // 每幀的持續(xù)時(shí)間(毫秒)
long nextFrameTime = 0;
while (!cancellationTokenSource.IsCancellationRequested)
{
lock (writerLock)
{
if (lastFrame != null && !lastFrame.IsDisposed())
{
long currentTime = stopwatch.ElapsedMilliseconds;
// 如果當(dāng)前時(shí)間大于等于下一個(gè)幀的時(shí)間,則寫(xiě)入幀
if (currentTime >= nextFrameTime)
{
writer.WriteVideoFrame(lastFrame);
actualFrameCount++;
nextFrameTime += frameDuration; // 更新下一個(gè)幀的時(shí)間
}
// 如果幀數(shù)不足,進(jìn)行補(bǔ)償
if (framesToAdd > 0)
{
for (int i = 0; i < framesToAdd; i++)
{
writer.WriteVideoFrame(lastFrame);
}
}
}
}
Thread.Sleep(1); // 確保不會(huì)占用過(guò)多CPU資源
}
}
private void CompensateFrames()
{
int expectedFrameCount = targetFrameRate;
int framesToAdd = expectedFrameCount - actualFrameCount;
if (framesToAdd > 0 && lastFrame != null && !lastFrame.IsDisposed())
{
lock (writerLock)
{
for (int i = 0; i < framesToAdd; i++)
{
writer.WriteVideoFrame(lastFrame);
}
}
}
}
private void LoopbackCapture_DataAvailable(object sender, WaveInEventArgs e)
{
if (loopbackWriter != null)
{
loopbackWriter.Write(e.Buffer, 0, e.BytesRecorded);
}
}
private void LoopbackCapture_RecordingStopped(object sender, StoppedEventArgs e)
{
loopbackWriter?.Dispose();
loopbackWriter = null;
loopbackCapture?.Dispose();
loopbackCapture = null;
}
private void MicrophoneCapture_DataAvailable(object sender, WaveInEventArgs e)
{
if (microphoneWriter != null)
{
microphoneWriter.Write(e.Buffer, 0, e.BytesRecorded);
}
}
private void MicrophoneCapture_RecordingStopped(object sender, StoppedEventArgs e)
{
microphoneWriter?.Dispose();
microphoneWriter = null;
microphoneCapture?.Dispose();
microphoneCapture = null;
}
private void btnOpenFolder_Click(object sender, EventArgs e)
{
string folderPath = Path.GetDirectoryName(outputFilePath);
Process.Start("explorer.exe", folderPath);
}
private int GetBitrate(string quality)
{
switch (quality)
{
case "低":
return 1500000;
case "中":
return 5000000;
case "高":
return 8000000;
default:
return 1500000;
}
}
private WaveFormat GetWaveFormat(string quality)
{
switch (quality)
{
case "低":
return new WaveFormat(8000, 1); // 8000 Hz 采樣率,單聲道
case "中":
return new WaveFormat(16000, 2); // 16000 Hz 采樣率,立體聲
case "高":
return new WaveFormat(44100, 2); // 44100 Hz 采樣率,立體聲
default:
return new WaveFormat(8000, 1);
}
}
private long totalDurationMs; // 用于存儲(chǔ)視頻總時(shí)長(zhǎng)
private long GetVideoDuration(string videoPath)
{
string ffmpegPath = @"E:\apk\ScreenRecorder\bin\Debug\錄像\ffmpega\bin\ffmpeg.exe";
string arguments = $"-i \"{videoPath}\"";
var processStartInfo = new ProcessStartInfo
{
FileName = ffmpegPath,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (var process = Process.Start(processStartInfo))
{
string output = process.StandardError.ReadToEnd();
process.WaitForExit();
// 解析輸出以獲取視頻時(shí)長(zhǎng)
int durationIndex = output.IndexOf("Duration: ");
if (durationIndex != -1)
{
int startIndex = durationIndex + "Duration: ".Length;
int endIndex = output.IndexOf(",", startIndex);
if (endIndex != -1)
{
string time = output.Substring(startIndex, endIndex - startIndex).Trim();
if (TimeSpan.TryParse(time, out TimeSpan timeSpan))
{
return (long)timeSpan.TotalMilliseconds;
}
}
}
}
return 0; // 如果無(wú)法獲取時(shí)長(zhǎng),返回 0
}
private async void btnMerge_Click(object sender, EventArgs e)
{
string directoryPath = Path.Combine(Application.StartupPath, "錄像");
if (!Directory.Exists(directoryPath))
{
MessageBox.Show("錄像文件夾不存在。");
return;
}
string videoPath = @"E:\apk\ScreenRecorder\bin\Debug\錄像\Loopback_20250217_161914.mp4";
string audioPath = @"E:\apk\ScreenRecorder\bin\Debug\錄像\Loopback_20250217_161914.aac";
string tempAudioPath = Path.Combine(directoryPath, "temp_audio.aac");
string outputPath = @"E:\apk\ScreenRecorder\bin\Debug\錄像\output.mp4";
string ffmpegPath = @"E:\apk\ScreenRecorder\bin\Debug\錄像\ffmpega\bin\ffmpeg.exe";
// 檢查文件是否存在
if (!File.Exists(videoPath))
{
MessageBox.Show("視頻文件不存在,請(qǐng)檢查路徑。");
return;
}
if (!File.Exists(audioPath))
{
MessageBox.Show("音頻文件不存在,請(qǐng)檢查路徑。");
return;
}
// 轉(zhuǎn)換音頻格式
// ConvertAudioToAAC(audioPath, tempAudioPath);
// 獲取視頻總時(shí)長(zhǎng)
totalDurationMs = GetVideoDuration(videoPath);
if (totalDurationMs == 0)
{
MessageBox.Show("無(wú)法獲取視頻總時(shí)長(zhǎng),請(qǐng)檢查視頻文件。");
return;
}
string arguments = $"-i \"{videoPath}\" -i \"{tempAudioPath}\" -c:v copy -c:a aac -strict experimental -progress pipe:1 -nostats \"{outputPath}\"";
lblProgress.Text = "合成進(jìn)度: 0%"; // 初始化進(jìn)度標(biāo)簽
await Task.Run(() =>
{
var processStartInfo = new ProcessStartInfo
{
FileName = ffmpegPath,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
try
{
using (var process = Process.Start(processStartInfo))
{
bool hasStartedProgress = false;
while (!process.StandardOutput.EndOfStream)
{
string line = process.StandardOutput.ReadLine();
if (line.StartsWith("out_time_ms="))
{
string[] parts = line.Split('=');
if (parts.Length == 2 && long.TryParse(parts[1], out long outTimeMs))
{
double progress = Math.Min(100, outTimeMs / (double)totalDurationMs * 100);
lblProgress.Invoke(new Action(() =>
{
lblProgress.Text = $"合成進(jìn)度: {progress:F2}%";
}));
hasStartedProgress = true;
}
}
}
if (!hasStartedProgress)
{
MessageBox.Show("未收到 FFmpeg 的進(jìn)度信息,請(qǐng)檢查 FFmpeg 配置或命令參數(shù)。");
}
string error = process.StandardError.ReadToEnd();
if (!string.IsNullOrEmpty(error))
{
MessageBox.Show($"FFmpeg 執(zhí)行出錯(cuò): {error}");
}
process.WaitForExit();
}
}
catch (Exception ex)
{
MessageBox.Show($"啟動(dòng) FFmpeg 進(jìn)程時(shí)出現(xiàn)錯(cuò)誤: {ex.Message}");
}
finally
{
// 刪除臨時(shí)音頻文件
if (File.Exists(tempAudioPath))
{
File.Delete(tempAudioPath);
}
}
});
MessageBox.Show("視頻合并完成!");
}
private void ConvertAudioToAAC(string inputAudioPath, string outputAudioPath)
{
string ffmpegPath = @"E:\apk\ScreenRecorder\bin\Debug\錄像\ffmpega\bin\ffmpeg.exe";
string arguments = $"-i \"{inputAudioPath}\" -c:a aac \"{outputAudioPath}\"";
var processStartInfo = new ProcessStartInfo
{
FileName = ffmpegPath,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true
};
try
{
using (var process = Process.Start(processStartInfo))
{
process.WaitForExit();
}
}
catch (Exception ex)
{
MessageBox.Show($"音頻格式轉(zhuǎn)換出錯(cuò): {ex.Message}");
}
}
}
public static class BitmapExtensions
{
public static bool IsDisposed(this Bitmap bitmap)
{
try
{
using (Graphics g = Graphics.FromImage(bitmap))
{
return false;
}
}
catch (ObjectDisposedException)
{
return true;
}
}
}
}
|