Skip to content

Commit 9cbe957

Browse files
author
Datanoise
committed
feat: improve WebRTC synchronization and add experimental debug console to player
1 parent f962ee6 commit 9cbe957

2 files changed

Lines changed: 73 additions & 4 deletions

File tree

relay/webrtc.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"strings"
89
"sync"
910
"time"
1011

@@ -38,6 +39,12 @@ func (wm *WebRTCManager) HandleOffer(mount string, offer webrtc.SessionDescripti
3839
return nil, fmt.Errorf("stream not found")
3940
}
4041

42+
// WebRTC requires Opus (usually in Ogg container for us)
43+
ct := strings.ToLower(stream.ContentType)
44+
if !strings.Contains(ct, "ogg") && !strings.Contains(ct, "opus") {
45+
return nil, fmt.Errorf("WebRTC requires Opus stream (got %s)", stream.ContentType)
46+
}
47+
4148
peerConnection, err := wm.api.NewPeerConnection(webrtc.Configuration{
4249
ICEServers: []webrtc.ICEServer{
4350
{URLs: []string{"stun:stun.l.google.com:19302"}},
@@ -98,7 +105,8 @@ func (wm *WebRTCManager) streamToTrack(pc *webrtc.PeerConnection, track *webrtc.
98105
// Seek to next "OggS" magic
99106
syncBuf := make([]byte, 16384)
100107
foundSync := false
101-
for !foundSync {
108+
var searched int64 = 0
109+
for !foundSync && searched < 512*1024 { // Safeguard: stop after 512KB
102110
select {
103111
case <-ctx.Done():
104112
return
@@ -107,7 +115,7 @@ func (wm *WebRTCManager) streamToTrack(pc *webrtc.PeerConnection, track *webrtc.
107115
if n == 0 {
108116
continue
109117
}
110-
for i := 0; i < n-4; i++ {
118+
for i := 0; i <= n-4; i++ {
111119
if syncBuf[i] == 'O' && syncBuf[i+1] == 'g' && syncBuf[i+2] == 'g' && syncBuf[i+3] == 'S' {
112120
offset += int64(i)
113121
foundSync = true
@@ -116,10 +124,16 @@ func (wm *WebRTCManager) streamToTrack(pc *webrtc.PeerConnection, track *webrtc.
116124
}
117125
if !foundSync {
118126
offset = next
127+
searched += int64(n)
119128
}
120129
}
121130
}
122131

132+
if !foundSync {
133+
logrus.Errorf("WebRTC: Could not find Ogg sync in first 512KB for %s. Is it an Opus stream?", stream.MountName)
134+
return
135+
}
136+
123137
reader := &StreamReader{
124138
Stream: stream,
125139
Offset: offset,

server/templates/webrtc_player.html

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@
4444
.visualizer { width: 100%; height: 100px; margin-top: 2rem; opacity: 0.5; }
4545

4646
#connection-state { font-size: 0.8rem; color: var(--secondary); margin-top: 1rem; text-transform: uppercase; letter-spacing: 0.1em; }
47+
48+
.debug-panel {
49+
margin-top: 3rem; padding: 1.5rem; background: rgba(0,0,0,0.3); border-radius: 20px;
50+
text-align: left; font-family: monospace; font-size: 0.75rem; color: #10b981;
51+
max-height: 200px; overflow-y: auto; border: 1px solid var(--border);
52+
}
53+
.debug-panel div { margin-bottom: 0.25rem; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 0.25rem; }
54+
.debug-label { color: var(--secondary); font-weight: bold; }
4755
</style>
4856
</head>
4957
<body>
@@ -58,54 +66,101 @@ <h1>{{.Stream.Name}}</h1>
5866

5967
<div id="connection-state">Ready</div>
6068
<canvas id="viz" class="visualizer"></canvas>
69+
70+
<div class="debug-panel" id="debug-log">
71+
<div><span class="debug-label">[SYSTEM]</span> Debug console initialized.</div>
72+
</div>
6173
</div>
6274

6375
<script>
6476
lucide.createIcons();
6577
let pc;
6678
const mount = "{{.Stream.MountName}}";
6779

80+
function log(label, msg) {
81+
const el = document.getElementById('debug-log');
82+
const div = document.createElement('div');
83+
div.innerHTML = `<span class="debug-label">[${label}]</span> ${msg}`;
84+
el.prepend(div);
85+
}
86+
6887
async function startWebRTC() {
6988
const btn = document.getElementById('start-btn');
7089
const stateEl = document.getElementById('connection-state');
7190

7291
btn.disabled = true;
7392
stateEl.textContent = "Connecting...";
93+
log('CORE', 'Starting RTCPeerConnection...');
7494

7595
pc = new RTCPeerConnection({
7696
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
7797
});
7898

99+
pc.onicecandidate = (e) => {
100+
if (e.candidate) log('ICE', `Found candidate: ${e.candidate.protocol} ${e.candidate.address}`);
101+
};
102+
103+
pc.onicegatheringstatechange = () => log('ICE', `Gathering State: ${pc.iceGatheringState}`);
104+
pc.oniceconnectionstatechange = () => log('ICE', `Conn State: ${pc.iceConnectionState}`);
105+
pc.onsignalingstatechange = () => log('SIG', `Signaling State: ${pc.signalingState}`);
106+
79107
pc.ontrack = (event) => {
108+
log('CORE', 'Audio track received!');
80109
const stream = event.streams[0];
81-
const audio = new Audio();
110+
let audio = document.getElementById('webrtc-audio');
111+
if (!audio) {
112+
audio = document.createElement('audio');
113+
audio.id = 'webrtc-audio';
114+
audio.style.display = 'none';
115+
document.body.appendChild(audio);
116+
}
82117
audio.srcObject = stream;
83-
audio.play();
118+
audio.play().then(() => log('CORE', 'Playback started successfully')).catch(e => log('ERROR', `Playback failed: ${e.message}`));
84119
initVisualizer(stream);
85120
};
86121

87122
pc.onconnectionstatechange = () => {
88123
stateEl.textContent = pc.connectionState;
124+
log('CORE', `Connection State: ${pc.connectionState}`);
89125
if (pc.connectionState === 'connected') {
90126
btn.innerHTML = '<i data-lucide="volume-2"></i>';
91127
lucide.createIcons();
92128
}
129+
if (pc.connectionState === 'failed') {
130+
stateEl.textContent = "Connection Failed";
131+
btn.disabled = false;
132+
btn.innerHTML = '<i data-lucide="refresh-cw"></i>';
133+
lucide.createIcons();
134+
}
93135
};
94136

95137
// Add receiver for audio
96138
pc.addTransceiver('audio', { direction: 'recvonly' });
139+
log('SIG', 'Added recvonly audio transceiver');
97140

98141
const offer = await pc.createOffer();
142+
log('SIG', 'SDP Offer created');
99143
await pc.setLocalDescription(offer);
100144

145+
log('HTTP', `Sending offer to server for mount ${mount}...`);
101146
const response = await fetch(`/webrtc/offer?mount=${encodeURIComponent(mount)}`, {
102147
method: 'POST',
103148
body: JSON.stringify(pc.localDescription),
104149
headers: { 'Content-Type': 'application/json' }
105150
});
106151

152+
if (!response.ok) {
153+
const err = await response.text();
154+
stateEl.textContent = "Error: " + err;
155+
log('ERROR', `Signaling failed: ${err}`);
156+
btn.disabled = false;
157+
return;
158+
}
159+
107160
const answer = await response.json();
161+
log('SIG', 'SDP Answer received from server');
108162
await pc.setRemoteDescription(answer);
163+
log('CORE', 'Remote description set, awaiting ICE...');
109164
}
110165

111166
function initVisualizer(stream) {

0 commit comments

Comments
 (0)