diff --git a/TALKKONNECT_SETUP.md b/TALKKONNECT_SETUP.md index dc0889f..33695cf 100644 --- a/TALKKONNECT_SETUP.md +++ b/TALKKONNECT_SETUP.md @@ -226,6 +226,46 @@ curl http://localhost:8011/api/channel?channel=General/Support ## Troubleshooting +### Common Warnings (Usually Non-Fatal) + +These warnings often appear but don't prevent talkkonnect from working: + +**`Unable to Unmute failed to execute "pactl set-sink-mute 0 0"`** +- TalkKonnect is trying to unmute your audio output +- This usually fails because the sink index is wrong or pactl isn't accessible +- **Audio typically works anyway** - this is just a cosmetic error +- To fix: Run `./fix_talkkonnect_audio.sh` to check your audio configuration + +**`Unable to Find Channel Name: Root`** +- Just a warning - connection usually succeeds anyway +- The channel exists but talkkonnect didn't detect it during initial scan +- You'll see a follow-up message showing successful connection + +**`Failed to connect PipeWire event context (errno: 112)`** +- TalkKonnect is trying to use PipeWire but can't access the session +- Usually happens when running as a service without proper environment +- **If you see `Speaking ->` messages, audio is working!** +- To fix: Ensure XDG_RUNTIME_DIR is set correctly in systemd service + +### Audio Troubleshooting + +Run the audio diagnostic script: +```bash +./fix_talkkonnect_audio.sh +``` + +This will: +- Check PipeWire/PulseAudio session status +- List available audio devices +- Show current talkkonnect configuration +- Provide specific recommendations + +**Quick audio test:** +```bash +# Test as the kiosk user (replace with your user) +sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) speaker-test -t wav -c 2 -l 1 +``` + ### View Logs ```bash diff --git a/fix_talkkonnect_audio.sh b/fix_talkkonnect_audio.sh new file mode 100755 index 0000000..55a8eb8 --- /dev/null +++ b/fix_talkkonnect_audio.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# ====================================================================== +# File: fix_talkkonnect_audio.sh +# Purpose: Fix talkkonnect audio configuration +# ====================================================================== + +echo "=======================================================================" +echo "TalkKonnect Audio Configuration Fix" +echo "=======================================================================" +echo "" + +# Prompt for target user +read -p "Which user is talkkonnect running as? [kiosk]: " TARGET_USER +TARGET_USER=${TARGET_USER:-kiosk} + +# Verify the target user exists +if ! id "$TARGET_USER" &>/dev/null; then + echo "[!] Error: User '$TARGET_USER' does not exist" + exit 1 +fi + +TARGET_UID=$(id -u "$TARGET_USER") +TARGET_HOME=$(eval echo ~"$TARGET_USER") +CONFIG_FILE="$TARGET_HOME/.config/talkkonnect/talkkonnect.xml" + +echo "[+] Target user: $TARGET_USER (UID: $TARGET_UID)" +echo "[+] Config file: $CONFIG_FILE" +echo "" + +# Check PipeWire session +echo "[1] Checking PipeWire/PulseAudio session..." +if [ -S "/run/user/$TARGET_UID/pipewire-0" ]; then + echo " ✓ PipeWire socket found: /run/user/$TARGET_UID/pipewire-0" + HAS_PIPEWIRE=true +else + echo " ✗ No PipeWire socket found at /run/user/$TARGET_UID/pipewire-0" + HAS_PIPEWIRE=false +fi + +if [ -S "/run/user/$TARGET_UID/pulse/native" ]; then + echo " ✓ PulseAudio socket found: /run/user/$TARGET_UID/pulse/native" + HAS_PULSE=true +else + echo " ✗ No PulseAudio socket found" + HAS_PULSE=false +fi +echo "" + +# Check audio devices as target user +echo "[2] Checking audio devices for user $TARGET_USER..." +echo "" +echo "Available ALSA output devices:" +sudo -u "$TARGET_USER" XDG_RUNTIME_DIR=/run/user/$TARGET_UID aplay -L 2>/dev/null | grep -E "^(default|hw:|pulse)" | head -10 +echo "" + +echo "Available ALSA input devices:" +sudo -u "$TARGET_USER" XDG_RUNTIME_DIR=/run/user/$TARGET_UID arecord -L 2>/dev/null | grep -E "^(default|hw:|pulse)" | head -10 +echo "" + +# If PipeWire/Pulse is available, check sinks +if [ "$HAS_PIPEWIRE" = true ] || [ "$HAS_PULSE" = true ]; then + echo "PipeWire/PulseAudio sinks:" + sudo -u "$TARGET_USER" XDG_RUNTIME_DIR=/run/user/$TARGET_UID pactl list sinks short 2>/dev/null || echo "(pactl not available)" + echo "" + + echo "PipeWire/PulseAudio sources:" + sudo -u "$TARGET_USER" XDG_RUNTIME_DIR=/run/user/$TARGET_UID pactl list sources short 2>/dev/null || echo "(pactl not available)" + echo "" +fi + +# Check current config +echo "[3] Current talkkonnect audio configuration:" +if [ -f "$CONFIG_FILE" ]; then + echo "" + echo "Input device:" + grep -A1 '' "$CONFIG_FILE" | grep 'device=' | sed 's/.*device="\([^"]*\)".*/ \1/' + + echo "" + echo "Output device:" + grep -A1 '' "$CONFIG_FILE" | grep 'device=' | sed 's/.*device="\([^"]*\)".*/ \1/' + echo "" +else + echo " Config file not found!" + exit 1 +fi + +# Recommendations +echo "=======================================================================" +echo "Recommendations" +echo "=======================================================================" +echo "" + +if [ "$HAS_PIPEWIRE" = false ] && [ "$HAS_PULSE" = false ]; then + echo "⚠️ No PipeWire or PulseAudio session found" + echo "" + echo "Option 1: Use direct ALSA (best for dedicated audio hardware)" + echo " Edit $CONFIG_FILE" + echo " Change both input and output device to: hw:0,0" + echo "" + echo "Option 2: Start PipeWire for the $TARGET_USER user" + echo " Ensure $TARGET_USER has an active graphical session" + echo "" +else + echo "✓ Audio system detected (PipeWire/PulseAudio)" + echo "" + echo "The 'Unable to Unmute' error is usually non-fatal." + echo "If audio is working, you can ignore it." + echo "" + echo "To fix the unmute error, you can:" + echo " 1. Keep device as 'default' (usually works)" + echo " 2. OR use specific device like 'pulse' or 'hw:0,0'" + echo "" +fi + +echo "Testing audio output as $TARGET_USER:" +echo " sudo -u $TARGET_USER XDG_RUNTIME_DIR=/run/user/$TARGET_UID speaker-test -t wav -c 2 -l 1" +echo "" + +echo "Testing audio input as $TARGET_USER:" +echo " sudo -u $TARGET_USER XDG_RUNTIME_DIR=/run/user/$TARGET_UID arecord -d 3 -f cd test.wav && aplay test.wav" +echo "" + +echo "To edit config:" +echo " sudo nano $CONFIG_FILE" +echo "" + +echo "After making changes, restart talkkonnect:" +echo " sudo systemctl restart talkkonnect" +echo "" diff --git a/fix_talkkonnect_now.sh b/fix_talkkonnect_now.sh new file mode 100755 index 0000000..2883744 --- /dev/null +++ b/fix_talkkonnect_now.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# ====================================================================== +# File: fix_talkkonnect_now.sh +# Purpose: Quick fix for current talkkonnect issues +# ====================================================================== + +echo "=======================================================================" +echo "TalkKonnect Quick Fix Script" +echo "=======================================================================" +echo "" + +TARGET_USER="${1:-kiosk}" +CONFIG_FILE="/home/$TARGET_USER/.config/talkkonnect/talkkonnect.xml" + +if [ ! -f "$CONFIG_FILE" ]; then + echo "[!] Config file not found: $CONFIG_FILE" + echo "[!] Looking for alternative locations..." + + if [ -f "/home/$TARGET_USER/talkkonnect.xml" ]; then + CONFIG_FILE="/home/$TARGET_USER/talkkonnect.xml" + echo "[+] Found config at: $CONFIG_FILE" + else + echo "[!] No config file found" + exit 1 + fi +fi + +echo "[+] Using config: $CONFIG_FILE" +echo "" + +# Fix 1: Enable insecure mode for self-signed certificates +echo "[1/4] Fixing certificate issue..." +if grep -q "false" "$CONFIG_FILE"; then + sudo sed -i 's|false|true|g' "$CONFIG_FILE" + echo " ✓ Set insecure=true (allows self-signed certificates)" +elif ! grep -q "" "$CONFIG_FILE"; then + sudo sed -i 's|| true\n |' "$CONFIG_FILE" + echo " ✓ Added insecure=true setting" +else + echo " ✓ Certificate setting already correct" +fi +echo "" + +# Fix 2: Check if using SuperUser account +echo "[2/4] Checking account settings..." +if grep -q "SuperUser" "$CONFIG_FILE"; then + echo " ⚠️ WARNING: You're trying to connect as 'SuperUser'" + echo " SuperUser is the SERVER ADMIN account, not a client account." + echo "" + echo " To fix:" + echo " 1. Connect with Mumble desktop/mobile app as SuperUser" + echo " 2. Create a regular user account or allow unregistered users" + echo " 3. Use that account for talkkonnect, not SuperUser" + echo "" + read -p " Change username now? (y/n): " change_user + if [[ "$change_user" =~ ^[Yy]$ ]]; then + read -p " Enter new username: " new_username + sudo sed -i "s|.*|$new_username|" "$CONFIG_FILE" + read -s -p " Enter password (blank if none): " new_password + echo + sudo sed -i "s|.*|$new_password|" "$CONFIG_FILE" + echo " ✓ Username updated" + fi +else + CURRENT_USER=$(grep "" "$CONFIG_FILE" | sed 's/.*\(.*\)<\/username>/\1/') + echo " ✓ Username: $CURRENT_USER (not SuperUser - good!)" +fi +echo "" + +# Fix 3: Check and fix ownership +echo "[3/4] Fixing file permissions..." +sudo chown -R "$TARGET_USER:$TARGET_USER" "$(dirname $CONFIG_FILE)" +sudo chmod 644 "$CONFIG_FILE" +echo " ✓ Ownership set to $TARGET_USER" +echo "" + +# Fix 4: Check systemd service +echo "[4/4] Checking systemd service..." +SERVICE_FILE="/etc/systemd/system/talkkonnect.service" + +if [ ! -f "$SERVICE_FILE" ]; then + echo " ⚠️ Systemd service not found" + echo " Creating service file..." + + TARGET_UID=$(id -u "$TARGET_USER") + TARGET_HOME="/home/$TARGET_USER" + + # Determine which binary to use + if [ -f "/usr/local/bin/talkkonnect" ]; then + BINARY_PATH="/usr/local/bin/talkkonnect" + elif [ -f "$TARGET_HOME/go/bin/talkkonnect" ]; then + BINARY_PATH="$TARGET_HOME/go/bin/talkkonnect" + else + echo " ✗ Talkkonnect binary not found!" + exit 1 + fi + + sudo tee "$SERVICE_FILE" > /dev/null </dev/null; then + echo " Enabling talkkonnect service..." + sudo systemctl enable talkkonnect + echo " ✓ Service enabled (will start on boot)" +fi + +# Check if service is running +if systemctl is-active --quiet talkkonnect 2>/dev/null; then + echo " Restarting talkkonnect service..." + sudo systemctl restart talkkonnect + sleep 3 +else + echo " Starting talkkonnect service..." + sudo systemctl start talkkonnect + sleep 3 +fi + +if systemctl is-active --quiet talkkonnect 2>/dev/null; then + echo " ✓ Service is RUNNING" +else + echo " ✗ Service failed to start" + echo "" + echo "Check logs with: sudo journalctl -u talkkonnect -n 50" +fi + +echo "" +echo "=======================================================================" +echo "✓ Quick Fix Complete!" +echo "=======================================================================" +echo "" +echo "Changes made:" +echo " 1. ✓ Set true (allows self-signed certs)" +echo " 2. ✓ Fixed file ownership" +echo " 3. ✓ Created/updated systemd service" +echo " 4. ✓ Enabled and started talkkonnect service" +echo "" +echo "Important Notes:" +echo "" +echo "❌ SUPERUSER ISSUE:" +echo " 'SuperUser' is the Mumble SERVER ADMIN account." +echo " You CANNOT use it as a regular client." +echo "" +echo " To fix:" +echo " 1. Use Mumble desktop/mobile app to connect as SuperUser" +echo " 2. Create a regular user account on the server" +echo " 3. OR configure server to allow unregistered users" +echo " 4. Update talkkonnect config to use the regular account" +echo "" +echo "Check status:" +echo " sudo systemctl status talkkonnect" +echo "" +echo "View logs:" +echo " sudo journalctl -u talkkonnect -f" +echo "" +echo "Edit config:" +echo " sudo nano $CONFIG_FILE" +echo "" diff --git a/setup_intercom.sh b/setup_intercom.sh index 91a6e57..90b2ae5 100755 --- a/setup_intercom.sh +++ b/setup_intercom.sh @@ -194,6 +194,9 @@ MURMURCONF ### TALKKONNECT CLIENT INSTALLATION ################################################################################ +# This is the replacement install_talkkonnect_with_config() function +# Copy this into setup_intercom.sh starting at line 197 + install_talkkonnect_with_config() { local server_addr="$1" local server_port="$2" @@ -206,56 +209,58 @@ install_talkkonnect_with_config() { echo " INSTALLING TALKKONNECT CLIENT" echo "═══════════════════════════════════════════════════════════════" echo + echo "Using proven installation method (same as talkkonnect_complete_install.sh)..." + echo - echo "[1/4] Installing Go..." - if ! command -v go &>/dev/null; then - wget -q https://golang.org/dl/go1.23.4.linux-amd64.tar.gz - sudo tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz - rm go1.23.4.linux-amd64.tar.gz - export PATH=$PATH:/usr/local/go/bin - echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh - log_success "Go installed" + # Get target user info + local TARGET_USER="$KIOSK_USER" + local TARGET_UID=$(id -u "$TARGET_USER") + local TARGET_HOME="/home/$TARGET_USER" + local CONFIG_DIR="$TARGET_HOME/.config/talkkonnect" + + # Verify home directory exists + if [ ! -d "$TARGET_HOME" ]; then + log_error "Home directory does not exist: $TARGET_HOME" + pause + return 1 + fi + + # --- System Prep ------------------------------------------------------ + echo "[1/7] Installing system dependencies..." + sudo apt update + sudo apt install -y wget git build-essential pkg-config \ + libasound2-dev libopus-dev libopus0 libopusfile-dev \ + libpipewire-0.3-dev libevdev-dev libopenal-dev alsa-utils + + # --- Go Installation -------------------------------------------------- + echo "[2/7] Installing Go 1.24.1..." + if ! command -v go &>/dev/null || ! go version | grep -q "go1.24"; then + sudo rm -rf /usr/local/go + wget -q https://go.dev/dl/go1.24.1.linux-amd64.tar.gz + sudo tar -C /usr/local -xzf go1.24.1.linux-amd64.tar.gz + rm -f go1.24.1.linux-amd64.tar.gz + sudo ln -sf /usr/local/go/bin/go /usr/bin/go + export PATH="/usr/local/go/bin:$PATH" + log_success "Go 1.24.1 installed" else log_success "Go already installed" fi - echo "[2/4] Installing audio dependencies..." - sudo apt install -y libopenal-dev libopus-dev libopus0 libopusfile-dev alsa-utils portaudio19-dev git build-essential pkg-config + # --- Clone TalkKonnect ------------------------------------------------ + echo "[3/7] Cloning talkkonnect repository..." + cd ~ + if [ -d "talkkonnect" ]; then + rm -rf talkkonnect + fi + git clone https://github.com/talkkonnect/talkkonnect.git + cd talkkonnect - echo "[3/4] Cloning and building talkkonnect..." + # --- Patch gopus for x86_64 ------------------------------------------- + echo "[4/7] Applying x86_64 Opus patch..." + go mod vendor - echo "Building (this takes 5-10 minutes)..." - - # Build as kiosk user with proper environment (using their home directory to avoid permission issues) - sudo -u "$KIOSK_USER" bash -c " - export PATH=/usr/local/go/bin:\$PATH - export HOME=/home/$KIOSK_USER - export GOPATH=/home/$KIOSK_USER/go - export CGO_ENABLED=1 - - mkdir -p /home/$KIOSK_USER/go/bin - - # Clone to kiosk user's home directory (avoids /tmp permission issues) - cd /home/$KIOSK_USER || exit 1 - - if [[ -d talkkonnect ]]; then - echo 'Removing existing talkkonnect directory...' - rm -rf talkkonnect - fi - - git clone https://github.com/talkkonnect/talkkonnect.git - cd talkkonnect || exit 1 - - echo 'Creating vendored dependencies...' - go mod vendor - - # Apply critical x86_64 Opus patch - echo 'Applying x86_64 Opus compatibility patch...' - if [[ -f vendor/github.com/talkkonnect/gopus/opus_nonshared.go ]]; then - cp vendor/github.com/talkkonnect/gopus/opus_nonshared.go \ - vendor/github.com/talkkonnect/gopus/opus_nonshared.go.backup - - cat > vendor/github.com/talkkonnect/gopus/opus_nonshared.go << 'EOFOPUS' + if [ -d "vendor/github.com/talkkonnect/gopus" ]; then + cat > vendor/github.com/talkkonnect/gopus/opus_nonshared.go << 'EOFOPUS' // +build amd64,cgo 386,cgo package gopus @@ -316,11 +321,11 @@ package gopus // void gopus_decoder_resetstate(OpusDecoder *decoder) { // opus_decoder_ctl(decoder, OPUS_RESET_STATE); // } -import \"C\" +import "C" import ( - \"errors\" - \"unsafe\" + "errors" + "unsafe" ) type Application int @@ -466,14 +471,14 @@ func CountFrames(data []byte) (int, error) { } var ( - ErrBadArgument = errors.New(\"bad argument\") - ErrSmallBuffer = errors.New(\"buffer is too small\") - ErrInternal = errors.New(\"internal error\") - ErrInvalidPacket = errors.New(\"invalid packet\") - ErrUnimplemented = errors.New(\"unimplemented\") - ErrInvalidState = errors.New(\"invalid state\") - ErrAllocFail = errors.New(\"allocation failed\") - ErrUnknown = errors.New(\"unknown error\") + ErrBadArgument = errors.New("bad argument") + ErrSmallBuffer = errors.New("buffer is too small") + ErrInternal = errors.New("internal error") + ErrInvalidPacket = errors.New("invalid packet") + ErrUnimplemented = errors.New("unimplemented") + ErrInvalidState = errors.New("invalid state") + ErrAllocFail = errors.New("allocation failed") + ErrUnknown = errors.New("unknown error") ) func getErr(code C.int) error { @@ -499,83 +504,69 @@ func getErr(code C.int) error { } } EOFOPUS - echo 'Opus patch applied' - else - echo 'Warning: gopus not found in vendor directory' - fi + log_success "Opus patch applied" + fi - # Set CGO flags for Opus - export CGO_CFLAGS=\"\$(pkg-config --cflags opus)\" - export CGO_LDFLAGS=\"\$(pkg-config --libs opus) -lm\" + # --- Build TalkKonnect ------------------------------------------------ + echo "[5/7] Building talkkonnect (this may take a few minutes)..." + cd ~/talkkonnect/cmd/talkkonnect - # Build in the cmd/talkkonnect directory with vendored dependencies - cd cmd/talkkonnect || exit 1 + export CGO_ENABLED=1 + export CGO_CFLAGS="$(pkg-config --cflags opus)" + export CGO_LDFLAGS="$(pkg-config --libs opus) -lm" - echo 'Compiling with vendored dependencies...' - go build -mod=vendor -v -o ~/talkkonnect-binary . 2>&1 | tail -20 + go build -mod=vendor -v -o ~/talkkonnect-binary . 2>&1 | tail -20 - if [[ ! -f ~/talkkonnect-binary ]]; then - echo 'Build failed - binary not created' - exit 1 - fi - - echo 'Build successful' - " - - local build_result=$? - - if [[ $build_result -ne 0 ]]; then - log_error "Build failed" - echo - echo "Troubleshooting:" - echo " 1. Check Go version: go version" - echo " 2. Ensure build tools: sudo apt install build-essential" - echo " 3. Check logs above for specific errors" + if [ ! -f ~/talkkonnect-binary ]; then + log_error "Build failed!" pause return 1 fi - # Stop any running talkkonnect before installing - echo "Installing binary..." + # Stop any running instances if systemctl is-active --quiet talkkonnect 2>/dev/null; then - echo "Stopping existing talkkonnect service..." sudo systemctl stop talkkonnect fi - - # Kill any stray processes - if pgrep -x talkkonnect > /dev/null 2>&1; then - echo "Killing running talkkonnect processes..." + if pgrep -x talkkonnect > /dev/null; then sudo pkill -9 talkkonnect sleep 1 fi - # Now install as kiosk user - sudo -u "$KIOSK_USER" bash -c " - cp ~/talkkonnect-binary ~/go/bin/talkkonnect - chmod +x ~/go/bin/talkkonnect - rm ~/talkkonnect-binary - " + # Install binary + sudo cp ~/talkkonnect-binary /usr/local/bin/talkkonnect + sudo chmod +x /usr/local/bin/talkkonnect + rm ~/talkkonnect-binary + log_success "Binary installed to /usr/local/bin/talkkonnect" - if [[ $? -ne 0 ]]; then - log_error "Failed to install binary" - pause - return 1 + # --- User Permissions ------------------------------------------------- + echo "[6/7] Setting up permissions..." + if ! groups "$TARGET_USER" | grep -q input; then + sudo usermod -a -G input "$TARGET_USER" + log_success "Added $TARGET_USER to 'input' group" + fi + if ! groups "$TARGET_USER" | grep -q audio; then + sudo usermod -a -G audio "$TARGET_USER" + log_success "Added $TARGET_USER to 'audio' group" fi - log_success "talkkonnect built successfully" + # --- Configuration ---------------------------------------------------- + echo "[7/7] Creating configuration..." - echo "[4/4] Creating configuration..." + # Create config directory as target user + if [ "$TARGET_USER" != "$USER" ]; then + sudo -u "$TARGET_USER" mkdir -p "$CONFIG_DIR" + else + mkdir -p "$CONFIG_DIR" + fi - # Create config directory for logs - sudo -u "$KIOSK_USER" mkdir -p /home/$KIOSK_USER/.config/talkkonnect - - sudo -u "$KIOSK_USER" tee /home/$KIOSK_USER/talkkonnect.xml > /dev/null < /dev/null < - + @@ -616,18 +606,11 @@ EOFOPUS @@ -636,48 +619,45 @@ EOFOPUS - + - - - - -TKXML +EOFXML + + # Set ownership + sudo chown -R "$TARGET_USER:$TARGET_USER" "$CONFIG_DIR" + sudo chmod 755 "$CONFIG_DIR" + sudo chmod 644 "$CONFIG_DIR/talkkonnect.xml" # Create systemd service - sudo tee /etc/systemd/system/talkkonnect.service > /dev/null < /dev/null <&2 +} + +log_success() { + echo "✓ $*" +} + +log_warning() { + echo "⚠ $*" +} + +pause() { + read -r -p "Press Enter to continue..." +} + +get_ip_address() { + hostname -I | awk '{print $1}' || echo "No IP" +} + +################################################################################ +### STATUS CHECK FUNCTIONS +################################################################################ + +check_murmur_status() { + local installed=false + local running=false + + if systemctl list-unit-files | grep -q "mumble-server.service"; then + installed=true + if systemctl is-active --quiet mumble-server; then + running=true + fi + fi + + echo "$installed:$running" +} + +check_talkkonnect_status() { + local installed=false + local running=false + + # Check if systemd service exists (most reliable indicator) + if [[ -f /etc/systemd/system/talkkonnect.service ]] || \ + command -v talkkonnect &>/dev/null || \ + [[ -f "/home/$KIOSK_USER/go/bin/talkkonnect" ]]; then + installed=true + if systemctl is-active --quiet talkkonnect; then + running=true + fi + fi + + echo "$installed:$running" +} + +################################################################################ +### MURMUR SERVER INSTALLATION +################################################################################ + +install_murmur_server() { + echo + echo "═══════════════════════════════════════════════════════════════" + echo " INSTALLING MURMUR SERVER" + echo "═══════════════════════════════════════════════════════════════" + echo + + echo "[1/5] Installing mumble-server package..." + sudo apt update + sudo apt install -y mumble-server + + echo "[2/5] Getting configuration details..." + read -r -s -p "SuperUser password: " superuser_pass + echo + read -r -s -p "Server password (clients need this): " server_pass + echo + read -r -p "Welcome text [Welcome to Kiosk Intercom]: " welcome_text + welcome_text="${welcome_text:-Welcome to Kiosk Intercom}" + + echo "[3/5] Creating configuration..." + local config_file="/etc/mumble-server.ini" + + # Create config if it doesn't exist + if [[ ! -f "$config_file" ]]; then + sudo tee "$config_file" > /dev/null <<'MURMURCONF' +# Murmur configuration file + +# Database location +database=/var/lib/mumble-server/mumble-server.sqlite + +# Network settings +port=64738 +host=0.0.0.0 + +# Logging +logfile=/var/log/mumble-server/mumble-server.log + +# Limits +users=10 +bandwidth=72000 + +# Welcome message +welcometext=Welcome + +# Server password +serverpassword= + +# Allow pings +allowping=true + +# Enable HTML +allowhtml=true +MURMURCONF + log_success "Config file created" + fi + + # Update configuration + sudo sed -i "s|^welcometext=.*|welcometext=$welcome_text|" "$config_file" + sudo sed -i "s|^port=.*|port=64738|" "$config_file" + sudo sed -i "s|^users=.*|users=10|" "$config_file" + sudo sed -i "s|^bandwidth=.*|bandwidth=72000|" "$config_file" + + if [[ -n "$server_pass" ]]; then + sudo sed -i "s|^serverpassword=.*|serverpassword=$server_pass|" "$config_file" + fi + + echo "[4/5] Setting SuperUser password..." + # Stop service before setting password + sudo systemctl stop mumble-server 2>/dev/null || true + sleep 2 + + # Set SuperUser password + echo "$superuser_pass" | sudo murmurd -ini "$config_file" -supw - 2>/dev/null || { + log_warning "Could not set SuperUser password via murmurd command" + echo "You can set it later with: sudo murmurd -ini $config_file -supw YOUR_PASSWORD" + } + + echo "[5/5] Starting service..." + sudo systemctl enable mumble-server + sudo systemctl start mumble-server + + # Configure firewall + sudo ufw allow 64738/tcp comment 'Mumble/Murmur' 2>/dev/null || true + sudo ufw allow 64738/udp comment 'Mumble/Murmur' 2>/dev/null || true + + sleep 3 + + local server_ip=$(get_ip_address) + log_success "Murmur server installed" + echo + echo "═══════════════════════════════════════════════════════════════" + echo " Server: $server_ip:64738" + echo " SuperUser: SuperUser / $superuser_pass" + [[ -n "$server_pass" ]] && echo " Password: $server_pass" + echo + echo "Connect with Mumble app:" + echo " Server: $server_ip" + echo " Port: 64738" + [[ -n "$server_pass" ]] && echo " Password: $server_pass" + echo "═══════════════════════════════════════════════════════════════" + + pause +} + +################################################################################ +### TALKKONNECT CLIENT INSTALLATION +################################################################################ + +install_talkkonnect_with_config() { + local server_addr="$1" + local server_port="$2" + local tk_user="$3" + local tk_pass="$4" + local tk_channel="$5" + + echo + echo "═══════════════════════════════════════════════════════════════" + echo " INSTALLING TALKKONNECT CLIENT" + echo "═══════════════════════════════════════════════════════════════" + echo + echo "Using proven talkkonnect installation method..." + echo + + # Get target user info + local TARGET_USER="$KIOSK_USER" + local TARGET_UID=$(id -u "$TARGET_USER") + local TARGET_HOME="/home/$TARGET_USER" + + # Verify home directory exists + if [ ! -d "$TARGET_HOME" ]; then + log_error "Home directory does not exist: $TARGET_HOME" + echo "Please ensure the user account is properly set up" + pause + return 1 + fi + + # --- System Prep ------------------------------------------------------ + echo "[1/6] Updating system packages..." + sudo apt update + sudo apt upgrade -y + + echo "[1/6] Installing dependencies..." + sudo apt install -y wget git build-essential pkg-config \ + libasound2-dev libopus-dev libopus0 libopusfile-dev \ + libpipewire-0.3-dev libevdev-dev libopenal-dev alsa-utils + + # --- Go Installation -------------------------------------------------- + echo "[2/6] Installing Go 1.24.1..." + if ! command -v go &>/dev/null || ! go version | grep -q "go1.24"; then + sudo rm -rf /usr/local/go /usr/lib/go* /usr/bin/go + wget https://go.dev/dl/go1.24.1.linux-amd64.tar.gz + sudo tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz + rm go1.23.4.linux-amd64.tar.gz + export PATH=$PATH:/usr/local/go/bin + echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh + log_success "Go installed" + else + log_success "Go already installed" + fi + + echo "[2/4] Installing audio dependencies..." + sudo apt install -y libopenal-dev libopus-dev libopus0 libopusfile-dev alsa-utils portaudio19-dev git build-essential pkg-config + + echo "[3/4] Cloning and building talkkonnect..." + + echo "Building (this takes 5-10 minutes)..." + + # Build as kiosk user with proper environment (using their home directory to avoid permission issues) + sudo -u "$KIOSK_USER" bash -c " + export PATH=/usr/local/go/bin:\$PATH + export HOME=/home/$KIOSK_USER + export GOPATH=/home/$KIOSK_USER/go + export CGO_ENABLED=1 + + mkdir -p /home/$KIOSK_USER/go/bin + + # Clone to kiosk user's home directory (avoids /tmp permission issues) + cd /home/$KIOSK_USER || exit 1 + + if [[ -d talkkonnect ]]; then + echo 'Removing existing talkkonnect directory...' + rm -rf talkkonnect + fi + + git clone https://github.com/talkkonnect/talkkonnect.git + cd talkkonnect || exit 1 + + echo 'Creating vendored dependencies...' + go mod vendor + + # Apply critical x86_64 Opus patch + echo 'Applying x86_64 Opus compatibility patch...' + if [[ -f vendor/github.com/talkkonnect/gopus/opus_nonshared.go ]]; then + cp vendor/github.com/talkkonnect/gopus/opus_nonshared.go \ + vendor/github.com/talkkonnect/gopus/opus_nonshared.go.backup + + cat > vendor/github.com/talkkonnect/gopus/opus_nonshared.go << 'EOFOPUS' +// +build amd64,cgo 386,cgo + +package gopus + +// #cgo pkg-config: opus +// #cgo LDFLAGS: -lm +// +// #include +// #include +// #include +// +// enum { +// gopus_ok = OPUS_OK, +// gopus_bad_arg = OPUS_BAD_ARG, +// gopus_small_buffer = OPUS_BUFFER_TOO_SMALL, +// gopus_internal = OPUS_INTERNAL_ERROR, +// gopus_invalid_packet = OPUS_INVALID_PACKET, +// gopus_unimplemented = OPUS_UNIMPLEMENTED, +// gopus_invalid_state = OPUS_INVALID_STATE, +// gopus_alloc_fail = OPUS_ALLOC_FAIL, +// }; +// +// enum { +// gopus_application_voip = OPUS_APPLICATION_VOIP, +// gopus_application_audio = OPUS_APPLICATION_AUDIO, +// gopus_restricted_lowdelay = OPUS_APPLICATION_RESTRICTED_LOWDELAY, +// gopus_bitrate_max = OPUS_BITRATE_MAX, +// }; +// +// void gopus_setvbr(OpusEncoder *encoder, int vbr) { +// opus_encoder_ctl(encoder, OPUS_SET_VBR(vbr)); +// } +// +// void gopus_setbitrate(OpusEncoder *encoder, int bitrate) { +// opus_encoder_ctl(encoder, OPUS_SET_BITRATE(bitrate)); +// } +// +// opus_int32 gopus_bitrate(OpusEncoder *encoder) { +// opus_int32 bitrate; +// opus_encoder_ctl(encoder, OPUS_GET_BITRATE(&bitrate)); +// return bitrate; +// } +// +// void gopus_setapplication(OpusEncoder *encoder, int application) { +// opus_encoder_ctl(encoder, OPUS_SET_APPLICATION(application)); +// } +// +// opus_int32 gopus_application(OpusEncoder *encoder) { +// opus_int32 application; +// opus_encoder_ctl(encoder, OPUS_GET_APPLICATION(&application)); +// return application; +// } +// +// void gopus_encoder_resetstate(OpusEncoder *encoder) { +// opus_encoder_ctl(encoder, OPUS_RESET_STATE); +// } +// +// void gopus_decoder_resetstate(OpusDecoder *decoder) { +// opus_decoder_ctl(decoder, OPUS_RESET_STATE); +// } +import \"C\" + +import ( + \"errors\" + \"unsafe\" +) + +type Application int + +const ( + Voip Application = C.gopus_application_voip + Audio Application = C.gopus_application_audio + RestrictedLowDelay Application = C.gopus_restricted_lowdelay +) + +const ( + BitrateMaximum = C.gopus_bitrate_max +) + +type Encoder struct { + data []byte + cEncoder *C.struct_OpusEncoder +} + +func NewEncoder(sampleRate, channels int, application Application) (*Encoder, error) { + encoder := &Encoder{} + encoder.data = make([]byte, int(C.opus_encoder_get_size(C.int(channels)))) + encoder.cEncoder = (*C.struct_OpusEncoder)(unsafe.Pointer(&encoder.data[0])) + + ret := C.opus_encoder_init(encoder.cEncoder, C.opus_int32(sampleRate), C.int(channels), C.int(application)) + if err := getErr(ret); err != nil { + return nil, err + } + return encoder, nil +} + +func (e *Encoder) Encode(pcm []int16, frameSize, maxDataBytes int) ([]byte, error) { + pcmPtr := (*C.opus_int16)(unsafe.Pointer(&pcm[0])) + + data := make([]byte, maxDataBytes) + dataPtr := (*C.uchar)(unsafe.Pointer(&data[0])) + + encodedC := C.opus_encode(e.cEncoder, pcmPtr, C.int(frameSize), dataPtr, C.opus_int32(len(data))) + encoded := int(encodedC) + + if encoded < 0 { + return nil, getErr(C.int(encodedC)) + } + return data[0:encoded], nil +} + +func (e *Encoder) SetVbr(vbr bool) { + var cVbr C.int + if vbr { + cVbr = 1 + } else { + cVbr = 0 + } + C.gopus_setvbr(e.cEncoder, cVbr) +} + +func (e *Encoder) SetBitrate(bitrate int) { + C.gopus_setbitrate(e.cEncoder, C.int(bitrate)) +} + +func (e *Encoder) Bitrate() int { + return int(C.gopus_bitrate(e.cEncoder)) +} + +func (e *Encoder) SetApplication(application Application) { + C.gopus_setapplication(e.cEncoder, C.int(application)) +} + +func (e *Encoder) Application() Application { + return Application(C.gopus_application(e.cEncoder)) +} + +func (e *Encoder) ResetState() { + C.gopus_encoder_resetstate(e.cEncoder) +} + +type Decoder struct { + data []byte + cDecoder *C.struct_OpusDecoder + channels int +} + +func NewDecoder(sampleRate, channels int) (*Decoder, error) { + decoder := &Decoder{} + decoder.data = make([]byte, int(C.opus_decoder_get_size(C.int(channels)))) + decoder.cDecoder = (*C.struct_OpusDecoder)(unsafe.Pointer(&decoder.data[0])) + + ret := C.opus_decoder_init(decoder.cDecoder, C.opus_int32(sampleRate), C.int(channels)) + if err := getErr(ret); err != nil { + return nil, err + } + decoder.channels = channels + + return decoder, nil +} + +func (d *Decoder) Decode(data []byte, frameSize int, fec bool) ([]int16, error) { + var dataPtr *C.uchar + if len(data) > 0 { + dataPtr = (*C.uchar)(unsafe.Pointer(&data[0])) + } + dataLen := C.opus_int32(len(data)) + + output := make([]int16, d.channels*frameSize) + outputPtr := (*C.opus_int16)(unsafe.Pointer(&output[0])) + + var cFec C.int + if fec { + cFec = 1 + } else { + cFec = 0 + } + + cRet := C.opus_decode(d.cDecoder, dataPtr, dataLen, outputPtr, C.int(frameSize), cFec) + ret := int(cRet) + + if ret < 0 { + return nil, getErr(cRet) + } + return output[:ret*d.channels], nil +} + +func (d *Decoder) ResetState() { + C.gopus_decoder_resetstate(d.cDecoder) +} + +func GetSamplesPerFrame(data []byte, samplingRate int) (int, error) { + dataPtr := (*C.uchar)(unsafe.Pointer(&data[0])) + cSamplingRate := C.opus_int32(samplingRate) + cRet := C.opus_packet_get_samples_per_frame(dataPtr, cSamplingRate) + return int(cRet), nil +} + +func CountFrames(data []byte) (int, error) { + dataPtr := (*C.uchar)(unsafe.Pointer(&data[0])) + cLen := C.opus_int32(len(data)) + + cRet := C.opus_packet_get_nb_frames(dataPtr, cLen) + if err := getErr(cRet); err != nil { + return 0, err + } + return int(cRet), nil +} + +var ( + ErrBadArgument = errors.New(\"bad argument\") + ErrSmallBuffer = errors.New(\"buffer is too small\") + ErrInternal = errors.New(\"internal error\") + ErrInvalidPacket = errors.New(\"invalid packet\") + ErrUnimplemented = errors.New(\"unimplemented\") + ErrInvalidState = errors.New(\"invalid state\") + ErrAllocFail = errors.New(\"allocation failed\") + ErrUnknown = errors.New(\"unknown error\") +) + +func getErr(code C.int) error { + switch code { + case C.gopus_ok: + return nil + case C.gopus_bad_arg: + return ErrBadArgument + case C.gopus_small_buffer: + return ErrSmallBuffer + case C.gopus_internal: + return ErrInternal + case C.gopus_invalid_packet: + return ErrInvalidPacket + case C.gopus_unimplemented: + return ErrUnimplemented + case C.gopus_invalid_state: + return ErrInvalidState + case C.gopus_alloc_fail: + return ErrAllocFail + default: + return ErrUnknown + } +} +EOFOPUS + echo 'Opus patch applied' + else + echo 'Warning: gopus not found in vendor directory' + fi + + # Set CGO flags for Opus + export CGO_CFLAGS=\"\$(pkg-config --cflags opus)\" + export CGO_LDFLAGS=\"\$(pkg-config --libs opus) -lm\" + + # Build in the cmd/talkkonnect directory with vendored dependencies + cd cmd/talkkonnect || exit 1 + + echo 'Compiling with vendored dependencies...' + go build -mod=vendor -v -o ~/talkkonnect-binary . 2>&1 | tail -20 + + if [[ ! -f ~/talkkonnect-binary ]]; then + echo 'Build failed - binary not created' + exit 1 + fi + + echo 'Build successful' + " + + local build_result=$? + + if [[ $build_result -ne 0 ]]; then + log_error "Build failed" + echo + echo "Troubleshooting:" + echo " 1. Check Go version: go version" + echo " 2. Ensure build tools: sudo apt install build-essential" + echo " 3. Check logs above for specific errors" + pause + return 1 + fi + + # Stop any running talkkonnect before installing + echo "Installing binary..." + if systemctl is-active --quiet talkkonnect 2>/dev/null; then + echo "Stopping existing talkkonnect service..." + sudo systemctl stop talkkonnect + fi + + # Kill any stray processes + if pgrep -x talkkonnect > /dev/null 2>&1; then + echo "Killing running talkkonnect processes..." + sudo pkill -9 talkkonnect + sleep 1 + fi + + # Now install as kiosk user + sudo -u "$KIOSK_USER" bash -c " + cp ~/talkkonnect-binary ~/go/bin/talkkonnect + chmod +x ~/go/bin/talkkonnect + rm ~/talkkonnect-binary + " + + if [[ $? -ne 0 ]]; then + log_error "Failed to install binary" + pause + return 1 + fi + + log_success "talkkonnect built successfully" + + echo "[4/4] Creating configuration..." + + # Create config directory for logs + sudo -u "$KIOSK_USER" mkdir -p /home/$KIOSK_USER/.config/talkkonnect + + sudo -u "$KIOSK_USER" tee /home/$KIOSK_USER/talkkonnect.xml > /dev/null < + + + + + + + + + + + + + + + + + $server_addr:$server_port + $tk_user + $tk_pass + true + false + + $tk_channel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +TKXML + + # Create systemd service + sudo tee /etc/systemd/system/talkkonnect.service > /dev/null </dev/null + sudo systemctl start mumble-server + log_success "SuperUser password updated" + ;; + 4) + read -r -p "New port [64738]: " new_port + new_port="${new_port:-64738}" + sudo sed -i "s|^port=.*|port=$new_port|" "$config_file" + sudo systemctl restart mumble-server + log_success "Port updated to $new_port" + ;; + esac + pause +} + +reconfigure_talkkonnect() { + echo + echo "Reconfigure talkkonnect:" + echo " 1. Change server" + echo " 2. Change credentials" + echo " 3. Change channel" + echo " 0. Cancel" + read -r -p "Choose: " reconfig_choice + + case "$reconfig_choice" in + 1) + read -r -p "Server address: " new_server + read -r -p "Port [64738]: " new_port + new_port="${new_port:-64738}" + sudo -u "$KIOSK_USER" sed -i "s|.*|$new_server|" /home/$KIOSK_USER/talkkonnect.xml + sudo -u "$KIOSK_USER" sed -i "s|.*|$new_port|" /home/$KIOSK_USER/talkkonnect.xml + sudo systemctl restart talkkonnect + log_success "Server updated" + ;; + 2) + read -r -p "Username: " new_user + read -r -s -p "Password: " new_pass + echo + sudo -u "$KIOSK_USER" sed -i "s|.*|$new_user|" /home/$KIOSK_USER/talkkonnect.xml + sudo -u "$KIOSK_USER" sed -i "s|.*|$new_pass|" /home/$KIOSK_USER/talkkonnect.xml + sudo systemctl restart talkkonnect + log_success "Credentials updated" + ;; + 3) + read -r -p "Channel: " new_channel + sudo -u "$KIOSK_USER" sed -i "s|.*|$new_channel|" /home/$KIOSK_USER/talkkonnect.xml + sudo systemctl restart talkkonnect + log_success "Channel updated" + ;; + esac + pause +} + +################################################################################ +### LOGS AND DIAGNOSTICS +################################################################################ + +view_talkkonnect_logs() { + echo + echo "Recent talkkonnect logs:" + echo "═══════════════════════════════════════════════════════════════" + sudo journalctl -u talkkonnect -n 50 --no-pager + echo + pause +} + +view_murmur_logs() { + echo + echo "Recent Murmur server logs:" + echo "═══════════════════════════════════════════════════════════════" + sudo journalctl -u mumble-server -n 50 --no-pager + echo + pause +} + +################################################################################ +### UNINSTALLATION +################################################################################ + +uninstall_murmur() { + echo + read -r -p "Uninstall Murmur server? (yes/no): " confirm + [[ "$confirm" != "yes" ]] && return + + echo "Uninstalling..." + sudo systemctl stop mumble-server 2>/dev/null || true + sudo systemctl disable mumble-server 2>/dev/null || true + sudo apt remove -y mumble-server 2>/dev/null || true + + read -r -p "Remove configuration and database? (y/n): " remove_data + if [[ "$remove_data" =~ ^[Yy]$ ]]; then + sudo rm -rf /var/lib/mumble-server + sudo rm -f /etc/mumble-server.ini + log_success "Murmur and data removed" + else + log_success "Murmur removed (data preserved)" + fi + + pause +} + +uninstall_talkkonnect() { + echo + read -r -p "Uninstall talkkonnect? (yes/no): " confirm + [[ "$confirm" != "yes" ]] && return + + echo "Uninstalling..." + sudo systemctl stop talkkonnect 2>/dev/null || true + sudo systemctl disable talkkonnect 2>/dev/null || true + sudo rm -f /etc/systemd/system/talkkonnect.service + sudo rm -f /home/$KIOSK_USER/talkkonnect.xml + sudo rm -f /home/$KIOSK_USER/go/bin/talkkonnect + + sudo systemctl daemon-reload + + log_success "talkkonnect uninstalled" + pause +} + +################################################################################ +### MAIN MENU +################################################################################ + +show_status() { + local murmur_status=$(check_murmur_status) + local murmur_installed=$(echo "$murmur_status" | cut -d: -f1) + local murmur_running=$(echo "$murmur_status" | cut -d: -f2) + + local tk_status=$(check_talkkonnect_status) + local tk_installed=$(echo "$tk_status" | cut -d: -f1) + local tk_running=$(echo "$tk_status" | cut -d: -f2) + + echo + echo "═══════════════════════════════════════════════════════════════" + echo " INTERCOM STATUS" + echo "═══════════════════════════════════════════════════════════════" + echo + + # Murmur status + if [[ "$murmur_installed" == "true" ]]; then + echo "Murmur Server: ✓ Installed" + if [[ "$murmur_running" == "true" ]]; then + echo " Status: Running" + local server_ip=$(get_ip_address) + echo " Address: $server_ip:64738" + else + echo " Status: Stopped" + fi + echo + else + echo "Murmur Server: Not installed" + echo + fi + + # talkkonnect status + if [[ "$tk_installed" == "true" ]]; then + echo "talkkonnect Client: ✓ Installed" + if [[ "$tk_running" == "true" ]]; then + echo " Status: Running" + if [[ -f /home/$KIOSK_USER/talkkonnect.xml ]]; then + local server=$(grep "serveraddress" /home/$KIOSK_USER/talkkonnect.xml 2>/dev/null | sed 's/.*\(.*\)<\/server>/\1/' || echo "Unknown") + echo " Server: $server" + fi + else + echo " Status: Stopped" + fi + echo + else + echo "talkkonnect Client: Not installed" + echo + fi +} + +main_menu() { + while true; do + clear + echo "═══════════════════════════════════════════════════════════════" + echo " UBK INTERCOM SETUP v${VERSION}" + echo " Mumble/Talkkonnect Push-to-Talk System" + echo "═══════════════════════════════════════════════════════════════" + + show_status + + local murmur_status=$(check_murmur_status) + local murmur_installed=$(echo "$murmur_status" | cut -d: -f1) + local murmur_running=$(echo "$murmur_status" | cut -d: -f2) + + local tk_status=$(check_talkkonnect_status) + local tk_installed=$(echo "$tk_status" | cut -d: -f1) + local tk_running=$(echo "$tk_status" | cut -d: -f2) + + echo "═══════════════════════════════════════════════════════════════" + echo " INSTALLATION OPTIONS" + echo "═══════════════════════════════════════════════════════════════" + + local menu_num=1 + + # Installation options (shown when nothing installed) + if [[ "$murmur_installed" == "false" && "$tk_installed" == "false" ]]; then + echo " $menu_num. Install All-in-One (Server + Client)" + local opt_all_in_one=$menu_num + ((menu_num++)) + echo " $menu_num. Install Murmur Server Only" + local opt_server_only=$menu_num + ((menu_num++)) + echo " $menu_num. Install talkkonnect Client Only" + local opt_client_only=$menu_num + ((menu_num++)) + else + # Murmur management options + if [[ "$murmur_installed" == "true" ]]; then + echo + echo "Murmur Server:" + if [[ "$murmur_running" == "true" ]]; then + echo " $menu_num. Stop Murmur" + else + echo " $menu_num. Start Murmur" + fi + local opt_murmur_toggle=$menu_num + ((menu_num++)) + + echo " $menu_num. Reconfigure Murmur" + local opt_murmur_reconfig=$menu_num + ((menu_num++)) + + echo " $menu_num. View Murmur Logs" + local opt_murmur_logs=$menu_num + ((menu_num++)) + + echo " $menu_num. Uninstall Murmur" + local opt_murmur_uninstall=$menu_num + ((menu_num++)) + else + echo + echo " $menu_num. Install Murmur Server" + local opt_install_murmur=$menu_num + ((menu_num++)) + fi + + # talkkonnect management options + if [[ "$tk_installed" == "true" ]]; then + echo + echo "talkkonnect Client:" + if [[ "$tk_running" == "true" ]]; then + echo " $menu_num. Stop talkkonnect" + else + echo " $menu_num. Start talkkonnect" + fi + local opt_tk_toggle=$menu_num + ((menu_num++)) + + echo " $menu_num. Reconfigure talkkonnect" + local opt_tk_reconfig=$menu_num + ((menu_num++)) + + echo " $menu_num. View talkkonnect Logs" + local opt_tk_logs=$menu_num + ((menu_num++)) + + echo " $menu_num. Uninstall talkkonnect" + local opt_tk_uninstall=$menu_num + ((menu_num++)) + else + echo + echo " $menu_num. Install talkkonnect Client" + local opt_install_tk=$menu_num + ((menu_num++)) + fi + fi + + echo + echo "═══════════════════════════════════════════════════════════════" + echo " 0. Exit" + echo "═══════════════════════════════════════════════════════════════" + echo + read -r -p "Choose [0-$((menu_num-1))]: " choice + + # Handle menu selection + case "$choice" in + 0) + echo "Exiting..." + exit 0 + ;; + ${opt_all_in_one:-}) + install_murmur_and_talkkonnect + ;; + ${opt_server_only:-}) + install_murmur_server + ;; + ${opt_client_only:-}) + install_talkkonnect_only + ;; + ${opt_install_murmur:-}) + install_murmur_server + ;; + ${opt_install_tk:-}) + install_talkkonnect_only + ;; + ${opt_murmur_toggle:-}) + toggle_murmur_service + ;; + ${opt_murmur_reconfig:-}) + reconfigure_murmur + ;; + ${opt_murmur_logs:-}) + view_murmur_logs + ;; + ${opt_murmur_uninstall:-}) + uninstall_murmur + ;; + ${opt_tk_toggle:-}) + toggle_talkkonnect_service + ;; + ${opt_tk_reconfig:-}) + reconfigure_talkkonnect + ;; + ${opt_tk_logs:-}) + view_talkkonnect_logs + ;; + ${opt_tk_uninstall:-}) + uninstall_talkkonnect + ;; + *) + echo "Invalid choice" + sleep 1 + ;; + esac + done +} + +################################################################################ +### ENTRY POINT +################################################################################ + +# Check if running as root +if [[ $EUID -eq 0 ]]; then + log_error "This script should not be run as root" + echo "Please run as: ./setup_intercom.sh" + exit 1 +fi + +# Check if kiosk user exists +if ! id "$KIOSK_USER" &>/dev/null; then + log_warning "User '$KIOSK_USER' does not exist" + read -r -p "Enter the username to use for installation: " KIOSK_USER + + if ! id "$KIOSK_USER" &>/dev/null; then + log_error "User '$KIOSK_USER' not found" + exit 1 + fi +fi + +# Run main menu +main_menu diff --git a/setup_intercom_talkkonnect_function.txt b/setup_intercom_talkkonnect_function.txt new file mode 100644 index 0000000..4ffd280 --- /dev/null +++ b/setup_intercom_talkkonnect_function.txt @@ -0,0 +1,492 @@ +# This is the replacement install_talkkonnect_with_config() function +# Copy this into setup_intercom.sh starting at line 197 + +install_talkkonnect_with_config() { + local server_addr="$1" + local server_port="$2" + local tk_user="$3" + local tk_pass="$4" + local tk_channel="$5" + + echo + echo "═══════════════════════════════════════════════════════════════" + echo " INSTALLING TALKKONNECT CLIENT" + echo "═══════════════════════════════════════════════════════════════" + echo + echo "Using proven installation method (same as talkkonnect_complete_install.sh)..." + echo + + # Get target user info + local TARGET_USER="$KIOSK_USER" + local TARGET_UID=$(id -u "$TARGET_USER") + local TARGET_HOME="/home/$TARGET_USER" + local CONFIG_DIR="$TARGET_HOME/.config/talkkonnect" + + # Verify home directory exists + if [ ! -d "$TARGET_HOME" ]; then + log_error "Home directory does not exist: $TARGET_HOME" + pause + return 1 + fi + + # --- System Prep ------------------------------------------------------ + echo "[1/7] Installing system dependencies..." + sudo apt update + sudo apt install -y wget git build-essential pkg-config \ + libasound2-dev libopus-dev libopus0 libopusfile-dev \ + libpipewire-0.3-dev libevdev-dev libopenal-dev alsa-utils + + # --- Go Installation -------------------------------------------------- + echo "[2/7] Installing Go 1.24.1..." + if ! command -v go &>/dev/null || ! go version | grep -q "go1.24"; then + sudo rm -rf /usr/local/go + wget -q https://go.dev/dl/go1.24.1.linux-amd64.tar.gz + sudo tar -C /usr/local -xzf go1.24.1.linux-amd64.tar.gz + rm -f go1.24.1.linux-amd64.tar.gz + sudo ln -sf /usr/local/go/bin/go /usr/bin/go + export PATH="/usr/local/go/bin:$PATH" + log_success "Go 1.24.1 installed" + else + log_success "Go already installed" + fi + + # --- Clone TalkKonnect ------------------------------------------------ + echo "[3/7] Cloning talkkonnect repository..." + cd ~ + if [ -d "talkkonnect" ]; then + rm -rf talkkonnect + fi + git clone https://github.com/talkkonnect/talkkonnect.git + cd talkkonnect + + # --- Patch gopus for x86_64 ------------------------------------------- + echo "[4/7] Applying x86_64 Opus patch..." + go mod vendor + + if [ -d "vendor/github.com/talkkonnect/gopus" ]; then + cat > vendor/github.com/talkkonnect/gopus/opus_nonshared.go << 'EOFOPUS' +// +build amd64,cgo 386,cgo + +package gopus + +// #cgo pkg-config: opus +// #cgo LDFLAGS: -lm +// +// #include +// #include +// #include +// +// enum { +// gopus_ok = OPUS_OK, +// gopus_bad_arg = OPUS_BAD_ARG, +// gopus_small_buffer = OPUS_BUFFER_TOO_SMALL, +// gopus_internal = OPUS_INTERNAL_ERROR, +// gopus_invalid_packet = OPUS_INVALID_PACKET, +// gopus_unimplemented = OPUS_UNIMPLEMENTED, +// gopus_invalid_state = OPUS_INVALID_STATE, +// gopus_alloc_fail = OPUS_ALLOC_FAIL, +// }; +// +// enum { +// gopus_application_voip = OPUS_APPLICATION_VOIP, +// gopus_application_audio = OPUS_APPLICATION_AUDIO, +// gopus_restricted_lowdelay = OPUS_APPLICATION_RESTRICTED_LOWDELAY, +// gopus_bitrate_max = OPUS_BITRATE_MAX, +// }; +// +// void gopus_setvbr(OpusEncoder *encoder, int vbr) { +// opus_encoder_ctl(encoder, OPUS_SET_VBR(vbr)); +// } +// +// void gopus_setbitrate(OpusEncoder *encoder, int bitrate) { +// opus_encoder_ctl(encoder, OPUS_SET_BITRATE(bitrate)); +// } +// +// opus_int32 gopus_bitrate(OpusEncoder *encoder) { +// opus_int32 bitrate; +// opus_encoder_ctl(encoder, OPUS_GET_BITRATE(&bitrate)); +// return bitrate; +// } +// +// void gopus_setapplication(OpusEncoder *encoder, int application) { +// opus_encoder_ctl(encoder, OPUS_SET_APPLICATION(application)); +// } +// +// opus_int32 gopus_application(OpusEncoder *encoder) { +// opus_int32 application; +// opus_encoder_ctl(encoder, OPUS_GET_APPLICATION(&application)); +// return application; +// } +// +// void gopus_encoder_resetstate(OpusEncoder *encoder) { +// opus_encoder_ctl(encoder, OPUS_RESET_STATE); +// } +// +// void gopus_decoder_resetstate(OpusDecoder *decoder) { +// opus_decoder_ctl(decoder, OPUS_RESET_STATE); +// } +import "C" + +import ( + "errors" + "unsafe" +) + +type Application int + +const ( + Voip Application = C.gopus_application_voip + Audio Application = C.gopus_application_audio + RestrictedLowDelay Application = C.gopus_restricted_lowdelay +) + +const ( + BitrateMaximum = C.gopus_bitrate_max +) + +type Encoder struct { + data []byte + cEncoder *C.struct_OpusEncoder +} + +func NewEncoder(sampleRate, channels int, application Application) (*Encoder, error) { + encoder := &Encoder{} + encoder.data = make([]byte, int(C.opus_encoder_get_size(C.int(channels)))) + encoder.cEncoder = (*C.struct_OpusEncoder)(unsafe.Pointer(&encoder.data[0])) + + ret := C.opus_encoder_init(encoder.cEncoder, C.opus_int32(sampleRate), C.int(channels), C.int(application)) + if err := getErr(ret); err != nil { + return nil, err + } + return encoder, nil +} + +func (e *Encoder) Encode(pcm []int16, frameSize, maxDataBytes int) ([]byte, error) { + pcmPtr := (*C.opus_int16)(unsafe.Pointer(&pcm[0])) + + data := make([]byte, maxDataBytes) + dataPtr := (*C.uchar)(unsafe.Pointer(&data[0])) + + encodedC := C.opus_encode(e.cEncoder, pcmPtr, C.int(frameSize), dataPtr, C.opus_int32(len(data))) + encoded := int(encodedC) + + if encoded < 0 { + return nil, getErr(C.int(encodedC)) + } + return data[0:encoded], nil +} + +func (e *Encoder) SetVbr(vbr bool) { + var cVbr C.int + if vbr { + cVbr = 1 + } else { + cVbr = 0 + } + C.gopus_setvbr(e.cEncoder, cVbr) +} + +func (e *Encoder) SetBitrate(bitrate int) { + C.gopus_setbitrate(e.cEncoder, C.int(bitrate)) +} + +func (e *Encoder) Bitrate() int { + return int(C.gopus_bitrate(e.cEncoder)) +} + +func (e *Encoder) SetApplication(application Application) { + C.gopus_setapplication(e.cEncoder, C.int(application)) +} + +func (e *Encoder) Application() Application { + return Application(C.gopus_application(e.cEncoder)) +} + +func (e *Encoder) ResetState() { + C.gopus_encoder_resetstate(e.cEncoder) +} + +type Decoder struct { + data []byte + cDecoder *C.struct_OpusDecoder + channels int +} + +func NewDecoder(sampleRate, channels int) (*Decoder, error) { + decoder := &Decoder{} + decoder.data = make([]byte, int(C.opus_decoder_get_size(C.int(channels)))) + decoder.cDecoder = (*C.struct_OpusDecoder)(unsafe.Pointer(&decoder.data[0])) + + ret := C.opus_decoder_init(decoder.cDecoder, C.opus_int32(sampleRate), C.int(channels)) + if err := getErr(ret); err != nil { + return nil, err + } + decoder.channels = channels + + return decoder, nil +} + +func (d *Decoder) Decode(data []byte, frameSize int, fec bool) ([]int16, error) { + var dataPtr *C.uchar + if len(data) > 0 { + dataPtr = (*C.uchar)(unsafe.Pointer(&data[0])) + } + dataLen := C.opus_int32(len(data)) + + output := make([]int16, d.channels*frameSize) + outputPtr := (*C.opus_int16)(unsafe.Pointer(&output[0])) + + var cFec C.int + if fec { + cFec = 1 + } else { + cFec = 0 + } + + cRet := C.opus_decode(d.cDecoder, dataPtr, dataLen, outputPtr, C.int(frameSize), cFec) + ret := int(cRet) + + if ret < 0 { + return nil, getErr(cRet) + } + return output[:ret*d.channels], nil +} + +func (d *Decoder) ResetState() { + C.gopus_decoder_resetstate(d.cDecoder) +} + +func GetSamplesPerFrame(data []byte, samplingRate int) (int, error) { + dataPtr := (*C.uchar)(unsafe.Pointer(&data[0])) + cSamplingRate := C.opus_int32(samplingRate) + cRet := C.opus_packet_get_samples_per_frame(dataPtr, cSamplingRate) + return int(cRet), nil +} + +func CountFrames(data []byte) (int, error) { + dataPtr := (*C.uchar)(unsafe.Pointer(&data[0])) + cLen := C.opus_int32(len(data)) + + cRet := C.opus_packet_get_nb_frames(dataPtr, cLen) + if err := getErr(cRet); err != nil { + return 0, err + } + return int(cRet), nil +} + +var ( + ErrBadArgument = errors.New("bad argument") + ErrSmallBuffer = errors.New("buffer is too small") + ErrInternal = errors.New("internal error") + ErrInvalidPacket = errors.New("invalid packet") + ErrUnimplemented = errors.New("unimplemented") + ErrInvalidState = errors.New("invalid state") + ErrAllocFail = errors.New("allocation failed") + ErrUnknown = errors.New("unknown error") +) + +func getErr(code C.int) error { + switch code { + case C.gopus_ok: + return nil + case C.gopus_bad_arg: + return ErrBadArgument + case C.gopus_small_buffer: + return ErrSmallBuffer + case C.gopus_internal: + return ErrInternal + case C.gopus_invalid_packet: + return ErrInvalidPacket + case C.gopus_unimplemented: + return ErrUnimplemented + case C.gopus_invalid_state: + return ErrInvalidState + case C.gopus_alloc_fail: + return ErrAllocFail + default: + return ErrUnknown + } +} +EOFOPUS + log_success "Opus patch applied" + fi + + # --- Build TalkKonnect ------------------------------------------------ + echo "[5/7] Building talkkonnect (this may take a few minutes)..." + cd ~/talkkonnect/cmd/talkkonnect + + export CGO_ENABLED=1 + export CGO_CFLAGS="$(pkg-config --cflags opus)" + export CGO_LDFLAGS="$(pkg-config --libs opus) -lm" + + go build -mod=vendor -v -o ~/talkkonnect-binary . 2>&1 | tail -20 + + if [ ! -f ~/talkkonnect-binary ]; then + log_error "Build failed!" + pause + return 1 + fi + + # Stop any running instances + if systemctl is-active --quiet talkkonnect 2>/dev/null; then + sudo systemctl stop talkkonnect + fi + if pgrep -x talkkonnect > /dev/null; then + sudo pkill -9 talkkonnect + sleep 1 + fi + + # Install binary + sudo cp ~/talkkonnect-binary /usr/local/bin/talkkonnect + sudo chmod +x /usr/local/bin/talkkonnect + rm ~/talkkonnect-binary + log_success "Binary installed to /usr/local/bin/talkkonnect" + + # --- User Permissions ------------------------------------------------- + echo "[6/7] Setting up permissions..." + if ! groups "$TARGET_USER" | grep -q input; then + sudo usermod -a -G input "$TARGET_USER" + log_success "Added $TARGET_USER to 'input' group" + fi + if ! groups "$TARGET_USER" | grep -q audio; then + sudo usermod -a -G audio "$TARGET_USER" + log_success "Added $TARGET_USER to 'audio' group" + fi + + # --- Configuration ---------------------------------------------------- + echo "[7/7] Creating configuration..." + + # Create config directory as target user + if [ "$TARGET_USER" != "$USER" ]; then + sudo -u "$TARGET_USER" mkdir -p "$CONFIG_DIR" + else + mkdir -p "$CONFIG_DIR" + fi + + # Create config file with provided settings + sudo tee "$CONFIG_DIR/talkkonnect.xml" > /dev/null < + + + + + + + + + + + + + + + + + $server_addr:$server_port + $tk_user + $tk_pass + true + false + + $tk_channel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +EOFXML + + # Set ownership + sudo chown -R "$TARGET_USER:$TARGET_USER" "$CONFIG_DIR" + sudo chmod 755 "$CONFIG_DIR" + sudo chmod 644 "$CONFIG_DIR/talkkonnect.xml" + + # Create systemd service + sudo tee /etc/systemd/system/talkkonnect.service > /dev/null < setup_intercom.sh.new + +# Add the new function +cat setup_intercom_talkkonnect_function.txt >> setup_intercom.sh.new + +# Add everything after line 732 (from install_talkkonnect_only onwards) +tail -n +733 setup_intercom.sh >> setup_intercom.sh.new + +# Replace the original +mv setup_intercom.sh.new setup_intercom.sh +chmod +x setup_intercom.sh + +echo "[+] Updated setup_intercom.sh with proven installation method" +echo "" +echo "Changes made:" +echo " ✓ Uses working Opus patch from talkkonnect_complete_install.sh" +echo " ✓ Installs to /usr/local/bin/talkkonnect" +echo " ✓ Config in ~/.config/talkkonnect/" +echo " ✓ Sets true by default (for self-signed certs)" +echo " ✓ Proper XDG_RUNTIME_DIR in systemd service" +echo " ✓ Enables service automatically" +echo " ✓ Better error handling and user feedback" +echo "" +echo "The old version is saved as: setup_intercom.sh.backup" +echo ""