用C#写了个录屏软件 最后音视频合并这块直接难住了 看了一圈 简单的是安装ffmpeg 实现 关键是折腾了几天 装了fmpeg 也没实现 代码我直接发出来 留给那个大神来解决吧。现在的问题是点击合成后 进度一直卡在0% 能不用ffmpeg 更好,可以大大减少问价大小。
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; // 用于捕获麦克风声音
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;
// 添加音频品质选项
comboAudioQuality.Items.AddRange(new string[] { "低", "中", "高" });
comboAudioQuality.SelectedIndex = 0;
lblRecordingTime.Text = "录制时间: 00:00:00";
this.Text = "屏幕录制器";
// 枚举音频设备
EnumerateAudioDevices();
}
private void EnumerateAudioDevices()
{
MMDeviceEnumerator enumerator = new MMDeviceEnumerator();
// 枚举音频输出设备
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("没有找到相应的设备");
cmbOutputDevices.Enabled = false;
cmbOutputDevices.SelectedIndex = 0; // 设置默认选中提示项
chkEnableOutput.Enabled = false;
}
else
{
cmbOutputDevices.SelectedIndex = 0;
chkEnableOutput.Checked = true;
}
// 枚举音频输入设备
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("没有找到相应的设备");
cmbInputDevices.Enabled = false;
cmbInputDevices.SelectedIndex = 0; // 设置默认选中提示项
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); // 创建音频录制线程
captureThread.Start();
timerThread.Start();
frameInsertThread.Start();
recordingThread.Start();
audioRecordingThread.Start(); // 启动音频录制线程
}
catch (Exception ex)
{
MessageBox.Show($"开始录屏时出现错误: {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(); // 等待音频录制线程结束
if (writer != null && !isWriterClosed)
{
CompensateFrames();
writer.Close();
writer.Dispose();
writer = null;
isWriterClosed = true;
}
// 停止音频录制
StopAudioRecording();
MessageBox.Show("录屏已停止,文件保存为 " + outputFilePath);
}
catch (Exception ex)
{
MessageBox.Show($"关闭视频文件写入器时出现错误: {ex.Message}");
}
finally
{
isRecording = false;
lastFrame?.Dispose();
lastFrame = null;
actualFrameCount = 0;
}
}
}
private void StartAudioRecording()
{
// 确保在主线程上访问控件
if (comboAudioQuality.InvokeRequired)
{
comboAudioQuality.Invoke(new Action(StartAudioRecording));
return; // 退出当前方法,等待主线程完成
}
var audioQuality = comboAudioQuality.SelectedItem.ToString();
var waveFormat = GetWaveFormat(audioQuality);
if (chkEnableOutput.Checked)
{
// 确保在主线程上访问控件
if (cmbOutputDevices.InvokeRequired)
{
cmbOutputDevices.Invoke(new Action(() => StartAudioRecording()));
return; // 退出当前方法,等待主线程完成
}
// 处理音频输出录制
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)
{
// 确保在主线程上访问控件
if (cmbInputDevices.InvokeRequired)
{
cmbInputDevices.Invoke(new Action(() => StartAudioRecording()));
return; // 退出当前方法,等待主线程完成
}
// 处理音频输入录制
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; // 记录当前帧的时间戳
}
}
Thread.Sleep(1000 / targetFrameRate);
}
}
private void UpdateTimer()
{
while (!cancellationTokenSource.IsCancellationRequested)
{
TimeSpan elapsedTime = stopwatch.Elapsed;
lblRecordingTime.Invoke(new Action(() =>
{
lblRecordingTime.Text = $"录制时间: {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; // 每帧的持续时间(毫秒)
long nextFrameTime = 0;
while (!cancellationTokenSource.IsCancellationRequested)
{
lock (writerLock)
{
if (lastFrame != null && !lastFrame.IsDisposed())
{
long currentTime = stopwatch.ElapsedMilliseconds;
// 如果当前时间大于等于下一个帧的时间,则写入帧
if (currentTime >= nextFrameTime)
{
writer.WriteVideoFrame(lastFrame);
actualFrameCount++;
nextFrameTime += frameDuration; // 更新下一个帧的时间
}
// 如果帧数不足,进行补偿
if (framesToAdd > 0)
{
for (int i = 0; i < framesToAdd; i++)
{
writer.WriteVideoFrame(lastFrame);
}
}
}
}
Thread.Sleep(1); // 确保不会占用过多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; // 用于存储视频总时长
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();
// 解析输出以获取视频时长
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; // 如果无法获取时长,返回 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("视频文件不存在,请检查路径。");
return;
}
if (!File.Exists(audioPath))
{
MessageBox.Show("音频文件不存在,请检查路径。");
return;
}
// 转换音频格式
// ConvertAudioToAAC(audioPath, tempAudioPath);
// 获取视频总时长
totalDurationMs = GetVideoDuration(videoPath);
if (totalDurationMs == 0)
{
MessageBox.Show("无法获取视频总时长,请检查视频文件。");
return;
}
string arguments = $"-i \"{videoPath}\" -i \"{tempAudioPath}\" -c:v copy -c:a aac -strict experimental -progress pipe:1 -nostats \"{outputPath}\"";
lblProgress.Text = "合成进度: 0%"; // 初始化进度标签
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 = $"合成进度: {progress:F2}%";
}));
hasStartedProgress = true;
}
}
}
if (!hasStartedProgress)
{
MessageBox.Show("未收到 FFmpeg 的进度信息,请检查 FFmpeg 配置或命令参数。");
}
string error = process.StandardError.ReadToEnd();
if (!string.IsNullOrEmpty(error))
{
MessageBox.Show($"FFmpeg 执行出错: {error}");
}
process.WaitForExit();
}
}
catch (Exception ex)
{
MessageBox.Show($"启动 FFmpeg 进程时出现错误: {ex.Message}");
}
finally
{
// 删除临时音频文件
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($"音频格式转换出错: {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;
}
}
}
}
|