/// <summary>
        /// Writes stored memory stream to <paramref name="outputStream"/>.
        /// </summary>
        /// <param name="outputStream"></param>
        /// <param name="content"></param>
        /// <param name="context"></param>
        /// <param name="callId"></param>
        /// <param name="ani"></param>
        /// <param name="username"></param>
        /// <param name="lineId"></param>
        /// <param name="unitId"></param>
        public static async Task WriteToAsync(Stream outputStream, HttpContent content, TransportContext context, Int32 callId, Int32 lineId, Int32 unitId, String ani, String username)
        {
            AudioData audioData = null;

            try
            {
                if (callId <= 0)
                {
                    throw new ArgumentException("AudioHandler.WriteToAsync - Invalid call ID value");
                }
                if (String.IsNullOrWhiteSpace(username))
                {
                    throw new ArgumentException("AudioHandler.WriteToAsync - Invalid username value");
                }

                if (_audioStreams == null)
                {
                    // This SHOULD never happen, but just to be safe..
                    throw new ArgumentException("AudioHandler.WriteToAsync - _audioStreams is null");
                }

                // Try retrieving audio data
                // Retries to make sure sufficient time is given to
                // method that creates audio data object and stream.
                Int32 ct = 0;
                while (ct < 10)
                {
                    if (_audioStreams.TryGetValue(callId, out audioData))
                    {
                        break;
                    }

                    Thread.Sleep(500);
                    ct++;
                }

                if (audioData == null)
                {
                    throw new ArgumentException($"AudioData requested and not found for username '{username}', call ID {callId}");
                }

                // Wait for data to be written before reading.
                ct = 0;
                while (ct < 10)
                {
                    if (audioData.CanReadMemStream())
                    {
                        break;
                    }

                    Thread.Sleep(500);
                    ct++;
                }

                // Check if we can read from audioData and write to outputStream
                CheckReadWrite(audioData, outputStream, callId);

                AudioUserListener newListener = new AudioUserListener();
                newListener.ForceStop         = false;
                newListener.CallIdListeningTo = callId;

                if (!_userListeners.TryAdd(username, newListener))
                {
                    // TryAdd can fail if key already exists, but there are other reasons for failing,
                    // so check here to make sure key already existing is the reason.
                    if (_userListeners.TryGetValue(username, out AudioUserListener existingListener))
                    {
                        if (existingListener.CallIdListeningTo != callId)
                        {
                            // The key did already exist, it just still had an old call ID.
                            // So update the existing user listener with the new call ID.
                            if (!_userListeners.TryUpdate(username, newListener, existingListener))
                            {
                                throw new ArgumentException($"Existing user listener '{username}' could not be updated with new call ID {callId}.");
                            }
                        }
                    }
                    else
                    {
                        throw new ArgumentException($"Unable to add new user listener '{username}' to audio stream for call ID {callId}.");
                    }
                }

                // ReSharper disable once PossibleNullReferenceException
                // Null check on audio data is done in CheckReadWrite method.
                audioData.IncrementListenerCt();

                // Create transport buffers; One reads from memory, while the
                // other writes to output stream. Then they swap jobs. Rinse and repeat.
                TransportBuffer[] transportBuffers =
                {
                    new TransportBuffer(audioData.DefBufferSize),
                    new TransportBuffer(audioData.DefBufferSize)
                };

                Int32 bufNum = 0;

                // Start reading towards the end of stream to get more up-to-date data
                // Subtract 1000 so that it's not reading too close to the process that's writing to memstream
                Int64 readPos = audioData.GetMemStreamLength() - 1000;

                _logger.LogInfo($"User '{username}': Starting stream, Call ID {callId}");

                Task <Int32> readTask  = audioData.ReadMemStreamAsync(readPos, transportBuffers[bufNum].Data, 0, transportBuffers[bufNum].Data.Length);
                Task         writeTask = null;

                Int32 retryCt                = 0;
                Int32 maxRetryCt             = 50;
                Int32 sleepMsBetweenAttempts = 100;

                while (CheckIsListening(username))
                {
                    await readTask;

                    transportBuffers[bufNum].Length = readTask.Result;
                    readPos += transportBuffers[bufNum].Length;                     // Update read position.

                    // If zero bytes were read, there's currently no data left to read.
                    if (readTask.Result == 0)
                    {
                        if (retryCt >= maxRetryCt)
                        {
                            break;
                        }

                        // Wait, then retry
                        Thread.Sleep(sleepMsBetweenAttempts);

                        readTask = audioData.ReadMemStreamAsync(readPos, transportBuffers[bufNum].Data, 0, transportBuffers[bufNum].Data.Length);

                        retryCt++;
                        continue;
                    }

                    // Data found, reset retry count
                    retryCt = 0;

                    if (writeTask != null)
                    {
                        await writeTask;
                        outputStream.Flush();
                    }

                    writeTask = outputStream.WriteAsync(transportBuffers[bufNum].Data, 0, transportBuffers[bufNum].Length);

                    // Reset currently active transport buffer length for later update of read position
                    transportBuffers[bufNum].Length = 0;

                    // Switch active transport buffers
                    bufNum ^= 1;                     //bufNum == 0 ? 1 : 0

                    readTask = audioData.ReadMemStreamAsync(readPos, transportBuffers[bufNum].Data, 0, transportBuffers[bufNum].Data.Length);
                }

                if (writeTask != null)
                {
                    await writeTask;
                }
            }
            catch (HttpException)
            {
                // Remote host closed the connection
                _logger.LogInfo($"Front-end aborted stream for username: '******'; call ID: {callId}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Exception thrown in AudioHandler.WriteToAsync");
            }
            finally
            {
                _logger.LogInfo($"User '{username ?? "NULL"}': ending stream; call ID {callId}");

                if (!String.IsNullOrWhiteSpace(username))
                {
                    if (_userListeners.TryRemove(username, out AudioUserListener listener))
                    {
                        _logger.LogInfo($"Removed audio user listener username '{username}', call ID: {listener.CallIdListeningTo}");
                    }
                    else
                    {
                        _logger.LogWarning($"Failed to remove audio user listener with username '{username}', call ID: {listener?.CallIdListeningTo ?? 0}");
                    }
                }
                else
                {
                    _logger.LogWarning("Cannot remove audio listener because username was null or whitespace.");
                }

                if (audioData != null)
                {
                    audioData.DecrementListenerCt();

                    Int32 activeListenerCt = GetActiveListenerCt(callId);
                    if (activeListenerCt < 1)
                    {
                        SendMonitorEndRequest(callId, lineId, unitId, ani);

                        // No listeners left on current audio stream;
                        // Safe to dispose memory stream and remove audio data object from dictionary of active streams.
                        audioData.DisposeMemStream();

                        if (_audioStreams.TryRemove(callId, out AudioData _))
                        {
                            _logger.LogInfo($"Removed audio stream from stack, call ID: {callId}");
                        }
                        else
                        {
                            // ReSharper disable once ConstantNullCoalescingCondition
                            _logger.LogError($"Failed to remove audio data stream after disposing. Username: '******', call ID: {callId}");
                        }
                    }
                    else
                    {
                        _logger.LogInfo($"Active listener count of {activeListenerCt}. call ID: {callId}");
                    }
                }

                // Signal that output stream is done being written to and dispose.
                outputStream.Flush();
                outputStream.Dispose();
            }
        }