@voltkit-rtc/client 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/cjs/broadcast.d.ts +130 -0
- package/dist/cjs/broadcast.d.ts.map +1 -0
- package/dist/cjs/broadcast.js +320 -0
- package/dist/cjs/broadcast.js.map +1 -0
- package/dist/cjs/chat.d.ts +138 -0
- package/dist/cjs/chat.d.ts.map +1 -0
- package/dist/cjs/chat.js +267 -0
- package/dist/cjs/chat.js.map +1 -0
- package/dist/cjs/index.d.ts +285 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +798 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/music.d.ts +76 -0
- package/dist/cjs/music.d.ts.map +1 -0
- package/dist/cjs/music.js +176 -0
- package/dist/cjs/music.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/broadcast.d.ts +130 -0
- package/dist/esm/broadcast.d.ts.map +1 -0
- package/dist/esm/broadcast.js +313 -0
- package/dist/esm/broadcast.js.map +1 -0
- package/dist/esm/chat.d.ts +138 -0
- package/dist/esm/chat.d.ts.map +1 -0
- package/dist/esm/chat.js +262 -0
- package/dist/esm/chat.js.map +1 -0
- package/dist/esm/index.d.ts +285 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +785 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/music.d.ts +76 -0
- package/dist/esm/music.d.ts.map +1 -0
- package/dist/esm/music.js +171 -0
- package/dist/esm/music.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/package.json +60 -0
- package/src/broadcast.ts +352 -0
- package/src/chat.ts +352 -0
- package/src/index.ts +945 -0
- package/src/music.ts +200 -0
package/src/music.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Music from inside the page.
|
|
3
|
+
*
|
|
4
|
+
* A host who plays a song through their speakers and lets the microphone pick
|
|
5
|
+
* it up sends the audience a recording of a room: thin, late, and mangled by
|
|
6
|
+
* the echo canceller, whose whole job is to remove exactly that. The audio
|
|
7
|
+
* here never leaves the browser as sound. It is taken from the element or
|
|
8
|
+
* stream that is playing it and published as audio in its own right.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** An `<audio>` or `<video>` element, or audio the page already holds. */
|
|
12
|
+
export type MusicInput = HTMLMediaElement | MediaStream | MediaStreamTrack;
|
|
13
|
+
|
|
14
|
+
export interface MusicOptions {
|
|
15
|
+
/** 0 to 1. What the audience hears. Default 1. */
|
|
16
|
+
volume?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the host hears it too. Defaults to true for an element -- routing
|
|
19
|
+
* it for publishing takes it off the speakers, so this puts it back -- and
|
|
20
|
+
* false for a stream, which is normally audible somewhere already.
|
|
21
|
+
*/
|
|
22
|
+
monitor?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type TrackSource = 'mic' | 'music' | 'camera' | 'screen';
|
|
26
|
+
|
|
27
|
+
let shared: AudioContext | null = null;
|
|
28
|
+
/** A media element can be wired to one audio graph, once, for its whole life. */
|
|
29
|
+
const elementNodes = new WeakMap<HTMLMediaElement, MediaElementAudioSourceNode>();
|
|
30
|
+
|
|
31
|
+
function audioContext(): AudioContext {
|
|
32
|
+
shared ??= new AudioContext({ latencyHint: 'interactive' });
|
|
33
|
+
// Browsers start a context suspended until the page has had a user gesture.
|
|
34
|
+
// Publishing music happens on a click, so this normally just succeeds.
|
|
35
|
+
if (shared.state === 'suspended') void shared.resume().catch(() => undefined);
|
|
36
|
+
return shared;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const clamp = (v: number) => Math.min(1, Math.max(0, Number.isFinite(v) ? v : 1));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One piece of music, as a track that can be published and a volume that can
|
|
43
|
+
* be turned without touching the microphone.
|
|
44
|
+
*/
|
|
45
|
+
export class MusicSource {
|
|
46
|
+
private readonly ctx = audioContext();
|
|
47
|
+
private readonly input: AudioNode;
|
|
48
|
+
private readonly isElement: boolean;
|
|
49
|
+
/** What is sent. */
|
|
50
|
+
private readonly level: GainNode;
|
|
51
|
+
/** What the host hears. */
|
|
52
|
+
private readonly monitor: GainNode;
|
|
53
|
+
private readonly out: MediaStreamAudioDestinationNode;
|
|
54
|
+
private vol: number;
|
|
55
|
+
private on = true;
|
|
56
|
+
private closed = false;
|
|
57
|
+
|
|
58
|
+
constructor(input: MusicInput, options: MusicOptions = {}) {
|
|
59
|
+
const ctx = this.ctx;
|
|
60
|
+
this.isElement = typeof HTMLMediaElement !== 'undefined' && input instanceof HTMLMediaElement;
|
|
61
|
+
if (this.isElement) {
|
|
62
|
+
const element = input as HTMLMediaElement;
|
|
63
|
+
let node = elementNodes.get(element);
|
|
64
|
+
if (!node) {
|
|
65
|
+
node = ctx.createMediaElementSource(element);
|
|
66
|
+
elementNodes.set(element, node);
|
|
67
|
+
}
|
|
68
|
+
// It may have been left playing straight to the speakers by an earlier
|
|
69
|
+
// MusicSource that was closed.
|
|
70
|
+
try { node.disconnect(); } catch { /* not connected */ }
|
|
71
|
+
this.input = node;
|
|
72
|
+
} else {
|
|
73
|
+
const stream = input instanceof MediaStream ? input : new MediaStream([input as MediaStreamTrack]);
|
|
74
|
+
if (stream.getAudioTracks().length === 0) throw new Error('the music stream has no audio track');
|
|
75
|
+
this.input = ctx.createMediaStreamSource(stream);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.vol = clamp(options.volume ?? 1);
|
|
79
|
+
this.level = ctx.createGain();
|
|
80
|
+
this.level.gain.value = this.vol;
|
|
81
|
+
this.monitor = ctx.createGain();
|
|
82
|
+
this.monitor.gain.value = (options.monitor ?? this.isElement) ? 1 : 0;
|
|
83
|
+
this.out = ctx.createMediaStreamDestination();
|
|
84
|
+
|
|
85
|
+
this.input.connect(this.level);
|
|
86
|
+
this.level.connect(this.out);
|
|
87
|
+
this.input.connect(this.monitor);
|
|
88
|
+
this.monitor.connect(ctx.destination);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The music as a track. Publish this. */
|
|
92
|
+
get track(): MediaStreamTrack {
|
|
93
|
+
return this.out.stream.getAudioTracks()[0]!;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The music after its volume, for mixing into something else. */
|
|
97
|
+
get node(): AudioNode {
|
|
98
|
+
return this.level;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get volume(): number {
|
|
102
|
+
return this.vol;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get enabled(): boolean {
|
|
106
|
+
return this.on;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 0 to 1. Changes what the audience hears, smoothly, without a click. */
|
|
110
|
+
setVolume(volume: number): void {
|
|
111
|
+
this.vol = clamp(volume);
|
|
112
|
+
if (this.on) this.level.gain.setTargetAtTime(this.vol, this.ctx.currentTime, 0.03);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Silence the music for the audience without stopping playback. */
|
|
116
|
+
setEnabled(enabled: boolean): void {
|
|
117
|
+
this.on = enabled;
|
|
118
|
+
this.level.gain.setTargetAtTime(enabled ? this.vol : 0, this.ctx.currentTime, 0.03);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Whether the host hears it locally. */
|
|
122
|
+
setMonitor(enabled: boolean): void {
|
|
123
|
+
this.monitor.gain.setTargetAtTime(enabled ? 1 : 0, this.ctx.currentTime, 0.03);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
close(): void {
|
|
127
|
+
if (this.closed) return;
|
|
128
|
+
this.closed = true;
|
|
129
|
+
try { this.input.disconnect(); } catch { /* already */ }
|
|
130
|
+
this.level.disconnect();
|
|
131
|
+
this.monitor.disconnect();
|
|
132
|
+
this.track.stop();
|
|
133
|
+
// An element stays wired to the audio graph for good. Hand it back to the
|
|
134
|
+
// speakers, or it would play in silence from now on.
|
|
135
|
+
if (this.isElement) this.input.connect(this.ctx.destination);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Microphone and music as ONE track, for a destination that takes one audio
|
|
141
|
+
* track -- a broadcast. Each side keeps its own switch.
|
|
142
|
+
*/
|
|
143
|
+
export class AudioMixer {
|
|
144
|
+
private readonly ctx = audioContext();
|
|
145
|
+
private readonly out = this.ctx.createMediaStreamDestination();
|
|
146
|
+
private readonly micGain = this.ctx.createGain();
|
|
147
|
+
private readonly micNode: MediaStreamAudioSourceNode | null = null;
|
|
148
|
+
private music: MusicSource | null = null;
|
|
149
|
+
|
|
150
|
+
constructor(mic: MediaStreamTrack | null) {
|
|
151
|
+
if (mic) {
|
|
152
|
+
this.micNode = this.ctx.createMediaStreamSource(new MediaStream([mic]));
|
|
153
|
+
this.micNode.connect(this.micGain);
|
|
154
|
+
this.micGain.connect(this.out);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Publish this instead of the microphone. */
|
|
159
|
+
get track(): MediaStreamTrack {
|
|
160
|
+
return this.out.stream.getAudioTracks()[0]!;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get hasMusic(): boolean {
|
|
164
|
+
return this.music !== null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get musicEnabled(): boolean {
|
|
168
|
+
return this.music?.enabled ?? false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setMicEnabled(enabled: boolean): void {
|
|
172
|
+
this.micGain.gain.setTargetAtTime(enabled ? 1 : 0, this.ctx.currentTime, 0.03);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setMusic(input: MusicInput, options?: MusicOptions): void {
|
|
176
|
+
this.clearMusic();
|
|
177
|
+
this.music = new MusicSource(input, options);
|
|
178
|
+
this.music.node.connect(this.out);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setMusicEnabled(enabled: boolean): void {
|
|
182
|
+
this.music?.setEnabled(enabled);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setMusicVolume(volume: number): void {
|
|
186
|
+
this.music?.setVolume(volume);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
clearMusic(): void {
|
|
190
|
+
this.music?.close();
|
|
191
|
+
this.music = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
close(): void {
|
|
195
|
+
this.clearMusic();
|
|
196
|
+
this.micNode?.disconnect();
|
|
197
|
+
this.micGain.disconnect();
|
|
198
|
+
this.track.stop();
|
|
199
|
+
}
|
|
200
|
+
}
|