void _u_ReceiveResponse() { responsesReceived++; // Requies convention public variable and event names UdonSharpBehaviour usb = connectionRequesters[currentID]; if (connectionIsWebSocket[currentID]) { // First byte is the text/binary flag, with highest bit indiciating // that this is a dummy 'connection closed' response. Second highest // bit indicates a websocket opened message. if ((connectionData[currentID][0] & 0x40) == 0x40) { usb.SetProgramVariable("connectionID", currentID); usb.SendCustomEvent("_u_WebSocketOpened"); } else if ((connectionData[currentID][0] & 0x80) == 0x80) { usb.SetProgramVariable("connectionID", currentID); usb.SendCustomEvent("_u_WebSocketClosed"); connectionRequesters[currentID] = null; connectionsOpen--; _u_SendCallback("_u_OnUdonMIDIWebHandlerConnectionCountChanged"); } else { usb.SetProgramVariable("connectionID", currentID); bool messageIsText = (connectionData[currentID][0] & 0x1) == 0x0; byte[] messsageData = new byte[connectionData[currentID].Length - 1]; Array.Copy(connectionData[currentID], 1, messsageData, 0, connectionData[currentID].Length - 1); usb.SetProgramVariable("messageIsText", messageIsText); if (messageIsText && connectionReturnsStrings[currentID]) { usb.SetProgramVariable("connectionString", new string(connectionDataChars[currentID])); } else { usb.SetProgramVariable("connectionData", messsageData); } usb.SendCustomEvent("_u_WebSocketReceive"); } } else { // Non-websocket response // First 4 bytes of data are an int for HTTP response code or request error code int responseCode = _u_BitConverterToInt32(connectionData[currentID], 0); byte[] responseData = new byte[connectionData[currentID].Length - 4]; Array.Copy(connectionData[currentID], 4, responseData, 0, connectionData[currentID].Length - 4); // Process loopback connection if (currentID == LOOPBACK_CONNECTION_ID) { string connectionString = new string(connectionDataChars[currentID]); // responseCode determines what kind of message it is, rather than typical HTTP // or extra Udon handler response codes. if (responseCode == 0) // ping response { if (online == false) { online = true; playersOnline++; _u_SendCallback("_u_OnUdonMIDIWebHandlerOnlineChanged"); } awaitingPong = false; framesSinceAwaitingPong = 0; string[] split = connectionString.Split(' '); queuedResponsesCount = Int32.Parse(split[0]); queuedBytesCount = Int32.Parse(split[1]); } else if (responseCode == 1) // avatar change { string[] split = connectionString.Split('\n'); _u_SendAvatarChangedCallback(split[0], split[1]); } } else { usb.SetProgramVariable("connectionID", currentID); usb.SetProgramVariable("responseCode", responseCode); if (connectionReturnsStrings[currentID]) { usb.SetProgramVariable("connectionString", new string(connectionDataChars[currentID])); } else { usb.SetProgramVariable("connectionData", responseData); } usb.SendCustomEvent("_u_WebRequestReceived"); connectionRequesters[currentID] = null; connectionsOpen--; _u_SendCallback("_u_OnUdonMIDIWebHandlerConnectionCountChanged"); } } // Reset response arary connectionData[currentID] = null; connectionDataOffsets[currentID] = 0; }
void Update() { secondsSinceLastPing += Time.deltaTime; if (secondsSinceLastPing > PING_INTERVAL_SECONDS) { // Ping the helper program using a dedicated command, on a dedicated connection ID. // The loopback response from this has priority over all 255 other connections. // This is done on a dedicated connectionID to ensure it is received like all other // MIDI frames, and to make sure there's always a free, prioritized line for auxiliary data. Debug.Log("[Udon-MIDI-Web-Helper] PING"); commandsSent++; secondsSinceLastPing = 0; awaitingPong = true; } if (awaitingPong) { // Measure time since ping response in number of frames, as the response should be // nearly instantaneous. VRChat may lag and drop connection for a while, but the // helper program never should. If VRChat is running successfully and pong isn't // received, its safe to assume the connection is dead. if (++framesSinceAwaitingPong > PONG_TIMEOUT_FRAMES && online) { online = false; playersOnline--; _u_SendCallback("_u_OnUdonMIDIWebHandlerOnlineChanged"); // If the handler goes offline, there is no hope of reconnecting until restart. // Cancel/close all open connections with a failure response code. for (int i = 0; i < MAX_USABLE_CONNECTIONS; i++) { if (connectionRequesters[i] != null) { UdonSharpBehaviour usb = connectionRequesters[i]; usb.SetProgramVariable("connectionID", i); if (connectionIsWebSocket[i]) { usb.SendCustomEvent("_u_WebSocketClosed"); } else { usb.SetProgramVariable("responseCode", RESPONSE_CODE_DISCONNECTED); usb.SendCustomEvent("_u_WebRequestReceived"); } connectionRequesters[i] = null; connectionsOpen--; _u_SendCallback("_u_OnUdonMIDIWebHandlerConnectionCountChanged"); } } } } // VRChat's update order for Udon appears to go: Update(), LateUpdate(), MidiNoteX(), at least // with VRC Midi Listener after Udon Behaviour on the same gameobject. // Therefore, it should be safe to print a READY or ACK each frame without waiting a game tick. // (This whole protocol is built around VRChat crashing if more than 1 midi frame of data is recevied per game tick) // Hopefully this means the Log-MIDI communication is fast enough to send a new frame // before the game loops around again, ideally resulting in one MIDI frame every game tick. if (currentFrameOffset == usableBytesThisFrame) { Debug.Log("[Udon-MIDI-Web-Helper] ACK"); currentFrameOffset = 0; secondsSinceLastReady = 0; // Don't send a ready, ACK doubles as a ready } secondsSinceLastReady += Time.deltaTime; if (secondsSinceLastReady > READY_TIMEOUT_SECONDS) { // Only send a RDY exactly READY_TIMEOUT_SECONDS after at least one connection was opened Debug.Log("[Udon-MIDI-Web-Helper] READY"); // Ready message lets the helper know its safe to send a new frame OR if a frame was dropped and never received secondsSinceLastReady = 0; } }