.NET 9.0 + macOS 15.0环境下使用ScreenCaptureKit无法录制屏幕与系统音频的问题求助
.NET 9.0 + macOS 15.0环境下使用ScreenCaptureKit无法录制屏幕与系统音频的问题求助
各位好,我最近在.NET 9.0 + macOS 15.0的环境下尝试用ScreenCaptureKit实现屏幕和系统音频的录制功能,但遇到了麻烦——录制完成后生成的MP4文件要么是空的,要么根本没有正常生成。我已经检查了基础的文件路径,也在系统设置里开启了屏幕录制权限,但问题还是没解决,想请大家帮忙看看我的代码哪里出了问题。
下面是我的完整代码:
using System; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Foundation; using AVFoundation; using CoreGraphics; using CoreVideo; using CoreMedia; using AppKit; using ScreenCaptureKit; using AudioToolbox; using CoreFoundation; using System.IO; namespace ScreenRecorder { class Program { static void Main() { if (File.Exists("../recording113.mp4")) File.Delete("../recording113.mp4"); TestRecorder recorder = new(); recorder.StartRecord(); Console.WriteLine("Started, enter to end..."); Console.ReadLine(); recorder.StopRecording(); } } internal class TestRecorder : NSObject, ISCStreamDelegate, ISCStreamOutput { private static string videoFormat = "mp4"; SCShareableContent? availableContent; SCContentFilter? filter; SCDisplay? screen; AudioSettings audioSettings; SCStream stream; SCStreamType? streamType; AVAssetWriter vW; AVAssetWriterInput vwInput, awInput; AVAudioEngine audioEngine = new AVAudioEngine(); public void StartRecord() { availableContent = SCShareableContent.GetShareableContentAsync(true, true).Result; PrepareRecord(); } public void PrepareRecord() { streamType = SCStreamType.Display; UpdateAudioSettings(); screen = availableContent?.Displays.First(); filter = new SCContentFilter(screen, [], [], SCContentFilterOption.Exclude); Record(filter); } public void Record(SCContentFilter filter) { var conf = new SCStreamConfiguration(); conf.Width = (nuint)(filter.ContentRect.Width * filter.PointPixelScale).Value; conf.Height = (nuint)(filter.ContentRect.Height * filter.PointPixelScale).Value; conf.MinimumFrameInterval = new CMTime(1, 30); conf.ShowsCursor = true; conf.CapturesAudio = true; conf.SampleRate = (nint)audioSettings.SampleRate; conf.ChannelCount = (int)audioSettings.NumberChannels; stream = new SCStream(filter, conf, this); NSError? errScreen; stream.AddStreamOutput(this, SCStreamOutputType.Screen, DispatchQueue.DefaultGlobalQueue, out errScreen); if (errScreen != null) { Console.WriteLine("Can't add screen output: " + errScreen.LocalizedDescription); } NSError? errAudio; stream.AddStreamOutput(this, SCStreamOutputType.Audio, DispatchQueue.DefaultGlobalQueue, out errAudio); if (errAudio != null) { Console.WriteLine("Can't add audio output: " + errAudio.LocalizedDescription); } InitVideo(conf); bool started = false; stream.StartCapture((err) => { Console.WriteLine("Recording started, error code: " + err?.Code); started = true; }); while (!started) { Thread.Sleep(100); } } public void StopRecording() { if (stream != null) { bool stopped = false; stream.StopCapture((err) => { Console.WriteLine("Recording stopped, error: " + err?.LocalizedDescription); stopped = true; }); while (!stopped) { Thread.Sleep(100); } } stream = null; CloseVideo(); streamType = null; } public void UpdateAudioSettings() { audioSettings = new AudioSettings() { SampleRate = 48000, NumberChannels = 2, Format = AudioToolbox.AudioFormatType.MPEG4AAC, EncoderBitRate = 128000 }; } public void InitVideo(SCStreamConfiguration conf) { var fileEnding = videoFormat; var fileType = AVFileTypes.Mpeg4; vW = new AVAssetWriter(NSUrl.FromFilename($"../recording113.{fileEnding}"), AVFileTypesExtensions.GetConstant(fileType), out NSError error); if (error != null) { Console.WriteLine("Failed to create asset writer: " + error.LocalizedDescription); } var fpsMultiplier = 30.0 / 8.0; var encoderMultiplier = 0.9; var targetBitrate = conf.Width * conf.Height * fpsMultiplier * encoderMultiplier; var videoSettings = new NSMutableDictionary(); videoSettings[AVVideo.CodecKey] = AVVideoCodecTypeExtensions.GetConstant(AVVideoCodecType.Hevc); videoSettings[AVVideo.WidthKey] = new NSNumber(conf.Width); videoSettings[AVVideo.HeightKey] = new NSNumber(conf.Height); var compressionProps = new NSMutableDictionary(); compressionProps[AVVideo.AverageBitRateKey] = new NSNumber(targetBitrate); compressionProps[AVVideo.ExpectedSourceFrameRateKey] = new NSNumber(30); videoSettings[AVVideo.CompressionPropertiesKey] = compressionProps; vwInput = new AVAssetWriterInput(AVMediaTypesExtensions.GetConstant(AVMediaTypes.Video), new AVVideoSettingsCompressed(videoSettings)); awInput = new AVAssetWriterInput(AVMediaTypesExtensions.GetConstant(AVMediaTypes.Audio), audioSettings); vwInput.ExpectsMediaDataInRealTime = true; awInput.ExpectsMediaDataInRealTime = true; if (vW.CanAddInput(vwInput)) { vW.AddInput(vwInput); } if (vW.CanAddInput(awInput)) { vW.AddInput(awInput); } if (!vW.StartWriting()) { Console.WriteLine("Can't start writing to file."); Console.WriteLine(vW?.Error?.LocalizedDescription); } } public void CloseVideo() { var dispatchGroup = new DispatchGroup(); dispatchGroup.Enter(); vwInput.MarkAsFinished(); awInput.MarkAsFinished(); vW.FinishWriting(() => dispatchGroup.Leave()); dispatchGroup.Wait(TimeSpan.MaxValue); } bool isSessionStarted = false; public void DidOutputSampleBuffer(SCStream stream, SCStreamOutputType outputType, CMSampleBuffer sampleBuffer, NSError error) { if (error != null) { Console.WriteLine($"Sample buffer error ({outputType}): {error.LocalizedDescription}"); return; } // 启动写入会话,用第一个样本的时间戳作为起始时间 if (!isSessionStarted) { vW.StartSessionAtSourceTime(sampleBuffer.PresentationTimeStamp); isSessionStarted = true; } if (outputType == SCStreamOutputType.Screen) { if (vwInput.IsReadyForMoreMediaData) { vwInput.AppendSampleBuffer(sampleBuffer); } } else if (outputType == SCStreamOutputType.Audio) { if (awInput.IsReadyForMoreMediaData) { awInput.AppendSampleBuffer(sampleBuffer); } } } } }
我目前怀疑几个点,想跟大家确认一下:
- 之前
DidOutputSampleBuffer方法没有完整实现,是不是必须要把样本数据写入到AVAssetWriterInput里才能生成有效文件?我后来补了这个方法,但不确定逻辑是否正确。 - macOS 15.0对ScreenCaptureKit的权限有没有新要求?我已经开了屏幕录制权限,会不会还有其他权限没配置?
AVAssetWriter的初始化或者写入逻辑有没有问题?比如用相对路径会不会有访问权限问题?HEVC编码在macOS 15下有没有兼容性问题?- 我看苹果原生示例里
AVAssetWriter需要调用StartSessionAtSourceTime方法,我之前的代码里没加,补了之后还是不确定是不是这个导致的核心问题。
希望有经验的朋友能帮我排查一下,谢谢大家!
内容来源于stack exchange




