diff --git a/README.md b/README.md index 9dbd75a..873d735 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ async def index(request): with open("index.html", "r") as f: return web.Response(content_type="text/html", text=f.read()) -async def cleanup(app): +async def cleanup(app=None): await conn.close() camera.close() diff --git a/docs/installing.rst b/docs/installing.rst index 2f9d9ac..542f19b 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -22,6 +22,9 @@ Then, you can complete the installation with pip:: sudo pip3 install rtcbot +.. warning:: + You might need to reboot your Pi for RTCBot to work! If rtcbot freezes on import, it means that you need to start PulseAudio. + .. note:: It is recommended that you use the Pi 4 with RTCBot. While it was tested to work down to the Raspberry Pi 3B, it was observed to have extra latency, since the CPU had difficulty keeping up with encoding the video stream while processing controller input. @@ -29,9 +32,6 @@ Then, you can complete the installation with pip:: meaning that all video encoding is done in software. .. note:: - You might need to reboot your Pi for RTCBot to work! - -.. warning:: These instructions were made with reference to Raspbian Buster. While the library *does* work on Raspbian Stretch, you'll need to install aiohttp through pip, and avoid installing opencv. @@ -50,8 +50,8 @@ Then, you can complete the installation with pip:: sudo pip3 install rtcbot -.. note:: - You might need to reboot, or manually start pulseaudio if it was not previously installed. +.. warning:: + You might need to reboot, or manually start PulseAudio if it was not previously installed. If RTCBot freezes on import, it means that PulseAudio is not running. Mac +++++++++++ diff --git a/docs/javascript.rst b/docs/javascript.rst index 26b5e0c..2ebf8c7 100644 --- a/docs/javascript.rst +++ b/docs/javascript.rst @@ -69,7 +69,7 @@ Next, to establish the connection with Python, you include the Python counterpar return web.json_response(response) - async def cleanup(app): + async def cleanup(app=None): if conn is not None: await conn.close() diff --git a/examples/index.rst b/examples/index.rst index 5f8ecef..74e51af 100644 --- a/examples/index.rst +++ b/examples/index.rst @@ -19,3 +19,4 @@ The full code for each of the tutorials can be seen in the `examples directory < remotecontrol/README.md mobile/README.md offloading/README.md + threads/README.md diff --git a/examples/mobile/README.md b/examples/mobile/README.md index 0abc80a..41eb641 100644 --- a/examples/mobile/README.md +++ b/examples/mobile/README.md @@ -125,7 +125,7 @@ async def index(request): """) -async def cleanup(app): +async def cleanup(app=None): global ws if ws is not None: c = ws.close() @@ -171,10 +171,9 @@ finally: With these two pieces of code, you first start the server, then start the robot, and finally open `http://localhost:8080` in the browser to view a video stream coming directly from the robot, even if the robot has an unknown IP. - ## rtcbot.dev -The above example requires you to have your own internet-accessible server at a known IP address to set up the connection, if your remote code is not on your local network. The server's only real purpose is to help *establish* a connection - once the connection is established, it does not do anything. +The above example requires you to have your own internet-accessible server at a known IP address to set up the connection, if your remote code is not on your local network. The server's only real purpose is to help _establish_ a connection - once the connection is established, it does not do anything. For this reason, I am hosting a free testing server online at `https://rtcbot.dev` that performs the equivalent of the following operation from the above server code: @@ -226,11 +225,12 @@ and the local browser's connection code becomes: ```js let response = await fetch("https://rtcbot.dev/myRandomSequence11", { - method: "POST", - cache: "no-cache", - body: JSON.stringify(offer) + method: "POST", + cache: "no-cache", + body: JSON.stringify(offer), }); ``` + With `rtcbot.dev`, you no longer need your local server code to run websockets or a connection service. Its only purpose is to give the browser the html and javascript necessary to establish a connection. We will get rid of the browser entirely in the next tutorial. ## If it doesn't work over 4G @@ -255,6 +255,7 @@ There are two options through which to setup a TURN server: [coTURN](https://git The Pion server is easy to set up on Windows,Mac and Linux - all you need to do is [download the executable](https://github.com/pion/turn/releases/tag/1.0.3), and run it from the command line as shown. **Linux/Mac**: + ```bash chmod +x ./simple-turn-linux-amd64 # allow executing the downloaded file export USERS='myusername=mypassword' @@ -264,6 +265,7 @@ export UDP_PORT=3478 ``` **Windows**: You can run the following from powershell: + ```powershell $env:USERS = "myusername=mypassword" $env:REALM = "my.server.ip" @@ -272,6 +274,7 @@ $env:UDP_PORT = 3478 ``` With the Pion server running, you will need to let both Python and Javascript know about it when creating your `RTCConnection`: + ```python from aiortc import RTCConfiguration, RTCIceServer @@ -286,7 +289,7 @@ myConnection = RTCConnection(rtcConfiguration=RTCConfiguration([ var conn = new rtcbot.RTCConnection(true, { iceServers:[ { urls: ["stun:stun.l.google.com:19302"] }, - { urls: "turn:my.server.ip:3478?transport=udp", + { urls: "turn:my.server.ip:3478?transport=udp", username: "myusername", credential: "mypassword", }, ]); ``` @@ -296,6 +299,7 @@ var conn = new rtcbot.RTCConnection(true, { Setting up a coTURN server takes a bit more work and is only supported on Linux and Mac. The following steps will assume a Linux system running Ubuntu. Install coTURN and stop the coTURN service to modify config files with + ```bash sudo apt install coturn sudo systemctl stop coturn @@ -304,6 +308,7 @@ sudo systemctl stop coturn Edit the file `/etc/default/coturn` by uncommenting the line `TURNSERVER_ENABLED=1`. This will allow coTURN to start in daemon mode on boot. Edit another file `/etc/turnserver.conf` and add the following lines. Be sure to put your system's public facing IP address in place of ``, your domain name in place of ``, and your own credentials in place of `` and ``. + ``` listening-port=3478 tls-listening-port=5349 @@ -323,6 +328,7 @@ lt-cred-mech ``` Restart the coTURN service, check that it's running, and reboot. + ```bash sudo systemctl start coturn sudo systemctl status coturn @@ -330,6 +336,7 @@ sudo reboot ``` With the coTURN server running, you will need to let both Python and Javascript know about it when creating your `RTCConnection`: + ```python from aiortc import RTCConfiguration, RTCIceServer @@ -344,7 +351,7 @@ myConnection = RTCConnection(rtcConfiguration=RTCConfiguration([ var conn = new rtcbot.RTCConnection(true, { iceServers:[ { urls: ["stun:stun.l.google.com:19302"] }, - { urls: "turn: """) -async def cleanup(app): +async def cleanup(app=None): await conn.close() app = web.Application() @@ -152,7 +152,7 @@ We now add keyboard support. This is done with the `rtcbot.Keyboard` javascript ) - async def cleanup(app): + async def cleanup(app=None): await conn.close() app = web.Application() @@ -349,7 +349,7 @@ async def index(request): """) -async def cleanup(app): +async def cleanup(app=None): await conn.close() app = web.Application() diff --git a/examples/remotecontrol/gamepad.py b/examples/remotecontrol/gamepad.py index efa2709..421c6d8 100644 --- a/examples/remotecontrol/gamepad.py +++ b/examples/remotecontrol/gamepad.py @@ -72,7 +72,7 @@ async def index(request): ) -async def cleanup(app): +async def cleanup(app=None): await conn.close() diff --git a/examples/remotecontrol/keyboard.py b/examples/remotecontrol/keyboard.py index 5170b2d..78f9926 100644 --- a/examples/remotecontrol/keyboard.py +++ b/examples/remotecontrol/keyboard.py @@ -72,7 +72,7 @@ async def index(request): ) -async def cleanup(app): +async def cleanup(app=None): await conn.close() diff --git a/examples/remotecontrol/rc.py b/examples/remotecontrol/rc.py index 9bbddee..70be39a 100644 --- a/examples/remotecontrol/rc.py +++ b/examples/remotecontrol/rc.py @@ -94,7 +94,7 @@ async def index(request): ) -async def cleanup(app): +async def cleanup(app=None): await conn.close() diff --git a/examples/remotecontrol/skeleton.py b/examples/remotecontrol/skeleton.py index 3ff1a0f..0c9ee62 100644 --- a/examples/remotecontrol/skeleton.py +++ b/examples/remotecontrol/skeleton.py @@ -64,7 +64,7 @@ async def index(request): ) -async def cleanup(app): +async def cleanup(app=None): await conn.close() diff --git a/examples/streaming/README.md b/examples/streaming/README.md index d9dcf08..a5219a3 100644 --- a/examples/streaming/README.md +++ b/examples/streaming/README.md @@ -71,7 +71,7 @@ async def index(request): """) -async def cleanup(app): +async def cleanup(app=None): await conn.close() app = web.Application() @@ -161,7 +161,7 @@ All you need is to add a couple lines of code to the skeleton to get a fully-fun """) - async def cleanup(app): + async def cleanup(app=None): await conn.close() + camera.close() # Singletons like a camera are not awaited on close @@ -295,7 +295,7 @@ async def index(request): """) -async def cleanup(app): +async def cleanup(app=None): await conn.close() display.close() speaker.close() diff --git a/examples/streaming/audiovideo.py b/examples/streaming/audiovideo.py index 1b9918f..ab794af 100644 --- a/examples/streaming/audiovideo.py +++ b/examples/streaming/audiovideo.py @@ -75,7 +75,7 @@ async def index(request): ) -async def cleanup(app): +async def cleanup(app=None): await conn.close() mic.close() camera.close() diff --git a/examples/streaming/browser_audiovideo.py b/examples/streaming/browser_audiovideo.py index e4372a0..2dc9df0 100644 --- a/examples/streaming/browser_audiovideo.py +++ b/examples/streaming/browser_audiovideo.py @@ -74,7 +74,6 @@ async def index(request): async def cleanup(app=None): - print("CLEANUP") await conn.close() display.close() speaker.close() diff --git a/examples/streaming/video.py b/examples/streaming/video.py index 78676fa..e52daf7 100644 --- a/examples/streaming/video.py +++ b/examples/streaming/video.py @@ -69,7 +69,7 @@ async def index(request): ) -async def cleanup(app): +async def cleanup(app=None): await conn.close() camera.close() diff --git a/examples/threads/README.md b/examples/threads/README.md new file mode 100644 index 0000000..45621f0 --- /dev/null +++ b/examples/threads/README.md @@ -0,0 +1,230 @@ +# Running Blocking Code + +RTCBot uses python's asyncio event loop. This means that Python runs in a loop, handling events as they come in, all in a single thread. Any long-running operation must be specially coded to be async, so that it does not block operation of the event loop. + +## A Common Issue + +Suppose that you have a sensor that you want to use with RTCBot. Your goal is to retrieve values from the sensor, and then send the results to the browser. + +We will use the function `get_sensor_data` to represent a sensor which takes half a second to retrieve data: + +```python +import time +import random + +def get_sensor_data(): + time.sleep(0.5) # Represents an operation that takes half a second to complete + return random.random() +``` + +Suppose we were to add this function to the code used for the video streaming tutorial. We will send the sensor reading once a second: + +```diff + from aiohttp import web + + routes = web.RouteTableDef() + + from rtcbot import RTCConnection, getRTCBotJS, CVCamera + + camera = CVCamera() + # For this example, we use just one global connection + conn = RTCConnection() + conn.video.putSubscription(camera) + ++import time ++import random ++import asyncio ++ ++ ++def get_sensor_data(): ++ time.sleep(0.5) # Represents an operation that takes half a second to complete ++ return random.random() ++ ++ ++async def send_sensor_data(): ++ while True: ++ await asyncio.sleep(1) ++ data = get_sensor_data() ++ conn.put_nowait(data) # Send data to browser ++ ++ ++asyncio.ensure_future(send_sensor_data()) + + # 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() + serverResponse = await conn.getLocalDescription(clientOffer) + return web.json_response(serverResponse) + + + @routes.get("/") + async def index(request): + return web.Response( + content_type="text/html", + text=""" + + + RTCBot: Video + + + + +

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

+ + + + """, + ) + + + async def cleanup(app=None): + await conn.close() + camera.close() + + + conn.onClose(cleanup) + + app = web.Application() + app.add_routes(routes) + app.on_shutdown.append(cleanup) + web.run_app(app) +``` + +If you try this code, the video will freeze for half a second each second, while the sensor is being queried (i.e. while `time.sleep(0.5)` is being run). +This is because all of RTCBot's tasks happen in the same thread, and while reading the sensor, RTCBot is not sending video frames! + +To fix this issue, the sensor needs to be read in a different thread, so that the event loop is not blocked. The sensor data then needs to be moved to the main thread, where it can be used by rtcbot. + +## Producing Data in Another Thread + +Thankfully, RTCBot has built-in helper classes that set everything up for you here. The `ThreadedSubscriptionProducer` runs in a system thread, allowing arbitrary blocking code, and has built-in mechanisms that let you queue up data for use from the asyncio event loop. + +The "bad" code: + +```python +import time +import random +import asyncio + +def get_sensor_data(): + time.sleep(0.5) # Represents an operation that takes half a second to complete + return random.random() + +async def send_sensor_data(): + while True: + await asyncio.sleep(1) + data = get_sensor_data() + conn.put_nowait(data) # Send data to browser + + +asyncio.ensure_future(send_sensor_data()) +``` + +can be fixed by moving the sensor-querying code into a `ThreadedSubscriptionProducer`: + +```python +import time +import random +import asyncio + +from rtcbot.base import ThreadedSubscriptionProducer + +def get_sensor_data(): + time.sleep(0.5) # Represents an operation that takes half a second to complete + return random.random() + +class MySensor(ThreadedSubscriptionProducer): + def _producer(self): + self._setReady(True) # Notify that ready to start gathering data + while not self._shouldClose: # Keep gathering until close is requested + time.sleep(1) + data = get_sensor_data() + # Send the data to the asyncio thread, + # so it can be retrieved with await mysensor.get() + self._put_nowait(data) + self._setReady(False) # Notify that sensor is no longer operational + +mysensor = MySensor() + +async def send_sensor_data(): + while True: + data = await mysensor.get() # we await the output of MySensor in a loop + conn.put_nowait(data) + +asyncio.ensure_future(send_sensor_data()) + +... + +async def cleanup(app=None): + await conn.close() + camera.close() + mysensor.close() +``` + +## Consuming Data in Another Thread + +RTCBot has an equivalent mechanism for ingesting data - you can retrieve data, and then use it to control things with blocking code. + +```python +import time + +def set_output_value(value): + time.sleep(0.5) # Represents an operation that takes half a second to complete + print(value) + +from rtcbot.base import ThreadedSubscriptionConsumer, SubscriptionClosed + +class MyOutput(ThreadedSubscriptionConsumer): + def _consumer(self): + self._setReady(True) + while not self._shouldClose: + try: + data = self._get() + set_output_value(data) + except SubscriptionClosed: + break + + self._setReady(False) + +myoutput = MyOutput() +``` + +You can now use `myoutput.put_nowait` in rtcbot to queue up data, which will be retrieved from the consumer thread. + +## Summary + +This tutorial introduced the `ThreadedSubscriptionProducer` and `ThreadedSubscriptionConsumer` classes, which allow you to use blocking code with the asyncio event loop. These functions allow handling the connection in the main thread, and doing all actions that might take a while in separate threads. diff --git a/examples/threads/bad_sensor.py b/examples/threads/bad_sensor.py new file mode 100644 index 0000000..ab46e93 --- /dev/null +++ b/examples/threads/bad_sensor.py @@ -0,0 +1,102 @@ +from aiohttp import web + +routes = web.RouteTableDef() + +from rtcbot import RTCConnection, getRTCBotJS, CVCamera + +camera = CVCamera() +# For this example, we use just one global connection +conn = RTCConnection() +conn.video.putSubscription(camera) + +import time +import random +import asyncio + + +def get_sensor_data(): + time.sleep(0.5) # Represents an operation that takes half a second to complete + return random.random() + + +async def send_sensor_data(): + while True: + await asyncio.sleep(1) + data = get_sensor_data() + conn.put_nowait(data) # Send data to browser + + +asyncio.ensure_future(send_sensor_data()) + +# 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() + serverResponse = await conn.getLocalDescription(clientOffer) + return web.json_response(serverResponse) + + +@routes.get("/") +async def index(request): + return web.Response( + content_type="text/html", + text=""" + + + RTCBot: Video + + + + +

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

+ + + + """, + ) + + +async def cleanup(app=None): + await conn.close() + camera.close() + + +conn.onClose(cleanup) + +app = web.Application() +app.add_routes(routes) +app.on_shutdown.append(cleanup) +web.run_app(app) diff --git a/examples/threads/threaded_sensor.py b/examples/threads/threaded_sensor.py new file mode 100644 index 0000000..7ed70ed --- /dev/null +++ b/examples/threads/threaded_sensor.py @@ -0,0 +1,119 @@ +from aiohttp import web + +routes = web.RouteTableDef() + +from rtcbot import RTCConnection, getRTCBotJS, CVCamera + +camera = CVCamera() +# For this example, we use just one global connection +conn = RTCConnection() +conn.video.putSubscription(camera) + +import time +import random +import asyncio + +from rtcbot.base import ThreadedSubscriptionProducer + + +def get_sensor_data(): + time.sleep(0.5) # Represents an operation that takes half a second to complete + return random.random() + + +class MySensor(ThreadedSubscriptionProducer): + def _producer(self): + self._setReady(True) # Notify that ready to start gathering data + while not self._shouldClose: # Keep gathering until close is requested + time.sleep(1) + data = get_sensor_data() + # Send the data to the asyncio thread, + # so it can be retrieved with await mysensor.get() + self._put_nowait(data) + self._setReady(False) # Notify that sensor is no longer operational + + +mysensor = MySensor() + + +async def send_sensor_data(): + while True: + data = await mysensor.get() # we await the output of MySensor in a loop + conn.put_nowait(data) + + +asyncio.ensure_future(send_sensor_data()) + +# 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() + serverResponse = await conn.getLocalDescription(clientOffer) + return web.json_response(serverResponse) + + +@routes.get("/") +async def index(request): + return web.Response( + content_type="text/html", + text=""" + + + RTCBot: Video + + + + +

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

+ + + + """, + ) + + +async def cleanup(app=None): + await conn.close() + camera.close() + mysensor.close() + + +conn.onClose(cleanup) + +app = web.Application() +app.add_routes(routes) +app.on_shutdown.append(cleanup) +web.run_app(app) diff --git a/examples/webrtc/README.md b/examples/webrtc/README.md index f39539e..d90b3d4 100644 --- a/examples/webrtc/README.md +++ b/examples/webrtc/README.md @@ -122,7 +122,7 @@ async def index(request): """) -async def cleanup(app): +async def cleanup(app=None): await conn.close() @@ -183,7 +183,7 @@ and send back the information necessary to complete the connection. Finally, on application exit, we close the connection: ```python -async def cleanup(app): +async def cleanup(app=None): await conn.close() app.on_shutdown.append(cleanup) @@ -201,7 +201,7 @@ async function connect() { let response = await fetch("/connect", { method: "POST", cache: "no-cache", - body: JSON.stringify(offer) + body: JSON.stringify(offer), }); await conn.setRemoteDescription(await response.json()); @@ -219,7 +219,7 @@ Finally, we replace the original `console.log` with a `conn.put_nowait` to send ```javascript var mybutton = document.querySelector("#mybutton"); -mybutton.onclick = function() { +mybutton.onclick = function () { conn.put_nowait("Button Clicked!"); }; ``` @@ -259,14 +259,14 @@ example given above to both send and receive JSON on button press: The first modification we make is subscribing to incoming messages in javascript, ```javascript -conn.subscribe(m => console.log("Received from python:", m)); +conn.subscribe((m) => console.log("Received from python:", m)); ``` ...and sending messages as json: ```javascript var mybutton = document.querySelector("#mybutton"); -mybutton.onclick = function() { +mybutton.onclick = function () { conn.put_nowait({ data: "ping" }); }; ``` @@ -359,7 +359,7 @@ async def index(request): """) -async def cleanup(app): +async def cleanup(app=None): await conn.close() app = web.Application() diff --git a/examples/webrtc/dataconnection.py b/examples/webrtc/dataconnection.py index c25cd86..b94006f 100644 --- a/examples/webrtc/dataconnection.py +++ b/examples/webrtc/dataconnection.py @@ -75,7 +75,7 @@ async def index(request): ) -async def cleanup(app): +async def cleanup(app=None): await conn.close() diff --git a/examples/webrtc/jsonbackandforth.py b/examples/webrtc/jsonbackandforth.py index 9b0e864..700762d 100644 --- a/examples/webrtc/jsonbackandforth.py +++ b/examples/webrtc/jsonbackandforth.py @@ -76,7 +76,7 @@ async def index(request): ) -async def cleanup(app): +async def cleanup(app=None): await conn.close() diff --git a/js/package.json b/js/package.json index c48634b..d40696d 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "rtcbot", - "version": "0.2.2", + "version": "0.2.3", "description": "", "main": "dist/rtcbot.cjs.js", "module": "dist/rtcbot.esm.js", diff --git a/rtcbot/__init__.py b/rtcbot/__init__.py index 94c1cd5..6ad074e 100644 --- a/rtcbot/__init__.py +++ b/rtcbot/__init__.py @@ -10,4 +10,4 @@ from .devices import * -__version__ = "0.2.2" +__version__ = "0.2.3" diff --git a/rtcbot/connection.py b/rtcbot/connection.py index b19607a..8c8412a 100644 --- a/rtcbot/connection.py +++ b/rtcbot/connection.py @@ -3,6 +3,7 @@ RTCSessionDescription, RTCConfiguration, RTCIceServer, + exceptions, ) import asyncio import logging @@ -64,7 +65,11 @@ async def _messageSender(self): self._rtcDataChannel.send(msg) except SubscriptionClosed: pass + except exceptions.InvalidStateError: + self._close() + break # The while loop should exit here + self._setReady(False) self._log.debug("Stopping message sender") def _put_preprocess(self, data): diff --git a/rtcbot/tracks.py b/rtcbot/tracks.py index d818a13..9965103 100644 --- a/rtcbot/tracks.py +++ b/rtcbot/tracks.py @@ -1,11 +1,10 @@ - from aiortc.mediastreams import ( MediaStreamError, AUDIO_PTIME, VIDEO_CLOCK_RATE, VIDEO_TIME_BASE, AudioStreamTrack, - VideoStreamTrack + VideoStreamTrack, ) from av import VideoFrame, AudioFrame @@ -30,7 +29,7 @@ class _audioSenderTrack(AudioStreamTrack): frames are of an arbitrary size, and come in whenever convenient, and converting it into a stream of data at 960 samples per frame. - + https://datatracker.ietf.org/doc/rfc7587/?include_text=1 """ @@ -62,7 +61,7 @@ async def recv(self): self.stop() raise MediaStreamError except: - self._log.exception("Got unknown error. Crashing video stream") + self._log.exception("Got unknown error. Crashing audio stream") self.stop() raise MediaStreamError @@ -82,7 +81,7 @@ async def recv(self): # We therefore force a conversion to 16 bit integer: data = (np.clip(data, -1, 1) * 32768).astype(np.int16) new_frame = AudioFrame.from_ndarray( - data.reshape(1,-1), format="s16", layout=str(data.shape[1]) + "c" + data.reshape(1, -1), format="s16", layout=str(data.shape[1]) + "c" ) # Use the sample rate for the base clock @@ -276,7 +275,9 @@ async def _trackReceiver(self): "Incoming audio frame's data type unsupported: %s", audioFrame ) else: - data = np.reshape(data, (audioFrame.samples, -1)).astype(np.float) / 32768 + data = ( + np.reshape(data, (audioFrame.samples, -1)).astype(np.float) / 32768 + ) self._put_nowait(data) # Get the next frame diff --git a/setup.py b/setup.py index ae77fd4..182362e 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ # This call to setup() does all the work setuptools.setup( name="rtcbot", - version="0.2.2", + version="0.2.3", description="An asyncio-focused library for webrtc robot control", long_description=README, long_description_content_type="text/markdown",