Overhaul obs mode

Wasn't robust at all, rewrote it to be more event driven, which seems to
have fixed some of the issues
trunk
Nathan McCarty 2023-06-27 03:11:31 -04:00
parent 9c1dffb48c
commit d274263718
Signed by: thatonelutenist
SSH Key Fingerprint: SHA256:hwQEcmak9E6sdU9bXc98RHw/Xd1AhpB5HZT7ZSVJkRM
1 changed files with 155 additions and 116 deletions

271
obs.el
View File

@ -4,22 +4,25 @@
(require 'json)
(require 'uuidgen)
(defcustom obs/pause-delay 0.3
(defcustom obs/pause-delay 0.5
"Allow idle for this ammount of seconds before pausing obs")
(defvar obs/ws nil)
(defvar obs/open nil)
(defvar obs/request-handlers nil)
(defvar obs/idle-timer nil)
(defvar obs/paused nil)
(defvar obs/after-ident nil)
(defvar obs/shutdown-from-ws)
(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")
(defun obs/build-object (type contents)
(let ((object (make-hash-table)))
(puthash "op" type object)
(puthash "d" contents object)
object))
(let ((object (make-hash-table)))
(puthash "op" type object)
(puthash "d" contents object)
object))
(defun obs/build-request (type uuid content)
(let ((request (make-hash-table)))
@ -30,122 +33,158 @@
(puthash "requestData" (make-hash-table) request))
(obs/build-object 6 request)))
(defun obs/pause ()
(interactive)
(when (not obs/paused)
(let* ((uuid (uuidgen-4))
(request (obs/build-request "PauseRecord" uuid nil)))
(puthash uuid
(lambda (response)
(if (gethash "result" response)
(progn
;; (print "Paused recording")
(setq obs/paused t))
(print "Failed to pause recording")))
obs/request-handlers)
(websocket-send-text obs/ws (json-serialize request))
(cancel-timer obs/idle-timer)
(setq obs/idle-timer nil))))
(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))))
(defun obs/resume ()
(interactive)
(when obs/paused
(let* ((uuid (uuidgen-4))
(request (obs/build-request "ResumeRecord" uuid nil)))
(puthash uuid
(lambda (response)
(if (gethash "result" response)
(progn
;; (print "Resumed Recording")
(setq obs/paused nil))
(print "Failed to resume recording")))
obs/request-handlers)
(websocket-send-text obs/ws (json-serialize request))
(setq obs/idle-timer (run-with-idle-timer obs/pause-delay nil #'obs/idle-timer-fn)))))
;; TODO: Properly setup/takedown idle-timer
;; TODO: Place on timer/message listener
(defun obs/import-pause-status ()
(let* ((uuid (uuidgen-4))
(request (obs/build-request "GetRecordStatus" uuid nil)))
(puthash uuid
(lambda (response)
(if (gethash "outputPaused" response)
(progn
(setq obs/paused t)
(add-hook 'pre-command-hook #'obs/return-fn))
(progn
(setq obs/paused nil)
(remove-hook 'pre-command-hook #'obs/return-fn))))
obs/request-handlers)
(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)))
(websocket-send-text obs/ws (json-serialize request))))
(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))))
(defun obs/process-message (_websocket frame)
(let* ((parsed (json-parse-string (websocket-frame-text frame))))
(when (eq 0 (gethash "op" parsed))
(print "Got hello") ;;; The quick brown fox jumps over the lazy dog
(let ((contents (make-hash-table)))
(puthash "rpcVersion" 1 contents)
(let* ((response (obs/build-object 1 contents))
(response-string (json-serialize response)))
(websocket-send-text obs/ws response-string)))
(dolist (hook obs/after-ident)
(funcall hook))
(setq obs/after-ident '()))
(when (eq 7 (gethash "op" parsed))
(let* ((body (gethash "d" parsed))
(id (gethash "requestId" body))
(status (gethash "requestStatus" body)))
(when (gethash id obs/request-handlers)
(funcall (gethash id obs/request-handlers) status)
(remhash id obs/request-handlers))))))
(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)))))
(defun obs/return-fn ()
(when obs/paused
(obs/resume)
(remove-hook 'pre-command-hook #'obs/return-fn)))
(defun obs/idle-timer-fn ()
(when (not obs/paused)
(obs/pause)
(add-hook 'pre-command-hook #'obs/return-fn)))
(defun obs/disable ()
(interactive)
(when obs/open
(when obs/ws
(websocket-close obs/ws))
(print "Manually closed web socket in disable")
(setq obs/open nil)
(when obs/idle-timer
(cancel-timer obs/idle-timer)))
(setq obs/ws nil
obs/request-handlers nil
obs/idle-timer nil)
(print "Close obs session"))
(defun obs/enable ()
(defun obs/start ()
(interactive)
(print "Starting obs mode")
;; Setup websocket connection and initalize variables
(setq websocket-debug t)
(setq obs/ws (websocket-open
(setq obs/request-handlers (make-hash-table :test 'equal)
obs/ws (websocket-open
"ws://localhost:4455"
:on-message #'obs/process-message
:on-close (lambda (_websocket)
(progn
(setq obs/ws nil)
(obs/disable)))))
(setq obs/open t
obs/after-ident '()
obs/request-handlers (make-hash-table :test 'equal)
obs/idle-timer (run-with-idle-timer obs/pause-delay nil #'obs/idle-timer-fn))
(add-to-list 'obs/after-ident #'obs/import-pause-status))
: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))))
(define-minor-mode obs-mode
"OBS mode"
:lighter nil
"OBS Mode"
:lighter " OBS"
:global t
(if obs-mode
(obs/enable)
(obs/disable)))
(obs/start)
(obs/stop)))
(provide 'obs-mode)