From d274263718deaa0325ed195517b1b457095a075b Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Tue, 27 Jun 2023 03:11:31 -0400 Subject: [PATCH] Overhaul obs mode Wasn't robust at all, rewrote it to be more event driven, which seems to have fixed some of the issues --- obs.el | 271 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 155 insertions(+), 116 deletions(-) diff --git a/obs.el b/obs.el index 561b49e..92fd343 100644 --- a/obs.el +++ b/obs.el @@ -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)