Search code examples
c#screen-capturedirect3d11screen-recordingwindows-graphics-capture

Pausing / Resuming Screen recording with Windows Graphics Capture API


I am building a screen recording app in C# using Windows Graphics Capture API. I am using this script. I can select monitor and can record it to mp4 file. I am trying to add Pause/Resume functionality.

Here is code of main Window that initiates Recording

try
{
    newFile = GetTempFile();
    using (var stream = new FileStream(newFile, FileMode.CreateNew).AsRandomAccessStream())
    using (_encoder = new Encoder(_device, item))
    {
        await _encoder.EncodeAsync(
            stream,
            width, height, bitrate,
            frameRate);
    }
}
catch (Exception ex)
{
  //
}

And here is the main function from Encoder class, which is used above

private async Task EncodeInternalAsync(IRandomAccessStream stream, uint width, uint height, uint bitrateInBps, uint frameRate)
{
    if (!_isRecording)
    {
        _isRecording = true;

        _frameGenerator = new CaptureFrameWait(
            _device,
            _captureItem,
            _captureItem.Size);

        using (_frameGenerator)
        {
            var encodingProfile = new MediaEncodingProfile();
            encodingProfile.Container.Subtype = "MPEG4";
            encodingProfile.Video.Subtype = "H264";
            encodingProfile.Video.Width = width;
            encodingProfile.Video.Height = height;
            encodingProfile.Video.Bitrate = bitrateInBps;
            encodingProfile.Video.FrameRate.Numerator = frameRate;
            encodingProfile.Video.FrameRate.Denominator = 1;
            encodingProfile.Video.PixelAspectRatio.Numerator = 1;
            encodingProfile.Video.PixelAspectRatio.Denominator = 1;
            var transcode = await _transcoder.PrepareMediaStreamSourceTranscodeAsync(_mediaStreamSource, stream, encodingProfile);

            await transcode.TranscodeAsync();
        }
    }
}

And finally this is the initializer function in CaptureFrameWait class

private void InitializeCapture(SizeInt32 size)
{
    _framePool = Direct3D11CaptureFramePool.CreateFreeThreaded(
        _device,
        DirectXPixelFormat.B8G8R8A8UIntNormalized,
        1,
        size);
    _framePool.FrameArrived += OnFrameArrived;
    _session = _framePool.CreateCaptureSession(_item);
    _session.IsBorderRequired = false;
    _session.StartCapture();
}

How can we modify this to pause the recording? I have tried to dispose the _framepool and _session objects on Pause and initialize them again on Resume in CaptureFrameWait class, like shown below. It works fine, but sometimes TranscodeAsync function terminates during pause and ends recording. How can we avoid that?

bool _paused = false;
public void PauseSession(bool status)
{
    if (status) {
        _paused = true;
        _framePool?.Dispose();
        _session?.Dispose();
    }
    else {
        InitializeCapture(_size);
        _paused = false;
    }
}

Solution

  • One solution is to get a deferral. Doc says:

    The MediaStreamSource will then wait for you to supply the MediaStreamSample until you mark the deferral as complete.

    So for example, add two private members to the Encoder class and two methods:

    private MediaStreamSourceSampleRequestedEventArgs _args;
    private MediaStreamSourceSampleRequestDeferral _def;
    
    public bool IsPaused { get; private set; }
    
    public void Pause()
    {
        IsPaused = true;
    }
    
    public void Resume()
    {
        IsPaused = false;
    
        // complete the request we saved earlier
        OnMediaStreamSourceSampleRequested(_mediaStreamSource, _args);
    }
    

    And modify OnMediaStreamSourceSampleRequested methods like this (where I've put comments):

    private void OnMediaStreamSourceSampleRequested(MediaStreamSource sender, MediaStreamSourceSampleRequestedEventArgs args)
    {
        if (_isRecording && !_closed)
        {
            // if paused get a deferral and save the current arguments.
            // OnMediaStreamSourceSampleRequested will not be called again until we complete the deferral
            if (IsPaused)
            {
                _def = args.Request.GetDeferral();
                _args = args;
                return;
            }
    
            try
            {
                using (var frame = _frameGenerator.WaitForNewFrame())
                {
                    if (frame == null)
                    {
                        args.Request.Sample = null;
                        DisposeInternal();
                        return;
                    }
    
                    var timeStamp = frame.SystemRelativeTime;
    
                    var sample = MediaStreamSample.CreateFromDirect3D11Surface(frame.Surface, timeStamp);
                    args.Request.Sample = sample;
    
                    // when called again (manually by us) complete the work
                    // and reset members
                    if (_def != null)
                    {
                        _def.Complete();
                        _def = null;
                        _args = null;
                    }
                }
            }
            catch (Exception e)
            {
              ...
            }
        }
        else
        {
          ...
        }
    }
    

    Another solution is to simply freeze the frames timestamp, so add these members:

    private TimeSpan _pausedTimestamp;
    public bool IsPaused { get; private set; }
    
    public void Pause()
    {
        IsPaused = true;
    }
    
    public void Resume()
    {
        IsPaused = false;
    }
    

    And modify OnMediaStreamSourceSampleRequested methods like this (where I've put comments):

    private void OnMediaStreamSourceSampleRequested(MediaStreamSource sender, MediaStreamSourceSampleRequestedEventArgs args)
    {
        if (_isRecording && !_closed)
        {
            try
            {
                using (var frame = _frameGenerator.WaitForNewFrame())
                {
                    if (frame == null)
                    {
                        args.Request.Sample = null;
                        DisposeInternal();
                        return;
                    }
    
                    // if paused, "freeze" the timestamp
                    TimeSpan timeStamp;
                    if (IsPaused)
                    {
                        timeStamp = _pausedTimestamp;
                    }
                    else
                    {
                        timeStamp = frame.SystemRelativeTime;
                        _pausedTimestamp = timeStamp;
                    }
    
                    var sample = MediaStreamSample.CreateFromDirect3D11Surface(frame.Surface, timeStamp);
                    args.Request.Sample = sample;
                }
            }
            catch (Exception e)
            {
              ...
            }
        }
        else
        {
          ...
        }
    }