dkumor / rtcbot

A python WebRTC remote control library
https://rtcbot.readthedocs.io/en/latest/?badge=latest
MIT License
71 stars 30 forks source link

Clients can't signal each other on initial connection without internet access #12

Open andrew103 opened 4 years ago

andrew103 commented 4 years ago

I'm looking to have two clients connect with each other over a local network that doesn't have internet access. The default constructors in RTCConnection use Google's free STUN server for signaling which requires an internet connection. Traditionally with WebRTC, you can simply remove the entry from the configuration and WebRTC would adopt the first client as the signaling server (at least I think so from what I know).

When attempting to do something similar here with

conn = RTCConnection(rtcConfiguration=RTCConfiguration())

and

var conn = new rtcbot.RTCConnection(rtcConfiguration={iceServers: []});

the connection fails to complete as it seems there is no signaling happening.

Now I noticed that in a previous issue and in the tutorial for connecting over 4G there was mention that the parent library aiortc doesn't have its own TURN server built in and one of the suggested solutions was to install coTURN as a local TURN server. I haven't done it yet but I wanted to confirm my thinking that this would be the solution to my problem of wanting RTC communication within an isolated network.

dkumor commented 4 years ago

In general, WebRTC should allow connections over a local network - I will check what is going on here.

dkumor commented 4 years ago

Alright, so there are two tiny changes needed to the posted code: 1) An empty RTCConfiguration actually has default STUN servers added to it (https://github.com/aiortc/aiortc/blob/main/src/aiortc/rtcicetransport.py#L177). You have to explicitly specify an empty list to not have any STUN servers used:

conn = RTCConnection(rtcConfiguration=RTCConfiguration([]))

2) In javascript, unfortunately, you can't set the second argument of the constructor by key as is done in Python. You have to explicitly specify both arguments.

var conn = new rtcbot.RTCConnection(true,{iceServers: []});

I was able to reproduce your problem, and the above changes fixed it for me - please let me know if they work for you too!

andrew103 commented 4 years ago

This didn't seem to solve my issue, but it did make some progress(ish?). Now it doesn't access the Google STUN server when connected to the internet which is good, but still fails to establish local connections.

Did you have a local STUN/TURN server running? I can share more code as well if necessary.

dkumor commented 4 years ago

Here is the code I used, based on the offloading example in tutorials:

Working code ```python # run this first on computer 1 from aiohttp import web routes = web.RouteTableDef() from rtcbot import RTCConnection from aiortc import RTCConfiguration conn = RTCConnection(rtcConfiguration=RTCConfiguration([])) @conn.subscribe def onMessage(msg): print("GOT MESSAGE", msg) @routes.post("/connect") async def connect(request): clientOffer = await request.json() serverResponse = await conn.getLocalDescription(clientOffer) print(serverResponse) return web.json_response(serverResponse) async def cleanup(app): await conn.close() app = web.Application() app.add_routes(routes) app.on_shutdown.append(cleanup) web.run_app(app) ``` and ```python # Run this second on computer 2 import asyncio import aiohttp import cv2 import json from rtcbot import RTCConnection from aiortc import RTCConfiguration conn = RTCConnection(rtcConfiguration=RTCConfiguration([])) @conn.subscribe def onMessage(msg): print("GOT MESSAGE", msg) async def connect(): localDescription = await conn.getLocalDescription() print(localDescription) async with aiohttp.ClientSession() as session: async with session.post( "http://192.168.1.112:8080/connect", data=json.dumps(localDescription) ) as resp: response = await resp.json() await conn.setRemoteDescription(response) conn.put_nowait("hi") asyncio.ensure_future(connect()) try: asyncio.get_event_loop().run_forever() finally: conn.close() ```

However, I did notice that an example with chrome on the other end DOES fail without any ICE servers, even if there is an internet connection if the browser is run on a different machine (I tested on the same machine yesterday):

Failing example ```python from aiohttp import web routes = web.RouteTableDef() from rtcbot import RTCConnection, getRTCBotJS import logging from aiortc import RTCConfiguration logging.basicConfig(level=logging.DEBUG) # For this example, we use just one global connection conn = RTCConnection(rtcConfiguration=RTCConfiguration([])) @conn.subscribe def onMessage(msg): # Called when each message is sent print("Got message:", msg) # Serve the RTCBot javascript library at /rtcbot.js @routes.get("/rtcbot.js") async def rtcbotjs(request): return web.Response(content_type="application/javascript", text=getRTCBotJS()) # This sets up the connection @routes.post("/connect") async def connect(request): clientOffer = await request.json() print("OFFER\n\n", clientOffer) serverResponse = await conn.getLocalDescription(clientOffer) print("RESPONSE\n\n", serverResponse) return web.json_response(serverResponse) @routes.get("/") async def index(request): return web.Response( content_type="text/html", text=""" RTCBot: Data Channel

Click the Button

Open the browser's developer tools to see console messages (CTRL+SHIFT+C)

""", ) async def cleanup(app): await conn.close() app = web.Application() app.add_routes(routes) app.on_shutdown.append(cleanup) web.run_app(app) ```

It therefore looks like the issue showing up right now must have something to do with how aiortc parses the offers from .local addresses from browsers, or something of the sort - I think this is an upstream issue - I will try to find the source and open an issue in aiortc.

andrew103 commented 4 years ago

So I managed to get things working by setting up a local TURN server with coturn. I still encourage trying to get the issue where no STUN/TURN servers are specified resolved upstream.

For anyone that comes across this through Google search (as there's not much in terms of resources for coturn out there), here's my /etc/turnserver.conf setup:

listening-port=3478
tls-listening-port=5349
listening-ip=<device_ip_on_connected_network>
relay-ip=<device_ip_on_connected_network>
external-ip=<device_ip_on_connected_network>
realm=rpi0.io    # This us usually the domain of the server if connected to the internet. If not, then basically anything
server-name=rpi0.io    # same here

user=rpi0_turn:rpi0_turn    # format is <username>:<password>
lt-cred-mech

The TURN server is accessed by my scripts with:

conn = RTCConnection(rtcConfiguration=RTCConfiguration([RTCIceServer("turn:<device_ip_on_connected_network>:3478", "rpi0_turn", "rpi0_turn")]))

in Python, and

var conn = new rtcbot.RTCConnection(true,{iceServers: [
    {
        "url":"turn:<device_ip_on_connected_network>:3478",
        "username":"rpi0_turn",
        "credential":"rpi0_turn"
    }
]});

in JavaScript.

Thanks @dkumor for your help!

dkumor commented 4 years ago

@andrew103 If you have a moment, it would be really appreciated if you'd open a PR adding this content to https://github.com/dkumor/rtcbot/blob/master/examples/mobile/README.md - it could replace the current content about pion, since coturn is much more commonly used.

I would do it myself, but I think you should get credit for the info in git history.

I will try to get this issue fixed, since it is quite absurd that an internet connection is required for LAN connections!

andrew103 commented 4 years ago

Done. Happy to help :+1:

dkumor commented 4 years ago

I'd like to leave this issue open for the time being if that's OK with you - the underlying problem still exists!