mirror of
https://github.com/vale981/stream
synced 2025-03-06 02:21:37 -05:00
implement higher level interface
This commit is contained in:
parent
7a7c134f6b
commit
b413c05d6f
3 changed files with 244 additions and 17 deletions
|
@ -1,4 +1,14 @@
|
||||||
(ns stream.commander.api)
|
(ns stream.commander.api
|
||||||
|
(:require [stream.commander.systemd :as sys]
|
||||||
|
[clojure.string :as string]
|
||||||
|
[taoensso.timbre :as timbre
|
||||||
|
:refer [log trace debug info warn error fatal report
|
||||||
|
logf tracef debugf infof warnf errorf fatalf reportf
|
||||||
|
spy get-env]]
|
||||||
|
[clojure.core.async
|
||||||
|
:as a
|
||||||
|
:refer [>! <! >!! <!! go chan buffer close! thread
|
||||||
|
alts! alts!! timeout]]))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -6,9 +16,102 @@
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
;; TODO: specs
|
;; TODO: specs
|
||||||
|
;; TODO: sanitize custom flags
|
||||||
|
|
||||||
(defrecord process
|
(defrecord process
|
||||||
[unit-name monitor-channel ffmpeg-config problems])
|
[id process-name unit-name monitor ffmpeg-config problems])
|
||||||
|
|
||||||
|
(def ^:private processes (ref {}))
|
||||||
|
|
||||||
(def processes (ref {}))
|
(defn- generate-process-id
|
||||||
|
[]
|
||||||
|
(loop []
|
||||||
|
(let [id (str (java.util.UUID/randomUUID))]
|
||||||
|
(if (contains? @processes id)
|
||||||
|
(recur)
|
||||||
|
id))))
|
||||||
|
|
||||||
|
;; TODO: load from config
|
||||||
|
(def default-ffmpeg-config
|
||||||
|
{:ffmpeg-path "ffmpeg" :rtsp-transport "tcp" :custom-flags ""
|
||||||
|
:audio-bitrate "128k" :audio-channels 1 :audio-sampling-rate 44100
|
||||||
|
:rtsp-user nil :rtsp-password nil :buffer-size "100M"})
|
||||||
|
|
||||||
|
(defn- input-string
|
||||||
|
[user pass ip port profile]
|
||||||
|
(str "rtsp://"
|
||||||
|
(if (and user pass) (str user ":" pass "@"))
|
||||||
|
ip ":" port "/" profile))
|
||||||
|
|
||||||
|
(defn ffmpeg-command
|
||||||
|
"Generate the ffmpeg command from the config."
|
||||||
|
[config]
|
||||||
|
(let [{:keys
|
||||||
|
[cam-ip
|
||||||
|
cam-rtsp-port
|
||||||
|
rtsp-user
|
||||||
|
rtsp-password
|
||||||
|
profile
|
||||||
|
stream-server
|
||||||
|
stream-key
|
||||||
|
ffmpeg-path
|
||||||
|
custom-flags
|
||||||
|
audio-bitrate
|
||||||
|
audio-channels
|
||||||
|
audio-sampling-rate
|
||||||
|
rtsp-transport
|
||||||
|
buffer-size]} (merge default-ffmpeg-config config)]
|
||||||
|
(str ffmpeg-path
|
||||||
|
" -rtsp_transport " rtsp-transport
|
||||||
|
" -i " (input-string rtsp-user rtsp-password cam-ip cam-rtsp-port profile)
|
||||||
|
" -vcodec copy -b:a " audio-bitrate
|
||||||
|
" -ac " audio-channels
|
||||||
|
" -ar " audio-sampling-rate
|
||||||
|
" -f flv -bufsize " buffer-size
|
||||||
|
" -tune film " stream-server "/" stream-key)))
|
||||||
|
|
||||||
|
(defn- sanitize-process-name [name]
|
||||||
|
(string/lower-case (string/replace name #"\W" "-")))
|
||||||
|
|
||||||
|
(defn create-process!
|
||||||
|
"Creates a process, adds it to the registry and assigns a monitoring
|
||||||
|
channel to it. Returns the process."
|
||||||
|
[process-name ffmpeg-config]
|
||||||
|
(let [id (generate-process-id)
|
||||||
|
unit-name (str (sanitize-process-name process-name)
|
||||||
|
"-" id)
|
||||||
|
path (sys/create-service! unit-name
|
||||||
|
(ffmpeg-command ffmpeg-config)
|
||||||
|
(get ffmpeg-config :description
|
||||||
|
"FFMPEG streaming process, created by `stream`."))
|
||||||
|
monitor (sys/create-monitor! unit-name)
|
||||||
|
process (->process id process-name unit-name monitor ffmpeg-config #{})]
|
||||||
|
(debug "Creating process with ID:" id)
|
||||||
|
(dosync
|
||||||
|
(commute processes assoc id process))
|
||||||
|
process))
|
||||||
|
|
||||||
|
(defn delete-process!
|
||||||
|
"Deletes a process from the process map, stops it and deletes the unit file.
|
||||||
|
Returns `true` on success, `false` otherwise."
|
||||||
|
[id]
|
||||||
|
(debug "Removing process with ID:" id)
|
||||||
|
(if-let [proc (get @processes id)]
|
||||||
|
(let [{:keys [unit-name monitor]} proc
|
||||||
|
[_ close] monitor]
|
||||||
|
(sys/remove-service! unit-name)
|
||||||
|
(println close)
|
||||||
|
(close)
|
||||||
|
(dosync (commute processes dissoc id))
|
||||||
|
true)
|
||||||
|
false))
|
||||||
|
|
||||||
|
(defn delete-all-processes! []
|
||||||
|
"Deletes all processes."
|
||||||
|
(doseq [[id _] @processes]
|
||||||
|
(delete-process! id)))
|
||||||
|
|
||||||
|
(defn get-process!
|
||||||
|
"Get the process with the id."
|
||||||
|
[id]
|
||||||
|
(get @processes id))
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
logf tracef debugf infof warnf errorf fatalf reportf
|
logf tracef debugf infof warnf errorf fatalf reportf
|
||||||
spy get-env]]
|
spy get-env]]
|
||||||
[clojure.java.shell :refer [sh]]
|
[clojure.java.shell :refer [sh]]
|
||||||
[slingshot.slingshot :refer [throw+]])
|
[slingshot.slingshot :refer [throw+]]
|
||||||
|
[clojure.core.async
|
||||||
|
:as a
|
||||||
|
:refer [>! <! >!! <!! go chan buffer close! thread
|
||||||
|
alts! alts!! timeout]])
|
||||||
(:import [de.thjom.java.systemd Systemd Manager Systemd$InstanceType
|
(:import [de.thjom.java.systemd Systemd Manager Systemd$InstanceType
|
||||||
UnitStateListener]))
|
UnitStateListener]))
|
||||||
|
|
||||||
|
@ -76,7 +80,8 @@
|
||||||
(if (= (:exit result) 0)
|
(if (= (:exit result) 0)
|
||||||
true
|
true
|
||||||
(throw+ {:type ::systemd-error
|
(throw+ {:type ::systemd-error
|
||||||
:name name :message (:err result)}))))
|
:detail-type ::command-error
|
||||||
|
:message (:err result)}))))
|
||||||
|
|
||||||
(defn reload-systemd!
|
(defn reload-systemd!
|
||||||
"Reloads the systemd user instance."
|
"Reloads the systemd user instance."
|
||||||
|
@ -136,11 +141,7 @@
|
||||||
(defn enable-service!
|
(defn enable-service!
|
||||||
"Enables the service with the name. Returns true on success."
|
"Enables the service with the name. Returns true on success."
|
||||||
[name]
|
[name]
|
||||||
(let [result (sh "systemctl" "--user" "enable" name)]
|
(run-systemd-command! "enable" name))
|
||||||
(if (= (:exit result) 0)
|
|
||||||
true
|
|
||||||
(throw+ {:type ::systemd-error :message "Service can't be enabled."
|
|
||||||
:name name :err (:err result)}))))
|
|
||||||
|
|
||||||
(defn disable-service!
|
(defn disable-service!
|
||||||
"Disables the service with the name."
|
"Disables the service with the name."
|
||||||
|
@ -149,9 +150,15 @@
|
||||||
|
|
||||||
(defn create-service!
|
(defn create-service!
|
||||||
"Creates a unit file and reloads systemd. See `create-unit-file`."
|
"Creates a unit file and reloads systemd. See `create-unit-file`."
|
||||||
[name command description target]
|
([name command description]
|
||||||
|
(create-service! name command description "default.target"))
|
||||||
|
([name command description target]
|
||||||
|
(if (re-find #"[0-9]" (str (first name)))
|
||||||
|
(throw+ {:type ::systemd-error
|
||||||
|
:detail-type ::create-error
|
||||||
|
:message "Service name can't start with a digit."}))
|
||||||
(create-unit-file! name command description target)
|
(create-unit-file! name command description target)
|
||||||
(reload-systemd!))
|
(reload-systemd!)))
|
||||||
|
|
||||||
(defn remove-service!
|
(defn remove-service!
|
||||||
"Stops the service, removes the unit file and reloads systemd."
|
"Stops the service, removes the unit file and reloads systemd."
|
||||||
|
@ -160,6 +167,32 @@
|
||||||
(remove-unit-file! name)
|
(remove-unit-file! name)
|
||||||
(reload-systemd!))
|
(reload-systemd!))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
; Process Management ;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- DBusMap->map [map]
|
||||||
|
(reduce (fn [result [key value]]
|
||||||
|
(assoc result (keyword key) (. value getValue)))
|
||||||
|
{} map))
|
||||||
|
|
||||||
|
;; TODO: implement removal
|
||||||
|
(defn create-monitor!
|
||||||
|
"Creates a monitoring channel for the service with the name.
|
||||||
|
Returns a vector with the channel and a function to close it."
|
||||||
|
[name]
|
||||||
|
(debug "Creating monitor for service:" name)
|
||||||
|
(let [chan (chan)
|
||||||
|
listener
|
||||||
|
(reify
|
||||||
|
UnitStateListener
|
||||||
|
(stateChanged [this unit properties]
|
||||||
|
(>!! chan (DBusMap->map properties))))]
|
||||||
|
(. (get-service! name) addListener listener)
|
||||||
|
[chan (fn []
|
||||||
|
(close! chan)
|
||||||
|
(. (get-service! name) removeListener listener))]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
; Graveyard ;
|
; Graveyard ;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
(:require [stream.commander.systemd :as systemd]
|
(:require [stream.commander.systemd :as systemd]
|
||||||
[stream.commander.api :as api]
|
[stream.commander.api :as api]
|
||||||
[clojure.test :refer :all]
|
[clojure.test :refer :all]
|
||||||
[clojure.java.io :as io]))
|
[clojure.java.io :as io]
|
||||||
|
[slingshot.slingshot :refer [try+]]
|
||||||
|
[clojure.core.async
|
||||||
|
:as a]))
|
||||||
|
|
||||||
(deftest unit-files
|
(deftest unit-files
|
||||||
(testing "The rendering."
|
(testing "The rendering."
|
||||||
|
@ -20,7 +23,7 @@
|
||||||
(is (not (.exists (io/as-file (systemd/get-unit-path name))))))))
|
(is (not (.exists (io/as-file (systemd/get-unit-path name))))))))
|
||||||
|
|
||||||
(deftest systemd-services
|
(deftest systemd-services
|
||||||
(let [name (str (java.util.UUID/randomUUID))
|
(let [name (str "testing-" (java.util.UUID/randomUUID))
|
||||||
unit-path (systemd/create-unit-file! name
|
unit-path (systemd/create-unit-file! name
|
||||||
"cat /dev/zero" "test service")]
|
"cat /dev/zero" "test service")]
|
||||||
(testing "loading the service"
|
(testing "loading the service"
|
||||||
|
@ -35,6 +38,11 @@
|
||||||
(systemd/stop-service! name)
|
(systemd/stop-service! name)
|
||||||
(is (= :inactive (systemd/get-service-state! name))))
|
(is (= :inactive (systemd/get-service-state! name))))
|
||||||
|
|
||||||
|
(testing "restarting the service"
|
||||||
|
(systemd/start-service! name)
|
||||||
|
(systemd/restart-service! name)
|
||||||
|
(is (= :active (systemd/get-service-state! name))))
|
||||||
|
|
||||||
(testing "enabling the service"
|
(testing "enabling the service"
|
||||||
(systemd/enable-service! name)
|
(systemd/enable-service! name)
|
||||||
(is (= :enabled (systemd/get-service-file-state! name))))
|
(is (= :enabled (systemd/get-service-file-state! name))))
|
||||||
|
@ -45,4 +53,87 @@
|
||||||
|
|
||||||
(testing "removing the service"
|
(testing "removing the service"
|
||||||
(systemd/remove-service! name)
|
(systemd/remove-service! name)
|
||||||
(is (= :not-found (systemd/get-service-load-state! name))))))
|
(is (= :not-found (systemd/get-service-load-state! name))))
|
||||||
|
|
||||||
|
(testing "creating a service automatically"
|
||||||
|
(systemd/create-service! name "cat /dev/zero" "test service")
|
||||||
|
(is (= :loaded (systemd/get-service-load-state! name))))
|
||||||
|
|
||||||
|
(let [[channel close] (systemd/create-monitor! name)]
|
||||||
|
(testing "detecting activity"
|
||||||
|
(systemd/restart-service! name)
|
||||||
|
(is (a/<!! channel) "No value in channel!")
|
||||||
|
(while (a/poll! channel))
|
||||||
|
(close)
|
||||||
|
(is (= nil (a/<!! channel)))))
|
||||||
|
|
||||||
|
(testing "failing systemd command"
|
||||||
|
(systemd/remove-service! name)
|
||||||
|
(try+
|
||||||
|
(systemd/start-service! "non-existend")
|
||||||
|
(is false "No exception thrown.")
|
||||||
|
(catch [:type :stream.commander.systemd/systemd-error] _
|
||||||
|
(is true))
|
||||||
|
(catch Object _
|
||||||
|
(is false "Wrong exception thrown."))))
|
||||||
|
|
||||||
|
(testing "creating service with digit as first char"
|
||||||
|
(try+
|
||||||
|
(systemd/create-service! "1234" "cat /dev/zero" "test service")
|
||||||
|
(catch [:type :stream.commander.systemd/systemd-error] _
|
||||||
|
(is true))
|
||||||
|
(catch Object _
|
||||||
|
(is false "Wrong exception thrown."))))
|
||||||
|
|
||||||
|
(testing "getting file state state of nonexisting service"
|
||||||
|
(is (not (systemd/get-service-file-state!
|
||||||
|
(str "does_not_exist"
|
||||||
|
(java.util.UUID/randomUUID))))))))
|
||||||
|
|
||||||
|
(deftest auxiliary-api
|
||||||
|
(testing "ffmpeg input string"
|
||||||
|
(is (= "rtsp://192.168.1.205:554/axis-media/media.amp"
|
||||||
|
(#'api/input-string nil nil "192.168.1.205" 554 "axis-media/media.amp")))
|
||||||
|
(is (= "rtsp://cam:pass@192.168.1.205:554/axis-media/media.amp"
|
||||||
|
(#'api/input-string "cam" "pass" "192.168.1.205" 554 "axis-media/media.amp"))))
|
||||||
|
|
||||||
|
(testing "unit name sanitizer"
|
||||||
|
(is (= "a-b-c-d-" (#'api/sanitize-process-name "a*b C?d.")))))
|
||||||
|
|
||||||
|
(deftest ffmpeg-process-management
|
||||||
|
(let [config {:cam-ip "localhost"
|
||||||
|
:cam-rtsp-port "554"
|
||||||
|
:profile "bla"
|
||||||
|
:stream-server "server"
|
||||||
|
:stream-key "key"}]
|
||||||
|
;; TODO: proper spec testing
|
||||||
|
(testing "getting an ffmpeg command"
|
||||||
|
(api/ffmpeg-command config))
|
||||||
|
|
||||||
|
(let [proc (api/create-process!
|
||||||
|
"tester" config)]
|
||||||
|
(testing "creating ffmpeg process the high-level way"
|
||||||
|
(is (= :loaded (systemd/get-service-load-state!
|
||||||
|
(:unit-name proc)))))
|
||||||
|
|
||||||
|
(testing "deleting the process"
|
||||||
|
(is (api/delete-process! (:id proc)))
|
||||||
|
(is (not (api/get-process! (:id proc))))))
|
||||||
|
|
||||||
|
(testing "creating two processes"
|
||||||
|
(api/create-process!
|
||||||
|
"tester" config)
|
||||||
|
(api/create-process!
|
||||||
|
"tester" config)
|
||||||
|
(is (= 2 (count @@#'api/processes))))
|
||||||
|
|
||||||
|
(testing "generated ids do not collide"
|
||||||
|
(doseq [i (range 100)]
|
||||||
|
(is (not (api/get-process! (#'api/generate-process-id))))))
|
||||||
|
|
||||||
|
(testing "deleting all processes"
|
||||||
|
(api/delete-all-processes!)
|
||||||
|
(is (= 0 (count @@#'api/processes))))
|
||||||
|
|
||||||
|
(testing "deleting non-existent process"
|
||||||
|
(is (not (api/delete-process! "nonexistent"))))))
|
||||||
|
|
Loading…
Add table
Reference in a new issue