feat: Implement dynamic video quality selection and mid-stream source switching, improve viewer connection handling by closing old connections, and optimize broadcaster notifications.
This commit is contained in:
@@ -267,6 +267,15 @@
|
||||
|
||||
<label class="label">Audio Source (Microphone/Virtual Sinks)</label>
|
||||
<select id="audioSelect"></select>
|
||||
|
||||
<label class="label">Video Quality</label>
|
||||
<select id="qualitySelect">
|
||||
<option value="1000000|30">Low (1 Mbps / 30fps)</option>
|
||||
<option value="4000000|30">Medium (4 Mbps / 30fps)</option>
|
||||
<option value="8000000|60" selected>High (8 Mbps / 60fps)</option>
|
||||
<option value="15000000|60">Ultra (15 Mbps / 60fps)</option>
|
||||
<option value="30000000|60">Max (30 Mbps / 60fps)</option>
|
||||
</select>
|
||||
|
||||
<div style="margin-top: auto;">
|
||||
<button id="startBtn" disabled style="margin-bottom: 0.5rem;">Start Broadcast</button>
|
||||
|
||||
@@ -9,6 +9,7 @@ const localVideo = document.getElementById('localVideo');
|
||||
const statusText = document.getElementById('statusText');
|
||||
const statsPanel = document.getElementById('statsPanel');
|
||||
const viewerCountDiv = document.getElementById('viewerCount');
|
||||
const qualitySelect = document.getElementById('qualitySelect');
|
||||
|
||||
function updateViewerCount() {
|
||||
if (viewerCountDiv) {
|
||||
@@ -31,9 +32,61 @@ const config = {
|
||||
]
|
||||
};
|
||||
|
||||
// 1. Get Desktop Sources from Main Process and populate raw select tags
|
||||
// Also enumerate native audio devices from navigator!
|
||||
// 1. Get Desktop Sources / Switch Video Source Mid-Stream
|
||||
getSourcesBtn.addEventListener('click', async () => {
|
||||
// --- Mid-Stream Video Source Switching ---
|
||||
if (activeStream) {
|
||||
try {
|
||||
// On Wayland, the compositor limits concurrent ScreenCast sessions.
|
||||
// We MUST stop the old session BEFORE requesting a new one.
|
||||
// Stop ALL video tracks to ensure the old PipeWire session is fully released.
|
||||
activeStream.getVideoTracks().forEach(t => {
|
||||
t.onended = null;
|
||||
t.stop();
|
||||
});
|
||||
|
||||
// Give the compositor time to tear down the old ScreenCast session
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Now request a new source — this opens the Wayland portal
|
||||
const newStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: { mandatory: { chromeMediaSource: 'desktop' } }
|
||||
});
|
||||
|
||||
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||
if (!newVideoTrack) return;
|
||||
|
||||
await newVideoTrack.applyConstraints({ frameRate: { ideal: 60 } });
|
||||
|
||||
// Swap the track in the active stream
|
||||
if (activeStream.getVideoTracks()[0]) {
|
||||
activeStream.removeTrack(activeStream.getVideoTracks()[0]);
|
||||
}
|
||||
activeStream.addTrack(newVideoTrack);
|
||||
|
||||
// Hot-swap on all peer connections without renegotiation
|
||||
Object.values(peerConnections).forEach(pc => {
|
||||
const sender = pc.getSenders().find(s => s.track && s.track.kind === 'video');
|
||||
if (sender) {
|
||||
sender.replaceTrack(newVideoTrack).catch(e => console.error("replaceTrack error:", e));
|
||||
}
|
||||
});
|
||||
|
||||
// Update local preview
|
||||
localVideo.srcObject = activeStream;
|
||||
|
||||
// Re-attach onended to auto-stop if the window closes
|
||||
newVideoTrack.onended = stopSharing;
|
||||
} catch (e) {
|
||||
console.error("Failed to switch video source:", e);
|
||||
// If switching failed, stop broadcast since we already killed the old track
|
||||
stopSharing();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Normal Source Selection (when not broadcasting) ---
|
||||
sourcesGrid.innerHTML = '<div style="color:var(--text-secondary); width:100%;">Loading sources...</div>';
|
||||
startBtn.disabled = true;
|
||||
selectedVideoSourceId = null;
|
||||
@@ -229,7 +282,8 @@ startBtn.addEventListener('click', async () => {
|
||||
}
|
||||
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
if (videoTrack) await videoTrack.applyConstraints({ frameRate: { ideal: 60 } });
|
||||
const [, targetFps] = (qualitySelect.value || '8000000|60').split('|');
|
||||
if (videoTrack) await videoTrack.applyConstraints({ frameRate: { ideal: parseInt(targetFps) } });
|
||||
|
||||
// Add audio if requested (virtual mic capture does NOT trigger a Wayland portal)
|
||||
if (targetAppName && targetAppName !== 'none') {
|
||||
@@ -281,10 +335,11 @@ function connectAndBroadcast(url, password) {
|
||||
|
||||
activeStream.getTracks().forEach(track => {
|
||||
const sender = peerConnection.addTrack(track, activeStream);
|
||||
const [targetBitrate] = (qualitySelect.value || '8000000|60').split('|');
|
||||
if (track.kind === 'video') {
|
||||
const params = sender.getParameters();
|
||||
if (!params.encodings) params.encodings = [{}];
|
||||
params.encodings[0].maxBitrate = 10000000;
|
||||
params.encodings[0].maxBitrate = parseInt(targetBitrate);
|
||||
sender.setParameters(params).catch(e => console.error(e));
|
||||
} else if (track.kind === 'audio') {
|
||||
const params = sender.getParameters();
|
||||
@@ -437,6 +492,31 @@ audioSelect.addEventListener('change', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Dynamic Quality Switching ---
|
||||
qualitySelect.addEventListener('change', async () => {
|
||||
if (!activeStream) return;
|
||||
|
||||
const [targetBitrate, targetFps] = qualitySelect.value.split('|');
|
||||
|
||||
// Update frame rate on the video track
|
||||
const videoTrack = activeStream.getVideoTracks()[0];
|
||||
if (videoTrack) {
|
||||
await videoTrack.applyConstraints({ frameRate: { ideal: parseInt(targetFps) } }).catch(e => console.error(e));
|
||||
}
|
||||
|
||||
// Update bitrate on all existing peer connections
|
||||
Object.values(peerConnections).forEach(pc => {
|
||||
pc.getSenders().forEach(sender => {
|
||||
if (sender.track && sender.track.kind === 'video') {
|
||||
const params = sender.getParameters();
|
||||
if (!params.encodings) params.encodings = [{}];
|
||||
params.encodings[0].maxBitrate = parseInt(targetBitrate);
|
||||
sender.setParameters(params).catch(e => console.error(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Stats Monitoring Loop ---
|
||||
let lastBytesSent = 0;
|
||||
let lastTimestamp = 0;
|
||||
|
||||
Reference in New Issue
Block a user