912 lines
31 KiB
C#
912 lines
31 KiB
C#
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Meta.Voice;
|
|
using Meta.Voice.Logging;
|
|
using Meta.WitAi.Configuration;
|
|
using Meta.WitAi.Data;
|
|
using Meta.WitAi.Data.Configuration;
|
|
using Meta.WitAi.Interfaces;
|
|
using Meta.WitAi.Json;
|
|
using Meta.WitAi.Requests;
|
|
|
|
namespace Meta.WitAi
|
|
{
|
|
/// <summary>
|
|
/// Manages a single request lifecycle when sending/receiving data from Wit.ai.
|
|
///
|
|
/// Note: This is not intended to be instantiated directly. Requests should be created with the
|
|
/// WitRequestFactory
|
|
/// </summary>
|
|
[LogCategory(LogCategory.Requests)]
|
|
public class WitRequest : VoiceServiceRequest, IAudioUploadHandler
|
|
{
|
|
#region PARAMETERS
|
|
/// <summary>
|
|
/// The wit Configuration to be used with this request
|
|
/// </summary>
|
|
public WitConfiguration Configuration { get; private set; }
|
|
/// <summary>
|
|
/// The request timeout in ms
|
|
/// </summary>
|
|
public int TimeoutMs => Options.TimeoutMs;
|
|
/// <summary>
|
|
/// Encoding settings for audio based requests
|
|
/// </summary>
|
|
public AudioEncoding AudioEncoding { get; set; }
|
|
[Obsolete("Deprecated for AudioEncoding")]
|
|
public AudioEncoding audioEncoding
|
|
{
|
|
get => AudioEncoding;
|
|
set => AudioEncoding = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Endpoint to be used for this request
|
|
/// </summary>
|
|
public string Path
|
|
{
|
|
get => _path;
|
|
set
|
|
{
|
|
if (_canSetPath)
|
|
{
|
|
_path = value;
|
|
}
|
|
else
|
|
{
|
|
Logger.Warning("Cannot set WitRequest.Path while after transmission.");
|
|
}
|
|
}
|
|
}
|
|
private string _path;
|
|
private bool _canSetPath = true;
|
|
|
|
/// <summary>
|
|
/// Final portion of the endpoint Path
|
|
/// </summary>
|
|
public string Command { get; private set; }
|
|
/// <summary>
|
|
/// Whether a post command should be called
|
|
/// </summary>
|
|
public bool IsPost { get; private set; }
|
|
/// <summary>
|
|
/// Key value pair that is sent as a query param in the Wit.ai uri
|
|
/// </summary>
|
|
[Obsolete("Deprecated for Options.QueryParams")]
|
|
public VoiceServiceRequestOptions.QueryParam[] queryParams
|
|
{
|
|
get
|
|
{
|
|
List<VoiceServiceRequestOptions.QueryParam> results = new List<VoiceServiceRequestOptions.QueryParam>();
|
|
foreach (var key in Options?.QueryParams?.Keys)
|
|
{
|
|
VoiceServiceRequestOptions.QueryParam p = new VoiceServiceRequestOptions.QueryParam()
|
|
{
|
|
key = key,
|
|
value = Options?.QueryParams[key]
|
|
};
|
|
results.Add(p);
|
|
}
|
|
return results.ToArray();
|
|
}
|
|
}
|
|
|
|
public byte[] postData;
|
|
public string postContentType;
|
|
public string forcedHttpMethodType = null;
|
|
|
|
// Decode in NLPRequest
|
|
protected override bool DecodeRawResponses => true;
|
|
#endregion PARAMETERS
|
|
|
|
#region REQUEST
|
|
/// <summary>
|
|
/// Returns true if the request is being performed
|
|
/// </summary>
|
|
public bool IsRequestStreamActive => IsActive || IsInputStreamReady;
|
|
/// <summary>
|
|
/// Returns true if the response had begun
|
|
/// </summary>
|
|
public bool HasResponseStarted { get; private set; }
|
|
/// <summary>
|
|
/// Returns true if the response had begun
|
|
/// </summary>
|
|
public bool IsInputStreamReady { get; private set; }
|
|
|
|
public AudioDurationTracker audioDurationTracker;
|
|
private HttpWebRequest _request;
|
|
private Stream _writeStream;
|
|
private object _streamLock = new object();
|
|
private int _bytesWritten;
|
|
private DateTime _requestStartTime;
|
|
private ConcurrentQueue<byte[]> _writeBuffer = new ConcurrentQueue<byte[]>();
|
|
#endregion REQUEST
|
|
|
|
#region RESULTS
|
|
/// <summary>
|
|
/// Simply return the Path to be called
|
|
/// </summary>
|
|
public override string ToString() => Path;
|
|
#endregion RESULTS
|
|
|
|
#region EVENTS
|
|
/// <summary>
|
|
/// Provides an opportunity to provide custom headers for the request just before it is
|
|
/// executed.
|
|
/// </summary>
|
|
public event OnProvideCustomHeadersEvent onProvideCustomHeaders;
|
|
public delegate Dictionary<string, string> OnProvideCustomHeadersEvent();
|
|
/// <summary>
|
|
/// Callback called when the server is ready to receive data from the WitRequest's input
|
|
/// stream. See WitRequest.Write()
|
|
/// </summary>
|
|
[Obsolete("Use OnInputStreamReady instead")]
|
|
public event Action<WitRequest> onInputStreamReady;
|
|
|
|
/// <summary>
|
|
/// Callback called when the server is ready to receive audio data from WitRequest's input stream.
|
|
/// </summary>
|
|
public Action OnInputStreamReady { get; set; }
|
|
|
|
/// <summary>
|
|
/// Returns the raw string response that was received before converting it to a JSON object.
|
|
///
|
|
/// NOTE: This response comes back on a different thread. Do not attempt ot set UI control
|
|
/// values or other interactions from this callback. This is intended to be used for demo
|
|
/// and test UI, not for regular use.
|
|
/// </summary>
|
|
[Obsolete("Deprecated for Events.OnRawResponse")]
|
|
public Action<string> onRawResponse;
|
|
|
|
/// <summary>
|
|
/// Provides an opportunity to customize the url just before a request executed
|
|
/// </summary>
|
|
[Obsolete("Deprecated for WitVRequest.OnProvideCustomUri")]
|
|
public OnCustomizeUriEvent onCustomizeUri;
|
|
public delegate Uri OnCustomizeUriEvent(UriBuilder uriBuilder);
|
|
/// <summary>
|
|
/// Allows customization of the request before it is sent out.
|
|
///
|
|
/// Note: This is for devs who are routing requests to their servers
|
|
/// before sending data to Wit.ai. This allows adding any additional
|
|
/// headers, url modifications, or customization of the request.
|
|
/// </summary>
|
|
public static PreSendRequestDelegate onPreSendRequest;
|
|
public delegate void PreSendRequestDelegate(ref Uri src_uri, out Dictionary<string,string> headers);
|
|
/// <summary>
|
|
/// Returns a partial utterance from an in process request
|
|
///
|
|
/// NOTE: This response comes back on a different thread.
|
|
/// </summary>
|
|
[Obsolete("Deprecated for Events.OnPartialTranscription")]
|
|
public event Action<string> onPartialTranscription;
|
|
/// <summary>
|
|
/// Returns a full utterance from a completed request
|
|
///
|
|
/// NOTE: This response comes back on a different thread.
|
|
/// </summary>
|
|
[Obsolete("Deprecated for Events.OnFullTranscription")]
|
|
public event Action<string> onFullTranscription;
|
|
|
|
/// <summary>
|
|
/// Callback called when a response is received from the server off a partial transcription
|
|
/// </summary>
|
|
[Obsolete("Deprecated for Events.OnPartialResponse")]
|
|
public event Action<WitRequest> onPartialResponse;
|
|
/// <summary>
|
|
/// Callback called when a response is received from the server
|
|
/// </summary>
|
|
[Obsolete("Deprecated for Events.OnComplete")]
|
|
public event Action<WitRequest> onResponse;
|
|
#endregion EVENTS
|
|
|
|
#region INITIALIZATION
|
|
/// <summary>
|
|
/// Initialize wit request with configuration & path to endpoint
|
|
/// </summary>
|
|
/// <param name="newConfiguration"></param>
|
|
/// <param name="newOptions"></param>
|
|
/// <param name="newEvents"></param>
|
|
public WitRequest(WitConfiguration newConfiguration, string newPath,
|
|
WitRequestOptions newOptions, VoiceServiceRequestEvents newEvents)
|
|
: base(NLPRequestInputType.Audio, newOptions, newEvents)
|
|
{
|
|
// Set Configuration & path
|
|
Configuration = newConfiguration;
|
|
Path = newPath;
|
|
|
|
// Finalize
|
|
_initialized = true;
|
|
SetState(VoiceRequestState.Initialized);
|
|
}
|
|
/// <summary>
|
|
/// Only set state if initialized
|
|
/// </summary>
|
|
private bool _initialized = false;
|
|
protected override void SetState(VoiceRequestState newState)
|
|
{
|
|
if (_initialized)
|
|
{
|
|
base.SetState(newState);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finalize initialization
|
|
/// </summary>
|
|
protected override void OnInit()
|
|
{
|
|
// Set request settings
|
|
Command = Path.Split('/').First();
|
|
IsPost = WitEndpointConfig.GetEndpointConfig(Configuration).Speech == this.Command
|
|
|| WitEndpointConfig.GetEndpointConfig(Configuration).Dictation == this.Command;
|
|
|
|
// Finalize bases
|
|
base.OnInit();
|
|
}
|
|
#endregion INITIALIZATION
|
|
|
|
#region AUDIO
|
|
// Handle audio activation
|
|
protected override void HandleAudioActivation()
|
|
{
|
|
SetAudioInputState(VoiceAudioInputState.On);
|
|
}
|
|
// Handle audio deactivation
|
|
protected override void HandleAudioDeactivation()
|
|
{
|
|
// If transmitting,
|
|
if (State == VoiceRequestState.Transmitting)
|
|
{
|
|
CloseRequestStream();
|
|
}
|
|
// Call deactivated
|
|
SetAudioInputState(VoiceAudioInputState.Off);
|
|
}
|
|
#endregion
|
|
|
|
#region REQUEST
|
|
//
|
|
private Thread _requestThread;
|
|
|
|
// Errors that prevent request submission
|
|
protected override string GetSendError()
|
|
{
|
|
// No configuration found
|
|
if (Configuration == null)
|
|
{
|
|
return "Configuration is not set. Cannot start request.";
|
|
}
|
|
// Cannot start without client access token
|
|
if (string.IsNullOrEmpty(Configuration.GetClientAccessToken()))
|
|
{
|
|
return "Client access token is not defined. Cannot start request.";
|
|
}
|
|
// Cannot perform without input stream delegate
|
|
if (OnInputStreamReady == null)
|
|
{
|
|
return "No input stream delegate found";
|
|
}
|
|
// Base
|
|
return base.GetSendError();
|
|
}
|
|
// Simple getter for final uri
|
|
private Uri GetUri()
|
|
{
|
|
// Get query parameters
|
|
Dictionary<string, string> queryParams = new Dictionary<string, string>(Options.QueryParams);
|
|
|
|
// Get uri using override
|
|
var uri = WitRequestSettings.GetUri(Configuration, Path, queryParams);
|
|
#pragma warning disable CS0618
|
|
if (onCustomizeUri != null)
|
|
{
|
|
#pragma warning disable CS0618
|
|
uri = onCustomizeUri(new UriBuilder(uri));
|
|
}
|
|
|
|
// Return uri
|
|
return uri;
|
|
}
|
|
// Simple getter for final uri
|
|
private Dictionary<string, string> GetHeaders()
|
|
{
|
|
// Get default headers
|
|
Dictionary<string, string> headers = WitRequestSettings.GetHeaders(Configuration, Options, false);
|
|
|
|
// Append additional headers
|
|
if (onProvideCustomHeaders != null)
|
|
{
|
|
foreach (OnProvideCustomHeadersEvent e in onProvideCustomHeaders.GetInvocationList())
|
|
{
|
|
Dictionary<string, string> customHeaders = e();
|
|
if (customHeaders != null)
|
|
{
|
|
foreach (var key in customHeaders.Keys)
|
|
{
|
|
headers[key] = customHeaders[key];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return headers
|
|
return headers;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start the async request for data from the Wit.ai servers
|
|
/// </summary>
|
|
protected override void HandleSend()
|
|
{
|
|
// Begin
|
|
HasResponseStarted = false;
|
|
|
|
// Generate results
|
|
_bytesWritten = 0;
|
|
_requestStartTime = DateTime.UtcNow;
|
|
|
|
#if UNITY_WEBGL && !UNITY_EDITOR || WEBGL_DEBUG
|
|
SetupSend(out Uri uri, out Dictionary<string, string> headers);
|
|
StartUnityRequest(uri, headers);
|
|
#else
|
|
#if UNITY_WEBGL && UNITY_EDITOR
|
|
if (IsPost)
|
|
{
|
|
_log.Warning("Voice input is not supported in WebGL this functionality is fully enabled at edit time, but may not work at runtime.");
|
|
}
|
|
#endif
|
|
|
|
// Run on background thread
|
|
_requestThread = new Thread(async () => await StartThreadedRequest(Logger.CorrelationID));
|
|
_requestThread.Start();
|
|
#endif
|
|
}
|
|
|
|
private void SetupSend(out Uri uri, out Dictionary<string, string> headers, CorrelationID correlationID)
|
|
{
|
|
Logger.CorrelationID = correlationID;
|
|
|
|
// Get uri & prevent further path changes
|
|
uri = GetUri();
|
|
_canSetPath = false;
|
|
|
|
Logger.Verbose(correlationID, "Setup request with URL: {0}", uri);
|
|
|
|
// Get headers
|
|
headers = GetHeaders();
|
|
|
|
// Allow overrides
|
|
onPreSendRequest?.Invoke(ref uri, out headers);
|
|
}
|
|
#endregion REQUEST
|
|
|
|
#region HTTP REQUEST
|
|
/// <summary>
|
|
/// Performs a threaded http request
|
|
/// </summary>
|
|
private async Task StartThreadedRequest(CorrelationID correlationID)
|
|
{
|
|
// Get uri & headers
|
|
SetupSend(out Uri uri, out Dictionary<string, string> headers, correlationID);
|
|
|
|
// Create http web request
|
|
_request = WebRequest.Create(uri.AbsoluteUri) as HttpWebRequest;
|
|
Logger.Verbose("Created web request: {0}", _request?.RequestUri.AbsoluteUri);
|
|
|
|
// Off to not wait for a response indefinitely
|
|
_request.KeepAlive = false;
|
|
|
|
// Configure request method, content type & chunked
|
|
if (forcedHttpMethodType != null)
|
|
{
|
|
_request.Method = forcedHttpMethodType;
|
|
}
|
|
if (null != postContentType)
|
|
{
|
|
if (forcedHttpMethodType == null)
|
|
{
|
|
_request.Method = "POST";
|
|
}
|
|
_request.ContentType = postContentType;
|
|
_request.ContentLength = postData.Length;
|
|
}
|
|
if (IsPost)
|
|
{
|
|
_request.Method = string.IsNullOrEmpty(forcedHttpMethodType) ? "POST" : forcedHttpMethodType;
|
|
_request.ContentType = AudioEncoding.ToString();
|
|
_request.SendChunked = true;
|
|
}
|
|
|
|
// Apply user agent
|
|
if (headers.ContainsKey(WitConstants.HEADER_USERAGENT))
|
|
{
|
|
_request.UserAgent = headers[WitConstants.HEADER_USERAGENT];
|
|
headers.Remove(WitConstants.HEADER_USERAGENT);
|
|
}
|
|
// Apply all other headers
|
|
foreach (var key in headers.Keys)
|
|
{
|
|
_request.Headers[key] = headers[key];
|
|
}
|
|
|
|
// Handle timeout on background thread
|
|
ThreadUtility.BackgroundAsync(Logger, WaitForTimeout).WrapErrors();
|
|
_request.Timeout = -1;
|
|
|
|
// If post or put, get post stream & wait for completion
|
|
if (_request.Method == "POST" || _request.Method == "PUT")
|
|
{
|
|
var getPostStream = _request.BeginGetRequestStream(HandleWriteStream, _request);
|
|
await TaskUtility.FromAsyncResult(getPostStream);
|
|
}
|
|
|
|
// Cancellation
|
|
if (_request == null)
|
|
{
|
|
MainThreadCallback(() => HandleFailure(WitConstants.ERROR_CODE_GENERAL, "Request canceled prior to start"));
|
|
return;
|
|
}
|
|
|
|
// Get response stream & wait for completion
|
|
var getResponseStream = _request.BeginGetResponse(HandleResponse, _request);
|
|
await TaskUtility.FromAsyncResult(getResponseStream);
|
|
}
|
|
|
|
// Handle timeout callback
|
|
private DateTime _timeoutLastUpdate;
|
|
private DateTime GetLastUpdate() => _timeoutLastUpdate;
|
|
private async Task WaitForTimeout()
|
|
{
|
|
// Await specified timeout
|
|
_timeoutLastUpdate = DateTime.UtcNow;
|
|
await TaskUtility.WaitForTimeout(TimeoutMs, GetLastUpdate);
|
|
|
|
// Ignore if no longer active
|
|
if (!IsActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get path for request uri
|
|
string path = "";
|
|
if (null != _request?.RequestUri?.PathAndQuery)
|
|
{
|
|
var uriSections = _request.RequestUri.PathAndQuery.Split(new char[] { '?' });
|
|
path = uriSections[0].Substring(1);
|
|
}
|
|
|
|
// Get error
|
|
var error = $"Request [{path}] timed out after {(DateTime.UtcNow - _timeoutLastUpdate).TotalMilliseconds:0.0} ms";
|
|
|
|
// Call error
|
|
MainThreadCallback(() => HandleFailure(WitConstants.ERROR_CODE_TIMEOUT, error));
|
|
|
|
// Clean up the current request if it is still going
|
|
if (null != _request)
|
|
{
|
|
_request.Abort();
|
|
}
|
|
|
|
// Close any open stream resources and clean up streaming state flags
|
|
CloseActiveStream();
|
|
}
|
|
|
|
// Write stream
|
|
private void HandleWriteStream(IAsyncResult ar)
|
|
{
|
|
try
|
|
{
|
|
// Get write stream
|
|
var stream = _request.EndGetRequestStream(ar);
|
|
|
|
// Got write stream
|
|
_bytesWritten = 0;
|
|
|
|
// Immediate post
|
|
if (postData != null && postData.Length > 0)
|
|
{
|
|
_bytesWritten += postData.Length;
|
|
stream.Write(postData, 0, postData.Length);
|
|
stream.Close();
|
|
}
|
|
// Wait for input stream
|
|
else
|
|
{
|
|
// Request stream is ready to go
|
|
IsInputStreamReady = true;
|
|
_writeStream = stream;
|
|
|
|
// Call input stream ready delegate
|
|
if (OnInputStreamReady != null)
|
|
{
|
|
MainThreadCallback(() =>
|
|
{
|
|
onInputStreamReady?.Invoke(this);
|
|
OnInputStreamReady();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
catch (WebException e)
|
|
{
|
|
// Ignore cancelation errors & if error already occured
|
|
if (e.Status == WebExceptionStatus.RequestCanceled
|
|
|| e.Status == WebExceptionStatus.Timeout
|
|
|| StatusCode != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Write stream error
|
|
MainThreadCallback(() => HandleFailure((int) e.Status, e.ToString()));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// Call an error if have not done so yet
|
|
if (StatusCode != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Non web error occured
|
|
MainThreadCallback(() => HandleFailure(WitConstants.ERROR_CODE_GENERAL, e.ToString()));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write request data to the Wit.ai post's body input stream
|
|
///
|
|
/// Note: If the stream is not open (IsActive) this will throw an IOException.
|
|
/// Data will be written synchronously. This should not be called from the main thread.
|
|
/// </summary>
|
|
/// <param name="data"></param>
|
|
/// <param name="offset"></param>
|
|
/// <param name="length"></param>
|
|
public void Write(byte[] data, int offset, int length)
|
|
{
|
|
// Ignore without write stream
|
|
if (!IsInputStreamReady || data == null || length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_writeStream.Write(data, offset, length);
|
|
_bytesWritten += length;
|
|
if (audioDurationTracker != null)
|
|
{
|
|
audioDurationTracker.AddBytes(length);
|
|
}
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
_writeStream = null;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Perform a cancellation if still waiting for a post
|
|
if (WaitingForPost())
|
|
{
|
|
MainThreadCallback(() => Cancel("Stream was closed with no data written."));
|
|
}
|
|
}
|
|
|
|
// Handles response from server
|
|
private void HandleResponse(IAsyncResult asyncResult)
|
|
{
|
|
// Begin response
|
|
HasResponseStarted = true;
|
|
string stringResponse = "";
|
|
|
|
// Status code
|
|
int statusCode = (int)HttpStatusCode.OK;
|
|
string error = null;
|
|
|
|
try
|
|
{
|
|
// Get response
|
|
using (var response = _request.EndGetResponse(asyncResult))
|
|
{
|
|
// Got response
|
|
HttpWebResponse httpResponse = response as HttpWebResponse;
|
|
|
|
// Apply status & description
|
|
int newStatus = (int)httpResponse.StatusCode;
|
|
if (statusCode != newStatus)
|
|
{
|
|
statusCode = newStatus;
|
|
error = httpResponse.StatusDescription;
|
|
}
|
|
// Decode response stream
|
|
else
|
|
{
|
|
using (var responseStream = httpResponse.GetResponseStream())
|
|
{
|
|
stringResponse = ProcessStreamResponses(responseStream);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (JSONParseException e)
|
|
{
|
|
statusCode = WitConstants.ERROR_CODE_INVALID_DATA_FROM_SERVER;
|
|
error = $"Server returned invalid data.\n\n{e}";
|
|
}
|
|
catch (WebException e)
|
|
{
|
|
if (e.Status != WebExceptionStatus.RequestCanceled
|
|
&& e.Status != WebExceptionStatus.Timeout)
|
|
{
|
|
// Apply status & error
|
|
statusCode = (int) e.Status;
|
|
error = e.ToString();
|
|
|
|
// Attempt additional parse
|
|
if (e.Response is HttpWebResponse errorResponse)
|
|
{
|
|
statusCode = (int) errorResponse.StatusCode;
|
|
try
|
|
{
|
|
using (var errorStream = errorResponse.GetResponseStream())
|
|
{
|
|
if (errorStream != null)
|
|
{
|
|
using (StreamReader errorReader = new StreamReader(errorStream))
|
|
{
|
|
stringResponse = errorReader.ReadToEnd();
|
|
if (!string.IsNullOrEmpty(stringResponse))
|
|
{
|
|
ProcessStringResponses(stringResponse);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (JSONParseException)
|
|
{
|
|
// Response wasn't encoded error, ignore it.
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// We've already caught that there is an error, we'll ignore any errors
|
|
// reading error response data and use the status/original error for validation
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
statusCode = WitConstants.ERROR_CODE_GENERAL;
|
|
error = e.ToString();
|
|
}
|
|
|
|
// Close request stream if possible
|
|
CloseRequestStream();
|
|
|
|
// Done
|
|
HasResponseStarted = false;
|
|
|
|
// Ignore if no longer active
|
|
if (!IsActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get error
|
|
if (statusCode != (int)HttpStatusCode.OK
|
|
&& !string.IsNullOrEmpty(stringResponse))
|
|
{
|
|
WitResponseNode decode = JsonConvert.DeserializeToken(stringResponse);
|
|
if (decode != null && decode.AsObject.HasChild(WitConstants.ENDPOINT_ERROR_PARAM))
|
|
{
|
|
error = decode[WitConstants.ENDPOINT_ERROR_PARAM].Value;
|
|
}
|
|
}
|
|
|
|
// Final callbacks
|
|
MainThreadCallback(() =>
|
|
{
|
|
// Handle failure
|
|
if (statusCode != (int)HttpStatusCode.OK)
|
|
{
|
|
HandleFailure(statusCode, error);
|
|
}
|
|
// No response
|
|
else if (ResponseData == null && !IsDecoding)
|
|
{
|
|
error = $"Server did not return a valid json response.";
|
|
#if UNITY_EDITOR
|
|
error += $"\nActual Response\n{stringResponse}";
|
|
#endif
|
|
HandleFailure(error);
|
|
}
|
|
// Success
|
|
else
|
|
{
|
|
MakeLastResponseFinal();
|
|
}
|
|
});
|
|
}
|
|
// Read stream until delimiter is hit
|
|
private string ProcessStreamResponses(Stream stream)
|
|
{
|
|
using (var reader = new StreamReader(stream))
|
|
{
|
|
StringBuilder builder = new StringBuilder();
|
|
while (!reader.EndOfStream)
|
|
{
|
|
// Read to buffer length
|
|
var chunk = reader.ReadLine();
|
|
if (string.IsNullOrEmpty(chunk))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Append Line
|
|
builder.Append(chunk);
|
|
|
|
// Assumes formatted json is returned and no spacing for end of json string
|
|
if (string.Equals(chunk, "}"))
|
|
{
|
|
ProcessStringResponse(builder.ToString());
|
|
builder.Clear();
|
|
}
|
|
}
|
|
if (builder.Length > 0)
|
|
{
|
|
ProcessStringResponse(builder.ToString());
|
|
return builder.ToString();
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
// Process individual piece
|
|
private void ProcessStringResponses(string stringResponse)
|
|
{
|
|
// Split by delimiter
|
|
foreach (var stringPart in stringResponse.Split(new string[]{WitConstants.ENDPOINT_JSON_DELIMITER}, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
ProcessStringResponse(stringPart);
|
|
}
|
|
}
|
|
// Handles raw response
|
|
private void ProcessStringResponse(string stringResponse)
|
|
{
|
|
_timeoutLastUpdate = DateTime.UtcNow;
|
|
HandleRawResponse(stringResponse, false);
|
|
}
|
|
// On raw response callback, ensure on main thread
|
|
protected override void OnRawResponse(string rawResponse)
|
|
{
|
|
MainThreadCallback(() =>
|
|
{
|
|
base.OnRawResponse(rawResponse);
|
|
onRawResponse?.Invoke(rawResponse);
|
|
});
|
|
}
|
|
// On text change callback
|
|
protected override void OnPartialTranscription()
|
|
{
|
|
base.OnPartialTranscription();
|
|
onPartialTranscription?.Invoke(Transcription);
|
|
}
|
|
protected override void OnFullTranscription()
|
|
{
|
|
base.OnFullTranscription();
|
|
onFullTranscription?.Invoke(Transcription);
|
|
}
|
|
// On response data change callback
|
|
protected override void OnPartialResponse(WitResponseNode responseNode)
|
|
{
|
|
base.OnPartialResponse(responseNode);
|
|
onPartialResponse?.Invoke(this);
|
|
}
|
|
// Check if data has been written to post stream while still receiving data
|
|
private bool WaitingForPost()
|
|
{
|
|
return IsPost && _bytesWritten == 0 && StatusCode == 0;
|
|
}
|
|
// Check if any data has been written
|
|
protected override bool HasSentAudio() => IsPost && _bytesWritten > 0;
|
|
// Close active stream & then abort if possible
|
|
private void CloseRequestStream()
|
|
{
|
|
// Cancel due to no audio if not an error
|
|
if (WaitingForPost())
|
|
{
|
|
Cancel("Request was closed with no audio captured.");
|
|
}
|
|
// Close
|
|
else
|
|
{
|
|
CloseActiveStream();
|
|
}
|
|
}
|
|
// Close stream
|
|
private void CloseActiveStream()
|
|
{
|
|
// No longer ready
|
|
IsInputStreamReady = false;
|
|
// Close write stream
|
|
lock (_streamLock)
|
|
{
|
|
if (null != _writeStream)
|
|
{
|
|
try
|
|
{
|
|
_writeStream.Close();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Warning("Write Stream - Close Failed\n{0}", e);
|
|
}
|
|
_writeStream = null;
|
|
}
|
|
}
|
|
// Abort request thread
|
|
if (_requestThread != null)
|
|
{
|
|
_requestThread.Abort();
|
|
_requestThread = null;
|
|
}
|
|
}
|
|
|
|
// Perform a cancellation/abort
|
|
protected override void HandleCancel()
|
|
{
|
|
// Close stream
|
|
CloseActiveStream();
|
|
|
|
// Abort request
|
|
if (null != _request)
|
|
{
|
|
_request.Abort();
|
|
_request = null;
|
|
}
|
|
}
|
|
|
|
// Add response callback & log for abort
|
|
protected override void OnComplete()
|
|
{
|
|
base.OnComplete();
|
|
|
|
// Close write stream if still existing
|
|
if (null != _writeStream)
|
|
{
|
|
CloseActiveStream();
|
|
}
|
|
// Abort request if still existing
|
|
if (null != _request)
|
|
{
|
|
_request.Abort();
|
|
_request = null;
|
|
}
|
|
|
|
// Finalize response
|
|
onResponse?.Invoke(this);
|
|
onResponse = null;
|
|
}
|
|
#endregion HTTP REQUEST
|
|
}
|
|
}
|