Skip to content

Commit

Permalink
Widget to show and toggle OBS recording state
Browse files Browse the repository at this point in the history
  • Loading branch information
lnqs committed Feb 22, 2022
1 parent 1ceb1ce commit 4769567
Show file tree
Hide file tree
Showing 16 changed files with 366 additions and 34 deletions.
39 changes: 25 additions & 14 deletions deckconnect/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

DeckConfig = TypedDict("DeckConfig", {"id": str, "widgets": List[Widget | None]})

global_config_schema = Schema(
device = Schema(
{
Optional("brightness"): And(int, lambda b: 0 <= b <= 100),
Optional("sleep_timeout"): And(float, lambda b: b > 0.0),
Expand All @@ -34,11 +34,11 @@ def exec_config(config: str) -> Tuple[Dict[str, Any], Deck, List[Deck]]:
decks = []
default = None

def set_config(c: Dict[str, Any]) -> None:
nonlocal global_config
if global_config:
raise RuntimeError("config already set")
global_config = create_global_config(c)
def config_(c: Dict[str, Any]) -> None:
type_, conf = create_config(c)
if type_ in global_config:
raise RuntimeError(f"Config {type_} already set")
global_config[type_] = conf

def deck(c: DeckConfig) -> Deck:
d = create_deck(c)
Expand All @@ -54,12 +54,12 @@ def default_deck(c: DeckConfig) -> Deck:
return d

def widget(c: Dict[str, Any]) -> Widget:
return create_widget(c)
return create_widget(c, global_config)

exec(
config,
{
"config": set_config,
"config": config_,
"deck": deck,
"default_deck": default_deck,
"widget": widget,
Expand All @@ -69,7 +69,7 @@ def widget(c: Dict[str, Any]) -> Widget:
if not default:
raise RuntimeError("No default deck specified")

return (global_config, default, decks)
return global_config, default, decks


def process_config(path: Path | None = None) -> Tuple[Dict[str, Any], Deck, List[Deck]]:
Expand All @@ -80,16 +80,27 @@ def process_config(path: Path | None = None) -> Tuple[Dict[str, Any], Deck, List
return exec_config(config)


def create_global_config(config: Dict[str, Any]) -> Dict[str, Any]:
global_config_schema.validate(config)
return config
def create_config(config: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
type_ = config["type"]
parts = type_.rsplit(".", 1)
module = import_module(parts[0])
schema: Schema = getattr(module, parts[-1])

if not isinstance(schema, Schema):
raise RuntimeError(f"{schema} isn't a Schema")

config = config.copy()
del config["type"]
schema.validate(config)

return type_, config


def create_deck(config: DeckConfig) -> Deck:
return Deck(**config)


def create_widget(config: Dict[str, Any]) -> Widget:
def create_widget(config: Dict[str, Any], global_config: Dict[str, Any]) -> Widget:
parts = config["type"].rsplit(".", 1)
module = import_module(parts[0])
class_: Type[Widget] = getattr(module, parts[-1])
Expand All @@ -102,4 +113,4 @@ def create_widget(config: Dict[str, Any]) -> Widget:
schema = class_.get_config_schema()
schema.validate(config)

return class_(config)
return class_(config, global_config)
5 changes: 3 additions & 2 deletions deckconnect/deckmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ def __init__(
) -> None:
self.active_deck = active_deck
self.decks = decks
self.brightness = global_config.get("brightness", 100)
self.sleep_timeout = global_config.get("sleep_timeout", None)
device_config = global_config.get("deckconnect.config.device", {})
self.brightness = device_config.get("brightness", 100)
self.sleep_timeout = device_config.get("sleep_timeout", None)
self.device = device

self.update_requested_event = Event()
Expand Down
10 changes: 9 additions & 1 deletion deckconnect/default.cfg
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
config({
'type': 'deckconnect.config.device',
'brightness': 100,
'sleep_timeout': 10.0,
})

config({
'type': 'deckconnect.widgets.obs.config',
'host': 'localhost',
'port': 4444,
'password': 'supersecret',
})

default_deck({
'id': 'main',
'widgets': [
widget({'type': 'deckconnect.widgets.MicMute'}),
widget({'type': 'deckconnect.widgets.Timer'}),
widget({'type': 'deckconnect.widgets.Clock', 'format': '%H:%M', 'size': 180}),
None,
widget({'type': 'deckconnect.widgets.obs.Recording'}),
widget({'type': 'deckconnect.widgets.Text', 'text': 'Switch\nDeck', 'switch_deck': 'another'}),
],
})
Expand Down
7 changes: 5 additions & 2 deletions deckconnect/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@


class Widget:
def __init__(self, config: Dict[str, Any]) -> None:
self.config = config
def __init__(
self, widget_config: Dict[str, Any], global_config: Dict[str, Any]
) -> None:
self.config = widget_config
self.global_config = global_config
self.update_requested_event: Event | None = None
self.needs_update = False
self.press_time: float | None = None
Expand Down
6 changes: 4 additions & 2 deletions deckconnect/widgets/mic_mute.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@


class MicMute(Widget):
def __init__(self, config: Dict[str, Any]) -> None:
super().__init__(config)
def __init__(
self, widget_config: Dict[str, Any], global_config: Dict[str, Any]
) -> None:
super().__init__(widget_config, global_config)
self.pulse: None | PulseAsync = None
self.event_listener: Task[None] | None = None

Expand Down
4 changes: 4 additions & 0 deletions deckconnect/widgets/obs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from deckconnect.widgets.obs.connector import config
from deckconnect.widgets.obs.recording import Recording

__all__ = ["config", "Recording"]
172 changes: 172 additions & 0 deletions deckconnect/widgets/obs/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import asyncio
from asyncio import Condition, Task, get_event_loop, sleep
from typing import Any, AsyncIterator, Dict, cast

import obswebsocket
from obswebsocket import obsws, requests
from obswebsocket.base_classes import Baseevents, Baserequests
from obswebsocket.events import (
RecordingStarted,
RecordingStopped,
StreamStarted,
StreamStopped,
)
from schema import Optional, Schema

from deckconnect.log import debug, info

config = Schema(
{
Optional("host"): str,
Optional("port"): int,
Optional("password"): str,
}
)


class ConnectionEstablished(Baseevents): # type: ignore
def __init__(self) -> None:
obswebsocket.events.Baseevents.__init__(self)
self.name = "ConnectionEstablished"


class ConnectionLost(Baseevents): # type: ignore
def __init__(self) -> None:
obswebsocket.events.Baseevents.__init__(self)
self.name = "ConnectionLost"


class StreamingStatus(Baseevents): # type: ignore
def __init__(self) -> None:
obswebsocket.events.Baseevents.__init__(self)
self.name = "StreamingStatus"


class OBS:
def __init__(self) -> None:
self.obsws = obsws()
self.connection_watcher: Task[None] | None = None

loop = get_event_loop()

self.streaming_status: Any | None = None
self.streaming_status_watcher: Task[None] | None = None

self.last_event = None
self.event_condition = Condition()

self.obsws.register(
lambda *args: asyncio.run_coroutine_threadsafe(
self.handle_event(*args), loop
)
)

@property
def connected(self) -> bool:
return bool(self.obsws.ws and self.obsws.ws.connected)

@property
def recording(self) -> bool:
return bool(self.streaming_status and self.streaming_status.getRecording())

@property
def recording_timecode(self) -> str | None:
if self.streaming_status:
return cast(str, self.streaming_status.getRecTimecode())
return None

async def connect(self, config: Dict[str, Any]) -> None:
if self.connection_watcher:
return

self.obsws.host = config.get("host", "localhost")
self.obsws.port = config.get("port", 4444)
self.obsws.password = config.get("password")

loop = get_event_loop()
self.connection_watcher = loop.create_task(self.watch_connection())

async def listen(self) -> AsyncIterator[str]:
while True:
async with self.event_condition:
await self.event_condition.wait()
assert self.last_event
event = self.last_event.name
yield event

async def start_recording(self) -> None:
info("Starting OBS recording")
await self.perform_request(requests.StartRecording())

async def stop_recording(self) -> None:
info("Stopping OBS recording")
await self.perform_request(requests.StopRecording())

async def watch_connection(self) -> None:
was_connected = False

while True:
if not self.connected and was_connected:
debug("Connection to OBS lost")
self.obsws.eventmanager.trigger(ConnectionLost())
was_connected = False

if not self.connected:
try:
debug("Trying to connect to OBS")
self.obsws.connect()
debug("Connected to OBS")
self.obsws.eventmanager.trigger(ConnectionEstablished())
was_connected = True
except obswebsocket.exceptions.ConnectionFailure:
debug("Failed to connect to OBS")

await sleep(3.0)

async def watch_streaming_status(self) -> None:
while True:
if self.connected:
status = self.obsws.call(requests.GetStreamingStatus())
if (
not self.streaming_status
or status.datain != self.streaming_status.datain
):
self.streaming_status = status
self.obsws.eventmanager.trigger(StreamingStatus())
if (
not self.connected
or not self.streaming_status
or (
not self.streaming_status.getRecording()
and not self.streaming_status.getStreaming()
)
):
self.streaming_status = None
self.streaming_status_watcher = None
return
await sleep(1.0)

async def handle_event(self, event: Baseevents) -> None:
debug(f"OBS event received: {event.name}")

if isinstance(event, (ConnectionEstablished, StreamStarted, RecordingStarted)):
self.streaming_status = self.obsws.call(requests.GetStreamingStatus())
if not self.streaming_status_watcher:
self.streaming_status_watcher = get_event_loop().create_task(
self.watch_streaming_status()
)
elif isinstance(event, (StreamStopped, RecordingStopped)):
self.streaming_status = self.obsws.call(requests.GetStreamingStatus())
elif isinstance(event, ConnectionLost):
self.streaming_status = None

async with self.event_condition:
self.last_event = event
self.event_condition.notify_all()

async def perform_request(self, request: Baserequests) -> None:
if self.connected:
self.obsws.call(request)


obs = OBS()
Loading

0 comments on commit 4769567

Please sign in to comment.