/// <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(); } }