5

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;
    }
}
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
Riz
  • 6,746
  • 16
  • 67
  • 89
  • 1
    One solution is when paused to just send the same timeStamp again and again. Another solution is to call `var def = args.Request.GetDeferral()` in `OnMediaStreamSourceSampleRequested` when asked for paused, and give back a sample and call `def.Complete()` once resumed. – Simon Mourier Jan 13 '23 at 12:28
  • @SimonMourier thank you for your suggestion. This seems to be working well. Please post this as an answer so I can accept that. – Riz Jan 17 '23 at 02:59

1 Answers1

2

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
    {
      ...
    }
}
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • second method doesn't work. Video file just gets corrupted and doesn't play. First method works fine but still *sometimes* video doesn't play. What could be issue? – Riz Jan 18 '23 at 16:24
  • Both work for me and I never get any corrupted file (HD 1080p 60 fps). But looking at the result, when resume happens, you have to wait few seconds before it takes effect. – Simon Mourier Jan 18 '23 at 18:03
  • Can you explain what do you mean by wait for few seconds after resume? – Riz Jan 18 '23 at 18:11
  • It depends on the player there may be a delay between the time when you resumed at recording time and the time when it visually resumes in the player – Simon Mourier Jan 18 '23 at 18:45