You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

.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);
                }
            }
        }
    }
}

我目前怀疑几个点,想跟大家确认一下:

  1. 之前DidOutputSampleBuffer方法没有完整实现,是不是必须要把样本数据写入到AVAssetWriterInput里才能生成有效文件?我后来补了这个方法,但不确定逻辑是否正确。
  2. macOS 15.0对ScreenCaptureKit的权限有没有新要求?我已经开了屏幕录制权限,会不会还有其他权限没配置?
  3. AVAssetWriter的初始化或者写入逻辑有没有问题?比如用相对路径会不会有访问权限问题?HEVC编码在macOS 15下有没有兼容性问题?
  4. 我看苹果原生示例里AVAssetWriter需要调用StartSessionAtSourceTime方法,我之前的代码里没加,补了之后还是不确定是不是这个导致的核心问题。

希望有经验的朋友能帮我排查一下,谢谢大家!

内容来源于stack exchange

火山引擎 最新活动