feat: Add WebRadio, TURN server, and Lyrion streaming integration
New packages: - luci-app-webradio: Web radio management with Lyrion bridge tab - luci-app-turn: TURN/STUN server UI for WebRTC (Jitsi integration) - secubox-app-lyrion-bridge: Lyrion → Squeezelite → FFmpeg → Icecast pipeline - secubox-app-squeezelite: Squeezelite audio player with FIFO output - secubox-app-turn: TURN server with ACME SSL and Jitsi setup - secubox-app-webradio: Icecast/ezstream web radio server Features: - HTTPS streaming via HAProxy (stream.gk2.secubox.in) - Lyrion Music Server bridge for streaming playlists to Icecast - TURN server with time-limited credential generation - CrowdSec integration for WebRadio security - Schedule-based radio programming with jingles Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
05d12ab130
commit
6db547f7f8
@ -2793,3 +2793,57 @@ git checkout HEAD -- index.html
|
||||
- `.sb-btn` action buttons with hover states
|
||||
- Dark mode via CSS media queries
|
||||
- No external CSS file dependencies — fully self-contained views
|
||||
|
||||
56. **Lyrion Stream Integration (2026-02-21)**
|
||||
- New `secubox-app-squeezelite` package — Virtual Squeezebox player for Lyrion Music Server.
|
||||
- New `secubox-app-lyrion-bridge` package — Audio bridge from Squeezelite to WebRadio/Icecast.
|
||||
- **Squeezelite CLI (squeezelitectl)**:
|
||||
- Service control: `start`, `stop`, `restart`, `enable`, `disable`, `status`
|
||||
- Connection: `discover` (auto-find Lyrion), `connect [server]`, `disconnect`
|
||||
- Audio: `devices` (list outputs), `output [device]` (set output)
|
||||
- Streaming: `fifo enable [path]`, `fifo disable`, `fifo status`
|
||||
- **Lyrion Bridge CLI (lyrionstreamctl)**:
|
||||
- Setup: `setup [lyrion-ip]` — Full pipeline configuration
|
||||
- Service: `start`, `stop`, `restart`, `enable`, `disable`, `status`
|
||||
- Config: `config mount|bitrate|name|server [value]`
|
||||
- Operations: `expose <domain>` (HAProxy+SSL), `logs [lines]`
|
||||
- **Pipeline Architecture**:
|
||||
- Lyrion Server → Squeezelite (FIFO output /tmp/squeezelite.pcm)
|
||||
- Squeezelite → FFmpeg (PCM to MP3 encoding)
|
||||
- FFmpeg → Icecast (HTTP streaming)
|
||||
- **FFmpeg Bridge (ffmpeg-bridge.sh)**:
|
||||
- Reads PCM from FIFO (s16le, 44100Hz, stereo)
|
||||
- Encodes to MP3 (configurable bitrate, default 192kbps)
|
||||
- Streams to Icecast mount point
|
||||
- Auto-syncs metadata from Lyrion (artist/title)
|
||||
- Auto-reconnect on stream errors
|
||||
- UCI configs: `/etc/config/squeezelite`, `/etc/config/lyrion-bridge`
|
||||
- Files:
|
||||
- `secubox-app-squeezelite/`: Makefile, UCI config, init script, squeezelitectl
|
||||
- `secubox-app-lyrion-bridge/`: Makefile, UCI config, init script, lyrionstreamctl, ffmpeg-bridge.sh
|
||||
|
||||
57. **TURN Server for WebRTC (2026-02-21)**
|
||||
- New `secubox-app-turn` package — coturn-based TURN/STUN server for NAT traversal.
|
||||
- Required for Jitsi Meet when direct P2P connections fail (symmetric NAT, firewalls).
|
||||
- **TURN CLI (turnctl)**:
|
||||
- Service: `start`, `stop`, `restart`, `enable`, `disable`, `status`
|
||||
- Setup: `setup-jitsi [jitsi-domain] [turn-domain]` — Configure for Jitsi Meet
|
||||
- SSL: `ssl [domain]` — Generate/install SSL certificates
|
||||
- Network: `expose [domain]` — Configure DNS and firewall rules
|
||||
- Auth: `credentials [user] [ttl]` — Generate time-limited WebRTC credentials
|
||||
- Testing: `test [host]` — Test TURN connectivity
|
||||
- Logs: `logs [lines]` — View server logs
|
||||
- **Ports**: 3478 (STUN/TURN), 5349 (TURN over TLS), 49152-65535 (media relay)
|
||||
- **Security**:
|
||||
- HMAC-SHA1 time-limited credentials (REST API compatible)
|
||||
- Blocked peer IPs: RFC1918, localhost, link-local
|
||||
- Auto-generated static auth secret
|
||||
- **Jitsi Integration**: Added `jitsctl setup-turn [domain]` command
|
||||
- UCI config: `/etc/config/turn` (sections: main, ssl, limits, log)
|
||||
- Files:
|
||||
- `secubox-app-turn/Makefile`
|
||||
- `secubox-app-turn/files/etc/config/turn`
|
||||
- `secubox-app-turn/files/etc/init.d/turn`
|
||||
- `secubox-app-turn/files/usr/sbin/turnctl`
|
||||
- Modified:
|
||||
- `secubox-app-jitsi/files/usr/sbin/jitsctl` — Added `setup-turn` command
|
||||
|
||||
@ -381,7 +381,14 @@
|
||||
"WebFetch(domain:cf.gk2.secubox.in)",
|
||||
"WebFetch(domain:streamlit.gk2.secubox.in)",
|
||||
"Bash(# Use SDK''s package tools cd /home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/sdk # Copy the manually created IPK to SDK''s output cp /home/reepost/CyberMindStudio/secubox-openwrt/package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-ipblocklist_1.0.0-r1_all.ipk bin/packages/aarch64_cortex-a72/secubox/ # Regenerate index for that feed cd bin/packages/aarch64_cortex-a72/secubox ../../../../scripts/ipkg-make-index.sh . gzip -k -f Packages # Now rebuild the bonus package which will include everything cd /home/reepost/CyberMindStudio/secubox-openwrt ./secubox-tools/local-build.sh build secubox-app-bonus 2>&1)",
|
||||
"WebFetch(domain:portal.secubox.in)"
|
||||
"WebFetch(domain:portal.secubox.in)",
|
||||
"Bash(# Test 1: Check that portal.secubox.in redirects to login when not authenticated curl -s -k -I https://portal.secubox.in/)",
|
||||
"Bash(# Test the complete flow echo \"\"=== Test 1: Root URL redirects to login ===\"\" curl -s -k -I https://portal.secubox.in/)",
|
||||
"Bash(__NEW_LINE_7fd1ab4a5ccb9b63__ echo \"\")",
|
||||
"Bash(__NEW_LINE_755a36c329effceb__ echo \"\")",
|
||||
"Bash(__NEW_LINE_02bd2dd51e90cbf8__ echo \"\")",
|
||||
"Bash(__NEW_LINE_70eb6f3ae1c26753__ echo \"\")",
|
||||
"WebFetch(domain:radio.gk2.secubox.in)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
15
package/secubox/luci-app-turn/Makefile
Normal file
15
package/secubox/luci-app-turn/Makefile
Normal file
@ -0,0 +1,15 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-turn
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
LUCI_TITLE:=LuCI TURN Server Management
|
||||
LUCI_DEPENDS:=+secubox-app-turn +luci-base
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/luci-app-turn/conffiles
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-turn))
|
||||
@ -0,0 +1,228 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
'require ui';
|
||||
|
||||
var callStatus = rpc.declare({ object: 'luci.turn', method: 'status', expect: {} });
|
||||
var callStart = rpc.declare({ object: 'luci.turn', method: 'start', expect: {} });
|
||||
var callStop = rpc.declare({ object: 'luci.turn', method: 'stop', expect: {} });
|
||||
var callEnable = rpc.declare({ object: 'luci.turn', method: 'enable', expect: {} });
|
||||
var callDisable = rpc.declare({ object: 'luci.turn', method: 'disable', expect: {} });
|
||||
var callSetupJitsi = rpc.declare({ object: 'luci.turn', method: 'setup_jitsi', params: ['jitsi_domain', 'turn_domain'], expect: {} });
|
||||
var callSSL = rpc.declare({ object: 'luci.turn', method: 'ssl', params: ['domain'], expect: {} });
|
||||
var callExpose = rpc.declare({ object: 'luci.turn', method: 'expose', params: ['domain'], expect: {} });
|
||||
var callCredentials = rpc.declare({ object: 'luci.turn', method: 'credentials', params: ['username', 'ttl'], expect: {} });
|
||||
var callLogs = rpc.declare({ object: 'luci.turn', method: 'logs', params: ['lines'], expect: {} });
|
||||
|
||||
return view.extend({
|
||||
data: {},
|
||||
|
||||
load: function() {
|
||||
return callStatus().then(function(r) { this.data = r; return r; }.bind(this));
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
this.data = data || {};
|
||||
|
||||
poll.add(function() {
|
||||
return callStatus().then(function(r) {
|
||||
self.data = r;
|
||||
self.updateUI(r);
|
||||
});
|
||||
}, 5);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('style', {}, this.getStyles()),
|
||||
|
||||
E('div', { 'class': 'sb-header' }, [
|
||||
E('h2', {}, 'TURN Server'),
|
||||
E('div', { 'class': 'sb-chips' }, [
|
||||
E('span', { 'class': 'chip', 'id': 'chip-status' },
|
||||
this.data.running ? 'Running' : 'Stopped'),
|
||||
E('span', { 'class': 'chip' }, 'Realm: ' + (this.data.realm || 'N/A')),
|
||||
E('span', { 'class': 'chip' }, 'Port: ' + (this.data.port || 3478))
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Service Control'),
|
||||
E('div', { 'class': 'btn-row' }, [
|
||||
E('button', { 'class': 'sb-btn sb-btn-success', 'click': ui.createHandlerFn(this, 'handleStart') }, 'Start'),
|
||||
E('button', { 'class': 'sb-btn sb-btn-danger', 'click': ui.createHandlerFn(this, 'handleStop') }, 'Stop'),
|
||||
E('button', { 'class': 'sb-btn', 'click': ui.createHandlerFn(this, 'handleEnable') }, 'Enable Autostart'),
|
||||
E('button', { 'class': 'sb-btn', 'click': ui.createHandlerFn(this, 'handleDisable') }, 'Disable Autostart')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Status'),
|
||||
E('div', { 'class': 'sb-grid' }, [
|
||||
this.renderCard('Server', this.data.running ? 'Running' : 'Stopped', this.data.running ? 'success' : 'danger'),
|
||||
this.renderCard('UDP 3478', this.data.udp_3478 ? 'Listening' : 'Closed', this.data.udp_3478 ? 'success' : 'warning'),
|
||||
this.renderCard('TCP 5349', this.data.tcp_5349 ? 'Listening' : 'Closed', this.data.tcp_5349 ? 'success' : 'warning'),
|
||||
this.renderCard('External IP', this.data.external_ip || this.data.detected_ip || 'Unknown', 'info')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Jitsi Integration'),
|
||||
E('p', {}, 'Configure TURN server for Jitsi Meet WebRTC connections'),
|
||||
E('div', { 'class': 'form-row' }, [
|
||||
E('input', { 'type': 'text', 'id': 'jitsi-domain', 'placeholder': 'jitsi.secubox.in', 'class': 'sb-input' }),
|
||||
E('input', { 'type': 'text', 'id': 'turn-domain', 'placeholder': 'turn.secubox.in', 'class': 'sb-input' }),
|
||||
E('button', { 'class': 'sb-btn sb-btn-primary', 'click': ui.createHandlerFn(this, 'handleSetupJitsi') }, 'Setup for Jitsi')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'SSL & Expose'),
|
||||
E('div', { 'class': 'form-row' }, [
|
||||
E('input', { 'type': 'text', 'id': 'ssl-domain', 'placeholder': 'turn.secubox.in', 'class': 'sb-input' }),
|
||||
E('button', { 'class': 'sb-btn', 'click': ui.createHandlerFn(this, 'handleSSL') }, 'Setup SSL'),
|
||||
E('button', { 'class': 'sb-btn sb-btn-primary', 'click': ui.createHandlerFn(this, 'handleExpose') }, 'Expose (DNS+FW)')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Generate Credentials'),
|
||||
E('p', {}, 'Generate time-limited TURN credentials for WebRTC clients'),
|
||||
E('div', { 'class': 'form-row' }, [
|
||||
E('input', { 'type': 'text', 'id': 'cred-user', 'placeholder': 'username', 'value': 'webrtc', 'class': 'sb-input' }),
|
||||
E('input', { 'type': 'number', 'id': 'cred-ttl', 'placeholder': 'TTL (seconds)', 'value': '86400', 'class': 'sb-input' }),
|
||||
E('button', { 'class': 'sb-btn sb-btn-primary', 'click': ui.createHandlerFn(this, 'handleCredentials') }, 'Generate')
|
||||
]),
|
||||
E('pre', { 'id': 'credentials-output', 'class': 'sb-output', 'style': 'display:none;' }, '')
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Logs'),
|
||||
E('button', { 'class': 'sb-btn', 'click': ui.createHandlerFn(this, 'handleShowLogs') }, 'Show Logs'),
|
||||
E('pre', { 'id': 'logs-output', 'class': 'sb-output', 'style': 'display:none; max-height:300px; overflow:auto;' }, '')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderCard: function(title, value, status) {
|
||||
var statusClass = status === 'success' ? 'card-success' : (status === 'danger' ? 'card-danger' : (status === 'warning' ? 'card-warning' : 'card-info'));
|
||||
return E('div', { 'class': 'sb-card ' + statusClass }, [
|
||||
E('div', { 'class': 'card-title' }, title),
|
||||
E('div', { 'class': 'card-value' }, value)
|
||||
]);
|
||||
},
|
||||
|
||||
updateUI: function(data) {
|
||||
var chip = document.getElementById('chip-status');
|
||||
if (chip) {
|
||||
chip.textContent = data.running ? 'Running' : 'Stopped';
|
||||
chip.className = 'chip ' + (data.running ? 'chip-success' : 'chip-danger');
|
||||
}
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
return callStart().then(function() {
|
||||
ui.addNotification(null, E('p', 'TURN server started'));
|
||||
});
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
return callStop().then(function() {
|
||||
ui.addNotification(null, E('p', 'TURN server stopped'));
|
||||
});
|
||||
},
|
||||
|
||||
handleEnable: function() {
|
||||
return callEnable().then(function() {
|
||||
ui.addNotification(null, E('p', 'TURN server enabled'));
|
||||
});
|
||||
},
|
||||
|
||||
handleDisable: function() {
|
||||
return callDisable().then(function() {
|
||||
ui.addNotification(null, E('p', 'TURN server disabled'));
|
||||
});
|
||||
},
|
||||
|
||||
handleSetupJitsi: function() {
|
||||
var jitsiDomain = document.getElementById('jitsi-domain').value || 'jitsi.secubox.in';
|
||||
var turnDomain = document.getElementById('turn-domain').value || 'turn.secubox.in';
|
||||
|
||||
return callSetupJitsi(jitsiDomain, turnDomain).then(function(res) {
|
||||
ui.addNotification(null, E('p', 'TURN configured for Jitsi. Auth secret: ' + (res.auth_secret || 'generated')));
|
||||
});
|
||||
},
|
||||
|
||||
handleSSL: function() {
|
||||
var domain = document.getElementById('ssl-domain').value || 'turn.secubox.in';
|
||||
return callSSL(domain).then(function(res) {
|
||||
ui.addNotification(null, E('p', 'SSL configured for ' + domain));
|
||||
});
|
||||
},
|
||||
|
||||
handleExpose: function() {
|
||||
var domain = document.getElementById('ssl-domain').value || 'turn.secubox.in';
|
||||
return callExpose(domain).then(function(res) {
|
||||
ui.addNotification(null, E('p', 'TURN exposed on ' + domain));
|
||||
});
|
||||
},
|
||||
|
||||
handleCredentials: function() {
|
||||
var username = document.getElementById('cred-user').value || 'webrtc';
|
||||
var ttl = parseInt(document.getElementById('cred-ttl').value) || 86400;
|
||||
|
||||
return callCredentials(username, ttl).then(function(res) {
|
||||
var output = document.getElementById('credentials-output');
|
||||
output.style.display = 'block';
|
||||
output.textContent = JSON.stringify({
|
||||
urls: ['turn:' + res.realm + ':3478', 'turn:' + res.realm + ':3478?transport=tcp'],
|
||||
username: res.username,
|
||||
credential: res.password
|
||||
}, null, 2);
|
||||
});
|
||||
},
|
||||
|
||||
handleShowLogs: function() {
|
||||
return callLogs(100).then(function(res) {
|
||||
var output = document.getElementById('logs-output');
|
||||
output.style.display = 'block';
|
||||
output.textContent = res.logs || 'No logs available';
|
||||
});
|
||||
},
|
||||
|
||||
getStyles: function() {
|
||||
return [
|
||||
'.sb-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }',
|
||||
'.sb-chips { display:flex; gap:10px; }',
|
||||
'.chip { padding:5px 12px; border-radius:15px; font-size:0.85em; background:#444; color:#fff; }',
|
||||
'.chip-success { background:#28a745; }',
|
||||
'.chip-danger { background:#dc3545; }',
|
||||
'.sb-section { background:#1a1a2e; padding:20px; margin-bottom:15px; border-radius:8px; }',
|
||||
'.sb-section h3 { margin:0 0 15px 0; color:#4fc3f7; }',
|
||||
'.sb-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:15px; }',
|
||||
'.sb-card { padding:15px; border-radius:8px; text-align:center; }',
|
||||
'.card-success { background:#155724; border:1px solid #28a745; }',
|
||||
'.card-danger { background:#721c24; border:1px solid #dc3545; }',
|
||||
'.card-warning { background:#856404; border:1px solid #ffc107; }',
|
||||
'.card-info { background:#0c5460; border:1px solid #17a2b8; }',
|
||||
'.card-title { font-size:0.85em; color:#aaa; margin-bottom:5px; }',
|
||||
'.card-value { font-size:1.1em; font-weight:bold; }',
|
||||
'.btn-row { display:flex; gap:10px; flex-wrap:wrap; }',
|
||||
'.sb-btn { padding:8px 16px; border:none; border-radius:5px; cursor:pointer; background:#444; color:#fff; }',
|
||||
'.sb-btn:hover { background:#555; }',
|
||||
'.sb-btn-primary { background:#007bff; }',
|
||||
'.sb-btn-primary:hover { background:#0056b3; }',
|
||||
'.sb-btn-success { background:#28a745; }',
|
||||
'.sb-btn-success:hover { background:#218838; }',
|
||||
'.sb-btn-danger { background:#dc3545; }',
|
||||
'.sb-btn-danger:hover { background:#c82333; }',
|
||||
'.form-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }',
|
||||
'.sb-input { padding:8px 12px; border:1px solid #444; border-radius:5px; background:#2a2a3e; color:#fff; }',
|
||||
'.sb-output { background:#0a0a15; padding:15px; border-radius:5px; font-family:monospace; font-size:0.9em; white-space:pre-wrap; word-break:break-all; }'
|
||||
].join('\n');
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
213
package/secubox/luci-app-turn/root/usr/libexec/rpcd/luci.turn
Normal file
213
package/secubox/luci-app-turn/root/usr/libexec/rpcd/luci.turn
Normal file
@ -0,0 +1,213 @@
|
||||
#!/bin/sh
|
||||
# RPCD handler for TURN server management
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
uci_get() { uci -q get "turn.$1" 2>/dev/null || echo "$2"; }
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"logs":{"lines":50},"test":{"host":""},"start":{},"stop":{},"restart":{},"enable":{},"disable":{},"setup_jitsi":{"jitsi_domain":"","turn_domain":""},"ssl":{"domain":""},"expose":{"domain":""},"credentials":{"username":"","ttl":86400}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status)
|
||||
json_init
|
||||
|
||||
local enabled=$(uci_get main.enabled 0)
|
||||
local realm=$(uci_get main.realm "turn.secubox.in")
|
||||
local port=$(uci_get main.listening_port "3478")
|
||||
local tls_port=$(uci_get main.tls_port "5349")
|
||||
local external_ip=$(uci_get main.external_ip "")
|
||||
|
||||
json_add_boolean enabled $([ "$enabled" = "1" ] && echo 1 || echo 0)
|
||||
json_add_string realm "$realm"
|
||||
json_add_int port "$port"
|
||||
json_add_int tls_port "$tls_port"
|
||||
json_add_string external_ip "$external_ip"
|
||||
|
||||
if pgrep -f "turnserver" >/dev/null 2>&1; then
|
||||
json_add_boolean running 1
|
||||
json_add_int pid $(pgrep -f "turnserver" | head -1)
|
||||
else
|
||||
json_add_boolean running 0
|
||||
json_add_int pid 0
|
||||
fi
|
||||
|
||||
# Check ports
|
||||
if grep -q ":0D92 " /proc/net/udp 2>/dev/null; then
|
||||
json_add_boolean udp_3478 1
|
||||
else
|
||||
json_add_boolean udp_3478 0
|
||||
fi
|
||||
|
||||
if grep -q ":14E5 " /proc/net/tcp 2>/dev/null; then
|
||||
json_add_boolean tcp_5349 1
|
||||
else
|
||||
json_add_boolean tcp_5349 0
|
||||
fi
|
||||
|
||||
# Auto-detect external IP if empty
|
||||
if [ -z "$external_ip" ]; then
|
||||
external_ip=$(curl -s -4 --connect-timeout 3 https://ifconfig.me 2>/dev/null || echo "")
|
||||
json_add_string detected_ip "$external_ip"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
logs)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var lines lines 50
|
||||
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
|
||||
local log_file=$(uci_get log.log_file "/var/log/turnserver.log")
|
||||
if [ -f "$log_file" ]; then
|
||||
json_add_string logs "$(tail -n "$lines" "$log_file" 2>/dev/null | head -c 50000)"
|
||||
else
|
||||
json_add_string logs "$(logread | grep -i turn | tail -n "$lines" | head -c 50000)"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
test)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var host host ""
|
||||
|
||||
[ -z "$host" ] && host=$(uci_get main.realm "turn.secubox.in")
|
||||
|
||||
json_init
|
||||
|
||||
# Test UDP 3478
|
||||
if nc -u -z -w 2 "$host" 3478 2>/dev/null; then
|
||||
json_add_boolean udp_reachable 1
|
||||
else
|
||||
json_add_boolean udp_reachable 0
|
||||
fi
|
||||
|
||||
# Test TCP 5349
|
||||
if nc -z -w 2 "$host" 5349 2>/dev/null; then
|
||||
json_add_boolean tcp_reachable 1
|
||||
else
|
||||
json_add_boolean tcp_reachable 0
|
||||
fi
|
||||
|
||||
json_add_string host "$host"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
start)
|
||||
/etc/init.d/turn start 2>&1
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
stop)
|
||||
/etc/init.d/turn stop 2>&1
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
restart)
|
||||
/etc/init.d/turn restart 2>&1
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
enable)
|
||||
uci set turn.main.enabled='1'
|
||||
uci commit turn
|
||||
/etc/init.d/turn enable
|
||||
/etc/init.d/turn start
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
disable)
|
||||
uci set turn.main.enabled='0'
|
||||
uci commit turn
|
||||
/etc/init.d/turn disable
|
||||
/etc/init.d/turn stop
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
setup_jitsi)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var jitsi_domain jitsi_domain ""
|
||||
json_get_var turn_domain turn_domain "turn.secubox.in"
|
||||
|
||||
output=$(turnctl setup-jitsi "$jitsi_domain" "$turn_domain" 2>&1)
|
||||
local auth_secret=$(uci_get main.static_auth_secret "")
|
||||
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_add_string turn_domain "$turn_domain"
|
||||
json_add_string auth_secret "$auth_secret"
|
||||
json_add_string output "$output"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
ssl)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var domain domain ""
|
||||
|
||||
output=$(turnctl ssl "$domain" 2>&1)
|
||||
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_add_string output "$output"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
expose)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var domain domain ""
|
||||
|
||||
output=$(turnctl expose "$domain" 2>&1)
|
||||
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_add_string output "$output"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
credentials)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var username username "webrtc"
|
||||
json_get_var ttl ttl 86400
|
||||
|
||||
local auth_secret=$(uci_get main.static_auth_secret "")
|
||||
local realm=$(uci_get main.realm "turn.secubox.in")
|
||||
local timestamp=$(($(date +%s) + ttl))
|
||||
local temp_username="${timestamp}:${username}"
|
||||
|
||||
# HMAC-SHA1 credential
|
||||
local password=$(echo -n "$temp_username" | openssl dgst -sha1 -hmac "$auth_secret" -binary | base64)
|
||||
|
||||
json_init
|
||||
json_add_string result "ok"
|
||||
json_add_string realm "$realm"
|
||||
json_add_string username "$temp_username"
|
||||
json_add_string password "$password"
|
||||
json_add_int ttl "$ttl"
|
||||
json_add_int expires "$timestamp"
|
||||
json_dump
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"admin/services/turn": {
|
||||
"title": "TURN Server",
|
||||
"order": 85,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "turn/overview"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-turn"],
|
||||
"uci": { "turn": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-turn": {
|
||||
"description": "Grant access to TURN server management",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.turn": ["status", "logs", "test"]
|
||||
},
|
||||
"uci": ["turn"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.turn": ["start", "stop", "restart", "enable", "disable", "setup_jitsi", "ssl", "expose", "credentials"]
|
||||
},
|
||||
"uci": ["turn"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,195 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require poll';
|
||||
'require ui';
|
||||
|
||||
var callBridgeStatus = rpc.declare({ object: 'luci.webradio', method: 'bridge_status', expect: {} });
|
||||
var callBridgeStart = rpc.declare({ object: 'luci.webradio', method: 'bridge_start', expect: {} });
|
||||
var callBridgeStop = rpc.declare({ object: 'luci.webradio', method: 'bridge_stop', expect: {} });
|
||||
var callBridgeSetup = rpc.declare({ object: 'luci.webradio', method: 'bridge_setup', params: ['lyrion_server'], expect: {} });
|
||||
|
||||
return view.extend({
|
||||
data: {},
|
||||
|
||||
load: function() {
|
||||
return callBridgeStatus().then(function(r) { this.data = r; return r; }.bind(this)).catch(function() { return {}; });
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
this.data = data || {};
|
||||
|
||||
poll.add(function() {
|
||||
return callBridgeStatus().then(function(r) {
|
||||
self.data = r;
|
||||
self.updateUI(r);
|
||||
}).catch(function() {});
|
||||
}, 5);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('style', {}, this.getStyles()),
|
||||
|
||||
E('div', { 'class': 'sb-header' }, [
|
||||
E('h2', {}, 'Lyrion Stream Bridge'),
|
||||
E('div', { 'class': 'sb-chips' }, [
|
||||
E('span', { 'class': 'chip', 'id': 'chip-bridge' }, this.data.bridge_running ? 'Bridge Running' : 'Bridge Stopped'),
|
||||
E('span', { 'class': 'chip', 'id': 'chip-lyrion' }, this.data.lyrion_online ? 'Lyrion Online' : 'Lyrion Offline')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Architecture'),
|
||||
E('div', { 'class': 'pipeline' }, [
|
||||
E('span', { 'class': 'pipe-node' }, 'Lyrion Server'),
|
||||
E('span', { 'class': 'pipe-arrow' }, '\u2192'),
|
||||
E('span', { 'class': 'pipe-node' }, 'Squeezelite'),
|
||||
E('span', { 'class': 'pipe-arrow' }, '\u2192'),
|
||||
E('span', { 'class': 'pipe-node' }, 'FIFO'),
|
||||
E('span', { 'class': 'pipe-arrow' }, '\u2192'),
|
||||
E('span', { 'class': 'pipe-node' }, 'FFmpeg'),
|
||||
E('span', { 'class': 'pipe-arrow' }, '\u2192'),
|
||||
E('span', { 'class': 'pipe-node' }, 'Icecast')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Status'),
|
||||
E('div', { 'class': 'sb-grid' }, [
|
||||
this.renderCard('Lyrion', this.data.lyrion_online ? 'Online' : 'Offline', this.data.lyrion_online ? 'success' : 'danger', 'lyrion-card'),
|
||||
this.renderCard('Squeezelite', this.data.squeezelite_running ? 'Running' : 'Stopped', this.data.squeezelite_running ? 'success' : 'warning', 'squeeze-card'),
|
||||
this.renderCard('FFmpeg', this.data.ffmpeg_running ? 'Encoding' : 'Idle', this.data.ffmpeg_running ? 'success' : 'warning', 'ffmpeg-card'),
|
||||
this.renderCard('Icecast Mount', this.data.mount_active ? 'Active' : 'Inactive', this.data.mount_active ? 'success' : 'warning', 'mount-card')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Now Playing'),
|
||||
E('div', { 'class': 'now-playing', 'id': 'now-playing' }, [
|
||||
E('span', { 'class': 'np-title' }, this.data.title || 'Nothing playing'),
|
||||
this.data.artist ? E('span', { 'class': 'np-artist' }, this.data.artist) : ''
|
||||
]),
|
||||
E('div', { 'style': 'margin-top:10px;' }, [
|
||||
E('span', {}, 'Listeners: '),
|
||||
E('strong', { 'id': 'listeners' }, String(this.data.listeners || 0))
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Quick Setup'),
|
||||
E('div', { 'class': 'form-row' }, [
|
||||
E('input', { 'type': 'text', 'id': 'lyrion-server', 'placeholder': 'Lyrion IP (e.g. 127.0.0.1)', 'value': this.data.lyrion_server || '', 'class': 'sb-input' }),
|
||||
E('button', { 'class': 'sb-btn sb-btn-primary', 'click': ui.createHandlerFn(this, 'handleSetup') }, 'Setup Pipeline')
|
||||
]),
|
||||
E('p', { 'style': 'color:#888; font-size:0.9em; margin-top:10px;' },
|
||||
'This will configure Squeezelite FIFO output, FFmpeg encoder, and Icecast mount.')
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Bridge Control'),
|
||||
E('div', { 'class': 'btn-row' }, [
|
||||
E('button', { 'class': 'sb-btn sb-btn-success', 'click': ui.createHandlerFn(this, 'handleStart') }, 'Start Bridge'),
|
||||
E('button', { 'class': 'sb-btn sb-btn-danger', 'click': ui.createHandlerFn(this, 'handleStop') }, 'Stop Bridge')
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'sb-section' }, [
|
||||
E('h3', {}, 'Stream URL'),
|
||||
E('div', { 'class': 'stream-url' }, [
|
||||
E('a', { 'href': this.data.stream_url || '#', 'target': '_blank', 'id': 'stream-url-link' },
|
||||
this.data.stream_url || 'http://127.0.0.1:8000/lyrion')
|
||||
]),
|
||||
E('audio', { 'controls': true, 'style': 'width:100%; max-width:400px; margin-top:10px;' }, [
|
||||
E('source', { 'src': this.data.stream_url || 'http://127.0.0.1:8000/lyrion', 'type': 'audio/mpeg' })
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderCard: function(title, value, status, id) {
|
||||
var cls = 'sb-card card-' + status;
|
||||
return E('div', { 'class': cls, 'id': id }, [
|
||||
E('div', { 'class': 'card-title' }, title),
|
||||
E('div', { 'class': 'card-value' }, value)
|
||||
]);
|
||||
},
|
||||
|
||||
updateUI: function(data) {
|
||||
var chipBridge = document.getElementById('chip-bridge');
|
||||
var chipLyrion = document.getElementById('chip-lyrion');
|
||||
if (chipBridge) chipBridge.textContent = data.bridge_running ? 'Bridge Running' : 'Bridge Stopped';
|
||||
if (chipLyrion) chipLyrion.textContent = data.lyrion_online ? 'Lyrion Online' : 'Lyrion Offline';
|
||||
|
||||
var np = document.getElementById('now-playing');
|
||||
if (np) {
|
||||
np.innerHTML = '';
|
||||
np.appendChild(E('span', { 'class': 'np-title' }, data.title || 'Nothing playing'));
|
||||
if (data.artist) np.appendChild(E('span', { 'class': 'np-artist' }, data.artist));
|
||||
}
|
||||
|
||||
var listeners = document.getElementById('listeners');
|
||||
if (listeners) listeners.textContent = String(data.listeners || 0);
|
||||
},
|
||||
|
||||
handleSetup: function() {
|
||||
var server = document.getElementById('lyrion-server').value || '127.0.0.1';
|
||||
return callBridgeSetup(server).then(function(res) {
|
||||
ui.addNotification(null, E('p', 'Pipeline setup complete. Stream URL: ' + (res.stream_url || 'http://127.0.0.1:8000/lyrion')));
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', 'Setup failed: ' + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleStart: function() {
|
||||
return callBridgeStart().then(function() {
|
||||
ui.addNotification(null, E('p', 'Lyrion bridge started'));
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', 'Failed: ' + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleStop: function() {
|
||||
return callBridgeStop().then(function() {
|
||||
ui.addNotification(null, E('p', 'Lyrion bridge stopped'));
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', 'Failed: ' + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
getStyles: function() {
|
||||
return [
|
||||
'.sb-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }',
|
||||
'.sb-chips { display:flex; gap:10px; }',
|
||||
'.chip { padding:5px 12px; border-radius:15px; font-size:0.85em; background:#444; color:#fff; }',
|
||||
'.sb-section { background:#1a1a2e; padding:20px; margin-bottom:15px; border-radius:8px; }',
|
||||
'.sb-section h3 { margin:0 0 15px 0; color:#4fc3f7; }',
|
||||
'.pipeline { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:15px; background:#0a0a15; border-radius:5px; }',
|
||||
'.pipe-node { padding:8px 15px; background:#2196f3; color:#fff; border-radius:20px; font-weight:bold; }',
|
||||
'.pipe-arrow { color:#4fc3f7; font-size:1.5em; }',
|
||||
'.sb-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:15px; }',
|
||||
'.sb-card { padding:15px; border-radius:8px; text-align:center; }',
|
||||
'.card-success { background:#155724; border:1px solid #28a745; }',
|
||||
'.card-danger { background:#721c24; border:1px solid #dc3545; }',
|
||||
'.card-warning { background:#856404; border:1px solid #ffc107; }',
|
||||
'.card-title { font-size:0.85em; color:#aaa; margin-bottom:5px; }',
|
||||
'.card-value { font-size:1.1em; font-weight:bold; }',
|
||||
'.now-playing { padding:15px; background:#0a0a15; border-radius:5px; }',
|
||||
'.np-title { display:block; font-size:1.2em; font-weight:bold; }',
|
||||
'.np-artist { display:block; color:#888; margin-top:5px; }',
|
||||
'.btn-row { display:flex; gap:10px; flex-wrap:wrap; }',
|
||||
'.sb-btn { padding:8px 16px; border:none; border-radius:5px; cursor:pointer; background:#444; color:#fff; }',
|
||||
'.sb-btn:hover { background:#555; }',
|
||||
'.sb-btn-primary { background:#007bff; }',
|
||||
'.sb-btn-success { background:#28a745; }',
|
||||
'.sb-btn-danger { background:#dc3545; }',
|
||||
'.form-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }',
|
||||
'.sb-input { padding:8px 12px; border:1px solid #444; border-radius:5px; background:#2a2a3e; color:#fff; }',
|
||||
'.stream-url { padding:15px; background:#0a0a15; border-radius:5px; }',
|
||||
'.stream-url a { color:#4fc3f7; text-decoration:none; font-family:monospace; }'
|
||||
].join('\n');
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -45,7 +45,11 @@ case "$1" in
|
||||
"list_audio_devices": {},
|
||||
"security_status": {},
|
||||
"install_crowdsec": {},
|
||||
"generate_ssl_cert": {"hostname": ""}
|
||||
"generate_ssl_cert": {"hostname": ""},
|
||||
"bridge_status": {},
|
||||
"bridge_start": {},
|
||||
"bridge_stop": {},
|
||||
"bridge_setup": {"lyrion_server": "127.0.0.1"}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
@ -660,6 +664,101 @@ EOF
|
||||
fi
|
||||
;;
|
||||
|
||||
bridge_status)
|
||||
json_init
|
||||
|
||||
# Check Lyrion server
|
||||
local lyrion_server=$(uci -q get lyrion-bridge.main.lyrion_server 2>/dev/null || echo "127.0.0.1")
|
||||
local lyrion_port=$(uci -q get lyrion-bridge.main.lyrion_port 2>/dev/null || echo "9000")
|
||||
local lyrion_online=0
|
||||
if curl -s "http://${lyrion_server}:${lyrion_port}/status.html" >/dev/null 2>&1; then
|
||||
lyrion_online=1
|
||||
fi
|
||||
|
||||
# Check Squeezelite
|
||||
local squeezelite_running=0
|
||||
pgrep -f "squeezelite" >/dev/null 2>&1 && squeezelite_running=1
|
||||
|
||||
# Check FFmpeg bridge
|
||||
local ffmpeg_running=0
|
||||
pgrep -f "ffmpeg-bridge.sh" >/dev/null 2>&1 && ffmpeg_running=1
|
||||
pgrep -f "ffmpeg.*lyrion" >/dev/null 2>&1 && ffmpeg_running=1
|
||||
|
||||
# Check Icecast mount
|
||||
local icecast_host=$(uci -q get lyrion-bridge.icecast.host 2>/dev/null || echo "127.0.0.1")
|
||||
local icecast_port=$(uci -q get lyrion-bridge.icecast.port 2>/dev/null || echo "8000")
|
||||
local icecast_mount=$(uci -q get lyrion-bridge.icecast.mount 2>/dev/null || echo "/lyrion")
|
||||
local mount_active=0
|
||||
local listeners=0
|
||||
local title=""
|
||||
local artist=""
|
||||
|
||||
local mount_status=$(curl -s "http://${icecast_host}:${icecast_port}/status-json.xsl" 2>/dev/null)
|
||||
if [ -n "$mount_status" ]; then
|
||||
local sources=$(echo "$mount_status" | jsonfilter -e '@.icestats.source' 2>/dev/null)
|
||||
if echo "$sources" | grep -q "lyrion"; then
|
||||
mount_active=1
|
||||
listeners=$(echo "$mount_status" | jsonfilter -e '@.icestats.source[@.listenurl="*lyrion*"].listeners' 2>/dev/null || echo "0")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get now playing from Lyrion
|
||||
if [ "$lyrion_online" = "1" ]; then
|
||||
local np=$(curl -s "http://${lyrion_server}:${lyrion_port}/jsonrpc.js" \
|
||||
-d '{"id":1,"method":"slim.request","params":["",[\"status\",\"-\",1,\"tags:adl\"]]}' 2>/dev/null)
|
||||
if [ -n "$np" ]; then
|
||||
title=$(echo "$np" | jsonfilter -e '@.result.playlist_loop[0].title' 2>/dev/null || echo "")
|
||||
artist=$(echo "$np" | jsonfilter -e '@.result.playlist_loop[0].artist' 2>/dev/null || echo "")
|
||||
fi
|
||||
fi
|
||||
|
||||
json_add_boolean lyrion_online "$lyrion_online"
|
||||
json_add_string lyrion_server "$lyrion_server"
|
||||
json_add_boolean squeezelite_running "$squeezelite_running"
|
||||
json_add_boolean ffmpeg_running "$ffmpeg_running"
|
||||
json_add_boolean bridge_running "$ffmpeg_running"
|
||||
json_add_boolean mount_active "$mount_active"
|
||||
json_add_int listeners "$listeners"
|
||||
json_add_string title "$title"
|
||||
json_add_string artist "$artist"
|
||||
json_add_string stream_url "http://${icecast_host}:${icecast_port}${icecast_mount}"
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
bridge_start)
|
||||
if [ -x /usr/sbin/lyrionstreamctl ]; then
|
||||
/usr/sbin/lyrionstreamctl start >/dev/null 2>&1
|
||||
echo '{"result": "ok"}'
|
||||
else
|
||||
echo '{"result": "error", "error": "lyrionstreamctl not found"}'
|
||||
fi
|
||||
;;
|
||||
|
||||
bridge_stop)
|
||||
if [ -x /usr/sbin/lyrionstreamctl ]; then
|
||||
/usr/sbin/lyrionstreamctl stop >/dev/null 2>&1
|
||||
echo '{"result": "ok"}'
|
||||
else
|
||||
echo '{"result": "error", "error": "lyrionstreamctl not found"}'
|
||||
fi
|
||||
;;
|
||||
|
||||
bridge_setup)
|
||||
read -r input
|
||||
local lyrion_server=$(echo "$input" | jsonfilter -e '@.lyrion_server' 2>/dev/null)
|
||||
lyrion_server=${lyrion_server:-127.0.0.1}
|
||||
|
||||
if [ -x /usr/sbin/lyrionstreamctl ]; then
|
||||
output=$(/usr/sbin/lyrionstreamctl setup "$lyrion_server" 2>&1)
|
||||
local icecast_port=$(uci -q get lyrion-bridge.icecast.port 2>/dev/null || echo "8000")
|
||||
local icecast_mount=$(uci -q get lyrion-bridge.icecast.mount 2>/dev/null || echo "/lyrion")
|
||||
echo '{"result": "ok", "stream_url": "http://127.0.0.1:'"$icecast_port$icecast_mount"'"}'
|
||||
else
|
||||
echo '{"result": "error", "error": "lyrionstreamctl not found"}'
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo '{"error": "unknown method"}'
|
||||
;;
|
||||
|
||||
@ -65,5 +65,13 @@
|
||||
"type": "view",
|
||||
"path": "webradio/security"
|
||||
}
|
||||
},
|
||||
"admin/services/webradio/lyrion": {
|
||||
"title": "Lyrion Bridge",
|
||||
"order": 80,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "webradio/lyrion"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,13 +3,13 @@
|
||||
"description": "Grant access to WebRadio configuration",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.webradio": ["status", "listeners", "playlist", "logs", "schedules", "current_show", "list_jingles", "live_status", "list_audio_devices", "security_status"]
|
||||
"luci.webradio": ["status", "listeners", "playlist", "logs", "schedules", "current_show", "list_jingles", "live_status", "list_audio_devices", "security_status", "bridge_status"]
|
||||
},
|
||||
"uci": ["icecast", "ezstream", "webradio", "darkice"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.webradio": ["start", "stop", "restart", "skip", "reload", "generate_playlist", "upload", "add_schedule", "update_schedule", "delete_schedule", "generate_cron", "play_jingle", "live_start", "live_stop", "install_crowdsec", "generate_ssl_cert"]
|
||||
"luci.webradio": ["start", "stop", "restart", "skip", "reload", "generate_playlist", "upload", "add_schedule", "update_schedule", "delete_schedule", "generate_cron", "play_jingle", "live_start", "live_stop", "install_crowdsec", "generate_ssl_cert", "bridge_start", "bridge_stop", "bridge_setup"]
|
||||
},
|
||||
"uci": ["icecast", "ezstream", "webradio", "darkice"]
|
||||
}
|
||||
|
||||
@ -565,6 +565,58 @@ restore() {
|
||||
log "Restore complete"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# TURN Server Integration
|
||||
# ============================================================================
|
||||
|
||||
setup_turn() {
|
||||
local turn_domain="${1:-turn.secubox.in}"
|
||||
|
||||
log "Setting up TURN server for Jitsi..."
|
||||
|
||||
# Check if turnctl is available
|
||||
if ! command -v turnctl >/dev/null 2>&1; then
|
||||
error "turnctl not found. Install secubox-app-turn first."
|
||||
echo " opkg install secubox-app-turn"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Setup TURN server for Jitsi
|
||||
local jitsi_domain=$(uci -q get jitsi.main.domain || echo "meet.secubox.local")
|
||||
turnctl setup-jitsi "$jitsi_domain" "$turn_domain"
|
||||
|
||||
# Get the TURN credentials
|
||||
local turn_secret=$(uci -q get turn.main.static_auth_secret)
|
||||
local external_ip=$(uci -q get turn.main.external_ip)
|
||||
|
||||
if [ -z "$turn_secret" ]; then
|
||||
error "TURN server not configured. Run 'turnctl setup-jitsi' first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Update Jitsi UCI config
|
||||
uci set jitsi.turn=jitsi
|
||||
uci set jitsi.turn.enabled='1'
|
||||
uci set jitsi.turn.server="$turn_domain"
|
||||
uci set jitsi.turn.use_secret='1'
|
||||
uci set jitsi.turn.secret="$turn_secret"
|
||||
uci commit jitsi
|
||||
|
||||
# Update STUN servers
|
||||
uci set jitsi.jvb.stun_servers="$turn_domain:3478"
|
||||
uci commit jitsi
|
||||
|
||||
# Regenerate config
|
||||
generate_config
|
||||
|
||||
log "TURN configured for Jitsi!"
|
||||
echo ""
|
||||
echo "TURN Server: $turn_domain"
|
||||
echo "External IP: $external_ip"
|
||||
echo ""
|
||||
echo "Restart Jitsi to apply: jitsctl restart"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
@ -596,6 +648,7 @@ Commands:
|
||||
|
||||
configure-haproxy Add HAProxy vhost
|
||||
configure-fw Configure firewall rules
|
||||
setup-turn [dom] Configure TURN server for NAT traversal
|
||||
|
||||
Examples:
|
||||
jitsctl install
|
||||
@ -655,6 +708,9 @@ case "$1" in
|
||||
configure-fw)
|
||||
configure_firewall
|
||||
;;
|
||||
setup-turn)
|
||||
setup_turn "$2"
|
||||
;;
|
||||
-h|--help|help)
|
||||
show_help
|
||||
;;
|
||||
|
||||
44
package/secubox/secubox-app-lyrion-bridge/Makefile
Normal file
44
package/secubox/secubox-app-lyrion-bridge/Makefile
Normal file
@ -0,0 +1,44 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-lyrion-bridge
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=Gerald Kerma <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-lyrion-bridge
|
||||
SECTION:=secubox
|
||||
CATEGORY:=SecuBox
|
||||
TITLE:=SecuBox Lyrion to WebRadio Bridge
|
||||
DEPENDS:=+secubox-app-squeezelite +secubox-app-webradio +ffmpeg
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/secubox-app-lyrion-bridge/description
|
||||
Bridge service connecting Lyrion Music Server to WebRadio/Icecast.
|
||||
Streams audio from Squeezelite player through FFmpeg to Icecast.
|
||||
Provides lyrionstreamctl CLI for management.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-lyrion-bridge/conffiles
|
||||
/etc/config/lyrion-bridge
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-app-lyrion-bridge/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lyrion-bridge
|
||||
$(INSTALL_CONF) ./files/etc/config/lyrion-bridge $(1)/etc/config/
|
||||
$(INSTALL_BIN) ./files/etc/init.d/lyrion-bridge $(1)/etc/init.d/
|
||||
$(INSTALL_BIN) ./files/usr/sbin/lyrionstreamctl $(1)/usr/sbin/
|
||||
$(INSTALL_BIN) ./files/usr/lib/lyrion-bridge/ffmpeg-bridge.sh $(1)/usr/lib/lyrion-bridge/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-lyrion-bridge))
|
||||
@ -0,0 +1,27 @@
|
||||
config bridge 'main'
|
||||
option enabled '0'
|
||||
option lyrion_server '127.0.0.1'
|
||||
option lyrion_port '9000'
|
||||
option auto_start '1'
|
||||
|
||||
config audio 'audio'
|
||||
option input_fifo '/tmp/squeezelite.pcm'
|
||||
option sample_rate '44100'
|
||||
option channels '2'
|
||||
option format 's16le'
|
||||
|
||||
config icecast 'icecast'
|
||||
option host '127.0.0.1'
|
||||
option port '8000'
|
||||
option mount '/lyrion'
|
||||
option password ''
|
||||
option bitrate '192'
|
||||
option name 'Lyrion Stream'
|
||||
option description 'Streaming from Lyrion Music Server'
|
||||
option genre 'Various'
|
||||
|
||||
config metadata 'metadata'
|
||||
option sync_enabled '1'
|
||||
option sync_interval '5'
|
||||
option show_artist '1'
|
||||
option show_album '1'
|
||||
@ -0,0 +1,61 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=96
|
||||
STOP=09
|
||||
USE_PROCD=1
|
||||
|
||||
BRIDGE_SCRIPT=/usr/lib/lyrion-bridge/ffmpeg-bridge.sh
|
||||
|
||||
start_service() {
|
||||
local enabled auto_start
|
||||
|
||||
config_load lyrion-bridge
|
||||
|
||||
config_get enabled main enabled '0'
|
||||
[ "$enabled" != "1" ] && return 0
|
||||
|
||||
config_get auto_start main auto_start '1'
|
||||
|
||||
# Ensure Squeezelite is configured for FIFO
|
||||
local fifo_enabled=$(uci -q get squeezelite.streaming.fifo_output)
|
||||
if [ "$fifo_enabled" != "1" ]; then
|
||||
logger -t lyrion-bridge "Enabling Squeezelite FIFO output..."
|
||||
uci set squeezelite.streaming.fifo_output='1'
|
||||
uci commit squeezelite
|
||||
/etc/init.d/squeezelite restart
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Get Icecast password from webradio config if not set
|
||||
local icecast_pass=$(uci -q get lyrion-bridge.icecast.password)
|
||||
if [ -z "$icecast_pass" ]; then
|
||||
icecast_pass=$(uci -q get webradio.main.source_password)
|
||||
[ -n "$icecast_pass" ] && uci set lyrion-bridge.icecast.password="$icecast_pass"
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command $BRIDGE_SCRIPT
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
procd_set_param stdout 1
|
||||
procd_set_param pidfile /var/run/lyrion-bridge.pid
|
||||
procd_close_instance
|
||||
|
||||
logger -t lyrion-bridge "Bridge started"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
# Kill ffmpeg processes related to bridge
|
||||
pkill -f "ffmpeg.*lyrion" 2>/dev/null || true
|
||||
pkill -f "ffmpeg-bridge.sh" 2>/dev/null || true
|
||||
logger -t lyrion-bridge "Bridge stopped"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop_service
|
||||
start_service
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "lyrion-bridge"
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
#!/bin/sh
|
||||
# Lyrion to Icecast Bridge - FFmpeg Audio Pipeline
|
||||
# Reads PCM from Squeezelite FIFO and streams to Icecast
|
||||
|
||||
LOG_FILE="/var/log/lyrion-bridge.log"
|
||||
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
|
||||
logger -t lyrion-bridge "$1"
|
||||
}
|
||||
|
||||
uci_get() { uci -q get "lyrion-bridge.$1" 2>/dev/null || echo "$2"; }
|
||||
|
||||
# Load configuration
|
||||
INPUT_FIFO=$(uci_get audio.input_fifo "/tmp/squeezelite.pcm")
|
||||
SAMPLE_RATE=$(uci_get audio.sample_rate "44100")
|
||||
CHANNELS=$(uci_get audio.channels "2")
|
||||
FORMAT=$(uci_get audio.format "s16le")
|
||||
|
||||
ICECAST_HOST=$(uci_get icecast.host "127.0.0.1")
|
||||
ICECAST_PORT=$(uci_get icecast.port "8000")
|
||||
ICECAST_MOUNT=$(uci_get icecast.mount "/lyrion")
|
||||
ICECAST_PASS=$(uci_get icecast.password "hackme")
|
||||
BITRATE=$(uci_get icecast.bitrate "192")
|
||||
STREAM_NAME=$(uci_get icecast.name "Lyrion Stream")
|
||||
STREAM_DESC=$(uci_get icecast.description "Streaming from Lyrion Music Server")
|
||||
STREAM_GENRE=$(uci_get icecast.genre "Various")
|
||||
|
||||
METADATA_SYNC=$(uci_get metadata.sync_enabled "1")
|
||||
METADATA_INTERVAL=$(uci_get metadata.sync_interval "5")
|
||||
|
||||
log "Starting Lyrion to Icecast bridge..."
|
||||
log "Input: $INPUT_FIFO (PCM $FORMAT, ${SAMPLE_RATE}Hz, ${CHANNELS}ch)"
|
||||
log "Output: icecast://${ICECAST_HOST}:${ICECAST_PORT}${ICECAST_MOUNT} (MP3 ${BITRATE}kbps)"
|
||||
|
||||
# Ensure FIFO exists
|
||||
if [ ! -p "$INPUT_FIFO" ]; then
|
||||
log "Creating FIFO: $INPUT_FIFO"
|
||||
mkfifo "$INPUT_FIFO"
|
||||
fi
|
||||
|
||||
# Wait for Squeezelite to start writing to FIFO
|
||||
log "Waiting for audio input..."
|
||||
while [ ! -s "$INPUT_FIFO" ] && [ -p "$INPUT_FIFO" ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Build Icecast URL
|
||||
ICECAST_URL="icecast://source:${ICECAST_PASS}@${ICECAST_HOST}:${ICECAST_PORT}${ICECAST_MOUNT}"
|
||||
|
||||
# Metadata update function (background process)
|
||||
update_metadata() {
|
||||
local lyrion_host=$(uci_get main.lyrion_server "127.0.0.1")
|
||||
local lyrion_port=$(uci_get main.lyrion_port "9000")
|
||||
|
||||
while true; do
|
||||
# Query Lyrion for current track info
|
||||
local status=$(curl -s "http://${lyrion_host}:${lyrion_port}/jsonrpc.js" \
|
||||
-d '{"id":1,"method":"slim.request","params":["",["status","-",1,"tags:adl"]]}' 2>/dev/null)
|
||||
|
||||
if [ -n "$status" ]; then
|
||||
local title=$(echo "$status" | jsonfilter -e '@.result.playlist_loop[0].title' 2>/dev/null)
|
||||
local artist=$(echo "$status" | jsonfilter -e '@.result.playlist_loop[0].artist' 2>/dev/null)
|
||||
local album=$(echo "$status" | jsonfilter -e '@.result.playlist_loop[0].album' 2>/dev/null)
|
||||
|
||||
if [ -n "$title" ]; then
|
||||
local metadata="${artist:+$artist - }${title}"
|
||||
# Update Icecast metadata via admin API
|
||||
curl -s "http://admin:$(uci -q get webradio.main.admin_password)@${ICECAST_HOST}:${ICECAST_PORT}/admin/metadata?mount=${ICECAST_MOUNT}&mode=updinfo&song=$(echo "$metadata" | sed 's/ /%20/g')" >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep "$METADATA_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
# Start metadata sync in background
|
||||
if [ "$METADATA_SYNC" = "1" ]; then
|
||||
update_metadata &
|
||||
METADATA_PID=$!
|
||||
log "Metadata sync started (PID: $METADATA_PID)"
|
||||
fi
|
||||
|
||||
# Cleanup on exit
|
||||
cleanup() {
|
||||
log "Stopping bridge..."
|
||||
[ -n "$METADATA_PID" ] && kill $METADATA_PID 2>/dev/null
|
||||
[ -n "$FFMPEG_PID" ] && kill $FFMPEG_PID 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap cleanup INT TERM
|
||||
|
||||
# Main streaming loop
|
||||
while true; do
|
||||
log "Starting FFmpeg stream..."
|
||||
|
||||
ffmpeg -re \
|
||||
-f $FORMAT -ar $SAMPLE_RATE -ac $CHANNELS \
|
||||
-i "$INPUT_FIFO" \
|
||||
-acodec libmp3lame -ab ${BITRATE}k -ar $SAMPLE_RATE -ac $CHANNELS \
|
||||
-f mp3 \
|
||||
-content_type audio/mpeg \
|
||||
"$ICECAST_URL" \
|
||||
2>> "$LOG_FILE" &
|
||||
|
||||
FFMPEG_PID=$!
|
||||
log "FFmpeg started (PID: $FFMPEG_PID)"
|
||||
|
||||
# Wait for FFmpeg to exit
|
||||
wait $FFMPEG_PID
|
||||
EXIT_CODE=$?
|
||||
|
||||
log "FFmpeg exited with code $EXIT_CODE"
|
||||
|
||||
# If exited normally (0 or 255), it means stream ended - wait and retry
|
||||
if [ $EXIT_CODE -eq 0 ] || [ $EXIT_CODE -eq 255 ]; then
|
||||
log "Stream ended, waiting for new audio..."
|
||||
sleep 2
|
||||
else
|
||||
# Error - wait longer before retry
|
||||
log "Stream error, retrying in 5 seconds..."
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
@ -0,0 +1,337 @@
|
||||
#!/bin/sh
|
||||
# Lyrion Stream Controller - Bridge Management CLI
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[LyrionStream]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[LyrionStream]${NC} $1"; }
|
||||
error() { echo -e "${RED}[LyrionStream]${NC} $1" >&2; }
|
||||
|
||||
uci_get() { uci -q get "lyrion-bridge.$1" 2>/dev/null || echo "$2"; }
|
||||
|
||||
#--- Status ---
|
||||
cmd_status() {
|
||||
echo -e "${CYAN}=== Lyrion Stream Bridge Status ===${NC}"
|
||||
|
||||
local enabled=$(uci_get main.enabled 0)
|
||||
local lyrion_server=$(uci_get main.lyrion_server "127.0.0.1")
|
||||
local lyrion_port=$(uci_get main.lyrion_port "9000")
|
||||
|
||||
echo "Bridge Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
|
||||
echo ""
|
||||
|
||||
# Check Lyrion
|
||||
echo -e "${CYAN}Lyrion Server:${NC}"
|
||||
if curl -s "http://${lyrion_server}:${lyrion_port}/status.html" >/dev/null 2>&1; then
|
||||
echo -e " Status: ${GREEN}Online${NC} (${lyrion_server}:${lyrion_port})"
|
||||
|
||||
# Get current playing info
|
||||
local status=$(curl -s "http://${lyrion_server}:${lyrion_port}/jsonrpc.js" \
|
||||
-d '{"id":1,"method":"slim.request","params":["",["status","-",1,"tags:adl"]]}' 2>/dev/null)
|
||||
|
||||
if [ -n "$status" ]; then
|
||||
local title=$(echo "$status" | jsonfilter -e '@.result.playlist_loop[0].title' 2>/dev/null)
|
||||
local artist=$(echo "$status" | jsonfilter -e '@.result.playlist_loop[0].artist' 2>/dev/null)
|
||||
local mode=$(echo "$status" | jsonfilter -e '@.result.mode' 2>/dev/null)
|
||||
|
||||
echo " Mode: $mode"
|
||||
[ -n "$title" ] && echo " Now Playing: ${artist:+$artist - }$title"
|
||||
fi
|
||||
else
|
||||
echo -e " Status: ${RED}Offline${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check Squeezelite
|
||||
echo -e "${CYAN}Squeezelite Player:${NC}"
|
||||
if pgrep -f "squeezelite" >/dev/null 2>&1; then
|
||||
echo -e " Status: ${GREEN}Running${NC}"
|
||||
local fifo=$(uci -q get squeezelite.streaming.fifo_path)
|
||||
[ -p "$fifo" ] && echo " FIFO: $fifo (active)" || echo " FIFO: Not configured"
|
||||
else
|
||||
echo -e " Status: ${RED}Stopped${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check FFmpeg Bridge
|
||||
echo -e "${CYAN}FFmpeg Bridge:${NC}"
|
||||
if pgrep -f "ffmpeg-bridge.sh" >/dev/null 2>&1 || pgrep -f "ffmpeg.*lyrion" >/dev/null 2>&1; then
|
||||
echo -e " Status: ${GREEN}Running${NC}"
|
||||
else
|
||||
echo -e " Status: ${YELLOW}Stopped${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check Icecast mount
|
||||
echo -e "${CYAN}Icecast Output:${NC}"
|
||||
local icecast_host=$(uci_get icecast.host "127.0.0.1")
|
||||
local icecast_port=$(uci_get icecast.port "8000")
|
||||
local icecast_mount=$(uci_get icecast.mount "/lyrion")
|
||||
|
||||
local mount_status=$(curl -s "http://${icecast_host}:${icecast_port}/status-json.xsl" 2>/dev/null | \
|
||||
jsonfilter -e "@.icestats.source[@.listenurl='http://${icecast_host}:${icecast_port}${icecast_mount}']" 2>/dev/null)
|
||||
|
||||
if [ -n "$mount_status" ]; then
|
||||
local listeners=$(echo "$mount_status" | jsonfilter -e '@.listeners' 2>/dev/null || echo "0")
|
||||
echo -e " Mount: ${GREEN}Active${NC} (${icecast_mount})"
|
||||
echo " Listeners: $listeners"
|
||||
echo " URL: http://${icecast_host}:${icecast_port}${icecast_mount}"
|
||||
else
|
||||
echo -e " Mount: ${YELLOW}Inactive${NC} (${icecast_mount})"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Setup Full Pipeline ---
|
||||
cmd_setup() {
|
||||
local lyrion_server="${1:-127.0.0.1}"
|
||||
|
||||
log "Setting up Lyrion → WebRadio bridge..."
|
||||
|
||||
# Step 1: Configure Squeezelite
|
||||
log "Step 1: Configuring Squeezelite..."
|
||||
uci set squeezelite.main.enabled='1'
|
||||
uci set squeezelite.main.server="$lyrion_server"
|
||||
uci set squeezelite.streaming.fifo_output='1'
|
||||
uci set squeezelite.streaming.fifo_path='/tmp/squeezelite.pcm'
|
||||
uci commit squeezelite
|
||||
|
||||
# Step 2: Configure bridge
|
||||
log "Step 2: Configuring bridge..."
|
||||
local icecast_pass=$(uci -q get webradio.main.source_password || echo "hackme")
|
||||
uci set lyrion-bridge.main.enabled='1'
|
||||
uci set lyrion-bridge.main.lyrion_server="$lyrion_server"
|
||||
uci set lyrion-bridge.icecast.password="$icecast_pass"
|
||||
uci commit lyrion-bridge
|
||||
|
||||
# Step 3: Create FIFO
|
||||
log "Step 3: Creating audio FIFO..."
|
||||
local fifo_path=$(uci_get audio.input_fifo "/tmp/squeezelite.pcm")
|
||||
[ -p "$fifo_path" ] || mkfifo "$fifo_path"
|
||||
|
||||
# Step 4: Start services
|
||||
log "Step 4: Starting services..."
|
||||
/etc/init.d/squeezelite restart
|
||||
sleep 2
|
||||
/etc/init.d/lyrion-bridge start
|
||||
|
||||
log "Setup complete!"
|
||||
echo ""
|
||||
cmd_status
|
||||
}
|
||||
|
||||
#--- Quick Start ---
|
||||
cmd_start() {
|
||||
log "Starting Lyrion stream bridge..."
|
||||
|
||||
# Ensure Squeezelite is running with FIFO
|
||||
if ! pgrep -f "squeezelite" >/dev/null 2>&1; then
|
||||
log "Starting Squeezelite..."
|
||||
/etc/init.d/squeezelite start
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Start bridge
|
||||
/etc/init.d/lyrion-bridge start
|
||||
|
||||
sleep 2
|
||||
if pgrep -f "ffmpeg-bridge.sh" >/dev/null 2>&1; then
|
||||
log "Bridge started"
|
||||
local icecast_port=$(uci_get icecast.port "8000")
|
||||
local icecast_mount=$(uci_get icecast.mount "/lyrion")
|
||||
echo "Stream URL: http://127.0.0.1:${icecast_port}${icecast_mount}"
|
||||
else
|
||||
error "Failed to start bridge"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Stop ---
|
||||
cmd_stop() {
|
||||
log "Stopping Lyrion stream bridge..."
|
||||
/etc/init.d/lyrion-bridge stop
|
||||
log "Bridge stopped"
|
||||
}
|
||||
|
||||
#--- Restart ---
|
||||
cmd_restart() {
|
||||
cmd_stop
|
||||
sleep 1
|
||||
cmd_start
|
||||
}
|
||||
|
||||
#--- Enable/Disable ---
|
||||
cmd_enable() {
|
||||
uci set lyrion-bridge.main.enabled='1'
|
||||
uci commit lyrion-bridge
|
||||
/etc/init.d/lyrion-bridge enable
|
||||
log "Bridge enabled"
|
||||
}
|
||||
|
||||
cmd_disable() {
|
||||
uci set lyrion-bridge.main.enabled='0'
|
||||
uci commit lyrion-bridge
|
||||
/etc/init.d/lyrion-bridge disable
|
||||
log "Bridge disabled"
|
||||
}
|
||||
|
||||
#--- Configure Icecast Settings ---
|
||||
cmd_config() {
|
||||
case "$1" in
|
||||
mount)
|
||||
if [ -n "$2" ]; then
|
||||
uci set lyrion-bridge.icecast.mount="$2"
|
||||
uci commit lyrion-bridge
|
||||
log "Mount point set to: $2"
|
||||
else
|
||||
echo "Mount: $(uci_get icecast.mount '/lyrion')"
|
||||
fi
|
||||
;;
|
||||
bitrate)
|
||||
if [ -n "$2" ]; then
|
||||
uci set lyrion-bridge.icecast.bitrate="$2"
|
||||
uci commit lyrion-bridge
|
||||
log "Bitrate set to: ${2}kbps"
|
||||
else
|
||||
echo "Bitrate: $(uci_get icecast.bitrate '192')kbps"
|
||||
fi
|
||||
;;
|
||||
name)
|
||||
shift
|
||||
if [ -n "$1" ]; then
|
||||
uci set lyrion-bridge.icecast.name="$*"
|
||||
uci commit lyrion-bridge
|
||||
log "Stream name set to: $*"
|
||||
else
|
||||
echo "Name: $(uci_get icecast.name 'Lyrion Stream')"
|
||||
fi
|
||||
;;
|
||||
server)
|
||||
if [ -n "$2" ]; then
|
||||
uci set lyrion-bridge.main.lyrion_server="$2"
|
||||
uci commit lyrion-bridge
|
||||
log "Lyrion server set to: $2"
|
||||
else
|
||||
echo "Lyrion Server: $(uci_get main.lyrion_server '127.0.0.1')"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: lyrionstreamctl config {mount|bitrate|name|server} [value]"
|
||||
echo ""
|
||||
echo "Current settings:"
|
||||
echo " Lyrion Server: $(uci_get main.lyrion_server '127.0.0.1')"
|
||||
echo " Mount Point: $(uci_get icecast.mount '/lyrion')"
|
||||
echo " Bitrate: $(uci_get icecast.bitrate '192')kbps"
|
||||
echo " Stream Name: $(uci_get icecast.name 'Lyrion Stream')"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#--- View Logs ---
|
||||
cmd_logs() {
|
||||
local lines="${1:-50}"
|
||||
if [ -f /var/log/lyrion-bridge.log ]; then
|
||||
tail -n "$lines" /var/log/lyrion-bridge.log
|
||||
else
|
||||
echo "No logs available"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Expose via HAProxy ---
|
||||
cmd_expose() {
|
||||
local domain="$1"
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
error "Usage: lyrionstreamctl expose <domain>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local icecast_port=$(uci_get icecast.port "8000")
|
||||
local icecast_mount=$(uci_get icecast.mount "/lyrion")
|
||||
|
||||
log "Exposing Lyrion stream on $domain..."
|
||||
|
||||
if command -v haproxyctl >/dev/null 2>&1; then
|
||||
# Create backend for Icecast
|
||||
haproxyctl backend add lyrion_stream 127.0.0.1 "$icecast_port" 2>/dev/null || true
|
||||
# Create vhost
|
||||
haproxyctl vhost add "$domain" lyrion_stream
|
||||
haproxyctl reload
|
||||
|
||||
log "Stream exposed at: https://$domain${icecast_mount}"
|
||||
else
|
||||
error "haproxyctl not available"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Help ---
|
||||
cmd_help() {
|
||||
cat <<EOF
|
||||
${CYAN}Lyrion Stream Controller - Bridge Management CLI${NC}
|
||||
|
||||
Usage: lyrionstreamctl <command> [options]
|
||||
|
||||
${GREEN}Quick Start:${NC}
|
||||
setup [lyrion-ip] Configure and start full pipeline
|
||||
start Start the bridge
|
||||
stop Stop the bridge
|
||||
restart Restart the bridge
|
||||
|
||||
${GREEN}Service Control:${NC}
|
||||
status Show detailed status
|
||||
enable Enable autostart
|
||||
disable Disable autostart
|
||||
|
||||
${GREEN}Configuration:${NC}
|
||||
config Show all settings
|
||||
config mount [path] Set Icecast mount point
|
||||
config bitrate [kb] Set stream bitrate
|
||||
config name [name] Set stream name
|
||||
config server [ip] Set Lyrion server IP
|
||||
|
||||
${GREEN}Operations:${NC}
|
||||
expose <domain> Expose stream via HAProxy/SSL
|
||||
logs [lines] View bridge logs
|
||||
|
||||
${GREEN}Examples:${NC}
|
||||
lyrionstreamctl setup 192.168.1.100
|
||||
lyrionstreamctl config bitrate 256
|
||||
lyrionstreamctl expose radio.secubox.in
|
||||
|
||||
${GREEN}Architecture:${NC}
|
||||
Lyrion Server → Squeezelite (FIFO) → FFmpeg → Icecast
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
#--- Main ---
|
||||
case "$1" in
|
||||
status) cmd_status ;;
|
||||
setup) shift; cmd_setup "$@" ;;
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
enable) cmd_enable ;;
|
||||
disable) cmd_disable ;;
|
||||
config) shift; cmd_config "$@" ;;
|
||||
expose) shift; cmd_expose "$@" ;;
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
help|--help|-h|"")
|
||||
cmd_help
|
||||
;;
|
||||
*)
|
||||
error "Unknown command: $1"
|
||||
cmd_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
42
package/secubox/secubox-app-squeezelite/Makefile
Normal file
42
package/secubox/secubox-app-squeezelite/Makefile
Normal file
@ -0,0 +1,42 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-squeezelite
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=Gerald Kerma <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-squeezelite
|
||||
SECTION:=secubox
|
||||
CATEGORY:=SecuBox
|
||||
TITLE:=SecuBox Squeezelite Player
|
||||
DEPENDS:=+squeezelite +alsa-lib
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/secubox-app-squeezelite/description
|
||||
Virtual Squeezebox player for SecuBox.
|
||||
Connects to Lyrion Music Server and can output to various sinks.
|
||||
Provides squeezelitectl CLI for management.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-squeezelite/conffiles
|
||||
/etc/config/squeezelite
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-app-squeezelite/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_CONF) ./files/etc/config/squeezelite $(1)/etc/config/
|
||||
$(INSTALL_BIN) ./files/etc/init.d/squeezelite $(1)/etc/init.d/
|
||||
$(INSTALL_BIN) ./files/usr/sbin/squeezelitectl $(1)/usr/sbin/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-squeezelite))
|
||||
@ -0,0 +1,21 @@
|
||||
config squeezelite 'main'
|
||||
option enabled '0'
|
||||
option name 'SecuBox-Player'
|
||||
option server ''
|
||||
option server_port '3483'
|
||||
option auto_discover '1'
|
||||
option output 'default'
|
||||
option mac ''
|
||||
option model 'squeezelite'
|
||||
|
||||
config audio 'audio'
|
||||
option sample_rate '44100'
|
||||
option buffer_size '2000'
|
||||
option codec_buffer '2000'
|
||||
option alsa_buffer '80'
|
||||
option alsa_period '4'
|
||||
|
||||
config streaming 'streaming'
|
||||
option fifo_output '0'
|
||||
option fifo_path '/tmp/squeezelite.pcm'
|
||||
option visualizer '0'
|
||||
@ -0,0 +1,80 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=95
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/bin/squeezelite
|
||||
|
||||
start_service() {
|
||||
local enabled name server server_port auto_discover output mac model
|
||||
local sample_rate buffer_size codec_buffer alsa_buffer alsa_period
|
||||
local fifo_output fifo_path
|
||||
|
||||
config_load squeezelite
|
||||
|
||||
config_get enabled main enabled '0'
|
||||
[ "$enabled" != "1" ] && return 0
|
||||
|
||||
config_get name main name 'SecuBox-Player'
|
||||
config_get server main server ''
|
||||
config_get server_port main server_port '3483'
|
||||
config_get auto_discover main auto_discover '1'
|
||||
config_get output main output 'default'
|
||||
config_get mac main mac ''
|
||||
config_get model main model 'squeezelite'
|
||||
|
||||
config_get sample_rate audio sample_rate '44100'
|
||||
config_get buffer_size audio buffer_size '2000'
|
||||
config_get codec_buffer audio codec_buffer '2000'
|
||||
config_get alsa_buffer audio alsa_buffer '80'
|
||||
config_get alsa_period audio alsa_period '4'
|
||||
|
||||
config_get fifo_output streaming fifo_output '0'
|
||||
config_get fifo_path streaming fifo_path '/tmp/squeezelite.pcm'
|
||||
|
||||
# Build command line
|
||||
local cmd_args="-n $name -o $output"
|
||||
|
||||
# Server connection
|
||||
if [ -n "$server" ]; then
|
||||
cmd_args="$cmd_args -s $server:$server_port"
|
||||
fi
|
||||
|
||||
# MAC address (auto-generate if not set)
|
||||
if [ -n "$mac" ]; then
|
||||
cmd_args="$cmd_args -m $mac"
|
||||
fi
|
||||
|
||||
# Model name
|
||||
cmd_args="$cmd_args -M $model"
|
||||
|
||||
# Audio settings
|
||||
cmd_args="$cmd_args -r $sample_rate"
|
||||
cmd_args="$cmd_args -b ${buffer_size}:${codec_buffer}"
|
||||
cmd_args="$cmd_args -a ${alsa_buffer}:${alsa_period}"
|
||||
|
||||
# FIFO output for streaming bridge
|
||||
if [ "$fifo_output" = "1" ]; then
|
||||
# Create FIFO if not exists
|
||||
[ -p "$fifo_path" ] || mkfifo "$fifo_path"
|
||||
cmd_args="$cmd_args -o $fifo_path"
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command $PROG $cmd_args
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/squeezelite.pid
|
||||
procd_close_instance
|
||||
|
||||
logger -t squeezelite "Started: $name"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
logger -t squeezelite "Stopped"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "squeezelite"
|
||||
}
|
||||
@ -0,0 +1,281 @@
|
||||
#!/bin/sh
|
||||
# Squeezelite Controller - SecuBox Virtual Player CLI
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[Squeezelite]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[Squeezelite]${NC} $1"; }
|
||||
error() { echo -e "${RED}[Squeezelite]${NC} $1" >&2; }
|
||||
|
||||
uci_get() { uci -q get "squeezelite.$1" 2>/dev/null || echo "$2"; }
|
||||
|
||||
#--- Status ---
|
||||
cmd_status() {
|
||||
echo -e "${CYAN}=== Squeezelite Player Status ===${NC}"
|
||||
|
||||
local enabled=$(uci_get main.enabled 0)
|
||||
local name=$(uci_get main.name "SecuBox-Player")
|
||||
local server=$(uci_get main.server "")
|
||||
local auto_discover=$(uci_get main.auto_discover 1)
|
||||
|
||||
echo "Player Name: $name"
|
||||
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
|
||||
|
||||
if pgrep -f "squeezelite" >/dev/null 2>&1; then
|
||||
echo -e "Process: ${GREEN}Running${NC}"
|
||||
local pid=$(pgrep -f "squeezelite" | head -1)
|
||||
echo "PID: $pid"
|
||||
else
|
||||
echo -e "Process: ${RED}Stopped${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ -n "$server" ]; then
|
||||
echo "Server: $server"
|
||||
elif [ "$auto_discover" = "1" ]; then
|
||||
echo "Server: Auto-discover (Lyrion on network)"
|
||||
else
|
||||
echo "Server: Not configured"
|
||||
fi
|
||||
|
||||
# Check Lyrion availability
|
||||
local lyrion_host=$(uci_get main.server "127.0.0.1")
|
||||
if curl -s "http://${lyrion_host}:9000/status.html" >/dev/null 2>&1; then
|
||||
echo -e "Lyrion Server: ${GREEN}Available${NC}"
|
||||
else
|
||||
echo -e "Lyrion Server: ${YELLOW}Not detected${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Discover Lyrion ---
|
||||
cmd_discover() {
|
||||
log "Searching for Lyrion Music Server..."
|
||||
|
||||
# Check localhost first
|
||||
if curl -s "http://127.0.0.1:9000/status.html" >/dev/null 2>&1; then
|
||||
log "Found Lyrion at 127.0.0.1:9000"
|
||||
echo "127.0.0.1"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check LXC container
|
||||
local lxc_ip=$(lxc-info -n lyrion 2>/dev/null | grep "IP:" | awk '{print $2}' | head -1)
|
||||
if [ -n "$lxc_ip" ] && curl -s "http://${lxc_ip}:9000/status.html" >/dev/null 2>&1; then
|
||||
log "Found Lyrion in LXC at $lxc_ip:9000"
|
||||
echo "$lxc_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Network scan for Slim Protocol (port 3483)
|
||||
for ip in $(ip route | grep -oE "192\.168\.[0-9]+\.[0-9]+" | head -1 | sed 's/\.[0-9]*$/.1/'); do
|
||||
local subnet=$(echo $ip | sed 's/\.[0-9]*$//')
|
||||
for host in $(seq 1 254); do
|
||||
local target="${subnet}.${host}"
|
||||
if nc -z -w 1 "$target" 3483 2>/dev/null; then
|
||||
log "Found Lyrion at $target"
|
||||
echo "$target"
|
||||
return 0
|
||||
fi
|
||||
done &
|
||||
done
|
||||
wait
|
||||
|
||||
error "No Lyrion server found"
|
||||
return 1
|
||||
}
|
||||
|
||||
#--- Connect to Server ---
|
||||
cmd_connect() {
|
||||
local server="$1"
|
||||
|
||||
if [ -z "$server" ]; then
|
||||
# Auto-discover
|
||||
server=$(cmd_discover 2>/dev/null)
|
||||
if [ -z "$server" ]; then
|
||||
error "No server specified and auto-discover failed"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Connecting to Lyrion at $server..."
|
||||
|
||||
uci set squeezelite.main.server="$server"
|
||||
uci set squeezelite.main.enabled='1'
|
||||
uci commit squeezelite
|
||||
|
||||
/etc/init.d/squeezelite restart
|
||||
|
||||
sleep 2
|
||||
if pgrep -f "squeezelite" >/dev/null 2>&1; then
|
||||
log "Connected to $server"
|
||||
else
|
||||
error "Failed to connect"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Disconnect ---
|
||||
cmd_disconnect() {
|
||||
log "Disconnecting..."
|
||||
/etc/init.d/squeezelite stop
|
||||
uci set squeezelite.main.enabled='0'
|
||||
uci commit squeezelite
|
||||
log "Disconnected"
|
||||
}
|
||||
|
||||
#--- Enable FIFO Output (for streaming) ---
|
||||
cmd_fifo() {
|
||||
case "$1" in
|
||||
enable)
|
||||
local path="${2:-/tmp/squeezelite.pcm}"
|
||||
log "Enabling FIFO output to $path..."
|
||||
uci set squeezelite.streaming.fifo_output='1'
|
||||
uci set squeezelite.streaming.fifo_path="$path"
|
||||
uci commit squeezelite
|
||||
|
||||
# Create FIFO
|
||||
[ -p "$path" ] || mkfifo "$path"
|
||||
|
||||
log "FIFO enabled. Restart player to apply."
|
||||
;;
|
||||
disable)
|
||||
log "Disabling FIFO output..."
|
||||
uci set squeezelite.streaming.fifo_output='0'
|
||||
uci commit squeezelite
|
||||
log "FIFO disabled. Restart player to apply."
|
||||
;;
|
||||
status)
|
||||
local enabled=$(uci_get streaming.fifo_output 0)
|
||||
local path=$(uci_get streaming.fifo_path "/tmp/squeezelite.pcm")
|
||||
echo "FIFO Output: $([ "$enabled" = "1" ] && echo "Enabled" || echo "Disabled")"
|
||||
echo "FIFO Path: $path"
|
||||
[ -p "$path" ] && echo "FIFO exists: Yes" || echo "FIFO exists: No"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: squeezelitectl fifo {enable|disable|status} [path]"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#--- List Audio Devices ---
|
||||
cmd_devices() {
|
||||
echo -e "${CYAN}=== Audio Output Devices ===${NC}"
|
||||
if command -v aplay >/dev/null 2>&1; then
|
||||
aplay -L 2>/dev/null | grep -E "^(default|hw:|plughw:|sysdefault)" | head -20
|
||||
else
|
||||
echo "alsa-utils not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Set Output Device ---
|
||||
cmd_output() {
|
||||
local device="$1"
|
||||
|
||||
if [ -z "$device" ]; then
|
||||
echo "Current output: $(uci_get main.output 'default')"
|
||||
echo ""
|
||||
echo "Available devices:"
|
||||
cmd_devices
|
||||
return
|
||||
fi
|
||||
|
||||
log "Setting output to: $device"
|
||||
uci set squeezelite.main.output="$device"
|
||||
uci commit squeezelite
|
||||
log "Output set. Restart player to apply."
|
||||
}
|
||||
|
||||
#--- Service Control ---
|
||||
cmd_start() {
|
||||
/etc/init.d/squeezelite start
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
/etc/init.d/squeezelite stop
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
/etc/init.d/squeezelite restart
|
||||
}
|
||||
|
||||
cmd_enable() {
|
||||
uci set squeezelite.main.enabled='1'
|
||||
uci commit squeezelite
|
||||
/etc/init.d/squeezelite enable
|
||||
log "Squeezelite enabled"
|
||||
}
|
||||
|
||||
cmd_disable() {
|
||||
uci set squeezelite.main.enabled='0'
|
||||
uci commit squeezelite
|
||||
/etc/init.d/squeezelite disable
|
||||
log "Squeezelite disabled"
|
||||
}
|
||||
|
||||
#--- Help ---
|
||||
cmd_help() {
|
||||
cat <<EOF
|
||||
${CYAN}Squeezelite Controller - SecuBox Virtual Player CLI${NC}
|
||||
|
||||
Usage: squeezelitectl <command> [options]
|
||||
|
||||
${GREEN}Service Commands:${NC}
|
||||
status Show player status
|
||||
start Start player
|
||||
stop Stop player
|
||||
restart Restart player
|
||||
enable Enable autostart
|
||||
disable Disable autostart
|
||||
|
||||
${GREEN}Connection:${NC}
|
||||
discover Find Lyrion servers on network
|
||||
connect [server] Connect to Lyrion (auto-discover if no server)
|
||||
disconnect Disconnect from server
|
||||
|
||||
${GREEN}Audio:${NC}
|
||||
devices List audio output devices
|
||||
output [device] Set/show output device
|
||||
|
||||
${GREEN}Streaming:${NC}
|
||||
fifo enable [path] Enable FIFO output for streaming bridge
|
||||
fifo disable Disable FIFO output
|
||||
fifo status Show FIFO status
|
||||
|
||||
${GREEN}Examples:${NC}
|
||||
squeezelitectl connect 192.168.1.100
|
||||
squeezelitectl fifo enable /tmp/lyrion.pcm
|
||||
squeezelitectl output hw:0,0
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
#--- Main ---
|
||||
case "$1" in
|
||||
status) cmd_status ;;
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
enable) cmd_enable ;;
|
||||
disable) cmd_disable ;;
|
||||
discover) cmd_discover ;;
|
||||
connect) shift; cmd_connect "$@" ;;
|
||||
disconnect) cmd_disconnect ;;
|
||||
devices) cmd_devices ;;
|
||||
output) shift; cmd_output "$@" ;;
|
||||
fifo) shift; cmd_fifo "$@" ;;
|
||||
help|--help|-h|"")
|
||||
cmd_help
|
||||
;;
|
||||
*)
|
||||
error "Unknown command: $1"
|
||||
cmd_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
42
package/secubox/secubox-app-turn/Makefile
Normal file
42
package/secubox/secubox-app-turn/Makefile
Normal file
@ -0,0 +1,42 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-turn
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=Gerald Kerma <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-turn
|
||||
SECTION:=secubox
|
||||
CATEGORY:=SecuBox
|
||||
TITLE:=SecuBox TURN/STUN Server
|
||||
DEPENDS:=+coturn
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/secubox-app-turn/description
|
||||
TURN/STUN server for WebRTC NAT traversal.
|
||||
Required for Jitsi Meet when direct P2P connections fail.
|
||||
Provides turnctl CLI for management.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-turn/conffiles
|
||||
/etc/config/turn
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-app-turn/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_CONF) ./files/etc/config/turn $(1)/etc/config/
|
||||
$(INSTALL_BIN) ./files/etc/init.d/turn $(1)/etc/init.d/
|
||||
$(INSTALL_BIN) ./files/usr/sbin/turnctl $(1)/usr/sbin/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-turn))
|
||||
26
package/secubox/secubox-app-turn/files/etc/config/turn
Normal file
26
package/secubox/secubox-app-turn/files/etc/config/turn
Normal file
@ -0,0 +1,26 @@
|
||||
config server 'main'
|
||||
option enabled '0'
|
||||
option realm 'turn.secubox.in'
|
||||
option listening_port '3478'
|
||||
option tls_port '5349'
|
||||
option min_port '49152'
|
||||
option max_port '65535'
|
||||
option external_ip ''
|
||||
option use_auth_secret '1'
|
||||
option static_auth_secret ''
|
||||
option verbose '0'
|
||||
|
||||
config ssl 'ssl'
|
||||
option cert_path '/etc/ssl/turn/cert.pem'
|
||||
option key_path '/etc/ssl/turn/key.pem'
|
||||
option use_acme '1'
|
||||
|
||||
config limits 'limits'
|
||||
option total_quota '100'
|
||||
option bps_capacity '0'
|
||||
option user_quota '0'
|
||||
option max_bps '0'
|
||||
|
||||
config log 'log'
|
||||
option log_file '/var/log/turnserver.log'
|
||||
option syslog '1'
|
||||
147
package/secubox/secubox-app-turn/files/etc/init.d/turn
Normal file
147
package/secubox/secubox-app-turn/files/etc/init.d/turn
Normal file
@ -0,0 +1,147 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=95
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
TURN_CONF=/var/run/turnserver.conf
|
||||
|
||||
generate_config() {
|
||||
local enabled realm listening_port tls_port min_port max_port
|
||||
local external_ip use_auth_secret static_auth_secret verbose
|
||||
local cert_path key_path
|
||||
local total_quota bps_capacity user_quota max_bps
|
||||
local log_file syslog
|
||||
|
||||
config_load turn
|
||||
|
||||
config_get enabled main enabled '0'
|
||||
[ "$enabled" != "1" ] && return 1
|
||||
|
||||
config_get realm main realm 'turn.secubox.in'
|
||||
config_get listening_port main listening_port '3478'
|
||||
config_get tls_port main tls_port '5349'
|
||||
config_get min_port main min_port '49152'
|
||||
config_get max_port main max_port '65535'
|
||||
config_get external_ip main external_ip ''
|
||||
config_get use_auth_secret main use_auth_secret '1'
|
||||
config_get static_auth_secret main static_auth_secret ''
|
||||
config_get verbose main verbose '0'
|
||||
|
||||
config_get cert_path ssl cert_path '/etc/ssl/turn/cert.pem'
|
||||
config_get key_path ssl key_path '/etc/ssl/turn/key.pem'
|
||||
|
||||
config_get total_quota limits total_quota '100'
|
||||
config_get bps_capacity limits bps_capacity '0'
|
||||
config_get user_quota limits user_quota '0'
|
||||
config_get max_bps limits max_bps '0'
|
||||
|
||||
config_get log_file log log_file '/var/log/turnserver.log'
|
||||
config_get syslog log syslog '1'
|
||||
|
||||
# Auto-detect external IP if not set
|
||||
if [ -z "$external_ip" ]; then
|
||||
external_ip=$(curl -s -4 https://ifconfig.me 2>/dev/null || curl -s -4 https://api.ipify.org 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Generate secret if not set
|
||||
if [ -z "$static_auth_secret" ]; then
|
||||
static_auth_secret=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 32)
|
||||
uci set turn.main.static_auth_secret="$static_auth_secret"
|
||||
uci commit turn
|
||||
logger -t turn "Generated new static auth secret"
|
||||
fi
|
||||
|
||||
cat > "$TURN_CONF" <<EOF
|
||||
# SecuBox TURN Server Configuration
|
||||
# Generated by /etc/init.d/turn
|
||||
|
||||
listening-port=$listening_port
|
||||
tls-listening-port=$tls_port
|
||||
realm=$realm
|
||||
fingerprint
|
||||
lt-cred-mech
|
||||
EOF
|
||||
|
||||
# Auth secret
|
||||
if [ "$use_auth_secret" = "1" ]; then
|
||||
echo "use-auth-secret" >> "$TURN_CONF"
|
||||
echo "static-auth-secret=$static_auth_secret" >> "$TURN_CONF"
|
||||
fi
|
||||
|
||||
# External IP
|
||||
[ -n "$external_ip" ] && echo "external-ip=$external_ip" >> "$TURN_CONF"
|
||||
|
||||
# Port range
|
||||
echo "min-port=$min_port" >> "$TURN_CONF"
|
||||
echo "max-port=$max_port" >> "$TURN_CONF"
|
||||
|
||||
# TLS certificates
|
||||
if [ -f "$cert_path" ] && [ -f "$key_path" ]; then
|
||||
echo "cert=$cert_path" >> "$TURN_CONF"
|
||||
echo "pkey=$key_path" >> "$TURN_CONF"
|
||||
fi
|
||||
|
||||
# Limits
|
||||
[ "$total_quota" != "0" ] && echo "total-quota=$total_quota" >> "$TURN_CONF"
|
||||
[ "$bps_capacity" != "0" ] && echo "bps-capacity=$bps_capacity" >> "$TURN_CONF"
|
||||
[ "$user_quota" != "0" ] && echo "user-quota=$user_quota" >> "$TURN_CONF"
|
||||
[ "$max_bps" != "0" ] && echo "max-bps=$max_bps" >> "$TURN_CONF"
|
||||
|
||||
# Logging
|
||||
[ "$syslog" = "1" ] && echo "syslog" >> "$TURN_CONF"
|
||||
[ -n "$log_file" ] && echo "log-file=$log_file" >> "$TURN_CONF"
|
||||
[ "$verbose" = "1" ] && echo "verbose" >> "$TURN_CONF"
|
||||
|
||||
# Additional hardening
|
||||
cat >> "$TURN_CONF" <<EOF
|
||||
no-multicast-peers
|
||||
no-cli
|
||||
denied-peer-ip=0.0.0.0-0.255.255.255
|
||||
denied-peer-ip=10.0.0.0-10.255.255.255
|
||||
denied-peer-ip=100.64.0.0-100.127.255.255
|
||||
denied-peer-ip=127.0.0.0-127.255.255.255
|
||||
denied-peer-ip=169.254.0.0-169.254.255.255
|
||||
denied-peer-ip=172.16.0.0-172.31.255.255
|
||||
denied-peer-ip=192.0.0.0-192.0.0.255
|
||||
denied-peer-ip=192.0.2.0-192.0.2.255
|
||||
denied-peer-ip=192.88.99.0-192.88.99.255
|
||||
denied-peer-ip=192.168.0.0-192.168.255.255
|
||||
denied-peer-ip=198.18.0.0-198.19.255.255
|
||||
denied-peer-ip=198.51.100.0-198.51.100.255
|
||||
denied-peer-ip=203.0.113.0-203.0.113.255
|
||||
denied-peer-ip=240.0.0.0-255.255.255.255
|
||||
EOF
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
start_service() {
|
||||
generate_config || {
|
||||
logger -t turn "TURN server disabled or config error"
|
||||
return 0
|
||||
}
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command /usr/bin/turnserver -c "$TURN_CONF"
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
procd_set_param stdout 1
|
||||
procd_set_param pidfile /var/run/turnserver.pid
|
||||
procd_close_instance
|
||||
|
||||
logger -t turn "TURN server started"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
logger -t turn "TURN server stopped"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "turn"
|
||||
}
|
||||
391
package/secubox/secubox-app-turn/files/usr/sbin/turnctl
Normal file
391
package/secubox/secubox-app-turn/files/usr/sbin/turnctl
Normal file
@ -0,0 +1,391 @@
|
||||
#!/bin/sh
|
||||
# TURN Server Controller - SecuBox WebRTC NAT Traversal
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[TURN]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[TURN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[TURN]${NC} $1" >&2; }
|
||||
|
||||
uci_get() { uci -q get "turn.$1" 2>/dev/null || echo "$2"; }
|
||||
|
||||
#--- Status ---
|
||||
cmd_status() {
|
||||
echo -e "${CYAN}=== TURN Server Status ===${NC}"
|
||||
|
||||
local enabled=$(uci_get main.enabled 0)
|
||||
local realm=$(uci_get main.realm "turn.secubox.in")
|
||||
local port=$(uci_get main.listening_port "3478")
|
||||
local tls_port=$(uci_get main.tls_port "5349")
|
||||
local external_ip=$(uci_get main.external_ip "")
|
||||
|
||||
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
|
||||
echo "Realm: $realm"
|
||||
echo ""
|
||||
|
||||
if pgrep -f "turnserver" >/dev/null 2>&1; then
|
||||
echo -e "Process: ${GREEN}Running${NC}"
|
||||
local pid=$(pgrep -f "turnserver" | head -1)
|
||||
echo "PID: $pid"
|
||||
else
|
||||
echo -e "Process: ${RED}Stopped${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}Ports:${NC}"
|
||||
echo " TURN/STUN (UDP/TCP): $port"
|
||||
echo " TURN TLS: $tls_port"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}Network:${NC}"
|
||||
if [ -n "$external_ip" ]; then
|
||||
echo " External IP: $external_ip"
|
||||
else
|
||||
local detected=$(curl -s -4 https://ifconfig.me 2>/dev/null || echo "unknown")
|
||||
echo " External IP: $detected (auto-detected)"
|
||||
fi
|
||||
|
||||
# Check if ports are open
|
||||
echo ""
|
||||
echo -e "${CYAN}Port Status:${NC}"
|
||||
if grep -q ":0D92 " /proc/net/udp 2>/dev/null; then
|
||||
echo -e " UDP $port: ${GREEN}Listening${NC}"
|
||||
else
|
||||
echo -e " UDP $port: ${YELLOW}Not listening${NC}"
|
||||
fi
|
||||
|
||||
if grep -q ":14E5 " /proc/net/tcp 2>/dev/null; then
|
||||
echo -e " TCP $tls_port: ${GREEN}Listening${NC}"
|
||||
else
|
||||
echo -e " TCP $tls_port: ${YELLOW}Not listening${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Setup TURN for Jitsi ---
|
||||
cmd_setup_jitsi() {
|
||||
local domain="${1:-jitsi.secubox.in}"
|
||||
local turn_domain="${2:-turn.secubox.in}"
|
||||
|
||||
log "Setting up TURN for Jitsi Meet..."
|
||||
|
||||
# Enable TURN
|
||||
uci set turn.main.enabled='1'
|
||||
uci set turn.main.realm="$turn_domain"
|
||||
|
||||
# Auto-detect external IP
|
||||
local external_ip=$(curl -s -4 https://ifconfig.me 2>/dev/null)
|
||||
if [ -n "$external_ip" ]; then
|
||||
uci set turn.main.external_ip="$external_ip"
|
||||
log "Detected external IP: $external_ip"
|
||||
fi
|
||||
|
||||
uci commit turn
|
||||
|
||||
# Start TURN server
|
||||
/etc/init.d/turn restart
|
||||
sleep 2
|
||||
|
||||
# Get auth secret
|
||||
local auth_secret=$(uci_get main.static_auth_secret "")
|
||||
|
||||
log "TURN server configured!"
|
||||
echo ""
|
||||
echo -e "${CYAN}Jitsi Meet Configuration:${NC}"
|
||||
echo ""
|
||||
echo "Add to your Jitsi config.js:"
|
||||
echo ""
|
||||
echo " p2p: {"
|
||||
echo " stunServers: ["
|
||||
echo " { urls: 'stun:${turn_domain}:3478' }"
|
||||
echo " ]"
|
||||
echo " },"
|
||||
echo ""
|
||||
echo "Add to your Prosody config:"
|
||||
echo ""
|
||||
echo " turncredentials_secret = \"${auth_secret}\";"
|
||||
echo " turncredentials = {"
|
||||
echo " { type = \"stun\", host = \"${turn_domain}\", port = \"3478\" },"
|
||||
echo " { type = \"turn\", host = \"${turn_domain}\", port = \"3478\", transport = \"udp\" },"
|
||||
echo " { type = \"turns\", host = \"${turn_domain}\", port = \"5349\", transport = \"tcp\" }"
|
||||
echo " };"
|
||||
echo ""
|
||||
}
|
||||
|
||||
#--- Generate Credentials ---
|
||||
cmd_credentials() {
|
||||
local username="${1:-$(date +%s)}"
|
||||
local ttl="${2:-86400}"
|
||||
|
||||
local auth_secret=$(uci_get main.static_auth_secret "")
|
||||
if [ -z "$auth_secret" ]; then
|
||||
error "No auth secret configured. Run 'turnctl setup-jitsi' first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local realm=$(uci_get main.realm "turn.secubox.in")
|
||||
local timestamp=$(($(date +%s) + ttl))
|
||||
local temp_username="${timestamp}:${username}"
|
||||
|
||||
# HMAC-SHA1 credential generation
|
||||
local password=$(echo -n "$temp_username" | openssl dgst -sha1 -hmac "$auth_secret" -binary | base64)
|
||||
|
||||
echo -e "${CYAN}=== TURN Credentials ===${NC}"
|
||||
echo ""
|
||||
echo "Realm: $realm"
|
||||
echo "Username: $temp_username"
|
||||
echo "Password: $password"
|
||||
echo "TTL: ${ttl}s (expires: $(date -d @$timestamp 2>/dev/null || date -r $timestamp))"
|
||||
echo ""
|
||||
echo "ICE Server config:"
|
||||
echo "{"
|
||||
echo " \"urls\": [\"turn:${realm}:3478\", \"turn:${realm}:3478?transport=tcp\"],"
|
||||
echo " \"username\": \"${temp_username}\","
|
||||
echo " \"credential\": \"${password}\""
|
||||
echo "}"
|
||||
}
|
||||
|
||||
#--- Test TURN Server ---
|
||||
cmd_test() {
|
||||
local host="${1:-$(uci_get main.realm "turn.secubox.in")}"
|
||||
|
||||
log "Testing TURN server at $host..."
|
||||
|
||||
# Test STUN binding
|
||||
echo ""
|
||||
echo -e "${CYAN}STUN Test:${NC}"
|
||||
if command -v stun-client >/dev/null 2>&1; then
|
||||
stun-client "$host" 3478 2>&1 | head -5
|
||||
elif command -v nc >/dev/null 2>&1; then
|
||||
if nc -u -z -w 2 "$host" 3478 2>/dev/null; then
|
||||
echo -e " UDP 3478: ${GREEN}Reachable${NC}"
|
||||
else
|
||||
echo -e " UDP 3478: ${RED}Unreachable${NC}"
|
||||
fi
|
||||
|
||||
if nc -z -w 2 "$host" 5349 2>/dev/null; then
|
||||
echo -e " TCP 5349: ${GREEN}Reachable${NC}"
|
||||
else
|
||||
echo -e " TCP 5349: ${RED}Unreachable${NC}"
|
||||
fi
|
||||
else
|
||||
warn "No test tools available (stun-client or nc)"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Expose via HAProxy ---
|
||||
cmd_expose() {
|
||||
local domain="${1:-turn.secubox.in}"
|
||||
|
||||
log "Exposing TURN on $domain..."
|
||||
|
||||
# TURN typically needs direct port access, not reverse proxy
|
||||
# But we can expose the REST API or add DNS records
|
||||
|
||||
if command -v dnsctl >/dev/null 2>&1; then
|
||||
local external_ip=$(uci_get main.external_ip "")
|
||||
if [ -z "$external_ip" ]; then
|
||||
external_ip=$(curl -s -4 https://ifconfig.me 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -n "$external_ip" ]; then
|
||||
log "Adding DNS record: $domain -> $external_ip"
|
||||
dnsctl record add "$domain" A "$external_ip"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Open firewall ports
|
||||
log "Configuring firewall..."
|
||||
|
||||
# Check if rules already exist
|
||||
if ! uci -q get firewall.turn_stun >/dev/null 2>&1; then
|
||||
uci add firewall rule
|
||||
uci rename firewall.@rule[-1]='turn_stun'
|
||||
uci set firewall.turn_stun.name='Allow-TURN-STUN'
|
||||
uci set firewall.turn_stun.src='wan'
|
||||
uci set firewall.turn_stun.dest_port='3478'
|
||||
uci set firewall.turn_stun.proto='udp tcp'
|
||||
uci set firewall.turn_stun.target='ACCEPT'
|
||||
|
||||
uci add firewall rule
|
||||
uci rename firewall.@rule[-1]='turn_tls'
|
||||
uci set firewall.turn_tls.name='Allow-TURN-TLS'
|
||||
uci set firewall.turn_tls.src='wan'
|
||||
uci set firewall.turn_tls.dest_port='5349'
|
||||
uci set firewall.turn_tls.proto='tcp'
|
||||
uci set firewall.turn_tls.target='ACCEPT'
|
||||
|
||||
uci add firewall rule
|
||||
uci rename firewall.@rule[-1]='turn_relay'
|
||||
uci set firewall.turn_relay.name='Allow-TURN-Relay'
|
||||
uci set firewall.turn_relay.src='wan'
|
||||
uci set firewall.turn_relay.dest_port='49152-65535'
|
||||
uci set firewall.turn_relay.proto='udp'
|
||||
uci set firewall.turn_relay.target='ACCEPT'
|
||||
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload
|
||||
log "Firewall rules added"
|
||||
else
|
||||
log "Firewall rules already exist"
|
||||
fi
|
||||
|
||||
log "TURN server exposed on $domain"
|
||||
}
|
||||
|
||||
#--- SSL Certificate ---
|
||||
cmd_ssl() {
|
||||
local domain="${1:-$(uci_get main.realm "turn.secubox.in")}"
|
||||
|
||||
log "Setting up SSL for TURN server..."
|
||||
|
||||
local cert_dir="/etc/ssl/turn"
|
||||
mkdir -p "$cert_dir"
|
||||
|
||||
# Try to use ACME cert from HAProxy
|
||||
if [ -f "/etc/ssl/acme/${domain}.crt" ]; then
|
||||
cp "/etc/ssl/acme/${domain}.crt" "$cert_dir/cert.pem"
|
||||
cp "/etc/ssl/acme/${domain}.key" "$cert_dir/key.pem"
|
||||
log "Using ACME certificate for $domain"
|
||||
elif command -v acme.sh >/dev/null 2>&1; then
|
||||
log "Requesting certificate via ACME..."
|
||||
acme.sh --issue -d "$domain" --standalone --httpport 8888 || true
|
||||
if [ -f "$HOME/.acme.sh/${domain}/${domain}.cer" ]; then
|
||||
cp "$HOME/.acme.sh/${domain}/${domain}.cer" "$cert_dir/cert.pem"
|
||||
cp "$HOME/.acme.sh/${domain}/${domain}.key" "$cert_dir/key.pem"
|
||||
log "Certificate obtained"
|
||||
fi
|
||||
else
|
||||
# Generate self-signed
|
||||
log "Generating self-signed certificate..."
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout "$cert_dir/key.pem" \
|
||||
-out "$cert_dir/cert.pem" \
|
||||
-subj "/CN=$domain" 2>/dev/null
|
||||
warn "Using self-signed certificate (clients may need to trust it)"
|
||||
fi
|
||||
|
||||
uci set turn.ssl.cert_path="$cert_dir/cert.pem"
|
||||
uci set turn.ssl.key_path="$cert_dir/key.pem"
|
||||
uci commit turn
|
||||
|
||||
# Restart to pick up new certs
|
||||
/etc/init.d/turn restart
|
||||
|
||||
log "SSL configured"
|
||||
}
|
||||
|
||||
#--- Service Control ---
|
||||
cmd_start() {
|
||||
/etc/init.d/turn start
|
||||
log "TURN server started"
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
/etc/init.d/turn stop
|
||||
log "TURN server stopped"
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
/etc/init.d/turn restart
|
||||
log "TURN server restarted"
|
||||
}
|
||||
|
||||
cmd_enable() {
|
||||
uci set turn.main.enabled='1'
|
||||
uci commit turn
|
||||
/etc/init.d/turn enable
|
||||
log "TURN server enabled"
|
||||
}
|
||||
|
||||
cmd_disable() {
|
||||
uci set turn.main.enabled='0'
|
||||
uci commit turn
|
||||
/etc/init.d/turn disable
|
||||
log "TURN server disabled"
|
||||
}
|
||||
|
||||
#--- Logs ---
|
||||
cmd_logs() {
|
||||
local lines="${1:-50}"
|
||||
local log_file=$(uci_get log.log_file "/var/log/turnserver.log")
|
||||
|
||||
if [ -f "$log_file" ]; then
|
||||
tail -n "$lines" "$log_file"
|
||||
else
|
||||
echo "No log file at $log_file"
|
||||
echo "Checking syslog..."
|
||||
logread | grep -i turn | tail -n "$lines"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Help ---
|
||||
cmd_help() {
|
||||
cat <<EOF
|
||||
${CYAN}TURN Server Controller - SecuBox WebRTC NAT Traversal${NC}
|
||||
|
||||
Usage: turnctl <command> [options]
|
||||
|
||||
${GREEN}Service Commands:${NC}
|
||||
status Show server status
|
||||
start Start TURN server
|
||||
stop Stop TURN server
|
||||
restart Restart TURN server
|
||||
enable Enable autostart
|
||||
disable Disable autostart
|
||||
|
||||
${GREEN}Setup:${NC}
|
||||
setup-jitsi [domain] [turn-domain]
|
||||
Configure TURN for Jitsi Meet
|
||||
ssl [domain] Setup SSL certificate
|
||||
expose [domain] Configure DNS and firewall
|
||||
|
||||
${GREEN}Operations:${NC}
|
||||
credentials [user] [ttl]
|
||||
Generate temp credentials (default: 24h)
|
||||
test [host] Test TURN connectivity
|
||||
logs [lines] View server logs
|
||||
|
||||
${GREEN}Examples:${NC}
|
||||
turnctl setup-jitsi jitsi.secubox.in turn.secubox.in
|
||||
turnctl ssl turn.secubox.in
|
||||
turnctl credentials webrtc-user 3600
|
||||
turnctl expose turn.secubox.in
|
||||
|
||||
${GREEN}Ports Used:${NC}
|
||||
3478/udp,tcp STUN/TURN
|
||||
5349/tcp TURN over TLS
|
||||
49152-65535 Media relay (UDP)
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
#--- Main ---
|
||||
case "$1" in
|
||||
status) cmd_status ;;
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
enable) cmd_enable ;;
|
||||
disable) cmd_disable ;;
|
||||
setup-jitsi) shift; cmd_setup_jitsi "$@" ;;
|
||||
ssl) shift; cmd_ssl "$@" ;;
|
||||
expose) shift; cmd_expose "$@" ;;
|
||||
credentials) shift; cmd_credentials "$@" ;;
|
||||
test) shift; cmd_test "$@" ;;
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
help|--help|-h|"")
|
||||
cmd_help
|
||||
;;
|
||||
*)
|
||||
error "Unknown command: $1"
|
||||
cmd_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
43
package/secubox/secubox-app-webradio/Makefile
Normal file
43
package/secubox/secubox-app-webradio/Makefile
Normal file
@ -0,0 +1,43 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-webradio
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=Gerald Kerma <contact@cybermind.fr>
|
||||
PKG_LICENSE:=MIT
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-webradio
|
||||
SECTION:=secubox
|
||||
CATEGORY:=SecuBox
|
||||
TITLE:=SecuBox WebRadio Backend
|
||||
DEPENDS:=+icecast +ezstream +alsa-utils +secubox-app-haproxy
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/secubox-app-webradio/description
|
||||
Backend service controller for SecuBox WebRadio streaming.
|
||||
Provides webradioctl CLI for managing Icecast/Ezstream/DarkIce.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-webradio/conffiles
|
||||
/etc/config/webradio
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-app-webradio/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_DIR) $(1)/usr/lib/webradio
|
||||
$(INSTALL_CONF) ./files/etc/config/webradio $(1)/etc/config/
|
||||
$(INSTALL_BIN) ./files/etc/init.d/webradio $(1)/etc/init.d/
|
||||
$(INSTALL_BIN) ./files/usr/sbin/webradioctl $(1)/usr/sbin/
|
||||
$(INSTALL_DATA) ./files/usr/lib/webradio/*.sh $(1)/usr/lib/webradio/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-webradio))
|
||||
@ -0,0 +1,44 @@
|
||||
config webradio 'main'
|
||||
option enabled '0'
|
||||
option name 'SecuBox Radio'
|
||||
option description 'Community streaming radio'
|
||||
option genre 'Various'
|
||||
option port '8000'
|
||||
option max_listeners '100'
|
||||
option source_password 'hackme'
|
||||
option admin_password 'admin123'
|
||||
option relay_password 'relay123'
|
||||
|
||||
config stream 'stream'
|
||||
option enabled '1'
|
||||
option name 'main'
|
||||
option mount '/stream'
|
||||
option format 'mp3'
|
||||
option bitrate '128'
|
||||
option samplerate '44100'
|
||||
option channels '2'
|
||||
|
||||
config playlist 'playlist'
|
||||
option enabled '1'
|
||||
option directory '/srv/webradio/music'
|
||||
option shuffle '1'
|
||||
option crossfade '3'
|
||||
option jingle_interval '4'
|
||||
option jingle_directory '/srv/webradio/jingles'
|
||||
|
||||
config live 'live'
|
||||
option enabled '0'
|
||||
option device 'default'
|
||||
option mount '/live'
|
||||
option bitrate '192'
|
||||
|
||||
config exposure 'exposure'
|
||||
option domain ''
|
||||
option ssl '1'
|
||||
option tor '0'
|
||||
option mesh '0'
|
||||
|
||||
config security 'security'
|
||||
option crowdsec '0'
|
||||
option rate_limit '10'
|
||||
option ban_duration '300'
|
||||
@ -0,0 +1,78 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=95
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
CONF_DIR="/srv/webradio/config"
|
||||
MUSIC_DIR="/srv/webradio/music"
|
||||
JINGLE_DIR="/srv/webradio/jingles"
|
||||
LOG_DIR="/var/log/webradio"
|
||||
PLAYLIST_FILE="/tmp/webradio_playlist.m3u"
|
||||
|
||||
start_service() {
|
||||
local enabled
|
||||
config_load webradio
|
||||
config_get enabled main enabled '0'
|
||||
|
||||
[ "$enabled" != "1" ] && return 0
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p "$CONF_DIR" "$MUSIC_DIR" "$JINGLE_DIR" "$LOG_DIR"
|
||||
|
||||
# Generate configurations
|
||||
/usr/sbin/webradioctl genconfig
|
||||
|
||||
# Start Icecast
|
||||
if [ -f "$CONF_DIR/icecast.xml" ]; then
|
||||
procd_open_instance icecast
|
||||
procd_set_param command /usr/bin/icecast -c "$CONF_DIR/icecast.xml"
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/icecast.pid
|
||||
procd_close_instance
|
||||
fi
|
||||
|
||||
# Wait for Icecast to start
|
||||
sleep 2
|
||||
|
||||
# Start Ezstream if playlist enabled
|
||||
local playlist_enabled
|
||||
config_get playlist_enabled playlist enabled '0'
|
||||
if [ "$playlist_enabled" = "1" ] && [ -f "$CONF_DIR/ezstream.xml" ]; then
|
||||
/usr/sbin/webradioctl playlist generate
|
||||
procd_open_instance ezstream
|
||||
procd_set_param command /usr/bin/ezstream -c "$CONF_DIR/ezstream.xml"
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/ezstream.pid
|
||||
procd_close_instance
|
||||
fi
|
||||
|
||||
# Start DarkIce if live enabled
|
||||
local live_enabled
|
||||
config_get live_enabled live enabled '0'
|
||||
if [ "$live_enabled" = "1" ] && [ -f "$CONF_DIR/darkice.cfg" ]; then
|
||||
procd_open_instance darkice
|
||||
procd_set_param command /usr/bin/darkice -c "$CONF_DIR/darkice.cfg"
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/darkice.pid
|
||||
procd_close_instance
|
||||
fi
|
||||
|
||||
logger -t webradio "WebRadio started"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
logger -t webradio "WebRadio stopped"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
/usr/sbin/webradioctl genconfig
|
||||
/usr/sbin/webradioctl playlist generate
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "webradio"
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
#!/bin/sh
|
||||
# Install CrowdSec scenarios for WebRadio/Icecast protection
|
||||
|
||||
PARSER_DIR="/usr/share/crowdsec/parsers/s01-parse"
|
||||
SCENARIO_DIR="/usr/share/crowdsec/scenarios"
|
||||
|
||||
log() { echo "[CrowdSec-WebRadio] $1"; }
|
||||
|
||||
install_parser() {
|
||||
mkdir -p "$PARSER_DIR"
|
||||
|
||||
cat > "$PARSER_DIR/icecast-logs.yaml" <<'EOF'
|
||||
name: secubox/icecast-logs
|
||||
description: "Parse Icecast access logs"
|
||||
filter: "evt.Parsed.program == 'icecast'"
|
||||
onsuccess: next_stage
|
||||
grok:
|
||||
pattern: '%{IP:source_ip} - - \[%{HTTPDATE:timestamp}\] "%{WORD:http_method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}" %{NUMBER:http_code} %{NUMBER:bytes_sent}'
|
||||
apply_on: message
|
||||
statics:
|
||||
- meta: log_type
|
||||
value: icecast_access
|
||||
- meta: service
|
||||
value: webradio
|
||||
EOF
|
||||
log "Installed icecast-logs parser"
|
||||
}
|
||||
|
||||
install_scenarios() {
|
||||
mkdir -p "$SCENARIO_DIR"
|
||||
|
||||
# Connection flood scenario
|
||||
cat > "$SCENARIO_DIR/icecast-flood.yaml" <<'EOF'
|
||||
type: leaky
|
||||
name: secubox/icecast-flood
|
||||
description: "Detect Icecast connection flooding"
|
||||
filter: "evt.Meta.log_type == 'icecast_access'"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 20
|
||||
leakspeed: 10s
|
||||
blackhole: 5m
|
||||
labels:
|
||||
service: webradio
|
||||
type: flood
|
||||
remediation: true
|
||||
EOF
|
||||
|
||||
# Bandwidth abuse scenario
|
||||
cat > "$SCENARIO_DIR/icecast-bandwidth-abuse.yaml" <<'EOF'
|
||||
type: leaky
|
||||
name: secubox/icecast-bandwidth-abuse
|
||||
description: "Detect excessive bandwidth consumption"
|
||||
filter: "evt.Meta.log_type == 'icecast_access' && evt.Parsed.bytes_sent > 10000000"
|
||||
groupby: evt.Meta.source_ip
|
||||
capacity: 5
|
||||
leakspeed: 1m
|
||||
blackhole: 10m
|
||||
labels:
|
||||
service: webradio
|
||||
type: bandwidth_abuse
|
||||
remediation: true
|
||||
EOF
|
||||
|
||||
log "Installed icecast scenarios"
|
||||
}
|
||||
|
||||
reload_crowdsec() {
|
||||
if pgrep -f "crowdsec" >/dev/null 2>&1; then
|
||||
/etc/init.d/crowdsec reload
|
||||
log "CrowdSec reloaded"
|
||||
else
|
||||
log "CrowdSec not running"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
log "Installing CrowdSec protection for WebRadio..."
|
||||
install_parser
|
||||
install_scenarios
|
||||
reload_crowdsec
|
||||
log "Installation complete"
|
||||
@ -0,0 +1,88 @@
|
||||
#!/bin/sh
|
||||
# WebRadio Scheduler - Cron-based show automation
|
||||
# Called by cron to switch between scheduled programs
|
||||
|
||||
CONF_DIR="/srv/webradio/config"
|
||||
LOG_DIR="/var/log/webradio"
|
||||
|
||||
log() {
|
||||
logger -t "webradio-scheduler" "$1"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_DIR/scheduler.log"
|
||||
}
|
||||
|
||||
uci_get() { uci -q get "webradio.$1" 2>/dev/null || echo "$2"; }
|
||||
|
||||
# Get current day/hour
|
||||
DAY=$(date +%u) # 1-7 (Mon-Sun)
|
||||
HOUR=$(date +%H)
|
||||
|
||||
# Find matching schedule
|
||||
find_schedule() {
|
||||
local schedules=$(uci show webradio 2>/dev/null | grep "=schedule$" | cut -d. -f2 | cut -d= -f1)
|
||||
|
||||
for sched in $schedules; do
|
||||
local enabled=$(uci_get "${sched}.enabled" "0")
|
||||
[ "$enabled" != "1" ] && continue
|
||||
|
||||
local days=$(uci_get "${sched}.days" "")
|
||||
local start_hour=$(uci_get "${sched}.start_hour" "0")
|
||||
local end_hour=$(uci_get "${sched}.end_hour" "24")
|
||||
|
||||
# Check if current day matches
|
||||
echo "$days" | grep -q "$DAY" || continue
|
||||
|
||||
# Check if current hour is in range
|
||||
if [ "$HOUR" -ge "$start_hour" ] && [ "$HOUR" -lt "$end_hour" ]; then
|
||||
echo "$sched"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "default"
|
||||
}
|
||||
|
||||
apply_schedule() {
|
||||
local sched="$1"
|
||||
|
||||
if [ "$sched" = "default" ]; then
|
||||
log "No active schedule - using default playlist"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local name=$(uci_get "${sched}.name" "Unknown")
|
||||
local playlist_dir=$(uci_get "${sched}.playlist_dir" "")
|
||||
local jingle=$(uci_get "${sched}.intro_jingle" "")
|
||||
|
||||
log "Activating schedule: $name"
|
||||
|
||||
# Play intro jingle if configured
|
||||
if [ -n "$jingle" ] && [ -f "$jingle" ]; then
|
||||
log "Playing intro jingle: $jingle"
|
||||
# Signal ezstream to queue jingle
|
||||
echo "$jingle" > /tmp/webradio_next_track
|
||||
pkill -USR1 -f "ezstream" 2>/dev/null
|
||||
fi
|
||||
|
||||
# If schedule has specific playlist directory, regenerate
|
||||
if [ -n "$playlist_dir" ] && [ -d "$playlist_dir" ]; then
|
||||
log "Switching to playlist: $playlist_dir"
|
||||
uci set webradio.playlist.directory="$playlist_dir"
|
||||
uci commit webradio
|
||||
/usr/sbin/webradioctl playlist generate
|
||||
/etc/init.d/webradio reload
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
current_schedule=$(find_schedule)
|
||||
log "Current schedule check: $current_schedule"
|
||||
|
||||
# Check if schedule changed
|
||||
LAST_SCHEDULE_FILE="/tmp/webradio_last_schedule"
|
||||
last_schedule=""
|
||||
[ -f "$LAST_SCHEDULE_FILE" ] && last_schedule=$(cat "$LAST_SCHEDULE_FILE")
|
||||
|
||||
if [ "$current_schedule" != "$last_schedule" ]; then
|
||||
apply_schedule "$current_schedule"
|
||||
echo "$current_schedule" > "$LAST_SCHEDULE_FILE"
|
||||
fi
|
||||
675
package/secubox/secubox-app-webradio/files/usr/sbin/webradioctl
Normal file
675
package/secubox/secubox-app-webradio/files/usr/sbin/webradioctl
Normal file
@ -0,0 +1,675 @@
|
||||
#!/bin/sh
|
||||
# WebRadio Controller - SecuBox Backend CLI
|
||||
# Manages Icecast, Ezstream, DarkIce streaming services
|
||||
|
||||
set -e
|
||||
|
||||
CONF_DIR="/srv/webradio/config"
|
||||
MUSIC_DIR="/srv/webradio/music"
|
||||
JINGLE_DIR="/srv/webradio/jingles"
|
||||
PLAYLIST_FILE="/tmp/webradio_playlist.m3u"
|
||||
ICECAST_XML="$CONF_DIR/icecast.xml"
|
||||
EZSTREAM_XML="$CONF_DIR/ezstream.xml"
|
||||
DARKICE_CFG="$CONF_DIR/darkice.cfg"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[WebRadio]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WebRadio]${NC} $1"; }
|
||||
error() { echo -e "${RED}[WebRadio]${NC} $1" >&2; }
|
||||
|
||||
uci_get() { uci -q get "webradio.$1" 2>/dev/null || echo "$2"; }
|
||||
|
||||
#--- Status ---
|
||||
cmd_status() {
|
||||
echo -e "${CYAN}=== WebRadio Status ===${NC}"
|
||||
|
||||
local enabled=$(uci_get main.enabled 0)
|
||||
local name=$(uci_get main.name "SecuBox Radio")
|
||||
local port=$(uci_get main.port 8000)
|
||||
|
||||
echo "Station: $name"
|
||||
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
|
||||
echo ""
|
||||
|
||||
# Icecast status
|
||||
if pgrep -f "icecast" >/dev/null 2>&1; then
|
||||
echo -e "Icecast: ${GREEN}Running${NC} (port $port)"
|
||||
# Get listener count
|
||||
local listeners=$(curl -s "http://127.0.0.1:$port/status-json.xsl" 2>/dev/null | jsonfilter -e '@.icestats.source.listeners' 2>/dev/null || echo "0")
|
||||
echo "Listeners: $listeners"
|
||||
else
|
||||
echo -e "Icecast: ${RED}Stopped${NC}"
|
||||
fi
|
||||
|
||||
# Ezstream status
|
||||
if pgrep -f "ezstream" >/dev/null 2>&1; then
|
||||
echo -e "Ezstream: ${GREEN}Running${NC} (playlist mode)"
|
||||
else
|
||||
echo -e "Ezstream: ${YELLOW}Stopped${NC}"
|
||||
fi
|
||||
|
||||
# DarkIce status
|
||||
if pgrep -f "darkice" >/dev/null 2>&1; then
|
||||
echo -e "DarkIce: ${GREEN}Running${NC} (live mode)"
|
||||
else
|
||||
echo -e "DarkIce: ${YELLOW}Stopped${NC}"
|
||||
fi
|
||||
|
||||
# Exposure status
|
||||
local domain=$(uci_get exposure.domain "")
|
||||
if [ -n "$domain" ]; then
|
||||
echo ""
|
||||
echo "Exposed at: https://$domain/"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Generate Configs ---
|
||||
cmd_genconfig() {
|
||||
log "Generating configuration files..."
|
||||
mkdir -p "$CONF_DIR"
|
||||
|
||||
# Generate Icecast config
|
||||
generate_icecast_config
|
||||
|
||||
# Generate Ezstream config
|
||||
generate_ezstream_config
|
||||
|
||||
# Generate DarkIce config
|
||||
generate_darkice_config
|
||||
|
||||
log "Configuration files generated in $CONF_DIR"
|
||||
}
|
||||
|
||||
generate_icecast_config() {
|
||||
local name=$(uci_get main.name "SecuBox Radio")
|
||||
local port=$(uci_get main.port 8000)
|
||||
local max_listeners=$(uci_get main.max_listeners 100)
|
||||
local source_pass=$(uci_get main.source_password "hackme")
|
||||
local admin_pass=$(uci_get main.admin_password "admin123")
|
||||
local relay_pass=$(uci_get main.relay_password "relay123")
|
||||
|
||||
cat > "$ICECAST_XML" <<EOF
|
||||
<icecast>
|
||||
<location>SecuBox</location>
|
||||
<admin>admin@secubox.local</admin>
|
||||
<limits>
|
||||
<clients>$max_listeners</clients>
|
||||
<sources>5</sources>
|
||||
<queue-size>524288</queue-size>
|
||||
<client-timeout>30</client-timeout>
|
||||
<header-timeout>15</header-timeout>
|
||||
<source-timeout>10</source-timeout>
|
||||
<burst-on-connect>1</burst-on-connect>
|
||||
<burst-size>65535</burst-size>
|
||||
</limits>
|
||||
<authentication>
|
||||
<source-password>$source_pass</source-password>
|
||||
<relay-password>$relay_pass</relay-password>
|
||||
<admin-user>admin</admin-user>
|
||||
<admin-password>$admin_pass</admin-password>
|
||||
</authentication>
|
||||
<hostname>localhost</hostname>
|
||||
<listen-socket>
|
||||
<port>$port</port>
|
||||
<bind-address>0.0.0.0</bind-address>
|
||||
</listen-socket>
|
||||
<mount>
|
||||
<mount-name>/stream</mount-name>
|
||||
<fallback-mount>/silence.mp3</fallback-mount>
|
||||
<fallback-override>1</fallback-override>
|
||||
<stream-name>$name</stream-name>
|
||||
<stream-description>SecuBox Community Radio</stream-description>
|
||||
<genre>Various</genre>
|
||||
<public>0</public>
|
||||
</mount>
|
||||
<mount>
|
||||
<mount-name>/live</mount-name>
|
||||
<stream-name>$name - Live</stream-name>
|
||||
<public>0</public>
|
||||
</mount>
|
||||
<fileserve>1</fileserve>
|
||||
<paths>
|
||||
<basedir>/usr/share/icecast</basedir>
|
||||
<logdir>/var/log/webradio</logdir>
|
||||
<webroot>/usr/share/icecast/web</webroot>
|
||||
<adminroot>/usr/share/icecast/admin</adminroot>
|
||||
<pidfile>/var/run/icecast.pid</pidfile>
|
||||
</paths>
|
||||
<logging>
|
||||
<accesslog>access.log</accesslog>
|
||||
<errorlog>error.log</errorlog>
|
||||
<loglevel>3</loglevel>
|
||||
</logging>
|
||||
<security>
|
||||
<chroot>0</chroot>
|
||||
<changeowner>
|
||||
<user>icecast</user>
|
||||
<group>icecast</group>
|
||||
</changeowner>
|
||||
</security>
|
||||
</icecast>
|
||||
EOF
|
||||
}
|
||||
|
||||
generate_ezstream_config() {
|
||||
local port=$(uci_get main.port 8000)
|
||||
local source_pass=$(uci_get main.source_password "hackme")
|
||||
local format=$(uci_get stream.format "mp3")
|
||||
local bitrate=$(uci_get stream.bitrate 128)
|
||||
local samplerate=$(uci_get stream.samplerate 44100)
|
||||
local channels=$(uci_get stream.channels 2)
|
||||
local shuffle=$(uci_get playlist.shuffle 1)
|
||||
|
||||
cat > "$EZSTREAM_XML" <<EOF
|
||||
<ezstream>
|
||||
<url>http://localhost:$port/stream</url>
|
||||
<sourcepassword>$source_pass</sourcepassword>
|
||||
<format>MP3</format>
|
||||
<filename>$PLAYLIST_FILE</filename>
|
||||
<playlist_program>0</playlist_program>
|
||||
<shuffle>$shuffle</shuffle>
|
||||
<stream_once>0</stream_once>
|
||||
<reconnect_attempts>-1</reconnect_attempts>
|
||||
<svrinfoname>SecuBox Radio</svrinfoname>
|
||||
<svrinfourl>https://secubox.in</svrinfourl>
|
||||
<svrinfogenre>Various</svrinfogenre>
|
||||
<svrinfodescription>SecuBox Community Radio</svrinfodescription>
|
||||
<svrinfobitrate>$bitrate</svrinfobitrate>
|
||||
<svrinfochannels>$channels</svrinfochannels>
|
||||
<svrinfosamplerate>$samplerate</svrinfosamplerate>
|
||||
<svrinfopublic>0</svrinfopublic>
|
||||
</ezstream>
|
||||
EOF
|
||||
}
|
||||
|
||||
generate_darkice_config() {
|
||||
local port=$(uci_get main.port 8000)
|
||||
local source_pass=$(uci_get main.source_password "hackme")
|
||||
local device=$(uci_get live.device "default")
|
||||
local bitrate=$(uci_get live.bitrate 192)
|
||||
local samplerate=$(uci_get stream.samplerate 44100)
|
||||
local channels=$(uci_get stream.channels 2)
|
||||
|
||||
cat > "$DARKICE_CFG" <<EOF
|
||||
[general]
|
||||
duration = 0
|
||||
bufferSecs = 5
|
||||
reconnect = yes
|
||||
|
||||
[input]
|
||||
device = $device
|
||||
sampleRate = $samplerate
|
||||
bitsPerSample = 16
|
||||
channel = $channels
|
||||
|
||||
[icecast2-0]
|
||||
bitrateMode = cbr
|
||||
format = mp3
|
||||
bitrate = $bitrate
|
||||
server = localhost
|
||||
port = $port
|
||||
password = $source_pass
|
||||
mountPoint = live
|
||||
name = SecuBox Radio - Live
|
||||
description = Live broadcast
|
||||
genre = Live
|
||||
public = no
|
||||
EOF
|
||||
}
|
||||
|
||||
#--- Playlist Management ---
|
||||
cmd_playlist() {
|
||||
case "$1" in
|
||||
generate)
|
||||
cmd_playlist_generate
|
||||
;;
|
||||
list)
|
||||
cmd_playlist_list
|
||||
;;
|
||||
add)
|
||||
shift
|
||||
cmd_playlist_add "$@"
|
||||
;;
|
||||
*)
|
||||
cmd_playlist_list
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_playlist_generate() {
|
||||
log "Generating playlist..."
|
||||
mkdir -p "$MUSIC_DIR"
|
||||
|
||||
local shuffle=$(uci_get playlist.shuffle 1)
|
||||
local jingle_interval=$(uci_get playlist.jingle_interval 4)
|
||||
local jingle_dir=$(uci_get playlist.jingle_directory "$JINGLE_DIR")
|
||||
|
||||
# Find all audio files
|
||||
find "$MUSIC_DIR" -type f \( -name "*.mp3" -o -name "*.ogg" -o -name "*.flac" \) > /tmp/music_files.txt
|
||||
|
||||
# Shuffle if enabled (using awk for BusyBox compatibility)
|
||||
if [ "$shuffle" = "1" ]; then
|
||||
awk 'BEGIN{srand()} {print rand()"\t"$0}' /tmp/music_files.txt | sort -n | cut -f2- > /tmp/music_shuffled.txt
|
||||
mv /tmp/music_shuffled.txt /tmp/music_files.txt
|
||||
fi
|
||||
|
||||
# Generate playlist with jingles
|
||||
rm -f "$PLAYLIST_FILE"
|
||||
local count=0
|
||||
while read -r file; do
|
||||
echo "$file" >> "$PLAYLIST_FILE"
|
||||
count=$((count + 1))
|
||||
|
||||
# Insert jingle every N tracks
|
||||
if [ "$jingle_interval" -gt 0 ] && [ $((count % jingle_interval)) -eq 0 ]; then
|
||||
local jingle=$(find "$jingle_dir" -type f -name "*.mp3" 2>/dev/null | sort -R | head -1)
|
||||
[ -n "$jingle" ] && echo "$jingle" >> "$PLAYLIST_FILE"
|
||||
fi
|
||||
done < /tmp/music_files.txt
|
||||
|
||||
rm -f /tmp/music_files.txt
|
||||
|
||||
local total=$(wc -l < "$PLAYLIST_FILE" 2>/dev/null || echo 0)
|
||||
log "Playlist generated: $total tracks"
|
||||
}
|
||||
|
||||
cmd_playlist_list() {
|
||||
echo -e "${CYAN}=== Playlist ===${NC}"
|
||||
if [ -f "$PLAYLIST_FILE" ]; then
|
||||
awk '{print NR": "$0}' "$PLAYLIST_FILE" | head -20
|
||||
local total=$(wc -l < "$PLAYLIST_FILE")
|
||||
echo "..."
|
||||
echo "Total: $total tracks"
|
||||
else
|
||||
warn "No playlist generated. Run: webradioctl playlist generate"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_playlist_add() {
|
||||
local file="$1"
|
||||
if [ -f "$file" ]; then
|
||||
cp "$file" "$MUSIC_DIR/"
|
||||
log "Added: $(basename "$file")"
|
||||
else
|
||||
error "File not found: $file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Stream Mode (FFmpeg) ---
|
||||
cmd_stream() {
|
||||
case "$1" in
|
||||
start)
|
||||
cmd_stream_start
|
||||
;;
|
||||
stop)
|
||||
cmd_stream_stop
|
||||
;;
|
||||
*)
|
||||
cmd_stream_status
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_stream_start() {
|
||||
if pgrep -f "ffmpeg.*icecast" >/dev/null 2>&1; then
|
||||
warn "Stream already running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Ensure playlist exists
|
||||
if [ ! -f "$PLAYLIST_FILE" ] || [ ! -s "$PLAYLIST_FILE" ]; then
|
||||
cmd_playlist_generate
|
||||
fi
|
||||
|
||||
if [ ! -s "$PLAYLIST_FILE" ]; then
|
||||
error "No music files found in $MUSIC_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local port=$(uci_get main.port 8000)
|
||||
local source_pass=$(uci_get main.source_password "hackme")
|
||||
local bitrate=$(uci_get stream.bitrate 128)
|
||||
local name=$(uci_get main.name "SecuBox Radio")
|
||||
|
||||
log "Starting stream to icecast..."
|
||||
|
||||
# Create a loop script for continuous streaming
|
||||
cat > /tmp/webradio_stream.sh << 'STREAMEOF'
|
||||
#!/bin/sh
|
||||
PLAYLIST="$1"
|
||||
PORT="$2"
|
||||
PASS="$3"
|
||||
BITRATE="$4"
|
||||
NAME="$5"
|
||||
|
||||
while true; do
|
||||
while read -r file; do
|
||||
[ -f "$file" ] || continue
|
||||
ffmpeg -re -i "$file" \
|
||||
-vn -acodec libmp3lame -ab ${BITRATE}k -ar 44100 -ac 2 \
|
||||
-content_type audio/mpeg \
|
||||
-f mp3 "icecast://source:${PASS}@127.0.0.1:${PORT}/stream" \
|
||||
2>/var/log/webradio/ffmpeg.log
|
||||
done < "$PLAYLIST"
|
||||
# Re-shuffle for next loop (BusyBox compatible)
|
||||
awk 'BEGIN{srand()} {print rand()"\t"$0}' "$PLAYLIST" | sort -n | cut -f2- > "${PLAYLIST}.tmp" && mv "${PLAYLIST}.tmp" "$PLAYLIST"
|
||||
done
|
||||
STREAMEOF
|
||||
chmod +x /tmp/webradio_stream.sh
|
||||
|
||||
nohup /tmp/webradio_stream.sh "$PLAYLIST_FILE" "$port" "$source_pass" "$bitrate" "$name" \
|
||||
>/dev/null 2>&1 &
|
||||
|
||||
sleep 3
|
||||
if pgrep -f "webradio_stream.sh" >/dev/null 2>&1 || pgrep -f "ffmpeg" >/dev/null 2>&1; then
|
||||
log "Stream started on http://127.0.0.1:$port/stream"
|
||||
else
|
||||
error "Failed to start stream - check /var/log/webradio/ffmpeg.log"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_stream_status() {
|
||||
local port=$(uci_get main.port 8000)
|
||||
if pgrep -f "webradio_stream.sh" >/dev/null 2>&1 || pgrep -f "ffmpeg" >/dev/null 2>&1; then
|
||||
echo -e "FFmpeg Stream: ${GREEN}Running${NC}"
|
||||
echo "URL: http://127.0.0.1:$port/stream"
|
||||
# Get current listener count
|
||||
local listeners=$(curl -s "http://127.0.0.1:$port/status-json.xsl" 2>/dev/null | jsonfilter -e '@.icestats.source.listeners' 2>/dev/null || echo "0")
|
||||
echo "Listeners: $listeners"
|
||||
else
|
||||
echo -e "FFmpeg Stream: ${YELLOW}Stopped${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_stream_stop() {
|
||||
log "Stopping stream..."
|
||||
pkill -f "webradio_stream.sh" 2>/dev/null || true
|
||||
pkill -f "ffmpeg.*icecast" 2>/dev/null || true
|
||||
log "Stream stopped"
|
||||
}
|
||||
|
||||
#--- Live Mode ---
|
||||
cmd_live() {
|
||||
case "$1" in
|
||||
start)
|
||||
cmd_live_start
|
||||
;;
|
||||
stop)
|
||||
cmd_live_stop
|
||||
;;
|
||||
status)
|
||||
cmd_live_status
|
||||
;;
|
||||
devices)
|
||||
cmd_live_devices
|
||||
;;
|
||||
*)
|
||||
cmd_live_status
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_live_start() {
|
||||
if pgrep -f "darkice" >/dev/null 2>&1; then
|
||||
warn "DarkIce already running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$DARKICE_CFG" ]; then
|
||||
cmd_genconfig
|
||||
fi
|
||||
|
||||
log "Starting live broadcast..."
|
||||
darkice -c "$DARKICE_CFG" &
|
||||
sleep 1
|
||||
|
||||
if pgrep -f "darkice" >/dev/null 2>&1; then
|
||||
log "Live broadcast started on /live"
|
||||
else
|
||||
error "Failed to start DarkIce"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_live_stop() {
|
||||
log "Stopping live broadcast..."
|
||||
pkill -f "darkice" 2>/dev/null || true
|
||||
log "Live broadcast stopped"
|
||||
}
|
||||
|
||||
cmd_live_status() {
|
||||
if pgrep -f "darkice" >/dev/null 2>&1; then
|
||||
echo -e "DarkIce: ${GREEN}Running${NC}"
|
||||
local device=$(uci_get live.device "default")
|
||||
echo "Device: $device"
|
||||
else
|
||||
echo -e "DarkIce: ${YELLOW}Stopped${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_live_devices() {
|
||||
echo -e "${CYAN}=== Audio Devices ===${NC}"
|
||||
if command -v arecord >/dev/null 2>&1; then
|
||||
arecord -l 2>/dev/null || echo "No capture devices found"
|
||||
else
|
||||
echo "alsa-utils not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Exposure (Punk Model) ---
|
||||
cmd_expose() {
|
||||
local domain="$1"
|
||||
local channel="${2:-dns}" # dns, tor, mesh, all
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
error "Usage: webradioctl expose <domain> [dns|tor|mesh|all]"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local port=$(uci_get main.port 8000)
|
||||
|
||||
log "Exposing WebRadio on $domain (channel: $channel)..."
|
||||
|
||||
case "$channel" in
|
||||
dns|all)
|
||||
# Create HAProxy vhost
|
||||
if command -v haproxyctl >/dev/null 2>&1; then
|
||||
# Create backend
|
||||
haproxyctl backend add webradio_stream 127.0.0.1 "$port" 2>/dev/null || true
|
||||
# Create vhost
|
||||
haproxyctl vhost add "$domain" webradio_stream
|
||||
haproxyctl reload
|
||||
log "DNS exposure: https://$domain/"
|
||||
else
|
||||
error "haproxyctl not available"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$channel" in
|
||||
tor|all)
|
||||
# Add Tor hidden service
|
||||
if command -v torctl >/dev/null 2>&1; then
|
||||
torctl hidden-service add webradio "$port"
|
||||
local onion=$(torctl hidden-service get webradio 2>/dev/null)
|
||||
[ -n "$onion" ] && log "Tor exposure: http://$onion/"
|
||||
else
|
||||
warn "torctl not available - skipping Tor"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$channel" in
|
||||
mesh|all)
|
||||
# Publish to mesh
|
||||
if command -v vortexctl >/dev/null 2>&1; then
|
||||
vortexctl mesh publish webradio "$domain" "$port"
|
||||
log "Mesh exposure: Published to P2P network"
|
||||
else
|
||||
warn "vortexctl not available - skipping Mesh"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Save exposure config
|
||||
uci set webradio.exposure.domain="$domain"
|
||||
[ "$channel" = "tor" ] || [ "$channel" = "all" ] && uci set webradio.exposure.tor='1'
|
||||
[ "$channel" = "mesh" ] || [ "$channel" = "all" ] && uci set webradio.exposure.mesh='1'
|
||||
uci commit webradio
|
||||
|
||||
log "Exposure complete!"
|
||||
}
|
||||
|
||||
cmd_unexpose() {
|
||||
local domain=$(uci_get exposure.domain "")
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
warn "No exposure configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Removing exposure for $domain..."
|
||||
|
||||
# Remove HAProxy vhost
|
||||
if command -v haproxyctl >/dev/null 2>&1; then
|
||||
haproxyctl vhost del "$domain" 2>/dev/null || true
|
||||
haproxyctl reload
|
||||
fi
|
||||
|
||||
# Remove Tor hidden service
|
||||
if command -v torctl >/dev/null 2>&1; then
|
||||
torctl hidden-service del webradio 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Remove from mesh
|
||||
if command -v vortexctl >/dev/null 2>&1; then
|
||||
vortexctl mesh unpublish webradio 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Clear config
|
||||
uci set webradio.exposure.domain=''
|
||||
uci set webradio.exposure.tor='0'
|
||||
uci set webradio.exposure.mesh='0'
|
||||
uci commit webradio
|
||||
|
||||
log "Exposure removed"
|
||||
}
|
||||
|
||||
#--- Service Control ---
|
||||
cmd_start() {
|
||||
/etc/init.d/webradio start
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
/etc/init.d/webradio stop
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
/etc/init.d/webradio restart
|
||||
}
|
||||
|
||||
cmd_enable() {
|
||||
uci set webradio.main.enabled='1'
|
||||
uci commit webradio
|
||||
/etc/init.d/webradio enable
|
||||
log "WebRadio enabled"
|
||||
}
|
||||
|
||||
cmd_disable() {
|
||||
uci set webradio.main.enabled='0'
|
||||
uci commit webradio
|
||||
/etc/init.d/webradio disable
|
||||
log "WebRadio disabled"
|
||||
}
|
||||
|
||||
#--- Skip Track ---
|
||||
cmd_skip() {
|
||||
if pgrep -f "ezstream" >/dev/null 2>&1; then
|
||||
pkill -USR1 -f "ezstream"
|
||||
log "Skipped to next track"
|
||||
else
|
||||
warn "Ezstream not running"
|
||||
fi
|
||||
}
|
||||
|
||||
#--- Help ---
|
||||
cmd_help() {
|
||||
cat <<EOF
|
||||
${CYAN}WebRadio Controller - SecuBox Backend CLI${NC}
|
||||
|
||||
Usage: webradioctl <command> [options]
|
||||
|
||||
${GREEN}Service Commands:${NC}
|
||||
status Show service status
|
||||
start Start WebRadio services
|
||||
stop Stop WebRadio services
|
||||
restart Restart services
|
||||
enable Enable autostart
|
||||
disable Disable autostart
|
||||
|
||||
${GREEN}Configuration:${NC}
|
||||
genconfig Generate Icecast/Ezstream/DarkIce configs
|
||||
|
||||
${GREEN}Playlist:${NC}
|
||||
playlist List current playlist
|
||||
playlist generate Regenerate playlist from music directory
|
||||
playlist add <file> Add file to music directory
|
||||
|
||||
${GREEN}Streaming:${NC}
|
||||
stream Show stream status
|
||||
stream start Start FFmpeg stream to Icecast
|
||||
stream stop Stop streaming
|
||||
skip Skip to next track
|
||||
|
||||
${GREEN}Live Broadcast:${NC}
|
||||
live Show live status
|
||||
live start Start DarkIce live input
|
||||
live stop Stop live broadcast
|
||||
live devices List audio capture devices
|
||||
|
||||
${GREEN}Exposure (Punk Model):${NC}
|
||||
expose <domain> [channel] Expose radio (dns|tor|mesh|all)
|
||||
unexpose Remove all exposure
|
||||
|
||||
${GREEN}Examples:${NC}
|
||||
webradioctl enable
|
||||
webradioctl playlist generate
|
||||
webradioctl stream start
|
||||
webradioctl expose radio.secubox.in dns
|
||||
webradioctl expose radio.secubox.in all
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
#--- Main ---
|
||||
case "$1" in
|
||||
status) cmd_status ;;
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
enable) cmd_enable ;;
|
||||
disable) cmd_disable ;;
|
||||
genconfig) cmd_genconfig ;;
|
||||
playlist) shift; cmd_playlist "$@" ;;
|
||||
stream) shift; cmd_stream "$@" ;;
|
||||
skip) cmd_skip ;;
|
||||
live) shift; cmd_live "$@" ;;
|
||||
expose) shift; cmd_expose "$@" ;;
|
||||
unexpose) cmd_unexpose ;;
|
||||
help|--help|-h|"")
|
||||
cmd_help
|
||||
;;
|
||||
*)
|
||||
error "Unknown command: $1"
|
||||
cmd_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user