Forum Discussion
patmw
5 years agoExplorer
Data Streaming from Unity VR app to mobile (iOS/android/web) client
Hi there,
I'm currently working on a VR app for a client, which will run on the Oculus Quest/Quest2. One of the base requirements is that the VR app can be interacted with from an external client application through two-way communication.
The mobile client should be able to trigger certain events within the Quest app - for example, pause/continue/next scene/trigger sound, etc.
The mobile client should also be able to receive data from the Quest app - things such as the user's performance metrics in a certain part of the experience.
In my initial research, I've stumbled upon several options: UDP/threading/OSC/PUN/Mirror/Normcore/WebRTC, and I'm feeling slightly overwhelmed (I have no solid networking experience in the past).
If possible I would like to avoid some of the heavier networking frameworks, as I want to avoid having to refactor all my code to accommodate for these. There isn't necessarily a need for full synchronization of game-states like there would on a real-time multiplayer game, only small custom data packets.
Any guidance would be massively appreciated!
I'm way in over my head at the moment.
Thanks in advance.
Pat
23 Replies
Replies have been turned off for this discussion
- brazzoniProtege
I would really appreciate any guidance or referrals to documentation for this as well. I've looked into the peer-to-peer networking documentation. That looks easy enough, but my interpretation of it is that it is only to be used with other headsets.
In unity, there is https://docs-multiplayer.unity3d.com/. The documentation is great, so I'm considering it. However, I think the path of least resistance would be to create your mobile app as a 'unity game' as well. I've been searching for a few days, and it doesn't seem like Headset -> Mobile App is a well trodden path. It would be awesome if API used for the Oculus Mobile App was publicly released, or something like that!
Am I missing anything?
- brazzoniProtege
The peer-to-peer networking looked really promising, and it would be easy to pick up. I got lost trying to figure out what a peerID is, and whether or not I could create a peerID through a mobile app.
- patmwExplorer
Hi brazzoni!
Appreciate your input here. The way I got around it was, as you said, to build a mobile app within unity as well. The networking solution I went with was less graceful, I ended up implementing a custom UDP/TCP client-server system - the mobile app acting as a server and the VR app acting as a client - the implementation I went with was based off the following YouTube tutorial series:
https://youtube.com/playlist?list=PLXkn83W0QkfnqsK8I0RAz5AbUxfg3bOQ5
I recommend this approach as it worked for me, however you may find that Unity's built-in networking is an easier solution (it was released officially after I finished this project so I didn't get chance to try it myself)
Hope this helps!
Best,
Patrick
- RossBallanHonored Guest
Hey how's it going everyone.
I am also attempting to do this as well. Any luck on getting this working using Unity's networking?
- brazzoniProtege
I've abandoned trying to do it with Unity's networking. I can make TCP connections between a react-native mobile and a unity build for the headset. I had to hack it together by logging the IP address of my headset and entering it into the mobile app. I'm struggling to find a good way to discover devices on a network. It would be awesome if I could use iOS's Bonjour or Androids NSD, but I'm kinda blocked on how I might run the Java code required for Androids NSD because I couldn't find any C# libraries for it.
I made an algorithm that discovers devices on a local network that are listening to a certain port. I got the IP address and Subnet from Node.js (os module). Then, used npm's NetMask to find the network address and I just iterated through every available address attempting to make connections. The next step would've been sending a handshake, so I can tell if the listener was a device that I programmed.
This is a bad system though because you're not guaranteed to be allowed to listen on the same port every time the app runs, and I worry about how flexible this system is on networks with different configurations.
- RossBallanHonored Guest
Thanks for sharing! I am currently trying to set up a local connection were 360 videos playing on a HMD device are casted unto an app on an android device (table or phone).
This is probably less compilated than what you guys are doing but I'll update if I get this to work properly on my end 🙂
- patmwExplorer
Hi guys,
The fella who created the original series for c# networking which I used for my custom TCP/UDP implementation has recently created a new open source library for networking with c# which seems to shortcut the process massively! If I were doing this again now then I would definitely take this approach over writing it completely from scratch.
https://github.com/tom-weiland/RiptideNetworking
Hope this helps anyone discovering this topic
- rossana.terraccianoExplorer
Hi guys!! this is so interesting! Do you guys know how can I create a tcp communication between my oculus and a python client on my pc? Thank you so much!! I don't even know where to start 😕
- brazzoniProtege
It depends a bit on what your end goal is. For you, it could be as simple as just manually entering in the IP address every time you need the devices to connect, or you could configure your router to give those devices static IP addresses.
The TCP connection is as simple as a server opening a port, and a client making a connection to the server's ip address and port.
My final iteration of device discovery over TCP was using Unity's old network discovery (their new networking library has some sample code for network discovery, but it wasn't plug and play like this older one is)
The app on my headset made the device a server, so it opened a listener on a port. I used the network discovery to broadcast/multicast my ip address and port. My tablet/pc app would then listen for the broadcast and the UI had a list of "discovered devices" to connect to.
Some issues with this are that Oculus' VRC checks, their submission guidelines, do not allow changing wifi state, and the network discovery requires CHANGE_WIFI_MULTICAST_STATE, so publicly providing the app on their app store would be met with resistance.
I'm now using a Node.js server on aws with Websockets instead! It's more similar to Facebooks own relay servers.- brazzoniProtege
Some of my potential clients had networks that blocked Multicasting.
The cloud solution is nice because I now have a static domain name that all devices know to connect to. The sockets are really similar to TCP sockets.
- rossana.terraccianoExplorer
Thank you so much!! It was really helpful!! Do you know by chance any example on the internet which includes a TCPlistener in C# that could be useful for me to really understand how to move the first steps? I basically have to connect a Python socket from another pc in the room to my TCPlistener in my Oculus Quest2.
Thank you for all the suggestions regarding the submission, but this application won't go on the Quest Store 🙂 it's just a learning exercise 🙂
- brazzoniProtege
I think their example would frequently block in Unity though. Causing your headset/desktop app to freeze. I'll share some bits of my code. It's a grouping of a few different solutions from online, and I can't find them anymore. This is contrived and shortened from my code because it contains another layer of command encapsulation that would be too much to breakdown in a few minutes:
using System.Collections; using System.Collections.Generic; using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using UnityEngine; public class CommandServer : MonoBehaviour { private Int32 port; // The port assigned to this listener by the OS. private IPAddress IPV4Address; TcpListener server; private bool isListening; // true if CommandServer is currently listening on a port. private const float LISTEN_INTERVAL = .3f; // this will be a bottlneck if we get more than 3 commands a second. void Start() { StartListener(); StartCoroutine(Listen()); } private void StartListener() { string ipv4 = IPManager.GetIP(ADDRESSFAM.IPv4); try { IPAddress localAddr = IPAddress.Parse(ipv4); // TcpListener server = new TcpListener(port); server = new TcpListener(localAddr, 0); // 0 for port means OS will assign port for us // Start listening for client requests. server.Start(); isListening = true; setPort(server); // make new port publicly accessible, so it can be included in broadcasts. setIPV4Address(localAddr); // making new IPV4 address visible for the same reason. Debug.Log("Binded to Port: " + port); Debug.Log("At IPV4: " + IPV4Address); } catch (FormatException e) // will happen if the IP is invalid. { Debug.LogException(e); Debug.Log("IPAddress: " + ipv4); } } private IEnumerator Listen() { while (true) { try { // Waiting for a connection if (server.Pending()) // this is so we only AcceptTcpClient if there is one ready { TcpClient client = server.AcceptTcpClient(); //StartCoroutine(ExchangeCommands(client)); // my protocol was to read one command then send a response. } } catch (SocketException e) { Debug.Log("SocketException: " + e); StopListener(); StartListener(); } catch (System.InvalidOperationException e) // will happen if we call server.Pending() before server is properly started i.e. StartListener fails. { Debug.Log("InvalidOperationException: " + e); StopListener(); StartListener(); } catch (NullReferenceException e) // I've seen this happen once, and it was because the server somehow got set to null { StopListener(); StartListener(); } yield return new WaitForSecondsRealtime(LISTEN_INTERVAL); // look for a new connection every LISTEN_INTERVAL seconds. } } private void StopListener() { try { server.Stop(); isListening = false; } catch (SocketException e) { Debug.Log("SocketException: " + e); } catch (NullReferenceException e) // server was never successfully initialized { Debug.LogException(e); } } public Int32 getPort() { return port; } public string getIPV4Address() { return IPV4Address.ToString(); } private void setIPV4Address(IPAddress IPV4Address) { this.IPV4Address = IPV4Address; // external objects cannot change value of private IPV4Address, so give them a string. } private void setPort(TcpListener server) { IPEndPoint localEndPoint = (IPEndPoint)server.LocalEndpoint; port = localEndPoint.Port; } public bool getIsListening() { return isListening; } private void OnDestroy() { StopListener(); } }Now this example doesn't bind to one specific port. You're not technically guaranteed to have access to the same port every time, so my server would bind to any available port and broadcast its signature using UNet Network Discovery. You'll need the ip address and the port this listener binds too. I've made it log to the console, so you can manually enter it into your python app to start.
Edit: Added Using Statements
- brazzoniProtege
If you're using unity, you're going to be working with the .net framework: microsoft docs for tcp listener
Those dos will be your best friend. They're some of the best docs out there really. They don't always provide examples, but they did on that page. The TCPClient has an example too, and I believe they were built to work with eachother. I'd try to understand how the example works, so you could then write your own tcpclient in python.
- rossana.terraccianoExplorer
Thank you so much!! This is awesome! I will try it soon with my python app and let you know!!!
- rossana.terraccianoExplorer
I think this part of the code is missing, correct?
public class IPManager { public static string GetIP(ADDRESSFAM Addfam) { //Return null if ADDRESSFAM is Ipv6 but Os does not support it if (Addfam == ADDRESSFAM.IPv6 && !Socket.OSSupportsIPv6) { return null; } string output = ""; foreach (NetworkInterface item in NetworkInterface.GetAllNetworkInterfaces()) { #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN NetworkInterfaceType _type1 = NetworkInterfaceType.Wireless80211; NetworkInterfaceType _type2 = NetworkInterfaceType.Ethernet; if ((item.NetworkInterfaceType == _type1 || item.NetworkInterfaceType == _type2) && item.OperationalStatus == OperationalStatus.Up) #endif { foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses) { //IPv4 if (Addfam == ADDRESSFAM.IPv4) { if (ip.Address.AddressFamily == AddressFamily.InterNetwork) { output = ip.Address.ToString(); } } //IPv6 else if (Addfam == ADDRESSFAM.IPv6) { if (ip.Address.AddressFamily == AddressFamily.InterNetworkV6) { output = ip.Address.ToString(); } } } } } return output; } } public enum ADDRESSFAM { IPv4, IPv6 }
- brazzoniProtege
Yes, good detective work. I forgot that wasn't a native library sorry! I changed one thing. I can't remember what conditions caused this, but periodically it would return a loopback address, and this won't work if you're trying to do inter computer communication. I just added one line under:
if ((item.NetworkInterfaceType == _type1 || item.NetworkInterfaceType == _type2) && item.OperationalStatus == OperationalStatus.Up)outside of the #endif
using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; public class IPManager { // loopback interfaces are skipped. // returns the interface belonging to IPV4, if multiple interfaces exist, the last interface found gets returned. public static string GetIP(ADDRESSFAM Addfam) { //Return null if ADDRESSFAM is Ipv6 but Os does not support it if (Addfam == ADDRESSFAM.IPv6 && !Socket.OSSupportsIPv6) { return null; } string output = ""; foreach (NetworkInterface item in NetworkInterface.GetAllNetworkInterfaces()) { #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN NetworkInterfaceType _type1 = NetworkInterfaceType.Wireless80211; NetworkInterfaceType _type2 = NetworkInterfaceType.Ethernet; // test for wireless or ethernet if ((item.NetworkInterfaceType == _type1 || item.NetworkInterfaceType == _type2) && item.OperationalStatus == OperationalStatus.Up) #endif if (item.NetworkInterfaceType != NetworkInterfaceType.Loopback) // skip loopback { { foreach (UnicastIPAddressInformation ip in item.GetIPProperties().UnicastAddresses ) { //IPv4 if (Addfam == ADDRESSFAM.IPv4) { if ( ip.Address.AddressFamily == AddressFamily.InterNetwork ) { output = ip.Address.ToString(); } } //IPv6 else if (Addfam == ADDRESSFAM.IPv6) { if ( ip.Address.AddressFamily == AddressFamily.InterNetworkV6 ) { output = ip.Address.ToString(); } } } } } } return output; } } public enum ADDRESSFAM { IPv4, IPv6 }Please excuse the formatting. I was using vscode's 'prettier' before I realized the path of least resistance is visual studio.
- rossana.terraccianoExplorer
Thank you so much!! I didn't try it yet on my oculus quest 2, but I did try my python socket with the TCPListener in Unity and they connect!! Last 2 things (hopefully🙈)! 1. Which function should I use in Unity to send data to my python app? I guess in your code you managed this aspect here:
//StartCoroutine(ExchangeCommands(client))
And 2. when I move the code on my oculus I guess I would need a unique ip address as well as the port this listener binds. Do you think I will have problems if I set a specific ip address and port in both TCPlistener and python code?
Thank you so much!!!🙏
- brazzoniProtege
Just for reference, my protocol is always a connection from the client to the headset. They both exchange one message. The client sends a message first, and upon receiving and acting on that message, the headset/listener sends a response. To read the clients message, my function stack eventually comes to this:
private const string END_OF_STREAM_STRING = "\n\n\nEOF_END_EOF_END\n\n\n"; private byte[] END_OF_STREAM; public static InterdeviceCommand readCommandFromStream(System.Net.Sockets.NetworkStream networkStream) { byte[] buffer = new byte[8192]; int bytesRead; string decodedRead = ""; networkStream.ReadTimeout = READ_TIMEOUT; // read stream until there is nothing left to read. while ((bytesRead = networkStream.Read(buffer, 0, buffer.Length)) > 0) { decodedRead += Encoding.ASCII.GetString(buffer, 0, bytesRead); // This only decodes correctly if ASCII chars are 1 byte. Otherwise, some chars will be half read from the stream. if (decodedRead.Contains(END_OF_STREAM_STRING)) { string jsonData= getDataBeforeEndOfStream(decodedRead); // cut everything from the END_OF_STREAM sequence onward. We only deserialize what comes before. return JsonUtility.FromJson<InterdeviceCommand>(jsonData); } } throw new EndOfCommandNotFound("END_OF_STREAM not found, so command could not be deserialized safely"); } private static string getDataBeforeEndOfStream(string data) { string result; int indexOfEndOfStream; indexOfEndOfStream = data.IndexOf(END_OF_STREAM_STRING); if (indexOfEndOfStream >= 0) result = data.Substring(0, indexOfEndOfStream); // remove END_OF_STREAM, and everthing after it, from the data. else // we shouldn't ever execute this else based on how I intend to call this message. Just incase the code changes { throw new EndOfCommandNotFound("END_OF_STREAM not found, so command could not be deserialized safely"); } return result; }I'd recommend using it as a guide rather than a full plug and play solution. There's stuff like custom exceptions and Data Types that I'm omitting for brevity. It won't be too much an undertaking to try and get your code to a point where the client sends a string and the listener responds with another string. You should try and get that working before you try to fully mimic this code.
My server and client have a shared data type, InterdeviceCommand. Each end creates a command, serializes it into json, then sends it with
string jsonData = JsonUtility.ToJson(this); if (jsonData.Contains(END_OF_STREAM_STRING)) { throw new SerializedObjectContainsEOF(string.Format("We can't Stream.Write() a serialized object-containing-EOF, our communication protocol will desync. Attempted to Stream.Write() this object: {0}", jsonData)); } byte[] bytes = Encoding.ASCII.GetBytes(jsonData); networkStream.WriteTimeout = WRITE_TIMEOUT; networkStream.Write(bytes, 0, bytes.Length); // todo, make this asynchronous networkStream.Write(END_OF_STREAM, 0, END_OF_STREAM.Length);I'm also ending each stream with an END_OF_STREAM string. I needed that string, to tell that a client has finished it's message. After reading a message, it's deserialized into that original class.
You're mostly working with the NetworkStream class in unity, and that's returned by
TcpClient client = server.AcceptTcpClient(); NetworkStream networkStream = client.GetStream();First step is just sending a string from the python client, reading it on the server, and Debug.Log() it. I wouldn't worry about anything else.
- brazzoniProtege
When I'm sending a command. I convert END_OF_STREAM_STRING to END_OF_STREAM with
END_OF_STREAM = Encoding.ASCII.GetBytes(END_OF_STREAM_STRING);A lot of this is 100% not the most efficient, or even optimized way to do things. But it is a start.
- brazzoniProtege
To answer your second question. This is where it gets tricky. You can't really assign the TCPListener an IP address. You can only listen on an IP that has been assigned to your system by your local router. IPManager is 'finding' the first available IP address that we have been assigned, and can listen on.
If your router assigns IP addresses dynamically, you won't be able to use the same IP addresses on every execution. You could make your router assign IP addresses statically, so every computer on your system will always be assigned the same IP address. A Static IP address isn't the cleanest solution, and depending on your networking experience, it might be easier to do one of the next two options.
Start with option 1, just to try and get it working. If you do, option 2 is another more robust step.
Option 1: Display that assigned port and ip address in a UI. When you launch your app, you can navigate to this UI, record the IP address and port. Then, you can manually enter it into your python app.
Option 2: Device Discovery
The most simple form of this is constantly broadcasting your registered port and ip address to a hardcoded port.
1. your headset is assigned an ip address by the router, and it binds to an arbitrary port assigned by the OS.
2. You headset app obtains the Broadcast IP address from the assigned IP address
3. Your headset app packages this information into a 'BroadcastMessage'
4. Your headset app serializes the BroadcastMessage and Broadcasts it
5. Your client listens for broadcasts on a shared port, shared between the listener and the client app.
6. Your client receives the broadcast and parses it into "BroadcastMessage'
7. Your client reads values from BroadcastMessage i.e. BroadcastMesssage.IP and BroadcastMessage.Port
8. Using those values, your python client can get a connection to your headset app without you having to constantly input the port and address.
UNet discovery kind of does this for you. It's going to be more sophisticated. I made a class that inherits NetworkDiscovery, and I changed the BroadcastData to be something that contains my IP address and Port.
Quick Links
- Horizon Developer Support
- Quest User Forums
- Troubleshooting Forum for problems with a game or app
- Quest Support for problems with your device
Other Meta Support
Related Content
- 3 months ago
- 6 months ago
- 9 months ago
- 10 months ago