;;; obs.el -*- lexical-binding: t; -*- (require 'websocket) (require 'cl) (require 'json) (require 'uuidgen) (defcustom obs/pause-delay 0.5 "Allow idle for this ammount of seconds before pausing obs") (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)) (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))) (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/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))) (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/start () (interactive) (print "Starting obs mode") ;; Setup websocket connection and initalize variables (setq websocket-debug t) (setq obs/request-handlers (make-hash-table :test 'equal) obs/ws (websocket-open "ws://localhost:4455" :on-message #'obs/process-message :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 " OBS" :global t (if obs-mode (obs/start) (obs/stop))) (provide 'obs-mode)