Git Product home page Git Product logo

pyslobs's Introduction

PySLOBS: A Python Wrapper for the StreamLabs Desktop API

About the API

Streamlabs Desktop (formerly StreamLabs OBS) is a live-streaming application based on a fork of Open Broadcaster Software with additional features.

It offers the Streamlabs Desktop API to allow third-party applications to interact with the application while it is running.

This includes selecting, editing and monitoring scenes, video sources and audio sources. It includes monitoring performance.

This doesn't include chat or getting notifications about donations, subscriptions and followers. You will need to look elsewhere for that.

Typically, it would be accessed from same computer that is running Streamlabs Desktop, or from a computer on the same LAN.

The API is based on WebSockets, so it can be accessed from a browser in JavaScript. (Apparently, it can also be accessed through a named pipe.)

About the Python Wrapper

Rather than accessing it from Javascript, this Python wrapper allows access to Streamlabs Desktop from a Python application. The details about websockets are hidden and more Pythonic interfaces are provided.

Versions

This API requires Python 3.9 or later. It is tested on Python 3.10.

Streamlabs Desktop versions from 1.2.0-1.13.1 have been tested.

Pythonic names and types

The Python interface is designed to allow you to write PEP8-compliant Python code.

Camel-case items in the original API are represented in their preferred PEP8 form - i.e. underscores between words to avoid ambiguity and capitalised constants.

Enumerated types and named tuples are used in preference to JSON dictionaries and numeric constants where possible.

Identifiers that clash with reserved words used in Python are suffixed with _ - e.g. id_().

Date-times from notifications do not have a timezone associated with them. They are in the timezone of the host running the API.

Testing Progress.

The API is moderately large. In this version of the Python wrapper, not every method offered has been tested. Testing has been focussed on the areas the developer actively wanted to use first.

See PROGRESS.md for an idea of what is and isn't tested.

Usage

Calling asyncio libraries

This Python wrapper is based on asyncio. If you have not used the asyncio features of Python before, please consult a tutorial. In particular:

  • You need to await all methods defined as async. Similarly, context managers defined as async need to be called with async with.

  • After creation, remember to await the SlobsConnection.background_processing() coroutine.

  • To shut-down cleanly, remember to close the connection.

  • To avoid exceptions being swallowed, handle them in your top-level code.

See the example code.

Connection

First, you need a SlobsConnection instance. Connections actively process received messages, so it is important that they be included in the asyncio event loop.

So your main program might look like this:

import asyncio
import logging
from pyslobs import connection

async def main():
    # Note: This assumes an INI file has been configured.
    conn = connection.SlobsConnection()  

    # Give CPU to both your task and the connection instance.
    await asyncio.gather(
            conn.background_processing(),
            do_your_thing(conn)
            )
            
logging.basicConfig(level=logging.DEBUG)
asyncio.run(main())      

Connections should be closed when complete, so the background processing can know to shut-down.

Configuration and Authentication

The SlobsConnection needs details about how to connect to the Streamlabs Desktop application.

It needs to know which host is running the application, which port it is listening to, and the "API Token".

The API Token is like a password. It is generated by the application and shared with any tools that want to talk to it. It should not be shared with others; if it is, a new token should be generated.

Obtaining connection details

To obtain all these details, start Streamlabs Desktop, open the settings page, and select "Remote Control". A (blurry) QR code will be displayed. Do not show that QR code on your stream.

If you click on the QR code, and click "Show Details" underneath it, you will be given the following details:

  • API token

  • Port: This should default to 59650. If it is a different value, your Python application will need to know this.

  • IP addresses: If your application is on a different host to Streamlabs Desktop, your Python application will need to know one of the IP addresses offered.

Using connection details

These details can be provided by directly instantiating a ConnectionConfig, and passing it to SlobsConnection().

Alternatively, if none is passed, the SlobsConnection constructor will look for their presence in INI files. The INI file should be stored in the current directory or the user's home directory. They should be called .pyslobs or pyslobs.ini.

The content of the ini file should be:

[connection]
domain=localhost
port=59650
token=<your secret token>

When running the examples or exercises, if no ini file is found, it will assume defaults and prompt for the user to type in the API token each time.

Services

Once you have a connection, it can be used to instantiate any of nine Services:

  1. AudioService
  2. NotificationsService
  3. PerformanceService
  4. SceneCollectionsService
  5. ScenesService
  6. SelectionService
  7. SourcesService
  8. StreamingService
  9. TransitionsService

Services can be used:

  • subscribe to events, such as when the user selects a new active scene.
  • to make some limited direct changes, such as setting an active scene by scene id.
  • fetch SlobsClass instances (see below) that can be manipulated more directly.

Classes

In the original API they describe "Classes". These are represented by subclasses of SlobsClass in the Python API.

SlobsClass subclasses include:

  1. AudioSource
  2. Scene
  3. SceneItem
  4. SceneItemFolder
  5. SceneNode
  6. Selection
  7. Source

Instances of SlobsClass should only be constructed through Services methods and methods on other instances.

Instances may have properties (such as names) that can be accessed. Be careful that the values of these properties may be out-of-date if the value was changed within the app or if it was changed through a different instance referencing the same StreamLabs Desktop resource.

Objects can be used to fetch other Objects or namedtuples describing other records in the API.

Subscriptions

Some Services offer the ability to subscribe to events. A list is provided below.

To subscribe, create a callback function with a signature like:

 async def on_event(key, message)

(A method could also be used, in which case there should be an additional self parameter.)

Then call the event's subscribe() method, passing the callback function. e.g.

subscription = ScenesService(conn).scene_switched.subscribe(on_event)

Each time the event happens, the callback will be called. The key parameter will indicate which sort of event has triggered the callback (which is useful if you want to reuse a call-back function for several event types). The message parameter is a dictionary containing details of the event. (The StreamLabs Desktop API is unclear about what fields will be present, so this may require some experimentation to find out what data is available.)

The result of subscribe is a Subscription object that supports unsubscribe(). (It can also be used as an asynchronous context manager to ensure unsubscriptions are not forgotten.)

The subscribe method also accepts a subscribe_preferences parameter which is an instance of SubscriptionPreferences. It indicates whether the callback should also be called when the subscription is ended due to a call to unsubscribe or by the connection being closed. In these events, the value of key will be set to special values: UNSUBSCRIBED and CLOSED as appropriate.

Subscribable Events by Service
  • SceneCollectionService
    • collection_added
    • collection_removed
    • collection_switched
    • collection_updated
    • collection_will_switch
  • ScenesService
    • item_added
    • item_removed
    • item_updated
    • scene_added
    • scene_removed
    • scene_switched
  • Sources Service
    • source_added
    • source_removed
    • source_updated
  • StreamingService
    • recording_status_change
    • replay_buffer_status_change
    • streaming_status_change
  • Transitions Service
    • studio_mode_changed

Examples:

The examples folder contains many small programs to demonstrate how to use the API. These are not included with the package, but can be found on the GitHub site. Install PySLOBS, copy the raw example files to your machine, start your copy of Streamlabs Desktop, and run the examples (once PySLOBS is installed).

Special cases:

  • Sources have an additional field configurable which isn't documented.
  • Sources have a method refresh which raises an Internal Server Error. This has been reported to Streamlabs.
  • SceneCollectionsService methods sometimes raise Internal Server Errors if called too rapidly.
  • TransitionsService methods sometimes raise Internal Server Errors if called too rapidly.
  • Notifications subtype may be NEWS, which is not mentioned in the documentation.
  • When a source is added, the source_updated callback may also be triggered an arbitrary number of times.
  • Commands may raise an asyncio.TimeoutError after 5 seconds if a websocket fails (e.g. a network error) between the time the command was sent and a reply was received.

pyslobs's People

Contributors

julian-o avatar onyx-and-iris avatar unkomamire avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

pyslobs's Issues

Should be able to close a SlobsConnection without an async loop.

I found that I wanted to create a SlobsConnection, launch a background thread to handle the background connection, and then close the connection.

Investigating this revealed several areas that could be improved:

  1. close() can be changed from an co-routine to a regular method. This is a minor change to the API, but it is a breaking change. Looks like it is time for v2.0.0!

  2. Clean-up should occur even if an unexpected exception is raised in receive_and_dispatch

  3. If the background task terminates, the logging should always show why.

  4. If the connection is already closed, calling methods on it should fail with ProtocolError

  5. References to clean-up tasks are not being kept, leaving a risk that they would be garbage-collected before completion.

StreamLabs OBS changed name to StreamLabs Desktop

References should be updated.

Possible steps, in order of priority:

  1. Change the documentation/comments to refer to the new name.
  2. Refactor the internal classes with new names (which won't affect the API). Not much value here.
  3. Rename the public classes to use SLD instead of SLOBS. Include aliases from old names to make transition seamless.
    4: Rename the whole package in PyPI?

The later steps don't look like they will be worth the effort - happy to hear opinions.

I want to play the video I've made during the live broadcast.

I apologize for raising this issue for very personal reasons...
My guess is to do get_scene to get the name of the scene and then create_source.
For now,I used list_all_scenes made by examples to get the name.
What should I do from here?

async def createing_sources(conn):
    list_all_scenes(conn)
    ss = SourcesService(conn)
    scene = await ss.create_source()
    ??

Thank you in advance.

Can I control someone else's OBS?

I'm sorry for asking personal questions again.
I'm thinking of getting someone else's API token to control it.
Is this technically possible?
If you could do that, what do you think you would need?

Thank you in advance!

Use of id_ inconsistent.

Background:

The API has many references to "id" fields. id() is a built-in function in Python, and PySLOBS avoids using it directly to avoid warnings that the built-in is being hidden.

Private fields in a record are consistently called _id.
Method parameters are consistently called id_.

The properties fields are inconsistent though: They are called id in Scene and Source (and in test code), but they are called id_ in SceneNode (and its subclasses).

Whichever decision is made, make the properties all consistent.

SceneNode.set_visibility does not update cached visibility.

SceneItem has some "attributes" (Python properties) recording_visible, stream_visible and visible. These are not used by PySlobs, but are local cache values to reduce the need for clients to have to refetch objects. (They can be out-of-date!)

SceneNode.set_settings() modifies a the visibility of a scene_node, and correctly updates the local cache values. It is exercised in ex_scenenodes.py.

SceneNode.set_visbility() also modifies a the visibility of a scene_node. It does NOT updates the local cache values. It is NOT exercised.

It should have the line self._visible = visible, and it should be exercised to show it works correctly.

Sceneitem_factory wrong return

Hey, i've noticed that the transform_postition of an Item is wrong.

instead of excpected something like:
IVec2(x=50, y=0) (with two integers, one as x and one as y)

it was:

IVec2(x='x', y='y')

Strings instead of the actual parameters in the tuple.

The problem is in the sceneitem_factory (factories.py line 33)

The faulty part is the else:
instead of this:

def sceneitem_factory(connection, json_dict):
    # We see inconsistency in choices of API here.
    if isinstance(json_dict["transform"]["position"], list):
        transform_position = (
            IVec2(
                x=json_dict["transform"]["position"][0],
                y=json_dict["transform"]["position"][1],
            ),
        )
    else:
        transform_position = IVec2(*json_dict["transform"]["position"])

It should be that:

def sceneitem_factory(connection, json_dict):
    # We see inconsistency in choices of API here.
    if isinstance(json_dict["transform"]["position"], list):
        transform_position = (
            IVec2(
                x=json_dict["transform"]["position"][0],
                y=json_dict["transform"]["position"][1],
            ),
        )
    else:
        transform_position = (
            IVec2(
                x=json_dict["transform"]["position"]['x'] ,
                y=json_dict["transform"]["position"]['y'] ,
            )
        )

Should i create a PR for that?

StreamLabs sends message without id or result field

SlobsConnection._receive_and_dispatch receives messages from StreamLabs Desktop, and assumes they have an id field (as a response to a request) or a result field, in response to a subscription.

While I was streaming, it got a response which did not contain either, causing a log message:

_receive_and_dispatch failing.
Traceback (most recent call last):
  File "[omitted]\pyslobs\connection.py", line 192, in _receive_and_dispatch
    key = message["result"]["resourceId"]
KeyError: 'result'

Whatever this message is, it should be handled.

ERROR:slobsapi.SlobsConnection:_receive_and_dispatch failing.

ERROR:slobsapi.SlobsConnection:_receive_and_dispatch failing.
Traceback (most recent call last):
File "/usr/local/lib/python3.10/site-packages/pyslobs/connection.py", line 209, in _receive_and_dispatch
message = await self._receive_message()
File "/usr/local/lib/python3.10/site-packages/pyslobs/connection.py", line 197, in _receive_message
return await loop.run_in_executor(None, self.websocket.receive_message)
File "/usr/local/lib/python3.10/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/local/lib/python3.10/site-packages/pyslobs/connection.py", line 65, in receive_message
raw_message = self.socket.recv()
File "/usr/local/lib/python3.10/site-packages/websocket/_core.py", line 362, in recv
opcode, data = self.recv_data()
File "/usr/local/lib/python3.10/site-packages/websocket/_core.py", line 385, in recv_data
opcode, frame = self.recv_data_frame(control_frame)
File "/usr/local/lib/python3.10/site-packages/websocket/_core.py", line 406, in recv_data_frame
frame = self.recv_frame()
File "/usr/local/lib/python3.10/site-packages/websocket/_core.py", line 445, in recv_frame
return self.frame_buffer.recv_frame()
File "/usr/local/lib/python3.10/site-packages/websocket/_abnf.py", line 338, in recv_frame
self.recv_header()
File "/usr/local/lib/python3.10/site-packages/websocket/_abnf.py", line 294, in recv_header
header = self.recv_strict(2)
File "/usr/local/lib/python3.10/site-packages/websocket/abnf.py", line 373, in recv_strict
bytes
= self.recv(min(16384, shortage))
File "/usr/local/lib/python3.10/site-packages/websocket/_core.py", line 529, in _recv
return recv(self.sock, bufsize)
File "/usr/local/lib/python3.10/site-packages/websocket/_socket.py", line 122, in recv
raise WebSocketConnectionClosedException(
websocket._exceptions.WebSocketConnectionClosedException: Connection to remote host was lost.
Error occurred: Connection to remote host was lost.

Is it possible to change the visibility of sources?

Hi,

i was wondering if there is an option to set the visibility of sources in a scene. I animated a little overlay for a game, which should be visible when ingame and invisible when i'm in the main menu. I searched throught the sourcesservice.py and source.py files to find a method which does that, but i could not find anything. I also tried to set every "visible" option in the properties to false and then update the source properties with "set_properties_form_data", but that did'nt work either, the settings were updated but the overlay was still visible. Can you help me out with that?
Thanks

AuthenticationFailure

This module will be very meaningful to me.
Thank you in advance.

I get an error with the following code.

File "D:\twitchbot\.venv\lib\site-packages\pyslobs\connection.py", line 104, in _authenticate
    raise AuthenticationFailure("%s" % response)
pyslobs.connection.AuthenticationFailure: {'id': 'auth_request', 'jsonrpc': '2.0', 'error': {'code': -32603, 'message': 'INTERNAL_JSON_RPC_ERROR Invalid token'}}

What kind of authentication is this supposed to be failing?

I've also included the ini file.

{pyslobs.ini}

[connection]
domain=localhost
port=59650
token=QRcode's APITOKEN

Sorry for my poor English.

Python 3.9 required for Typing

When I try to import SlobsConnection, I have an error with AudioService.

  File "myscript.py", line 4, in <module>
    from pyslobs import SlobsConnection
  File "D:\Apps\Python\Python38-32\lib\site-packages\pyslobs\__init__.py", line 3, in <module>
    from .slobs.audioservice import AudioService
  File "D:\Apps\Python\Python38-32\lib\site-packages\pyslobs\slobs\audioservice.py", line 8, in <module>
    class AudioService(SlobsService):
  File "D:\Apps\Python\Python38-32\lib\site-packages\pyslobs\slobs\audioservice.py", line 18, in AudioService
    async def get_sources(self) -> list[AudioSource]:
TypeError: 'type' object is not subscriptable

Else, thanks for work. :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.