Compare commits

..

No commits in common. "f02ec71a3b1c5db12938294352c4f73b7d3e22b9" and "2b07a5ee429092e85adab4ec677b3e00da820d1c" have entirely different histories.

2 changed files with 154 additions and 216 deletions

View File

@ -4,7 +4,6 @@
### Dependencies ### Dependencies
* zenity (unless called with option `--no-gui`)
* jq * jq
* Audio subsystem: * Audio subsystem:
- PulseAudio or - PulseAudio or

View File

@ -17,144 +17,81 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
##
# Arguments
operation=$1
##
# Configuration
config_dir="$HOME"/.config/pulseaudio-tcp
config="$config_dir"/config.inc.sh
## ##
# Functions # Functions
usage() {
cat >&2 <<EOF
Usage: $0 [OPTIONS] OPERATION
Options:
--debug Enable additional debug output for start and stop operations.
--no-gui Do not display GUI dialogs, use terminal for input and output.
Operations:
setup Interactively gather settings.
start Start redirection to remote host.
stop Stop redirection to remote host.
restart Stop and then start redirection.
status Check if redirection is set up and enabled.
EOF
}
log() {
level=$1
shift
msg=$*
echo "$level: $msg" >&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() { _ssh() {
if ssh -S "$USER"-pulseaudio "$remote_user"@"$remote_ip" "$@" ; then if ssh -S "$USER"-pulseaudio "$remote_user"@"$remote_ip" "$@" ; then
return 0 return 0
else else
error "SSH remote_ip=$remote_ip failed." echo "ERROR: SSH remote_ip=$remote_ip failed." >&2
return 1 return 1
fi 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 # Perform setup operation
do_setup() { do_setup() {
if test -e "$config" ; then if test -e "$config" ; then
if question_yesno "$config already exists, overwrite it?"; then read -r -p "config=$config already exists, overwrite it? (Y|n) " answer
if ! test -w "$config" ; then
error "$config is not writable." case "$answer" in
return 1 ; y|Y|"")
elif ! test -r "$config" ; then if ! test -w "$config" ; then
error "$config is not readable." echo "ERROR: config=$config is not writable." >&2 ;
return 1 ; return 1 ;
else elif ! test -r "$config" ; then
. "$config" echo "ERROR: config=$config is not readable." >&2 ;
fi return 1 ;
else else
error "Setup aborted." . "$config"
fi
;;
*)
echo "ERROR: Setup aborted." >&2
return 1 return 1
fi esac
elif ! test -e "$config_dir" ; then elif ! test -e "$config_dir" ; then
mkdir -p "$config_dir" || { mkdir -p "$config_dir" || {
error "Could not create $config_dir" echo "ERROR: Could not create config_dir=$config_dir." >&2 ;
return 1 ; return 1 ;
} }
elif ! test -d "$config_dir" ; then 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 return 1
elif ! test -w "$config_dir" ; then elif ! test -w "$config_dir" ; then
error "$config_dir is not writable" echo "ERROR: config_dir=$config_dir is not writable." >&2
return 1 return 1
fi fi
while true ; do 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 break
fi fi
done done
while true ; do 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 break
fi fi
done done
@ -162,25 +99,65 @@ do_setup() {
inbound=${inbound:-true} inbound=${inbound:-true}
while true ; do while true ; do
if question_yesno "Enable inbound audio?"; then if "$inbound" ; then
inbound_in=true prompt="Y/n"
break
else else
inbound_in=false prompt="y/N"
break
fi 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 done
outbound=${outbound:-true} outbound=${outbound:-true}
while true ; do while true ; do
if question_yesno "Enable outbound audio?"; then if "$outbound" ; then
outbound_in=true prompt="Y/n"
break
else else
outbound_in=false prompt="y/N"
break
fi 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 done
cat > "$config" << EOF cat > "$config" << EOF
@ -215,40 +192,37 @@ check_pa_ssh() {
# Perform status operation # Perform status operation
do_status() { do_status() {
rv=0 rv=0
errors=()
if ! check_pa_ssh ; then if ! check_pa_ssh ; then
rv=1 rv=1
fi fi
if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then 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 rv=1
fi fi
if "$outbound" ; then if "$outbound" ; then
if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; 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 rv=1
fi fi
if ! pactl get-default-sink | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then 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 rv=1
fi fi
fi fi
if "$inbound" ; then if "$inbound" ; then
if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; 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 rv=1
fi fi
fi fi
if [[ $rv -eq 0 ]] ; then if test "$rv" -eq 0 ; then
info "All checks passed; pulseaudio-tcp status is okay." echo "INFO: All checks passed; pulseaudio-tcp status is okay." >&2
else
error "pulseaudio-tcp status is not okay: ${errors[*]}"
fi fi
return "$rv" return "$rv"
@ -257,10 +231,10 @@ do_status() {
# Acquire PulseAudio cookie from remote host # Acquire PulseAudio cookie from remote host
sync_pa_cookie() { sync_pa_cookie() {
if scp -q "$remote_user"@"$remote_ip":.config/pulse/cookie ~/.config/pulse/cookie ; then 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 return 0
else 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 return 1
fi fi
} }
@ -273,13 +247,13 @@ establish_ssh_portforward() {
# Enable PulseAudio TCP tunnel server on remote host # Enable PulseAudio TCP tunnel server on remote host
enable_remote_pa_tunnel_server() { enable_remote_pa_tunnel_server() {
if _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then 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 return 0
elif _ssh pactl load-module module-native-protocol-tcp listen=127.0.0.1 auth-ip-acl=127.0.0.1 ; then 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 return 0
else 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 return 1
fi fi
} }
@ -287,13 +261,13 @@ enable_remote_pa_tunnel_server() {
# Enable tunnel sink on local host # Enable tunnel sink on local host
enable_local_pa_tunnel_sink() { enable_local_pa_tunnel_sink() {
if pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then 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 return 0
elif pactl load-module module-tunnel-sink server=tcp:127.0.0.1 ; then 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 return 0
else else
error "Unable to load PulseAudio module \"module-tunnel-sink\"." echo "ERROR: Unable to load PulseAudio module \"module-tunnel-sink\"." >&2
return 1 return 1
fi fi
} }
@ -301,13 +275,13 @@ enable_local_pa_tunnel_sink() {
# Enable tunnel source on local host # Enable tunnel source on local host
enable_local_pa_tunnel_source() { enable_local_pa_tunnel_source() {
if pactl list modules | grep -Fq "Name: module-tunnel-source" ; then 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 return 0
elif pactl load-module module-tunnel-source server=tcp:127.0.0.1 ; then 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 return 0
else else
error "Unable to load PulseAudio module \"module-tunnel-source\"." echo "ERROR: Unable to load PulseAudio module \"module-tunnel-source\"." >&2
return 1 return 1
fi fi
} }
@ -315,10 +289,10 @@ enable_local_pa_tunnel_source() {
# Set tunnel sink as default sink on local host # Set tunnel sink as default sink on local host
set_local_pa_tunnel_sink_as_default() { set_local_pa_tunnel_sink_as_default() {
if pactl set-default-sink tunnel-sink.tcp:127.0.0.1 ; then 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 return 0
else 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 return 1
fi fi
} }
@ -331,7 +305,6 @@ do_start() {
if "$outbound" ; then if "$outbound" ; then
enable_local_pa_tunnel_sink || return 1 enable_local_pa_tunnel_sink || return 1
# workaround, local tunnel is not available immediately
sleep 1 sleep 1
set_local_pa_tunnel_sink_as_default || return 1 set_local_pa_tunnel_sink_as_default || return 1
fi fi
@ -346,10 +319,10 @@ do_start() {
# Remove PulseAudio TCP tunnel sink on local host # Remove PulseAudio TCP tunnel sink on local host
remove_local_pa_tunnel_sink() { remove_local_pa_tunnel_sink() {
if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then 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 return 0
elif ! pactl list sinks | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then 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 return 0
else else
owner_module=$( owner_module=$(
@ -358,10 +331,10 @@ remove_local_pa_tunnel_sink() {
) )
if ! pactl unload-module "$owner_module" ; then 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 return 1
else 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 return 0
fi fi
fi fi
@ -370,10 +343,10 @@ remove_local_pa_tunnel_sink() {
# Remove PulseAudio TCP tunnel source on local host # Remove PulseAudio TCP tunnel source on local host
remove_local_pa_tunnel_source() { remove_local_pa_tunnel_source() {
if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then 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 return 0
elif ! pactl list sources | grep -Fq "tunnel-source.tcp:127.0.0.1" ; then 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 return 0
else else
owner_module=$( owner_module=$(
@ -382,10 +355,10 @@ remove_local_pa_tunnel_source() {
) )
if ! pactl unload-module "$owner_module" ; then 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 return 1
else 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 return 0
fi fi
fi fi
@ -394,13 +367,13 @@ remove_local_pa_tunnel_source() {
# Stop PulseAudio TCP tunnel server on remote host. # Stop PulseAudio TCP tunnel server on remote host.
disable_remote_pa_tunnel_server() { disable_remote_pa_tunnel_server() {
if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then 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 return 0
elif ! _ssh pactl unload-module module-native-protocol-tcp ; then 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 return 1
else 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 return 0
fi fi
} }
@ -430,93 +403,58 @@ do_stop() {
return 0 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 # Main Program
rv=0 rv=0
if test "$operation" = setup ; then if test "$operation" = setup ; then
info "Entering setup mode ..." echo "Entering setup mode ..."
else else
if ! test -e "$config" ; then 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 rv=1
elif ! test -r "$config" ; then elif ! test -r "$config" ; then
error "Configfile $config is not readable." echo "ERROR: Configfile $config is not readable." >&2
rv=1 rv=1
elif ! test -f "$config" ; then 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 rv=1
else else
. "$config" . "$config"
if [[ -z $remote_ip ]] ; then if test -z "$remote_ip" ; then
error "\"remote_ip=<IP address>\" not set in configfile $config." echo "ERROR: \"remote_ip=<IP address>\" not set in configfile $config." >&2
rv=1 rv=1
elif [[ -z $remote_user ]] ; then elif test -z "$remote_user" ; then
error "\"remote_user=<username>\" not set in configfile $config." echo "ERROR: \"remote_user=<username>\" not set in configfile $config." >&2
rv=1 rv=1
fi fi
fi fi
required_cmds=( jq pactl ssh ) if test -z "$(which jq)" ; then
"$gui" && required_cmds+=( zenity ) echo "ERROR: Required executable \"jq\" not found." >&2
rv=1
for exe in "${required_cmds[@]}" ; do elif test -z "$(which pactl)" ; then
if [[ -z $(type -p "$exe") ]] ; then echo "ERROR: Required executable \"pactl\" not found." >&2
error "Required executable \"$exe\" not found." rv=1
rv=1 elif test -z "$(which ssh)" ; then
fi echo "ERROR: Required executable \"ssh\" not found." >&2
done rv=1
fi
fi fi
if [[ $rv -ne 0 ]] ; then if test "$rv" -ne 0 ; then
error "Preliminary checks failed, skipping operation." echo "ERROR: Preliminary checks failed, skipping operation." >&2
else else
case "$operation" in 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) setup)
do_setup do_setup
rv=$? rv=$?
@ -539,10 +477,11 @@ else
rv=$? rv=$?
;; ;;
*) *)
usage echo "ERROR: Usage: $0 restart|setup|start|status|stop" >&2
rv=1 rv=1
;; ;;
esac esac
fi fi
exit "$rv" exit "$rv"