@voltkit-rtc/client 0.9.1 → 0.9.3
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 +32 -0
- package/README.md +13 -6
- package/dist/cjs/adaptive.d.ts +9 -5
- package/dist/cjs/adaptive.d.ts.map +1 -1
- package/dist/cjs/adaptive.js +10 -6
- package/dist/cjs/adaptive.js.map +1 -1
- package/dist/cjs/diagnostics.d.ts +1 -1
- package/dist/cjs/diagnostics.js +1 -1
- package/dist/cjs/index.d.ts +54 -11
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +200 -30
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/adaptive.d.ts +9 -5
- package/dist/esm/adaptive.d.ts.map +1 -1
- package/dist/esm/adaptive.js +10 -6
- package/dist/esm/adaptive.js.map +1 -1
- package/dist/esm/diagnostics.d.ts +1 -1
- package/dist/esm/diagnostics.js +1 -1
- package/dist/esm/index.d.ts +54 -11
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +200 -30
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adaptive.ts +10 -6
- package/src/diagnostics.ts +1 -1
- package/src/index.ts +193 -30
package/dist/cjs/index.js
CHANGED
|
@@ -285,6 +285,14 @@ class StreamClient {
|
|
|
285
285
|
screenProducerIds = new Set();
|
|
286
286
|
remoteVideoLayers = new Map();
|
|
287
287
|
receiveQualities = new Map();
|
|
288
|
+
/**
|
|
289
|
+
* BR-09 (the Flutter SDK's V092-02): per video, the choice the server last accepted, and a revision every change
|
|
290
|
+
* bumps. A choice is stored before its request (what is asked for is read from it), so a request that fails goes
|
|
291
|
+
* back to the last accepted choice -- not merely the one before it, which may itself be waiting or refused -- and
|
|
292
|
+
* only the latest change may go back, so a late failure never undoes a newer choice. Replies come in request order.
|
|
293
|
+
*/
|
|
294
|
+
receiveAccepted = new Map();
|
|
295
|
+
receiveRevision = new Map();
|
|
288
296
|
cameraFrameRate = 30;
|
|
289
297
|
frameRateFallback = false;
|
|
290
298
|
frameRateLowSamples = 0;
|
|
@@ -658,10 +666,9 @@ class StreamClient {
|
|
|
658
666
|
// A video that ended while this client was away sends it no
|
|
659
667
|
// producerClosed: forget the layers asked for it.
|
|
660
668
|
const present = new Set(result.producers.map((p) => p.producerId));
|
|
661
|
-
for (const producerId of this.preferredLayers.keys()) {
|
|
669
|
+
for (const producerId of new Set([...this.preferredLayers.keys(), ...this.receiveAccepted.keys()])) {
|
|
662
670
|
if (!present.has(producerId)) {
|
|
663
|
-
this.
|
|
664
|
-
this.receiveQualities.delete(producerId);
|
|
671
|
+
this.forgetReceiveChoice(producerId);
|
|
665
672
|
this.remoteVideoLayers.delete(producerId);
|
|
666
673
|
}
|
|
667
674
|
}
|
|
@@ -881,6 +888,8 @@ class StreamClient {
|
|
|
881
888
|
this.teardownMedia();
|
|
882
889
|
this.preferredLayers.clear();
|
|
883
890
|
this.receiveQualities.clear();
|
|
891
|
+
this.receiveAccepted.clear();
|
|
892
|
+
this.receiveRevision.clear();
|
|
884
893
|
this.remoteVideoLayers.clear();
|
|
885
894
|
this.releaseScreen();
|
|
886
895
|
this.music?.close();
|
|
@@ -1167,8 +1176,7 @@ class StreamClient {
|
|
|
1167
1176
|
case 'producerClosed': {
|
|
1168
1177
|
const { producerId, identity } = data;
|
|
1169
1178
|
this.subscriptions.delete(producerId);
|
|
1170
|
-
this.
|
|
1171
|
-
this.receiveQualities.delete(producerId);
|
|
1179
|
+
this.forgetReceiveChoice(producerId);
|
|
1172
1180
|
this.remoteVideoLayers.delete(producerId);
|
|
1173
1181
|
this.skippedVideos.delete(producerId);
|
|
1174
1182
|
this.adaptivePaused.delete(producerId);
|
|
@@ -1761,7 +1769,10 @@ class StreamClient {
|
|
|
1761
1769
|
const shown = current?.track ?? this.localStream?.getVideoTracks()[0] ?? null;
|
|
1762
1770
|
const previousSource = shown ? this.cameraSource(shown) : null;
|
|
1763
1771
|
const before = this.camOut;
|
|
1772
|
+
const beforeRaw = this.camRaw;
|
|
1773
|
+
const wasPaused = current?.paused ?? false;
|
|
1764
1774
|
const sent = await this.processCamera(track);
|
|
1775
|
+
let swapped = false;
|
|
1765
1776
|
try {
|
|
1766
1777
|
if (!current) {
|
|
1767
1778
|
// The republish reads the camera's state off the track it sends, and
|
|
@@ -1775,8 +1786,10 @@ class StreamClient {
|
|
|
1775
1786
|
}
|
|
1776
1787
|
}
|
|
1777
1788
|
this.adoptVideoTrack(sent);
|
|
1778
|
-
if (!current || current.track === sent)
|
|
1789
|
+
if (!current || current.track === sent) {
|
|
1790
|
+
swapped = true;
|
|
1779
1791
|
return;
|
|
1792
|
+
}
|
|
1780
1793
|
const previous = current.track;
|
|
1781
1794
|
const paused = current.paused;
|
|
1782
1795
|
try {
|
|
@@ -1787,11 +1800,13 @@ class StreamClient {
|
|
|
1787
1800
|
if (this.cameraEncodings && (0, quality_js_1.sameLayout)(this.cameraEncodings, next) &&
|
|
1788
1801
|
previousSize?.width === width && previousSize?.height === height) {
|
|
1789
1802
|
await current.replaceTrack({ track: sent });
|
|
1803
|
+
swapped = true;
|
|
1790
1804
|
this.watchTrackEnded(current);
|
|
1791
1805
|
await this.applyLayerBitrates(current, next);
|
|
1792
1806
|
return;
|
|
1793
1807
|
}
|
|
1794
1808
|
await this.reproduceCamera(current, sent);
|
|
1809
|
+
swapped = true;
|
|
1795
1810
|
}
|
|
1796
1811
|
finally {
|
|
1797
1812
|
// Pausing switched the old track off (mediasoup-client pauses that
|
|
@@ -1802,13 +1817,57 @@ class StreamClient {
|
|
|
1802
1817
|
previous.enabled = true;
|
|
1803
1818
|
}
|
|
1804
1819
|
}
|
|
1820
|
+
catch (err) {
|
|
1821
|
+
if (!swapped) {
|
|
1822
|
+
await this.undoCameraSwap({ producer: current ?? null, given: track, made: sent, shown, source: previousSource, raw: beforeRaw, out: before, paused: wasPaused });
|
|
1823
|
+
}
|
|
1824
|
+
throw err;
|
|
1825
|
+
}
|
|
1805
1826
|
finally {
|
|
1806
|
-
// The processor's previous picture is no longer sent or shown.
|
|
1807
|
-
if (before && before !== this.camOut)
|
|
1827
|
+
// The processor's previous picture is no longer sent or shown -- once the new one is.
|
|
1828
|
+
if (swapped && before && before !== this.camOut)
|
|
1808
1829
|
before.stop();
|
|
1809
1830
|
}
|
|
1810
1831
|
});
|
|
1811
1832
|
}
|
|
1833
|
+
/**
|
|
1834
|
+
* A camera swap that failed (BR-12; the Flutter SDK's V090-02). What is sent is still the old picture -- or nothing,
|
|
1835
|
+
* when the producer was replaced and the new one failed. The client's stream goes back to the old picture, so the
|
|
1836
|
+
* app's preview and a reconnect never use a track that did not go on air. With a processor, the old camera is put
|
|
1837
|
+
* back through it if it dropped the old picture when given the new track. The picture made for the new track is
|
|
1838
|
+
* released; the new track itself is the caller's.
|
|
1839
|
+
*/
|
|
1840
|
+
async undoCameraSwap(undo) {
|
|
1841
|
+
const { producer, given, made, shown, source, raw, out, paused } = undo;
|
|
1842
|
+
if (made !== given)
|
|
1843
|
+
made.stop();
|
|
1844
|
+
const sending = () => producer !== null && !producer.closed && this.cameraProducer() === producer;
|
|
1845
|
+
let picture = shown;
|
|
1846
|
+
if (shown && shown.readyState === 'ended' && source && source !== shown && source.readyState !== 'ended') {
|
|
1847
|
+
// The processor dropped the picture on air: make it again from the old camera, and send that.
|
|
1848
|
+
picture = await this.processCamera(source);
|
|
1849
|
+
if (sending() && producer && producer.track !== picture) {
|
|
1850
|
+
try {
|
|
1851
|
+
await producer.replaceTrack({ track: picture });
|
|
1852
|
+
this.watchTrackEnded(producer);
|
|
1853
|
+
}
|
|
1854
|
+
catch (err) {
|
|
1855
|
+
this.emit('error', new Error(`the camera could not be put back after a failed switch: ${err?.message ?? err}`));
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
else {
|
|
1860
|
+
// The picture on air is as it was, and so is what stands behind it.
|
|
1861
|
+
this.camRaw = raw;
|
|
1862
|
+
this.camOut = out;
|
|
1863
|
+
}
|
|
1864
|
+
if (!picture || picture.readyState === 'ended')
|
|
1865
|
+
return;
|
|
1866
|
+
// With nothing sending it, a republish reads the camera's state off the track: a paused camera stays paused.
|
|
1867
|
+
if (!sending())
|
|
1868
|
+
picture.enabled = !paused;
|
|
1869
|
+
this.adoptVideoTrack(picture);
|
|
1870
|
+
}
|
|
1812
1871
|
/**
|
|
1813
1872
|
* Give a camera producer whose track was swapped in place the bitrates of
|
|
1814
1873
|
* its new picture. The layout matched, but a bigger picture at the same
|
|
@@ -1882,8 +1941,10 @@ class StreamClient {
|
|
|
1882
1941
|
* 1280×720), and a layer above the biggest means the biggest. Leave
|
|
1883
1942
|
* `temporalLayer` out to keep the full frame rate. It is the most this
|
|
1884
1943
|
* client is sent: a connection that cannot carry it still gets less. The
|
|
1885
|
-
* choice is kept across reconnects and forgotten when the track ends
|
|
1886
|
-
*
|
|
1944
|
+
* choice is kept across reconnects and forgotten when the track ends; a
|
|
1945
|
+
* call that fails (refused, or the connection lost before the answer)
|
|
1946
|
+
* leaves the previous choice in place. Rejects for a producer whose video
|
|
1947
|
+
* this client is not receiving.
|
|
1887
1948
|
*/
|
|
1888
1949
|
async setPreferredLayers(producerId, spatialLayer, temporalLayer) {
|
|
1889
1950
|
if (!isLayer(spatialLayer) || (temporalLayer !== undefined && !isLayer(temporalLayer))) {
|
|
@@ -1894,12 +1955,19 @@ class StreamClient {
|
|
|
1894
1955
|
throw new Error(`setPreferredLayers: this client is not receiving a video with producerId ${producerId}`);
|
|
1895
1956
|
}
|
|
1896
1957
|
const layers = temporalLayer === undefined ? { spatialLayer } : { spatialLayer, temporalLayer };
|
|
1897
|
-
|
|
1898
|
-
//
|
|
1899
|
-
this.
|
|
1958
|
+
// Stored before the request, which reads it; a request that fails (refused, or the connection lost before the
|
|
1959
|
+
// answer) leaves the last accepted choice in place (BR-09).
|
|
1960
|
+
const change = this.changeReceive(producerId, { quality: undefined, layers });
|
|
1900
1961
|
// Shown through attachVideo: the lower of this and the size it is shown at.
|
|
1901
1962
|
const adaptive = this.adaptive.current(producerId);
|
|
1902
|
-
|
|
1963
|
+
try {
|
|
1964
|
+
await this.rpc('setPreferredLayers', { consumerId: consumer.id, ...(adaptive ? this.effectiveLayers(producerId, adaptive) : layers) });
|
|
1965
|
+
}
|
|
1966
|
+
catch (err) {
|
|
1967
|
+
change.failed();
|
|
1968
|
+
throw err;
|
|
1969
|
+
}
|
|
1970
|
+
change.accepted();
|
|
1903
1971
|
}
|
|
1904
1972
|
async collectVideoStats(baseline) {
|
|
1905
1973
|
const producer = this.cameraProducer();
|
|
@@ -1918,7 +1986,14 @@ class StreamClient {
|
|
|
1918
1986
|
(0, stats_js_1.pruneBaseline)(baseline, (key) => key.startsWith(prefix));
|
|
1919
1987
|
return (0, stats_js_1.parseSendStats)(report.values(), { quality: this.videoQuality, baseline, keyPrefix: prefix });
|
|
1920
1988
|
}
|
|
1921
|
-
/**
|
|
1989
|
+
/**
|
|
1990
|
+
* The quality this viewer gets of one remote video. '1080p', '720p' and '480p' pick the publisher's biggest layer
|
|
1991
|
+
* at or below that size (a 720p camera cannot give 1080p), and the viewer gets it while the video is shown, however
|
|
1992
|
+
* big it is on screen: someone who picks 1080p expects 1080p, not the smaller copy that fits the tile. A connection
|
|
1993
|
+
* that cannot carry it still gets less until it recovers, and a video shown through attachVideo is still paused
|
|
1994
|
+
* while nobody can see it. 'auto' (the default) goes back to the size it is shown at, or the server's choice.
|
|
1995
|
+
* Kept across reconnects; a call that fails leaves the previous choice in place.
|
|
1996
|
+
*/
|
|
1922
1997
|
async setReceiveQuality(producerId, quality) {
|
|
1923
1998
|
if (!['auto', '1080p', '720p', '480p'].includes(quality))
|
|
1924
1999
|
throw new RangeError('setReceiveQuality: unsupported quality');
|
|
@@ -1936,10 +2011,52 @@ class StreamClient {
|
|
|
1936
2011
|
throw new errors_js_1.RpcError('quality-unavailable', 'this publisher does not describe its video layers; use Auto');
|
|
1937
2012
|
}
|
|
1938
2013
|
}
|
|
1939
|
-
this.
|
|
1940
|
-
this.preferredLayers.set(producerId, layers);
|
|
2014
|
+
const change = this.changeReceive(producerId, { quality, layers });
|
|
1941
2015
|
const adaptive = this.adaptive.current(producerId);
|
|
1942
|
-
|
|
2016
|
+
try {
|
|
2017
|
+
await this.rpc('setPreferredLayers', { consumerId: consumer.id, ...(adaptive ? this.effectiveLayers(producerId, adaptive) : layers) });
|
|
2018
|
+
}
|
|
2019
|
+
catch (err) {
|
|
2020
|
+
change.failed();
|
|
2021
|
+
throw err;
|
|
2022
|
+
}
|
|
2023
|
+
change.accepted();
|
|
2024
|
+
}
|
|
2025
|
+
/** Store `choice` for one video now, before its request; the caller reports how the request ended (BR-09). */
|
|
2026
|
+
changeReceive(producerId, choice) {
|
|
2027
|
+
if (!this.receiveAccepted.has(producerId)) {
|
|
2028
|
+
this.receiveAccepted.set(producerId, { quality: this.receiveQualities.get(producerId), layers: this.preferredLayers.get(producerId) });
|
|
2029
|
+
}
|
|
2030
|
+
const revision = (this.receiveRevision.get(producerId) ?? 0) + 1;
|
|
2031
|
+
this.receiveRevision.set(producerId, revision);
|
|
2032
|
+
this.setReceiveChoice(producerId, choice);
|
|
2033
|
+
return {
|
|
2034
|
+
// Forgotten meanwhile (the video ended, the session closed): a late answer changes nothing.
|
|
2035
|
+
accepted: () => { if (this.receiveRevision.has(producerId))
|
|
2036
|
+
this.receiveAccepted.set(producerId, choice); },
|
|
2037
|
+
failed: () => {
|
|
2038
|
+
const back = this.receiveAccepted.get(producerId);
|
|
2039
|
+
if (this.receiveRevision.get(producerId) === revision && back)
|
|
2040
|
+
this.setReceiveChoice(producerId, back);
|
|
2041
|
+
},
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
setReceiveChoice(producerId, choice) {
|
|
2045
|
+
if (choice.quality === undefined)
|
|
2046
|
+
this.receiveQualities.delete(producerId);
|
|
2047
|
+
else
|
|
2048
|
+
this.receiveQualities.set(producerId, choice.quality);
|
|
2049
|
+
if (choice.layers === undefined)
|
|
2050
|
+
this.preferredLayers.delete(producerId);
|
|
2051
|
+
else
|
|
2052
|
+
this.preferredLayers.set(producerId, choice.layers);
|
|
2053
|
+
}
|
|
2054
|
+
/** The video ended: its choices go with it. */
|
|
2055
|
+
forgetReceiveChoice(producerId) {
|
|
2056
|
+
this.preferredLayers.delete(producerId);
|
|
2057
|
+
this.receiveQualities.delete(producerId);
|
|
2058
|
+
this.receiveAccepted.delete(producerId);
|
|
2059
|
+
this.receiveRevision.delete(producerId);
|
|
1943
2060
|
}
|
|
1944
2061
|
cameraProducer() {
|
|
1945
2062
|
for (const p of this.producers.values()) {
|
|
@@ -2707,26 +2824,35 @@ class StreamClient {
|
|
|
2707
2824
|
* Adaptive stream (like LiveKit's adaptiveStream and Tencent TRTC's small
|
|
2708
2825
|
* stream with receiveWhenViewVisible): tell the SDK which element shows a
|
|
2709
2826
|
* remote video. It then receives the smallest layer that fills that element
|
|
2710
|
-
*
|
|
2827
|
+
* without stretching it (its size on screen × devicePixelRatio, at most 2) and pauses the
|
|
2711
2828
|
* video at the server while the element is not visible (scrolled away,
|
|
2712
2829
|
* `display: none`, or the tab in the background), so a grid of small tiles
|
|
2713
2830
|
* stops decoding full-size video. Attach the same video to several elements
|
|
2714
2831
|
* and the largest visible one decides. Returns the function that detaches
|
|
2715
|
-
* it; detach when the element goes away. A
|
|
2716
|
-
* `setReceiveQuality`
|
|
2717
|
-
*
|
|
2718
|
-
*
|
|
2832
|
+
* it; detach when the element goes away. A quality chosen with
|
|
2833
|
+
* `setReceiveQuality` replaces the size it is shown at (the video is still
|
|
2834
|
+
* paused while hidden), and a ceiling set with `setPreferredLayers` still
|
|
2835
|
+
* applies. Works with any server; one without `pauseConsumer` gets the
|
|
2836
|
+
* smallest layer instead of a pause.
|
|
2719
2837
|
*/
|
|
2720
2838
|
attachVideo(producerId, element) {
|
|
2721
2839
|
return this.adaptive.attach(producerId, element);
|
|
2722
2840
|
}
|
|
2723
|
-
/**
|
|
2841
|
+
/**
|
|
2842
|
+
* The layers to ask for. A quality chosen with setReceiveQuality is the layer, whatever the size on screen (owner,
|
|
2843
|
+
* 2026-10-20: viewers who picked 1080p got 540p, because it was only a ceiling). A ceiling set with
|
|
2844
|
+
* setPreferredLayers and adaptive stream's size: the lower of the two.
|
|
2845
|
+
*/
|
|
2724
2846
|
effectiveLayers(producerId, want) {
|
|
2847
|
+
// Hidden: paused at the server, or held at the smallest layer where it cannot pause. A chosen quality or a ceiling
|
|
2848
|
+
// applies once the video is shown again, never while nobody can see it (BR-08; the Flutter SDK's V092-01).
|
|
2849
|
+
if (want && !want.visible)
|
|
2850
|
+
return { spatialLayer: 0, temporalLayer: 0 };
|
|
2725
2851
|
let manual = this.preferredLayers.get(producerId);
|
|
2726
2852
|
const quality = this.receiveQualities.get(producerId);
|
|
2727
2853
|
if (quality && quality !== 'auto') {
|
|
2728
2854
|
try {
|
|
2729
|
-
|
|
2855
|
+
return { spatialLayer: (0, quality_js_1.receiveSpatialLayer)(this.remoteVideoLayers.get(producerId) ?? [], quality) };
|
|
2730
2856
|
}
|
|
2731
2857
|
catch {
|
|
2732
2858
|
manual = undefined;
|
|
@@ -3115,7 +3241,9 @@ class StreamClient {
|
|
|
3115
3241
|
* captured with the constraints publish was given (size, frame rate, echo cancellation) and its track replaces the
|
|
3116
3242
|
* published one: a camera of the same size swaps with no pause for viewers, a microphone always does. The track it
|
|
3117
3243
|
* replaces is stopped, releasing that device; the client's own stream follows (`localStreamChanged`), and a
|
|
3118
|
-
* reconnect captures the chosen device again. A camera or microphone that is switched off stays off.
|
|
3244
|
+
* reconnect captures the chosen device again. A camera or microphone that is switched off stays off. A swap that
|
|
3245
|
+
* fails releases the new device and keeps the old one on air, put back through a noise suppressor or video effects
|
|
3246
|
+
* if one is set.
|
|
3119
3247
|
*
|
|
3120
3248
|
* For a camera published as a canvas or another processed track, capture the new camera yourself and pass your
|
|
3121
3249
|
* processed track to `replaceVideoTrack`; this rejects rather than replace your picture with the raw camera.
|
|
@@ -3162,15 +3290,38 @@ class StreamClient {
|
|
|
3162
3290
|
track.stop();
|
|
3163
3291
|
throw new Error('switchDevice: the microphone was unpublished meanwhile');
|
|
3164
3292
|
}
|
|
3165
|
-
|
|
3293
|
+
try {
|
|
3294
|
+
// Releases the new microphone itself if the swap fails.
|
|
3295
|
+
await this.adoptAudioTrack(producer, track);
|
|
3296
|
+
}
|
|
3297
|
+
catch (err) {
|
|
3298
|
+
if (producer.closed || this.micProducer() !== producer)
|
|
3299
|
+
throw new Error('switchDevice: the microphone was unpublished meanwhile');
|
|
3300
|
+
throw err;
|
|
3301
|
+
}
|
|
3166
3302
|
this.publishConstraints = { ...(this.publishConstraints ?? {}), audio };
|
|
3167
3303
|
});
|
|
3168
3304
|
}
|
|
3169
|
-
/**
|
|
3305
|
+
/**
|
|
3306
|
+
* Put `track` (one the SDK captured) on the microphone producer and in the client's own stream; the replaced track is
|
|
3307
|
+
* stopped. A swap that fails releases `track` and the sound made from it, and with a processor puts the microphone
|
|
3308
|
+
* still on air back through it: given the new track, the processor dropped that sound (BR-10, BR-11).
|
|
3309
|
+
*/
|
|
3170
3310
|
async adoptAudioTrack(producer, track) {
|
|
3171
3311
|
const previous = this.micRaw ?? producer.track;
|
|
3172
|
-
const
|
|
3173
|
-
|
|
3312
|
+
const previousRaw = this.micRaw;
|
|
3313
|
+
let sent = track;
|
|
3314
|
+
try {
|
|
3315
|
+
sent = await this.processMic(track);
|
|
3316
|
+
await producer.replaceTrack({ track: sent });
|
|
3317
|
+
}
|
|
3318
|
+
catch (err) {
|
|
3319
|
+
if (sent !== track)
|
|
3320
|
+
sent.stop();
|
|
3321
|
+
track.stop();
|
|
3322
|
+
await this.restoreMic(producer, previousRaw);
|
|
3323
|
+
throw err;
|
|
3324
|
+
}
|
|
3174
3325
|
this.micRaw = sent !== track ? track : null;
|
|
3175
3326
|
const video = this.localStream ? this.localStream.getTracks().filter((t) => t.kind !== 'audio') : [];
|
|
3176
3327
|
this.localStream = new MediaStream([track, ...video]);
|
|
@@ -3179,6 +3330,25 @@ class StreamClient {
|
|
|
3179
3330
|
previous.stop();
|
|
3180
3331
|
this.watchTrackEnded(producer);
|
|
3181
3332
|
}
|
|
3333
|
+
/** After a failed microphone swap with a processor: send the microphone on air through it again (it dropped that sound). */
|
|
3334
|
+
async restoreMic(producer, raw) {
|
|
3335
|
+
const current = () => !producer.closed && this.micProducer() === producer;
|
|
3336
|
+
if (!this.audioProcessor || !raw || raw.readyState === 'ended' || !current())
|
|
3337
|
+
return;
|
|
3338
|
+
try {
|
|
3339
|
+
const again = await this.processMic(raw);
|
|
3340
|
+
if (!current()) {
|
|
3341
|
+
if (again !== raw)
|
|
3342
|
+
again.stop();
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
await producer.replaceTrack({ track: again });
|
|
3346
|
+
this.micRaw = again !== raw ? raw : null;
|
|
3347
|
+
}
|
|
3348
|
+
catch (err) {
|
|
3349
|
+
this.emit('error', new Error(`the microphone could not be put back after a failed switch: ${err?.message ?? err}`));
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3182
3352
|
/** Notice when the producer's present track stops from outside (unplugged, permission revoked). */
|
|
3183
3353
|
watchTrackEnded(producer) {
|
|
3184
3354
|
// The device's track, not the processed one a processor sends: that is the one a device ends.
|