diff --git a/README.md b/README.md index 8f8da78..9e3b7e2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ ### Dependencies -* zenity (unless called with option `--no-gui`) * jq * Audio subsystem: - PulseAudio or diff --git a/pulseaudio-tcp b/pulseaudio-tcp index 20e531e..8f8e4c6 100644 --- a/pulseaudio-tcp +++ b/pulseaudio-tcp @@ -17,144 +17,81 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +## +# Arguments + +operation=$1 + +## +# Configuration + +config_dir="$HOME"/.config/pulseaudio-tcp +config="$config_dir"/config.inc.sh + ## # Functions -usage() { - cat >&2 <&2 - - if [[ -t 1 ]] && $gui && [[ -n $(type -p zenity) ]] ; then - case "$level" in - ERROR) - zenity --error --text="$msg" - ;; - *) - zenity --info --text="$msg" - ;; - esac - fi -} - -error() { - log ERROR "$@" -} - -info() { - log INFO "$@" -} - -debug() { - if "$debug" ; then - log DEBUG "$@" - else - gui=false log DEBUG "$@" - fi -} - _ssh() { if ssh -S "$USER"-pulseaudio "$remote_user"@"$remote_ip" "$@" ; then return 0 else - error "SSH remote_ip=$remote_ip failed." + echo "ERROR: SSH remote_ip=$remote_ip failed." >&2 return 1 fi } -question_yesno() { - question=$* - - if "$gui" ; then - if zenity --question --text="$question" ; then - return 0 - else - return 1 - fi - else - while read -r -p "$question [y|n] " answer ; do - case $answer in - [yY]) - return 0 - ;; - [nN]) - return 1 - ;; - esac - done - fi -} - -question_input() { - title=$1 - question=$2 - value=$3 - - if $gui ; then - zenity --entry --title="$title" --text="$question" --entry-text="$value" - else - echo "$title" >&2 - read -r -p "$question ($value)" answer - [[ -z $answer ]] && answer=$value - echo "$answer" - fi -} - # Perform setup operation do_setup() { if test -e "$config" ; then - if question_yesno "$config already exists, overwrite it?"; then - if ! test -w "$config" ; then - error "$config is not writable." - return 1 ; - elif ! test -r "$config" ; then - error "$config is not readable." - return 1 ; - else - . "$config" - fi - else - error "Setup aborted." + read -r -p "config=$config already exists, overwrite it? (Y|n) " answer + + case "$answer" in + y|Y|"") + if ! test -w "$config" ; then + echo "ERROR: config=$config is not writable." >&2 ; + return 1 ; + elif ! test -r "$config" ; then + echo "ERROR: config=$config is not readable." >&2 ; + return 1 ; + else + . "$config" + fi + ;; + *) + echo "ERROR: Setup aborted." >&2 return 1 - fi + esac elif ! test -e "$config_dir" ; then mkdir -p "$config_dir" || { - error "Could not create $config_dir" + echo "ERROR: Could not create config_dir=$config_dir." >&2 ; return 1 ; } elif ! test -d "$config_dir" ; then - error "$config_dir is not a directory" + echo "ERROR: config_dir=$config_dir is not a directory." >&2 return 1 elif ! test -w "$config_dir" ; then - error "$config_dir is not writable" + echo "ERROR: config_dir=$config_dir is not writable." >&2 return 1 fi while true ; do - if remote_ip_in=$(question_input "Set remote host" "Enter name or IP address of remote host:" "$remote_ip") ; then + read -r -p "Enter IP address of remote host ($remote_ip): " remote_ip_in + + if test -n "$remote_ip_in" ; then + break + elif test -n "$remote_ip" ; then + remote_ip_in=$remote_ip break fi done while true ; do - if remote_user_in=$(question_input "Set remote user" "Enter username on remote host:" "$remote_user") ; then + read -r -p "Enter username on remote host $remote_ip ($remote_user): " remote_user_in + + if test -n "$remote_user_in" ; then + break + elif test -n "$remote_user" ; then + remote_user_in=$remote_user break fi done @@ -162,25 +99,65 @@ do_setup() { inbound=${inbound:-true} while true ; do - if question_yesno "Enable inbound audio?"; then - inbound_in=true - break + if "$inbound" ; then + prompt="Y/n" else - inbound_in=false - break + prompt="y/N" fi + + read -r -p "Enable inbound audio ($prompt): " inbound_in + + case "$inbound_in" in + "") + if test -n "$inbound" ; then + inbound_in=$inbound + break 2 + fi + ;; + y|Y) + inbound_in=true + break 2 + ;; + n|N) + inbound_in=false + break 2 + ;; + *) + echo "ERROR: Please type \"y\" or \"n\"." >&2 + ;; + esac done outbound=${outbound:-true} while true ; do - if question_yesno "Enable outbound audio?"; then - outbound_in=true - break + if "$outbound" ; then + prompt="Y/n" else - outbound_in=false - break + prompt="y/N" fi + + read -r -p "Enable outbound audio ($prompt): " outbound_in + + case "$outbound_in" in + "") + if test -n "$outbound" ; then + outbound_in=$outbound + break 2 + fi + ;; + y|Y) + outbound_in=true + break 2 + ;; + n|N) + outbound_in=false + break 2 + ;; + *) + echo "ERROR: Please type \"y\" or \"n\"." >&2 + ;; + esac done cat > "$config" << EOF @@ -215,40 +192,37 @@ check_pa_ssh() { # Perform status operation do_status() { rv=0 - errors=() if ! check_pa_ssh ; then rv=1 fi if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then - errors+=("PulseAudio module \"module-native-protocol-tcp\" is not loaded on remote host $remote_ip.") + echo "ERROR: PulseAudio module \"module-native-protocol-tcp\" is not loaded on remote_ip=$remote_ip." >&2 rv=1 fi if "$outbound" ; then if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then - errors+=("PulseAudio module \"module-tunnel-sink\" is not loaded.") + echo "ERROR: PulseAudio module \"module-tunnel-sink\" is not loaded." >&2 rv=1 fi if ! pactl get-default-sink | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then - errors+=("\"tunnel-sink.tcp:127.0.0.1\" is not the default PulseAudio sink.") + echo "ERROR: \"tunnel-sink.tcp:127.0.0.1\" is not the default PulseAudio sink." >&2 rv=1 fi fi if "$inbound" ; then if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then - errors+=("PulseAudio module \"module-tunnel-source\" is not loaded.") + echo "ERROR: PulseAudio module \"module-tunnel-source\" is not loaded." >&2 rv=1 fi fi - if [[ $rv -eq 0 ]] ; then - info "All checks passed; pulseaudio-tcp status is okay." - else - error "pulseaudio-tcp status is not okay: ${errors[*]}" + if test "$rv" -eq 0 ; then + echo "INFO: All checks passed; pulseaudio-tcp status is okay." >&2 fi return "$rv" @@ -257,10 +231,10 @@ do_status() { # Acquire PulseAudio cookie from remote host sync_pa_cookie() { if scp -q "$remote_user"@"$remote_ip":.config/pulse/cookie ~/.config/pulse/cookie ; then - debug "Synced PulseAudio cookie from remote host $remote_ip." + echo "INFO: Synced PulseAudio cookie from remote_ip=$remote_ip." >&2 return 0 else - error "Unable to sync PulseAudio cookie from remote host $remote_ip." + echo "ERROR: Unable to sync PulseAudio cookie from remote_ip=$remote_ip." >&2 return 1 fi } @@ -273,13 +247,13 @@ establish_ssh_portforward() { # Enable PulseAudio TCP tunnel server on remote host enable_remote_pa_tunnel_server() { if _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then - debug "PulseAudio module \"module-native-protocol-tcp\" already loaded on remote host $remote_ip." + echo "INFO: PulseAudio module \"module-native-protocol-tcp\" already loaded on remote_ip=$remote_ip." >&2 return 0 elif _ssh pactl load-module module-native-protocol-tcp listen=127.0.0.1 auth-ip-acl=127.0.0.1 ; then - debug "Loaded PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip." + echo "INFO: Loaded PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2 return 0 else - error "Unable to load PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip." + echo "ERROR: Unable to load PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2 return 1 fi } @@ -287,13 +261,13 @@ enable_remote_pa_tunnel_server() { # Enable tunnel sink on local host enable_local_pa_tunnel_sink() { if pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then - debug "PulseAudio module \"module-tunnel-sink\" already loaded." + echo "INFO: PulseAudio module \"module-tunnel-sink\" already loaded." >&2 return 0 elif pactl load-module module-tunnel-sink server=tcp:127.0.0.1 ; then - debug "Loaded PulseAudio module \"module-tunnel-sink\"." + echo "INFO: Loaded PulseAudio module \"module-tunnel-sink\"." >&2 return 0 else - error "Unable to load PulseAudio module \"module-tunnel-sink\"." + echo "ERROR: Unable to load PulseAudio module \"module-tunnel-sink\"." >&2 return 1 fi } @@ -301,13 +275,13 @@ enable_local_pa_tunnel_sink() { # Enable tunnel source on local host enable_local_pa_tunnel_source() { if pactl list modules | grep -Fq "Name: module-tunnel-source" ; then - debug "PulseAudio module \"module-tunnel-source\" already loaded." + echo "INFO: PulseAudio module \"module-tunnel-source\" already loaded." >&2 return 0 elif pactl load-module module-tunnel-source server=tcp:127.0.0.1 ; then - debug "Loaded PulseAudio module \"module-tunnel-source\"." + echo "INFO: Loaded PulseAudio module \"module-tunnel-source\"." >&2 return 0 else - error "Unable to load PulseAudio module \"module-tunnel-source\"." + echo "ERROR: Unable to load PulseAudio module \"module-tunnel-source\"." >&2 return 1 fi } @@ -315,10 +289,10 @@ enable_local_pa_tunnel_source() { # Set tunnel sink as default sink on local host set_local_pa_tunnel_sink_as_default() { if pactl set-default-sink tunnel-sink.tcp:127.0.0.1 ; then - debug "Set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." + echo "INFO: Set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." >&2 return 0 else - error "Failed to set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." + echo "ERROR: Failed to set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." >&2 return 1 fi } @@ -331,7 +305,6 @@ do_start() { if "$outbound" ; then enable_local_pa_tunnel_sink || return 1 - # workaround, local tunnel is not available immediately sleep 1 set_local_pa_tunnel_sink_as_default || return 1 fi @@ -346,10 +319,10 @@ do_start() { # Remove PulseAudio TCP tunnel sink on local host remove_local_pa_tunnel_sink() { if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then - debug " PulseAudio module \"module-tunnel-sink\" is not loaded." + echo "INFO: PulseAudio module \"module-tunnel-sink\" is not loaded." >&2 return 0 elif ! pactl list sinks | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then - debug "No PulseAudio tunnel sink to 127.0.0.1 exists." + echo "INFO: No PulseAudio tunnel sink to 127.0.0.1 exists." >&2 return 0 else owner_module=$( @@ -358,10 +331,10 @@ remove_local_pa_tunnel_sink() { ) if ! pactl unload-module "$owner_module" ; then - error "Could not unload owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." + echo "ERROR: Could not unload owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." >&2 return 1 else - debug "Unloaded owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." + echo "INFO: Unloaded owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." >&2 return 0 fi fi @@ -370,10 +343,10 @@ remove_local_pa_tunnel_sink() { # Remove PulseAudio TCP tunnel source on local host remove_local_pa_tunnel_source() { if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then - debug "PulseAudio module \"module-tunnel-source\" is not loaded." + echo "INFO: PulseAudio module \"module-tunnel-source\" is not loaded." >&2 return 0 elif ! pactl list sources | grep -Fq "tunnel-source.tcp:127.0.0.1" ; then - debug "No PulseAudio tunnel source from 127.0.0.1 exists." + echo "INFO: No PulseAudio tunnel source from 127.0.0.1 exists." >&2 return 0 else owner_module=$( @@ -382,10 +355,10 @@ remove_local_pa_tunnel_source() { ) if ! pactl unload-module "$owner_module" ; then - error "Could not unload owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." + echo "ERROR: Could not unload owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." >&2 return 1 else - debug "Unloaded owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." + echo "INFO: Unloaded owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." >&2 return 0 fi fi @@ -394,13 +367,13 @@ remove_local_pa_tunnel_source() { # Stop PulseAudio TCP tunnel server on remote host. disable_remote_pa_tunnel_server() { if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then - debug "PulseAudio module \"module-native-protocol-tcp\" not loaded on remote_ip=$remote_ip." + echo "INFO: PulseAudio module \"module-native-protocol-tcp\" not loaded on remote_ip=$remote_ip." >&2 return 0 elif ! _ssh pactl unload-module module-native-protocol-tcp ; then - error "Could not unload PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip." + echo "ERROR: Could not unload PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2 return 1 else - debug "Unloaded PulseAudio module \"module-native-protocol-tcp\" on remote host $remote_ip." + echo "INFO: Unloaded PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2 return 0 fi } @@ -430,93 +403,58 @@ do_stop() { return 0 } -## -# Arguments - -for arg in "$@" ; do - case "$arg" in - --debug) - debug_cmdline=true - ;; - --no-gui) - gui_cmdline=false - ;; - --help) - help_cmdline=true - ;; - *) - operation=$arg - ;; - esac -done - -## -# Configuration - -config_dir="$HOME"/.config/pulseaudio-tcp -config=$config_dir/config.inc.sh - -if [[ $help_cmdline = true ]] ; then - usage - exit 0 -fi - -if [[ $debug_cmdline = true ]] ; then - debug=true -else - debug=false -fi - -if [[ $gui_cmdline = false ]] ; then - gui=false -else - gui=true -fi - ## # Main Program rv=0 if test "$operation" = setup ; then - info "Entering setup mode ..." + echo "Entering setup mode ..." else if ! test -e "$config" ; then - error "Configfile $config does not exist (use \"$0 setup\" first)." + echo "ERROR: Configfile $config does not exist (use \"$0 setup\" first)." >&2 rv=1 - elif ! test -r "$config" ; then - error "Configfile $config is not readable." + elif ! test -r "$config" ; then + echo "ERROR: Configfile $config is not readable." >&2 rv=1 elif ! test -f "$config" ; then - error "Configfile $config is not a regular file." + echo "ERROR: Configfile $config is not a regular file." >&2 rv=1 else . "$config" - if [[ -z $remote_ip ]] ; then - error "\"remote_ip=\" not set in configfile $config." + if test -z "$remote_ip" ; then + echo "ERROR: \"remote_ip=\" not set in configfile $config." >&2 rv=1 - elif [[ -z $remote_user ]] ; then - error "\"remote_user=\" not set in configfile $config." + elif test -z "$remote_user" ; then + echo "ERROR: \"remote_user=\" not set in configfile $config." >&2 rv=1 fi fi - required_cmds=( jq pactl ssh ) - "$gui" && required_cmds+=( zenity ) - - for exe in "${required_cmds[@]}" ; do - if [[ -z $(type -p "$exe") ]] ; then - error "Required executable \"$exe\" not found." - rv=1 - fi - done + if test -z "$(which jq)" ; then + echo "ERROR: Required executable \"jq\" not found." >&2 + rv=1 + elif test -z "$(which pactl)" ; then + echo "ERROR: Required executable \"pactl\" not found." >&2 + rv=1 + elif test -z "$(which ssh)" ; then + echo "ERROR: Required executable \"ssh\" not found." >&2 + rv=1 + fi fi -if [[ $rv -ne 0 ]] ; then - error "Preliminary checks failed, skipping operation." +if test "$rv" -ne 0 ; then + echo "ERROR: Preliminary checks failed, skipping operation." >&2 else case "$operation" in + ""|-h|--help) + cat << EOF +Setup and run encrypted connection to remote PulseAudio/Pipewire server +Usage: $0 restart|setup|start|status|stop +EOF + rv=0 + ;; setup) do_setup rv=$? @@ -539,10 +477,11 @@ else rv=$? ;; *) - usage + echo "ERROR: Usage: $0 restart|setup|start|status|stop" >&2 rv=1 ;; esac fi exit "$rv" +