705 lines
17 KiB
Bash
705 lines
17 KiB
Bash
#!/bin/bash
|
||
|
||
# Interactive Kiosk Mumble Client Installer
|
||
# With systemd service and fixed audio
|
||
|
||
set -e
|
||
|
||
echo "========================================"
|
||
echo "Kiosk Mumble Client Installer"
|
||
echo "Interactive Setup with Systemd Service"
|
||
echo "========================================"
|
||
echo ""
|
||
|
||
# Colors
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m'
|
||
|
||
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||
print_error() { echo -e "${RED}✗${NC} $1"; }
|
||
print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
||
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||
|
||
TARGET_USER="${1:-kiosk}"
|
||
USER_HOME=$(eval echo "~$TARGET_USER")
|
||
|
||
echo ""
|
||
print_info "Target user: $TARGET_USER"
|
||
print_info "Home directory: $USER_HOME"
|
||
echo ""
|
||
|
||
# Check if user exists
|
||
if ! id "$TARGET_USER" &>/dev/null; then
|
||
print_error "User $TARGET_USER does not exist"
|
||
exit 1
|
||
fi
|
||
|
||
# Interactive configuration
|
||
echo "========================================="
|
||
echo "Mumble Server Configuration"
|
||
echo "========================================="
|
||
echo ""
|
||
|
||
read -p "Mumble server address [mumble.example.org]: " MUMBLE_SERVER
|
||
MUMBLE_SERVER=${MUMBLE_SERVER:-mumble.example.org}
|
||
|
||
read -p "Port [64738]: " MUMBLE_PORT
|
||
MUMBLE_PORT=${MUMBLE_PORT:-64738}
|
||
|
||
read -p "Username [$TARGET_USER]: " MUMBLE_USERNAME
|
||
MUMBLE_USERNAME=${MUMBLE_USERNAME:-$TARGET_USER}
|
||
|
||
read -sp "Password (leave empty if none): " MUMBLE_PASSWORD
|
||
echo ""
|
||
|
||
read -p "Channel to join (leave empty for root): " MUMBLE_CHANNEL
|
||
|
||
read -p "Skip SSL verification? (y/N): " MUMBLE_INSECURE
|
||
MUMBLE_INSECURE=${MUMBLE_INSECURE:-n}
|
||
|
||
echo ""
|
||
echo "Configuration Summary:"
|
||
echo "----------------------"
|
||
echo "Server: $MUMBLE_SERVER:$MUMBLE_PORT"
|
||
echo "Username: $MUMBLE_USERNAME"
|
||
echo "Password: $([ -n "$MUMBLE_PASSWORD" ] && echo '(set)' || echo '(none)')"
|
||
echo "Channel: ${MUMBLE_CHANNEL:-(root)}"
|
||
echo "Skip SSL: $MUMBLE_INSECURE"
|
||
echo ""
|
||
read -p "Continue with installation? (Y/n): " CONFIRM
|
||
if [[ "$CONFIRM" =~ ^[Nn]$ ]]; then
|
||
echo "Installation cancelled"
|
||
exit 0
|
||
fi
|
||
|
||
# Check prerequisites
|
||
print_info "Checking prerequisites..."
|
||
|
||
if ! command -v go &> /dev/null; then
|
||
print_warning "Go not installed, installing..."
|
||
sudo apt update
|
||
sudo apt install -y golang-go
|
||
fi
|
||
print_success "Go installed: $(go version)"
|
||
|
||
# Install system dependencies
|
||
print_info "Installing system dependencies..."
|
||
sudo apt install -y \
|
||
libopus-dev \
|
||
portaudio19-dev \
|
||
pkg-config \
|
||
build-essential \
|
||
git \
|
||
pulseaudio-utils
|
||
|
||
print_success "System dependencies installed"
|
||
|
||
# Create workspace
|
||
WORK_DIR="$USER_HOME/kiosk-mumble"
|
||
print_info "Creating workspace at $WORK_DIR..."
|
||
sudo -u "$TARGET_USER" mkdir -p "$WORK_DIR"
|
||
|
||
# Save the Go source code with FIXED audio callback
|
||
print_info "Creating source files..."
|
||
sudo -u "$TARGET_USER" bash << 'EOF'
|
||
cat > "$HOME/kiosk-mumble/main.go" << 'GOSRC'
|
||
package main
|
||
|
||
import (
|
||
"crypto/tls"
|
||
"encoding/binary"
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"os"
|
||
"os/signal"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/gordonklaus/portaudio"
|
||
"layeh.com/gumble/gumble"
|
||
"layeh.com/gumble/gumbleutil"
|
||
_ "layeh.com/gumble/opus"
|
||
)
|
||
|
||
const (
|
||
AudioSampleRate = 48000
|
||
AudioFrameSize = 960
|
||
AudioChannels = 1
|
||
)
|
||
|
||
type MumbleClient struct {
|
||
client *gumble.Client
|
||
config *gumble.Config
|
||
stream *portaudio.Stream
|
||
transmitting bool
|
||
targetChannel string
|
||
audioOut chan<- gumble.AudioBuffer
|
||
pttDevice string
|
||
pttKeyCode int
|
||
}
|
||
|
||
func main() {
|
||
server := flag.String("server", "localhost:64738", "Mumble server (host:port)")
|
||
username := flag.String("username", "kiosk-client", "Username")
|
||
password := flag.String("password", "", "Server password")
|
||
channel := flag.String("channel", "", "Channel to join")
|
||
insecure := flag.Bool("insecure", false, "Skip TLS verification")
|
||
vad := flag.Bool("vad", false, "Voice activity detection")
|
||
pttKey := flag.String("ptt-key", "", "PTT key: 'space', 'mute', 'f13', etc")
|
||
pttDevice := flag.String("ptt-device", "", "PTT input device")
|
||
listDevices := flag.Bool("list-devices", false, "List input devices and exit")
|
||
|
||
flag.Parse()
|
||
|
||
if *listDevices {
|
||
listInputDevices()
|
||
return
|
||
}
|
||
|
||
fmt.Println("╔════════════════════════════════╗")
|
||
fmt.Println("║ Kiosk Mumble Client v1.0 ║")
|
||
fmt.Println("╚════════════════════════════════╝")
|
||
fmt.Println()
|
||
|
||
if err := portaudio.Initialize(); err != nil {
|
||
log.Fatalf("PortAudio init failed: %v", err)
|
||
}
|
||
defer portaudio.Terminate()
|
||
|
||
config := gumble.NewConfig()
|
||
config.Username = *username
|
||
config.Password = *password
|
||
|
||
tlsConfig := &tls.Config{InsecureSkipVerify: *insecure}
|
||
|
||
mc := &MumbleClient{
|
||
config: config,
|
||
transmitting: *vad,
|
||
targetChannel: *channel,
|
||
pttDevice: *pttDevice,
|
||
}
|
||
|
||
if *pttKey != "" {
|
||
mc.pttKeyCode = parsePTTKey(*pttKey)
|
||
if mc.pttDevice == "" {
|
||
mc.pttDevice = findBestInputDevice()
|
||
}
|
||
}
|
||
|
||
config.Attach(gumbleutil.Listener{
|
||
Connect: mc.onConnect,
|
||
Disconnect: mc.onDisconnect,
|
||
UserChange: mc.onUserChange,
|
||
TextMessage: mc.onTextMessage,
|
||
})
|
||
|
||
fmt.Printf("Connecting to %s as '%s'...\n", *server, *username)
|
||
if *password != "" {
|
||
fmt.Println("Using password authentication")
|
||
}
|
||
|
||
client, err := gumble.DialWithDialer(new(net.Dialer), *server, config, tlsConfig)
|
||
if err != nil {
|
||
log.Fatalf("Connection failed: %v", err)
|
||
}
|
||
mc.client = client
|
||
defer client.Disconnect()
|
||
|
||
fmt.Println("✓ Connected!")
|
||
|
||
if err := mc.setupAudio(); err != nil {
|
||
log.Fatalf("Audio setup failed: %v", err)
|
||
}
|
||
defer mc.stream.Close()
|
||
|
||
if err := mc.stream.Start(); err != nil {
|
||
log.Fatalf("Audio start failed: %v", err)
|
||
}
|
||
defer mc.stream.Stop()
|
||
|
||
fmt.Println()
|
||
fmt.Println("Controls:")
|
||
if *vad {
|
||
fmt.Println(" Mode: Voice Activity Detection (always transmitting)")
|
||
} else if *pttKey != "" {
|
||
fmt.Printf(" Mode: Push-to-Talk (key: %s)\n", *pttKey)
|
||
if mc.pttDevice != "" {
|
||
fmt.Printf(" Device: %s\n", mc.pttDevice)
|
||
}
|
||
} else {
|
||
fmt.Println(" Mode: Manual (no transmission)")
|
||
}
|
||
fmt.Println(" Ctrl+C: Quit")
|
||
fmt.Println()
|
||
|
||
if *pttKey != "" && mc.pttDevice != "" {
|
||
go mc.monitorPTT()
|
||
}
|
||
|
||
if mc.targetChannel != "" {
|
||
time.Sleep(500 * time.Millisecond)
|
||
mc.joinChannel(mc.targetChannel)
|
||
}
|
||
|
||
sigChan := make(chan os.Signal, 1)
|
||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||
<-sigChan
|
||
|
||
fmt.Println("\nDisconnecting...")
|
||
}
|
||
|
||
func (mc *MumbleClient) setupAudio() error {
|
||
mc.audioOut = mc.client.AudioOutgoing()
|
||
|
||
h, err := portaudio.DefaultHostApi()
|
||
if err != nil {
|
||
return fmt.Errorf("failed to get default host API: %w", err)
|
||
}
|
||
|
||
p := portaudio.LowLatencyParameters(h.DefaultInputDevice, nil)
|
||
p.Input.Channels = AudioChannels
|
||
|
||
fmt.Printf("Audio: Using device '%s'\n", h.DefaultInputDevice.Name)
|
||
|
||
stream, err := portaudio.OpenStream(p, func(in, out []int16) {
|
||
if !mc.transmitting || mc.audioOut == nil {
|
||
return
|
||
}
|
||
pcm := make([]int16, len(in))
|
||
copy(pcm, in)
|
||
select {
|
||
case mc.audioOut <- pcm:
|
||
default:
|
||
}
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("failed to open audio stream: %w", err)
|
||
}
|
||
|
||
mc.stream = stream
|
||
return nil
|
||
}
|
||
|
||
func (mc *MumbleClient) monitorPTT() {
|
||
file, err := os.Open(mc.pttDevice)
|
||
if err != nil {
|
||
fmt.Printf("⚠ Failed to open PTT device %s: %v\n", mc.pttDevice, err)
|
||
fmt.Println("⚠ PTT will not work. Try running with sudo or add user to 'input' group:")
|
||
fmt.Printf(" sudo usermod -a -G input %s\n", os.Getenv("USER"))
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
fmt.Printf("✓ PTT monitoring started on %s\n", mc.pttDevice)
|
||
|
||
type inputEvent struct {
|
||
Time syscall.Timeval
|
||
Type uint16
|
||
Code uint16
|
||
Value int32
|
||
}
|
||
|
||
for {
|
||
var event inputEvent
|
||
err := binary.Read(file, binary.LittleEndian, &event)
|
||
if err != nil {
|
||
fmt.Printf("⚠ PTT monitoring error: %v\n", err)
|
||
return
|
||
}
|
||
|
||
if event.Type == 1 && int(event.Code) == mc.pttKeyCode {
|
||
if event.Value == 1 {
|
||
mc.transmitting = true
|
||
fmt.Println("🎤 Transmitting...")
|
||
} else if event.Value == 0 {
|
||
mc.transmitting = false
|
||
fmt.Println("🔇 Not transmitting")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func parsePTTKey(key string) int {
|
||
keyMap := map[string]int{
|
||
"space": 57,
|
||
"mute": 113,
|
||
"volumeup": 115,
|
||
"volumedown": 114,
|
||
"playpause": 164,
|
||
"previoussong": 165,
|
||
"nextsong": 163,
|
||
"f13": 183,
|
||
"f14": 184,
|
||
"f15": 185,
|
||
"f16": 186,
|
||
"f17": 187,
|
||
"f18": 188,
|
||
"f19": 189,
|
||
"f20": 190,
|
||
"leftctrl": 29,
|
||
"rightctrl": 97,
|
||
"leftalt": 56,
|
||
"rightalt": 100,
|
||
"capslock": 58,
|
||
"numlock": 69,
|
||
"scrolllock": 70,
|
||
}
|
||
|
||
key = strings.ToLower(strings.TrimSpace(key))
|
||
if code, ok := keyMap[key]; ok {
|
||
return code
|
||
}
|
||
|
||
var code int
|
||
if _, err := fmt.Sscanf(key, "%d", &code); err == nil {
|
||
return code
|
||
}
|
||
|
||
fmt.Printf("⚠ Unknown key '%s', using Space\n", key)
|
||
return 57
|
||
}
|
||
|
||
func findBestInputDevice() string {
|
||
devices, err := filepath.Glob("/dev/input/event*")
|
||
if err != nil || len(devices) == 0 {
|
||
return ""
|
||
}
|
||
|
||
for _, dev := range devices {
|
||
name := getDeviceName(dev)
|
||
if strings.Contains(strings.ToLower(name), "keyboard") {
|
||
return dev
|
||
}
|
||
}
|
||
|
||
if len(devices) > 0 {
|
||
return devices[0]
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func getDeviceName(devicePath string) string {
|
||
sysPath := "/sys/class/input/" + filepath.Base(devicePath) + "/device/name"
|
||
data, err := os.ReadFile(sysPath)
|
||
if err == nil {
|
||
return strings.TrimSpace(string(data))
|
||
}
|
||
return devicePath
|
||
}
|
||
|
||
func listInputDevices() {
|
||
fmt.Println("Available Input Devices:")
|
||
fmt.Println("========================")
|
||
fmt.Println()
|
||
|
||
devices, err := filepath.Glob("/dev/input/event*")
|
||
if err != nil {
|
||
fmt.Println("Error listing devices:", err)
|
||
return
|
||
}
|
||
|
||
if len(devices) == 0 {
|
||
fmt.Println("No input devices found")
|
||
fmt.Println("Note: You may need to run with sudo")
|
||
return
|
||
}
|
||
|
||
re := regexp.MustCompile(`event(\d+)`)
|
||
for _, dev := range devices {
|
||
name := getDeviceName(dev)
|
||
match := re.FindStringSubmatch(dev)
|
||
eventNum := ""
|
||
if len(match) > 1 {
|
||
eventNum = match[1]
|
||
}
|
||
|
||
fmt.Printf(" %s\n", dev)
|
||
fmt.Printf(" Name: %s\n", name)
|
||
if eventNum != "" {
|
||
fmt.Printf(" Event: %s\n", eventNum)
|
||
}
|
||
fmt.Println()
|
||
}
|
||
|
||
fmt.Println("Common PTT Keys:")
|
||
fmt.Println(" -ptt-key mute # Mute media key")
|
||
fmt.Println(" -ptt-key space # Spacebar")
|
||
fmt.Println(" -ptt-key f13 # F13")
|
||
fmt.Println(" -ptt-key capslock # Caps Lock")
|
||
fmt.Println()
|
||
}
|
||
|
||
func (mc *MumbleClient) onConnect(e *gumble.ConnectEvent) {
|
||
fmt.Println("✓ Connected to server")
|
||
fmt.Printf(" Server: %s\n", e.Client.Conn.RemoteAddr())
|
||
fmt.Printf(" Users: %d online\n", len(e.Client.Users))
|
||
fmt.Printf(" Channels: %d\n", len(e.Client.Channels))
|
||
}
|
||
|
||
func (mc *MumbleClient) onDisconnect(e *gumble.DisconnectEvent) {
|
||
fmt.Println("✗ Disconnected from server")
|
||
if e.String != "" {
|
||
fmt.Printf(" Reason: %s\n", e.String)
|
||
}
|
||
}
|
||
|
||
func (mc *MumbleClient) onUserChange(e *gumble.UserChangeEvent) {
|
||
if e.Type.Has(gumble.UserChangeConnected) {
|
||
fmt.Printf("→ %s joined\n", e.User.Name)
|
||
}
|
||
if e.Type.Has(gumble.UserChangeDisconnected) {
|
||
fmt.Printf("← %s left\n", e.User.Name)
|
||
}
|
||
if e.Type.Has(gumble.UserChangeChannel) {
|
||
if e.User.Channel != nil {
|
||
fmt.Printf("↔ %s moved to %s\n", e.User.Name, e.User.Channel.Name)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (mc *MumbleClient) onTextMessage(e *gumble.TextMessageEvent) {
|
||
if e.Sender != nil {
|
||
fmt.Printf("💬 %s: %s\n", e.Sender.Name, e.Message)
|
||
} else {
|
||
fmt.Printf("💬 Server: %s\n", e.Message)
|
||
}
|
||
}
|
||
|
||
func (mc *MumbleClient) joinChannel(channelPath string) {
|
||
if mc.client == nil || mc.client.Self == nil {
|
||
fmt.Printf("⚠ Client not ready yet, retrying...\n")
|
||
time.Sleep(1 * time.Second)
|
||
if mc.client == nil || mc.client.Self == nil {
|
||
fmt.Printf("⚠ Cannot join channel: client not initialized\n")
|
||
return
|
||
}
|
||
}
|
||
|
||
parts := strings.Split(channelPath, "/")
|
||
|
||
var targetChannel *gumble.Channel
|
||
for _, part := range parts {
|
||
part = strings.TrimSpace(part)
|
||
if part == "" {
|
||
continue
|
||
}
|
||
|
||
found := false
|
||
for _, ch := range mc.client.Channels {
|
||
if ch.Name == part {
|
||
if targetChannel == nil {
|
||
if ch.Parent == nil || ch.Parent.Parent == nil {
|
||
targetChannel = ch
|
||
found = true
|
||
break
|
||
}
|
||
} else {
|
||
if ch.Parent != nil && ch.Parent.ID == targetChannel.ID {
|
||
targetChannel = ch
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if !found {
|
||
fmt.Printf("⚠ Channel not found: %s\n", part)
|
||
fmt.Println("Available channels:")
|
||
mc.listChannels()
|
||
return
|
||
}
|
||
}
|
||
|
||
if targetChannel != nil {
|
||
mc.client.Self.Move(targetChannel)
|
||
fmt.Printf("✓ Joined channel: %s\n", channelPath)
|
||
}
|
||
}
|
||
|
||
func (mc *MumbleClient) listChannels() {
|
||
for _, ch := range mc.client.Channels {
|
||
indent := strings.Repeat(" ", getChannelDepth(ch))
|
||
fmt.Printf("%s- %s\n", indent, ch.Name)
|
||
}
|
||
}
|
||
|
||
func getChannelDepth(ch *gumble.Channel) int {
|
||
depth := 0
|
||
current := ch
|
||
for current.Parent != nil {
|
||
depth++
|
||
current = current.Parent
|
||
}
|
||
return depth
|
||
}
|
||
GOSRC
|
||
EOF
|
||
|
||
print_success "Source code created"
|
||
|
||
# Build
|
||
print_info "Building client (this may take ~45 minutes)..."
|
||
sudo -u "$TARGET_USER" bash << 'EOF'
|
||
cd "$HOME/kiosk-mumble"
|
||
|
||
go mod init kiosk-mumble 2>/dev/null || true
|
||
|
||
echo "Downloading dependencies..."
|
||
go get layeh.com/gumble/gumble@latest
|
||
go get layeh.com/gumble/gumbleutil@latest
|
||
go get layeh.com/gumble/opus@latest
|
||
go get github.com/gordonklaus/portaudio@latest
|
||
|
||
go mod tidy
|
||
|
||
echo "Compiling..."
|
||
CGO_ENABLED=1 go build -o kiosk-mumble main.go
|
||
|
||
if [ -f kiosk-mumble ]; then
|
||
echo "✓ Build successful!"
|
||
mkdir -p "$HOME/bin"
|
||
cp kiosk-mumble "$HOME/bin/"
|
||
chmod +x "$HOME/bin/kiosk-mumble"
|
||
echo "✓ Installed to $HOME/bin/kiosk-mumble"
|
||
else
|
||
echo "✗ Build failed"
|
||
exit 1
|
||
fi
|
||
EOF
|
||
|
||
BUILD_RESULT=$?
|
||
|
||
if [ $BUILD_RESULT -ne 0 ]; then
|
||
print_error "Build failed!"
|
||
exit 1
|
||
fi
|
||
|
||
print_success "Build complete!"
|
||
|
||
# Configure permissions
|
||
print_info "Configuring permissions..."
|
||
|
||
if ! groups "$TARGET_USER" | grep -q '\binput\b'; then
|
||
sudo usermod -a -G input "$TARGET_USER"
|
||
print_success "Added to input group"
|
||
else
|
||
print_success "Already in input group"
|
||
fi
|
||
|
||
if ! groups "$TARGET_USER" | grep -q '\baudio\b'; then
|
||
sudo usermod -a -G audio "$TARGET_USER"
|
||
print_success "Added to audio group"
|
||
else
|
||
print_success "Already in audio group"
|
||
fi
|
||
|
||
# Create systemd service
|
||
print_info "Creating systemd service..."
|
||
|
||
INSECURE_FLAG=""
|
||
if [[ "$MUMBLE_INSECURE" =~ ^[Yy]$ ]]; then
|
||
INSECURE_FLAG="-insecure"
|
||
fi
|
||
|
||
PASSWORD_FLAG=""
|
||
if [ -n "$MUMBLE_PASSWORD" ]; then
|
||
PASSWORD_FLAG="-password \"$MUMBLE_PASSWORD\""
|
||
fi
|
||
|
||
CHANNEL_FLAG=""
|
||
if [ -n "$MUMBLE_CHANNEL" ]; then
|
||
CHANNEL_FLAG="-channel \"$MUMBLE_CHANNEL\""
|
||
fi
|
||
|
||
sudo tee /etc/systemd/system/kiosk-mumble.service > /dev/null << SERVICEEOF
|
||
[Unit]
|
||
Description=Kiosk Mumble Client
|
||
After=network-online.target sound.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=$TARGET_USER
|
||
Group=$TARGET_USER
|
||
Environment="XDG_RUNTIME_DIR=/run/user/$(id -u $TARGET_USER)"
|
||
Environment="PULSE_SERVER=/run/user/$(id -u $TARGET_USER)/pulse/native"
|
||
ExecStart=$USER_HOME/bin/kiosk-mumble -server $MUMBLE_SERVER:$MUMBLE_PORT -username $MUMBLE_USERNAME $PASSWORD_FLAG $CHANNEL_FLAG $INSECURE_FLAG -vad
|
||
Restart=always
|
||
RestartSec=10
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
SERVICEEOF
|
||
|
||
sudo systemctl daemon-reload
|
||
print_success "Systemd service created"
|
||
|
||
echo ""
|
||
echo "========================================"
|
||
echo "Installation Complete!"
|
||
echo "========================================"
|
||
echo ""
|
||
print_success "Kiosk Mumble Client installed"
|
||
echo ""
|
||
echo "Service Management:"
|
||
echo "-------------------"
|
||
echo "Start service: sudo systemctl start kiosk-mumble"
|
||
echo "Stop service: sudo systemctl stop kiosk-mumble"
|
||
echo "Enable on boot: sudo systemctl enable kiosk-mumble"
|
||
echo "View logs: sudo journalctl -u kiosk-mumble -f"
|
||
echo "Service status: sudo systemctl status kiosk-mumble"
|
||
echo ""
|
||
echo "Manual Testing:"
|
||
echo "---------------"
|
||
echo "Test audio:"
|
||
if [ -n "$MUMBLE_CHANNEL" ]; then
|
||
echo " sudo -u $TARGET_USER $USER_HOME/bin/kiosk-mumble \\"
|
||
echo " -server $MUMBLE_SERVER:$MUMBLE_PORT \\"
|
||
echo " -username $MUMBLE_USERNAME \\"
|
||
echo " -channel \"$MUMBLE_CHANNEL\" \\"
|
||
echo " -vad"
|
||
else
|
||
echo " sudo -u $TARGET_USER $USER_HOME/bin/kiosk-mumble \\"
|
||
echo " -server $MUMBLE_SERVER:$MUMBLE_PORT \\"
|
||
echo " -username $MUMBLE_USERNAME \\"
|
||
echo " -vad"
|
||
fi
|
||
echo ""
|
||
echo "Audio Troubleshooting:"
|
||
echo "----------------------"
|
||
echo "Check audio devices:"
|
||
echo " pactl list short sinks"
|
||
echo " pactl list short sources"
|
||
echo ""
|
||
echo "Test microphone:"
|
||
echo " arecord -d 5 test.wav && aplay test.wav"
|
||
echo ""
|
||
print_warning "Note: User must log out and back in for group changes to take effect!"
|
||
echo ""
|
||
|
||
read -p "Enable and start service now? (Y/n): " START_NOW
|
||
if [[ ! "$START_NOW" =~ ^[Nn]$ ]]; then
|
||
sudo systemctl enable kiosk-mumble
|
||
sudo systemctl start kiosk-mumble
|
||
print_success "Service enabled and started"
|
||
echo ""
|
||
echo "Checking status..."
|
||
sleep 2
|
||
sudo systemctl status kiosk-mumble --no-pager
|
||
fi
|
||
|
||
echo ""
|
||
print_info "Installation complete! Check logs with: sudo journalctl -u kiosk-mumble -f"
|
||
echo ""
|