Skip to content

Commit 3744ccc

Browse files
committed
Full rewrite for v5, with many improvements
1 parent 7f38435 commit 3744ccc

File tree

3 files changed

+120
-79
lines changed

3 files changed

+120
-79
lines changed

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,33 @@ Click [here](https://github.com/IRLToolkit/obs-websocket-http/tree/old-4.x) to g
99
## Installing on Ubuntu:
1010
- Clone/download the repository
1111
- Edit `config.ini` and set the address, port, and authentication details for the HTTP server (leave `authentication_key` empty for no auth). Set your obs-websocket connection settings in the `[obsws]` section.
12-
- `sudo apt update && sudo apt install python3.7 python3-pip`
13-
- `python3.7 -m pip install -r requirements.txt`
12+
- `sudo apt update && sudo apt install python3.8 python3-pip`
13+
- `python3.8 -m pip install -r requirements.txt`
1414
- CD into the `obs-websocket-http` directory
15-
- Run with `python3.7 main.py`
15+
- Run with `python3.8 main.py`
1616

17-
## Running with Docker
17+
Use `python3.8 main.py --help` to see command line options, which allow you to run this script without a config.ini.
1818

19+
## Running with Docker
1920
- Clone/download the repository
2021
- Edit `docker-compose.yml` to have the correct IPs and ports for this machine and the one running OBS Studio (it may be the same machine). You do NOT need to edit `config.ini` if using docker because it will be created by the container from the values in `docker-compose.yml`.
2122
- Start obs-websocket-http by running `docker-compose up -d && docker-compose logs -f`. This will give you log output and you can press `Ctrl-C` when you wish to return to terminal and the container will run in the background.
2223

2324
## Protocol:
24-
This code contains two request endpoints. `/emit/{requesttype}` and `/call/{requesttype}`.
25-
- `/emit/{requesttype}` sends off a websocket event without waiting for a response, and immediately returns a generic `{"status":"ok"}` json response after sending the event, regardless of whether it errors out on the OBS instance.
26-
- `/call/{requesttype}` Makes a full request to obs-websocket, and waits for a response. The recieved response is then returned to the HTTP caller.
25+
The web server contains these endpoints:
26+
- `/emit/{requestType}` sends off a websocket event without waiting for a response, and immediately returns a generic `{"result": true}` JSON response without a request result.
27+
- `/call/{requestType}` Makes a full request to obs-websocket, and waits for a response. The recieved response is then returned to the HTTP caller.
28+
- Example JSON response: `{"result": true, "requestResult": {"requestType": "GetCurrentProgramScene", "requestStatus": {"result": true, "code": 100}, "responseData": {"currentProgramSceneName": "Scene 5"}}}`
2729

28-
If authentication is set, then each request much contain an `AuthKey` header with the configured password as the value.
30+
If authentication is set, then each request much contain an `Authorization` header with the configured auth key as the value.
2931

30-
A request type is always required, however a json body depends on the underlying request in obs-websocket as to whether any data is necessary.
32+
A request type is always required, however the request body is optional, and is forwarded as the request data for the obs-websocket request. If your obs-websocket request does not require request data, then no body is needed.
3133

32-
For a list of request types, refer to the [obs-websocket protocol docs](https://github.com/Palakis/obs-websocket/blob/4.x-current/docs/generated/protocol.md#requests)
34+
For a list of request types, refer to the [obs-websocket protocol docs](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)
3335

3436
## Example cURL commands:
35-
- `curl -XPOST -H "Content-type: application/json" -d '{"scene-name":"Scene 2"}' 'http://127.0.0.1:4445/emit/SetCurrentScene'`
36-
- `curl -XPOST -H 'AuthKey: agoodpassword' -H "Content-type: application/json" -d '{"scene-name":"Scene 2"}' 'http://127.0.0.1:4445/emit/SetCurrentScene'`
37-
- `curl -XPOST -H 'AuthKey: agoodpassword' -H "Content-type: application/json" 'http://127.0.0.1:4445/call/GetSceneList'`
37+
- `curl -XPOST -H 'Authorization: pp123' -H "Content-type: application/json" -d '{"sceneName": "Scene 5"}' 'http://127.0.0.1:4445/emit/SetCurrentProgramScene'`
38+
- `curl -XPOST -H "Content-type: application/json" 'http://127.0.0.1:4445/call/GetCurrentProgramScene'`
3839

3940
## IRLTookit Links
4041

config.ini

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ bind_to_port = 4445
55
authentication_key =
66

77
[obsws]
8-
ws_address = 127.0.0.1
9-
ws_port = 4444
8+
ws_url = ws://127.0.0.1:4444
109
#Only necessary if "Enable authentication" is checked in the obs-websocket settings menu.
1110
ws_password =

main.py

Lines changed: 105 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,132 @@
1+
import logging
2+
logging.basicConfig(level=logging.INFO)
13
import os
4+
import argparse
25
import asyncio
36
import json
47
import simpleobsws
58
import aiohttp
69
from aiohttp import web
710
from configparser import ConfigParser
811

9-
config = ConfigParser()
10-
config.read('config.ini')
11-
httpAddress = config.get('http', 'bind_to_address')
12-
httpPort = config.getint('http', 'bind_to_port')
13-
httpAuthKey = config.get('http', 'authentication_key')
14-
if httpAuthKey:
15-
print('Starting HTTP server with AuthKey set to "{}"'.format(httpAuthKey))
16-
else:
17-
print('Starting HTTP server without authentication.')
18-
httpAuthKey = None
19-
wsAddress = config.get('obsws', 'ws_address')
20-
wsPort = config.getint('obsws', 'ws_port')
21-
wsPassword = config.get('obsws', 'ws_password')
2212
loop = asyncio.get_event_loop()
23-
ws = simpleobsws.WebSocketClient(url='ws://{}:{}'.format(wsAddress, wsPort), password=wsPassword)
13+
app = web.Application()
14+
ws = None
15+
16+
# Make aiohttp shut up
17+
aiohttpLogger = logging.getLogger('aiohttp')
18+
aiohttpLogger.setLevel(logging.WARNING)
19+
20+
def fail_response(comment):
21+
return web.json_response({'result': False, 'comment': comment})
2422

25-
def statusmessage(message):
26-
print(str(message) + '... ', end='', flush=True)
23+
def validate_request(request):
24+
if not httpAuthKey:
25+
return True, None
26+
if 'Authorization' not in request.headers:
27+
return False, 'You are missing the `Authorization` header.'
28+
if request.headers['Authorization'] != httpAuthKey:
29+
return False, 'Invalid authorization key.'
30+
return True, None
31+
32+
async def get_json(request):
33+
try:
34+
return await request.json()
35+
except json.decoder.JSONDecodeError:
36+
return None
2737

2838
def response_to_object(response: simpleobsws.RequestResponse):
2939
ret = {}
3040
ret['requestType'] = response.requestType
3141
ret['requestStatus'] = {'result': response.requestStatus.result, 'code': response.requestStatus.code}
3242
if response.requestStatus.comment:
3343
ret['requestStatus']['comment'] = response.requestStatus.comment
34-
if ret.has_data():
44+
if response.responseData:
3545
ret['responseData'] = response.responseData
3646
return ret
3747

38-
async def handle_emit_request(request):
39-
"""Handler function for all emit-based HTTP requests. Assumes that you know what you are doing because it will never return an error."""
40-
if ('AuthKey' not in request.headers) and httpAuthKey != None:
41-
return web.json_response({'status': False, 'comment': 'AuthKey header is required.'})
42-
if httpAuthKey == None or (request.headers['AuthKey'] == httpAuthKey):
43-
requesttype = request.match_info['type']
44-
try:
45-
requestdata = await request.json()
46-
except json.decoder.JSONDecodeError:
47-
requestdata = None
48-
req = simpleobsws.Request(requesttype, requestdata)
48+
async def request_callback(request, emit):
49+
if not ws or not ws.is_identified():
50+
return fail_response('obs-websocket is not connected.')
51+
authOk, comment = validate_request(request)
52+
if not authOk:
53+
return fail_response(comment)
54+
55+
requestType = request.match_info.get('requestType')
56+
if not requestType:
57+
return fail_response('Your path is missing a request type.')
58+
requestData = await get_json(request)
59+
req = simpleobsws.Request(requestType, requestData)
60+
61+
logging.info('Performing request for request type `{}` | Emit: {} | Client IP: {}'.format(requestType, emit, request.remote))
62+
logging.debug('Request data:\n{}'.format(requestData))
63+
64+
if emit:
4965
await ws.emit(req)
50-
return web.json_response({'status': True})
51-
else:
52-
return web.json_response({'status': False, 'comment': 'Bad AuthKey'})
53-
54-
async def handle_call_request(request):
55-
"""Handler function for all call-based HTTP requests."""
56-
if ('AuthKey' not in request.headers) and httpAuthKey != None:
57-
return web.json_response({'status': False, 'comment': 'AuthKey header is required.'})
58-
if httpAuthKey == None or (request.headers['AuthKey'] == httpAuthKey):
59-
requesttype = request.match_info['type']
60-
try:
61-
requestdata = await request.json()
62-
except json.decoder.JSONDecodeError:
63-
if (await request.text()) == '':
64-
requestdata = None
65-
try:
66-
req = simpleobsws.Request(requesttype, requestdata)
67-
ret = await ws.call(req)
68-
responsedata = {'status': True, 'response': response_to_object(ret)}
69-
except simpleobsws.MessageTimeout:
70-
responsedata = {'status': False, 'comment': 'The obs-websocket request timed out.'}
71-
return web.json_response(responsedata)
72-
else:
73-
return web.json_response({'status': False, 'comment': 'Bad AuthKey'})
66+
return web.json_response({'result': True})
67+
68+
try:
69+
ret = await ws.call(req)
70+
except simpleobsws.MessageTimeout:
71+
return fail_response('The obs-websocket request timed out.')
72+
responseData = {'result': True, 'requestResult': response_to_object(ret)}
73+
return web.json_response(responseData)
74+
75+
async def call_request_callback(request):
76+
return await request_callback(request, False)
77+
78+
async def emit_request_callback(request):
79+
return await request_callback(request, True)
7480

75-
async def init_ws():
81+
async def init():
82+
logging.info('Connecting to obs-websocket...')
7683
await ws.connect()
7784
if not await ws.wait_until_identified():
78-
print('Identification with obs-websocket timed out. Could it be using 4.x?')
85+
logging.error('Identification with obs-websocket timed out. Could it be using 4.x?')
7986
return False
87+
logging.info('Connected to obs-websocket.')
8088
return True
8189

82-
app = web.Application()
83-
app.add_routes([web.post('/emit/{type}', handle_emit_request), web.post('/call/{type}', handle_call_request)])
84-
statusmessage('Connecting to obs-websocket')
85-
if not loop.run_until_complete(init_ws()):
86-
os._exit(1)
87-
print('[Connected.]')
88-
try:
89-
web.run_app(app, host=httpAddress, port=httpPort)
90-
except KeyboardInterrupt:
91-
print('Shutting down...')
90+
async def shutdown(app):
91+
logging.info('Shutting down...')
92+
if ws.is_identified():
93+
logging.info('Disconnecting from obs-websocket...')
94+
await ws.disconnect()
95+
logging.info('Disconnected from obs-websocket.')
96+
else:
97+
logging.info('Not connected to obs-websocket, not disconnecting.')
98+
99+
if __name__ == '__main__':
100+
config = ConfigParser()
101+
config.read('config.ini')
102+
103+
# Command line args take priority, with fallback to config.ini, and further fallback to defaults.
104+
parser = argparse.ArgumentParser(description='A Python-based program that provides HTTP endpoints for obs-websocket')
105+
parser.add_argument('--http_bind_addres', dest='http_bind_addres', default=config.get('http', 'bind_to_address', fallback='0.0.0.0'))
106+
parser.add_argument('--http_bind_port', dest='http_bind_port', type=int, default=config.getint('http', 'bind_to_port', fallback=4445))
107+
parser.add_argument('--http_auth_key', dest='http_auth_key', default=config.get('http', 'authentication_key', fallback=''))
108+
parser.add_argument('--ws_url', dest='ws_url', default=config.get('obsws', 'ws_url', fallback='ws://127.0.0.1:4444'))
109+
parser.add_argument('--ws_password', dest='ws_password', default=config.get('obsws', 'ws_password', fallback=''))
110+
args = parser.parse_args()
111+
112+
httpAddress = args.http_bind_addres
113+
httpPort = args.http_bind_port
114+
httpAuthKey = args.http_auth_key
115+
wsUrl = args.ws_url
116+
wsPassword = args.ws_password
117+
118+
if httpAuthKey:
119+
logging.info('HTTP server will start with AuthKey set to `{}`'.format(httpAuthKey))
120+
else:
121+
logging.info('HTTP server will start without authentication.')
122+
httpAuthKey = None
123+
124+
ws = simpleobsws.WebSocketClient(url=wsUrl, password=wsPassword)
125+
126+
if not loop.run_until_complete(init()):
127+
os._exit(1)
128+
129+
app.add_routes([web.post('/call/{requestType}', call_request_callback), web.post('/emit/{requestType}', emit_request_callback)])
130+
app.on_cleanup.append(shutdown)
131+
132+
web.run_app(app, host=httpAddress, port=httpPort, loop=loop)

0 commit comments

Comments
 (0)