113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
// Manages named media sources on an RTCPeerConnection.
|
|
//
|
|
// Each "source" has a label (e.g. 'camera', 'screen', 'mic') and maps to
|
|
// one MediaStream plus the RTCRtpSenders the peer connection created for it.
|
|
//
|
|
// Usage:
|
|
// const sm = new StreamManager(peerConnection);
|
|
// sm.addStream('camera', await navigator.mediaDevices.getUserMedia({ video: true }));
|
|
// await sm.replaceTrack('camera', newVideoTrack); // hot-swap, no renegotiation
|
|
// await sm.setEncodings('camera', 'video', { maxBitrate: 2_000_000 });
|
|
// sm.removeStream('camera'); // stops tracks + renegotiates
|
|
export default class StreamManager {
|
|
constructor(pc) {
|
|
this._pc = pc; // PeerConnection instance
|
|
this._sources = new Map(); // label → { stream, senders }
|
|
}
|
|
|
|
// Add all tracks from a MediaStream. Triggers renegotiation.
|
|
addStream(label, stream) {
|
|
if (this._sources.has(label)) {
|
|
throw new Error(`StreamManager: "${label}" already exists — call removeStream first`);
|
|
}
|
|
const senders = [];
|
|
for (const track of stream.getTracks()) {
|
|
const sender = this._pc.addTrack(track, stream);
|
|
senders.push(sender);
|
|
}
|
|
this._sources.set(label, { stream, senders });
|
|
return senders;
|
|
}
|
|
|
|
// Swap one track inside an existing source by kind ('video' | 'audio').
|
|
// Uses RTCRtpSender.replaceTrack — no renegotiation needed.
|
|
async replaceTrack(label, newTrack) {
|
|
const src = this._sources.get(label);
|
|
if (!src) throw new Error(`StreamManager: source "${label}" not found`);
|
|
for (const sender of src.senders) {
|
|
if (sender.track?.kind === newTrack.kind) {
|
|
await sender.replaceTrack(newTrack);
|
|
return sender;
|
|
}
|
|
}
|
|
throw new Error(`StreamManager: no ${newTrack.kind} sender in "${label}"`);
|
|
}
|
|
|
|
// Remove a source: stops all its tracks and calls removeTrack (renegotiates).
|
|
removeStream(label) {
|
|
const src = this._sources.get(label);
|
|
if (!src) return;
|
|
for (const sender of src.senders) {
|
|
try { this._pc.removeTrack(sender); } catch (_) {}
|
|
}
|
|
for (const track of src.stream.getTracks()) {
|
|
track.stop();
|
|
}
|
|
this._sources.delete(label);
|
|
}
|
|
|
|
// Adjust encoding parameters (bitrate, framerate, resolution scale) for a
|
|
// specific source+kind. RTCRtpSender.setParameters is used — no renegotiation.
|
|
//
|
|
// params: { maxBitrate, maxFramerate, scaleResolutionDownBy, ... }
|
|
async setEncodings(label, kind, params) {
|
|
const src = this._sources.get(label);
|
|
if (!src) return;
|
|
for (const sender of src.senders) {
|
|
if (sender.track?.kind !== kind) continue;
|
|
const p = sender.getParameters();
|
|
if (!p.encodings?.length) p.encodings = [{}];
|
|
p.encodings = p.encodings.map(enc => ({ ...enc, ...params }));
|
|
await sender.setParameters(p);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Mute/unmute a track by label+kind (local-only, no signaling).
|
|
setEnabled(label, kind, enabled) {
|
|
const src = this._sources.get(label);
|
|
if (!src) return;
|
|
for (const track of src.stream.getTracks()) {
|
|
if (track.kind === kind) track.enabled = enabled;
|
|
}
|
|
}
|
|
|
|
// Return a snapshot of all managed sources.
|
|
list() {
|
|
const out = [];
|
|
for (const [label, { stream, senders }] of this._sources) {
|
|
out.push({
|
|
label,
|
|
tracks: stream.getTracks().map(t => ({
|
|
id: t.id,
|
|
kind: t.kind,
|
|
enabled: t.enabled,
|
|
label: t.label
|
|
})),
|
|
senders: senders.length
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
has(label) { return this._sources.has(label); }
|
|
|
|
// Get the MediaStream for a label.
|
|
stream(label) { return this._sources.get(label)?.stream ?? null; }
|
|
|
|
// Stop all sources and clear the map.
|
|
destroy() {
|
|
for (const [label] of this._sources) this.removeStream(label);
|
|
}
|
|
}
|