Optimize saving render buffer to file

I have found a lot of questions and answers on this topic, but no complete recommended way to do this. We are rendering HD frames and want to save them to disk in raw format (i.e. not JPEG or PNG) at 60FPS. right now we are getting around 8FPS with the following code:

IEnumerator RecordFrame()
    {
        yield return new WaitForEndOfFrame();

        var renderTexture = new RenderTexture(Screen.width, Screen.height, 32, RenderTextureFormat.ARGB32);
        cam.targetTexture = renderTexture;
        cam.Render();
        cam.targetTexture = null;

        RenderTexture.active = renderTexture;
        Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGBA32, false);
        screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
        screenshot.Apply();
        RenderTexture.active = null;
              
        String path = "Screenshot_" + m_num + ".bin";
        byte[] bytes = screenshot.GetRawTextureData();
        File.WriteAllBytes(path, bytes);
        m_num++;
    }

    public void LateUpdate()
    {
        StartCoroutine(RecordFrame());
    }

From past posts it seems that ReadPixels is the culprit, and needs optimization. I am trying glReadPixels but it returns a black frame, like the one in this post: glReadPixels in Unity 5 always get black frame - Questions & Answers - Unity Discussions

Is there a definitive answer to this question? Or any asset in the store that can achieve this functionality out of the box?

I ended up using the solution from here: GitHub - keijiro/AsyncCaptureTest: Non-blocking screen capture example with asynchronous GPU readback
I get ~40 FPS for HD resolution, main bottleneck seems to be the write to disk function which blocks Update().

For future reference, with unity 2018.2:

public class AsyncCapture : MonoBehaviour
{
    Queue<AsyncGPUReadbackRequest> _requests = new Queue<AsyncGPUReadbackRequest>();
    int m_num = 0;
  
    void Update()
    {
        while (_requests.Count > 0)
        {
            var req = _requests.Peek();
  
            if (req.hasError)
            {
                Debug.Log("GPU readback error detected.");
                _requests.Dequeue();
            }
            else if (req.done)
            {
                var data = req.GetData<byte>().ToArray(); // request bytes so that they are directly writeable to the file
                string filename = "Screenshot_" + m_num + ".raw";
                using (BinaryWriter writer = new BinaryWriter(File.Open(filename, FileMode.Create)))
                {
                    writer.Write(data);
                }
                m_num++;
                _requests.Dequeue();
            }
            else
            {
                break;
            }
        }
    }
     
    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (_requests.Count < 8)
            _requests.Enqueue(AsyncGPUReadback.Request(source));
        else
            Debug.Log("Too many requests.");
  
        Graphics.Blit(source, destination);
    }
}