2023-06-26 02:35:59 -04:00
|
|
|
;;; obs.el -*- lexical-binding: t; -*-
|
|
|
|
(require 'websocket)
|
|
|
|
(require 'cl)
|
|
|
|
(require 'json)
|
|
|
|
(require 'uuidgen)
|
|
|
|
|
2023-06-27 03:11:31 -04:00
|
|
|
(defcustom obs/pause-delay 0.5
|
2023-06-26 02:35:59 -04:00
|
|
|
"Allow idle for this ammount of seconds before pausing obs")
|
|
|
|
|
2023-06-27 03:11:31 -04:00
|
|
|
(defvar obs/ws nil
|
|
|
|
"OBS Web socket connection")
|
|
|
|
(defvar obs/request-handlers nil
|
|
|
|
"Request callbacks")
|
|
|
|
(defvar obs/recording nil
|
|
|
|
"Is obs recording")
|
|
|
|
(defvar obs/paused nil
|
|
|
|
"Is obs paused")
|
|
|
|
(defvar obs/idle-timer nil
|
|
|
|
"Idle timer to pause OBS")
|
2023-06-26 02:35:59 -04:00
|
|
|
|
|
|
|
(defun obs/build-object (type contents)
|
2023-06-27 03:11:31 -04:00
|
|
|
(let ((object (make-hash-table)))
|
|
|
|
(puthash "op" type object)
|
|
|
|
(puthash "d" contents object)
|
|
|
|
object))
|
2023-06-26 02:35:59 -04:00
|
|
|
|
|
|
|
(defun obs/build-request (type uuid content)
|
|
|
|
(let ((request (make-hash-table)))
|
|
|
|
(puthash "requestType" type request)
|
|
|
|
(puthash "requestId" uuid request)
|
|
|
|
(if content
|
|
|
|
(puthash "requestData" content request)
|
|
|
|
(puthash "requestData" (make-hash-table) request))
|
|
|
|
(obs/build-object 6 request)))
|
|
|
|
|
2023-06-27 03:11:31 -04:00
|
|
|
(defun obs/hello (body)
|
|
|
|
(let ((ident-map (make-hash-table)))
|
|
|
|
(puthash "rpcVersion" 1 ident-map)
|
|
|
|
;; Listen to events inthe outputs category
|
|
|
|
(puthash "eventSubscriptions" 64 ident-map)
|
|
|
|
(let* ((ident (obs/build-object 1 ident-map))
|
|
|
|
(ident-ser (json-serialize ident)))
|
|
|
|
(websocket-send-text obs/ws ident-ser))))
|
2023-06-26 02:35:59 -04:00
|
|
|
|
2023-06-27 03:11:31 -04:00
|
|
|
(defun obs/pause-hook ()
|
|
|
|
;; Clear out the timer now that we have been executed
|
|
|
|
(setq obs/idle-timer nil)
|
|
|
|
;; Send a message to pause OBS
|
|
|
|
(let ((request (obs/build-request "PauseRecord" (uuidgen-4) nil)))
|
|
|
|
(websocket-send-text obs/ws (json-serialize request))))
|
|
|
|
|
|
|
|
(defun obs/setup-pause-hook ()
|
|
|
|
(when (not (and obs/recording obs/paused))
|
|
|
|
(run-with-idle-timer obs/pause-delay nil #'obs/pause-hook)))
|
|
|
|
|
|
|
|
(defun obs/unpause-hook ()
|
|
|
|
;; First remove the hook now that we have been executed
|
|
|
|
(remove-hook 'pre-command-hook #'obs/unpause-hook)
|
|
|
|
;; Send a message to unpause OBS
|
|
|
|
(let ((request (obs/build-request "ResumeRecord" (uuidgen-4) nil)))
|
2023-06-26 02:35:59 -04:00
|
|
|
(websocket-send-text obs/ws (json-serialize request))))
|
|
|
|
|
2023-06-27 03:11:31 -04:00
|
|
|
(defun obs/setup-unpause-hook ()
|
|
|
|
(add-hook 'pre-command-hook #'obs/unpause-hook))
|
|
|
|
|
|
|
|
(defun obs/handle-start (event)
|
|
|
|
"Handle a started event"
|
|
|
|
(when (not (and obs/recording (not obs/paused)))
|
|
|
|
;; First set the new state flags
|
|
|
|
(setq obs/recording t
|
|
|
|
obs/paused nil)
|
|
|
|
(obs/setup-pause-hook)))
|
|
|
|
|
|
|
|
(defun obs/handle-paused (event)
|
|
|
|
"Handle a paused event"
|
|
|
|
(when (not (and obs/recording obs/paused))
|
|
|
|
;; First set the new state flags
|
|
|
|
(setq obs/recording t
|
|
|
|
obs/paused t)
|
|
|
|
(obs/setup-unpause-hook)))
|
|
|
|
|
|
|
|
(defun obs/handle-stopped (event)
|
|
|
|
"Handle a stopped event"
|
|
|
|
(when obs/recording
|
|
|
|
;; First set the new state flags
|
|
|
|
(setq obs/recording nil
|
|
|
|
obs/paused nil)
|
|
|
|
;; Then bring down the mode
|
|
|
|
(obs/stop)))
|
|
|
|
|
|
|
|
(defun obs/post-ident-response (response)
|
|
|
|
"Process the response with the current state and apply it"
|
|
|
|
(let* ((response-data (gethash "responseData" response))
|
|
|
|
(recording (gethash "outputActive" response-data))
|
|
|
|
(paused (not (equal :false (gethash "outputPaused" response-data)))))
|
|
|
|
;; Call the correct state transition handler
|
|
|
|
(cond ((and recording (not paused))
|
|
|
|
(obs/handle-start nil))
|
|
|
|
((and recording paused)
|
|
|
|
(obs/handle-paused nil))
|
|
|
|
(t
|
|
|
|
(obs/handle-stopped nil)))))
|
|
|
|
|
|
|
|
(defun obs/identified ()
|
|
|
|
"After we are identified, send a message to get the current state"
|
|
|
|
;; Go ahead and send a message to get the current state
|
|
|
|
(let* ((uuid (uuidgen-4))
|
|
|
|
(record-status (obs/build-request "GetRecordStatus" uuid nil)))
|
|
|
|
(puthash uuid #'obs/post-ident-response obs/request-handlers)
|
|
|
|
(websocket-send-text obs/ws (json-serialize record-status))))
|
|
|
|
|
|
|
|
(defun obs/process-event (event)
|
|
|
|
(let* ((event-data (gethash "eventData" event))
|
|
|
|
(output-state (gethash "outputState" event-data)))
|
|
|
|
(cond ((equal output-state "OBS_WEBSOCKET_OUTPUT_STOPPED")
|
|
|
|
(obs/handle-stopped event))
|
|
|
|
((or (equal output-state "OBS_WEBSOCKET_OUTPUT_STARTED")
|
|
|
|
(equal output-state "OBS_WEBSOCKET_OUTPUT_RESUMED"))
|
|
|
|
(obs/handle-start event))
|
|
|
|
((equal output-state "OBS_WEBSOCKET_OUTPUT_PAUSED")
|
|
|
|
(obs/handle-paused event))
|
|
|
|
;; Ignore irrelevant types
|
|
|
|
((or (equal output-state "OBS_WEBSOCKET_OUTPUT_STARTING")
|
|
|
|
(equal output-state "OBS_WEBSOCKET_OUTPUT_STOPPING")
|
|
|
|
(equal output-state "OBS_WEBSOCKET_OUTPUT_PAUSING"))
|
|
|
|
nil)
|
|
|
|
(t
|
|
|
|
(print "Unknown event")
|
|
|
|
(print event)))))
|
|
|
|
|
|
|
|
(defun obs/process-response (response)
|
|
|
|
(let ((uuid (gethash "requestId" response)))
|
|
|
|
(when (gethash uuid obs/request-handlers)
|
|
|
|
(funcall (gethash uuid obs/request-handlers) response)
|
|
|
|
(remhash uuid obs/request-handlers))))
|
|
|
|
|
2023-06-26 02:35:59 -04:00
|
|
|
(defun obs/process-message (_websocket frame)
|
2023-06-27 03:11:31 -04:00
|
|
|
(let* ((parsed (json-parse-string (websocket-frame-text frame)))
|
|
|
|
(op (gethash "op" parsed))
|
|
|
|
(body (gethash "d" parsed)))
|
|
|
|
(cond ((equal op 0)
|
|
|
|
(obs/hello body))
|
|
|
|
((equal op 2) (obs/identified))
|
|
|
|
((equal op 5) (obs/process-event body))
|
|
|
|
((equal op 7) (obs/process-response body))
|
|
|
|
(t
|
|
|
|
(print "Unhandled message")
|
|
|
|
(print parsed)))))
|
2023-06-26 02:35:59 -04:00
|
|
|
|
2023-06-27 03:11:31 -04:00
|
|
|
(defun obs/start ()
|
2023-06-26 02:35:59 -04:00
|
|
|
(interactive)
|
2023-06-27 03:11:31 -04:00
|
|
|
(print "Starting obs mode")
|
|
|
|
;; Setup websocket connection and initalize variables
|
2023-06-26 02:35:59 -04:00
|
|
|
(setq websocket-debug t)
|
2023-06-27 03:11:31 -04:00
|
|
|
(setq obs/request-handlers (make-hash-table :test 'equal)
|
|
|
|
obs/ws (websocket-open
|
2023-06-26 02:35:59 -04:00
|
|
|
"ws://localhost:4455"
|
|
|
|
:on-message #'obs/process-message
|
2023-06-27 03:11:31 -04:00
|
|
|
:on-close #'obs/handle-shutdown)
|
|
|
|
obs/idle-timer nil))
|
|
|
|
|
|
|
|
(defun obs/handle-shutdown (_websocket)
|
|
|
|
(obs/stop-inner nil))
|
|
|
|
|
|
|
|
(defun obs/stop ()
|
|
|
|
(interactive)
|
|
|
|
(obs/stop-inner t))
|
|
|
|
|
|
|
|
(defun obs/stop-inner (should-close)
|
|
|
|
(when obs-mode
|
|
|
|
;; Close everything and reset to nil
|
|
|
|
(when (and obs/ws should-close)
|
|
|
|
(websocket-close obs/ws))
|
|
|
|
(setq obs/request-handlers nil)
|
|
|
|
(when obs/idle-timer
|
|
|
|
(cancel-timer obs/idle-timer)
|
|
|
|
(setq obs/idle-timer nil))
|
|
|
|
(print "Closing obs-mode")
|
|
|
|
(when obs-mode
|
|
|
|
(obs-mode -1))))
|
2023-06-26 02:35:59 -04:00
|
|
|
|
|
|
|
(define-minor-mode obs-mode
|
2023-06-27 03:11:31 -04:00
|
|
|
"OBS Mode"
|
|
|
|
:lighter " OBS"
|
2023-06-26 02:35:59 -04:00
|
|
|
:global t
|
|
|
|
(if obs-mode
|
2023-06-27 03:11:31 -04:00
|
|
|
(obs/start)
|
|
|
|
(obs/stop)))
|
2023-06-26 02:35:59 -04:00
|
|
|
|
|
|
|
(provide 'obs-mode)
|