@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/esm/index.js
CHANGED
|
@@ -252,6 +252,14 @@ export class StreamClient {
|
|
|
252
252
|
screenProducerIds = new Set();
|
|
253
253
|
remoteVideoLayers = new Map();
|
|
254
254
|
receiveQualities = new Map();
|
|
255
|
+
/**
|
|
256
|
+
* BR-09 (the Flutter SDK's V092-02): per video, the choice the server last accepted, and a revision every change
|
|
257
|
+
* bumps. A choice is stored before its request (what is asked for is read from it), so a request that fails goes
|
|
258
|
+
* back to the last accepted choice -- not merely the one before it, which may itself be waiting or refused -- and
|
|
259
|
+
* only the latest change may go back, so a late failure never undoes a newer choice. Replies come in request order.
|
|
260
|
+
*/
|
|
261
|
+
receiveAccepted = new Map();
|
|
262
|
+
receiveRevision = new Map();
|
|
255
263
|
cameraFrameRate = 30;
|
|
256
264
|
frameRateFallback = false;
|
|
257
265
|
frameRateLowSamples = 0;
|
|
@@ -625,10 +633,9 @@ export class StreamClient {
|
|
|
625
633
|
// A video that ended while this client was away sends it no
|
|
626
634
|
// producerClosed: forget the layers asked for it.
|
|
627
635
|
const present = new Set(result.producers.map((p) => p.producerId));
|
|
628
|
-
for (const producerId of this.preferredLayers.keys()) {
|
|
636
|
+
for (const producerId of new Set([...this.preferredLayers.keys(), ...this.receiveAccepted.keys()])) {
|
|
629
637
|
if (!present.has(producerId)) {
|
|
630
|
-
this.
|
|
631
|
-
this.receiveQualities.delete(producerId);
|
|
638
|
+
this.forgetReceiveChoice(producerId);
|
|
632
639
|
this.remoteVideoLayers.delete(producerId);
|
|
633
640
|
}
|
|
634
641
|
}
|
|
@@ -848,6 +855,8 @@ export class StreamClient {
|
|
|
848
855
|
this.teardownMedia();
|
|
849
856
|
this.preferredLayers.clear();
|
|
850
857
|
this.receiveQualities.clear();
|
|
858
|
+
this.receiveAccepted.clear();
|
|
859
|
+
this.receiveRevision.clear();
|
|
851
860
|
this.remoteVideoLayers.clear();
|
|
852
861
|
this.releaseScreen();
|
|
853
862
|
this.music?.close();
|
|
@@ -1134,8 +1143,7 @@ export class StreamClient {
|
|
|
1134
1143
|
case 'producerClosed': {
|
|
1135
1144
|
const { producerId, identity } = data;
|
|
1136
1145
|
this.subscriptions.delete(producerId);
|
|
1137
|
-
this.
|
|
1138
|
-
this.receiveQualities.delete(producerId);
|
|
1146
|
+
this.forgetReceiveChoice(producerId);
|
|
1139
1147
|
this.remoteVideoLayers.delete(producerId);
|
|
1140
1148
|
this.skippedVideos.delete(producerId);
|
|
1141
1149
|
this.adaptivePaused.delete(producerId);
|
|
@@ -1728,7 +1736,10 @@ export class StreamClient {
|
|
|
1728
1736
|
const shown = current?.track ?? this.localStream?.getVideoTracks()[0] ?? null;
|
|
1729
1737
|
const previousSource = shown ? this.cameraSource(shown) : null;
|
|
1730
1738
|
const before = this.camOut;
|
|
1739
|
+
const beforeRaw = this.camRaw;
|
|
1740
|
+
const wasPaused = current?.paused ?? false;
|
|
1731
1741
|
const sent = await this.processCamera(track);
|
|
1742
|
+
let swapped = false;
|
|
1732
1743
|
try {
|
|
1733
1744
|
if (!current) {
|
|
1734
1745
|
// The republish reads the camera's state off the track it sends, and
|
|
@@ -1742,8 +1753,10 @@ export class StreamClient {
|
|
|
1742
1753
|
}
|
|
1743
1754
|
}
|
|
1744
1755
|
this.adoptVideoTrack(sent);
|
|
1745
|
-
if (!current || current.track === sent)
|
|
1756
|
+
if (!current || current.track === sent) {
|
|
1757
|
+
swapped = true;
|
|
1746
1758
|
return;
|
|
1759
|
+
}
|
|
1747
1760
|
const previous = current.track;
|
|
1748
1761
|
const paused = current.paused;
|
|
1749
1762
|
try {
|
|
@@ -1754,11 +1767,13 @@ export class StreamClient {
|
|
|
1754
1767
|
if (this.cameraEncodings && sameLayout(this.cameraEncodings, next) &&
|
|
1755
1768
|
previousSize?.width === width && previousSize?.height === height) {
|
|
1756
1769
|
await current.replaceTrack({ track: sent });
|
|
1770
|
+
swapped = true;
|
|
1757
1771
|
this.watchTrackEnded(current);
|
|
1758
1772
|
await this.applyLayerBitrates(current, next);
|
|
1759
1773
|
return;
|
|
1760
1774
|
}
|
|
1761
1775
|
await this.reproduceCamera(current, sent);
|
|
1776
|
+
swapped = true;
|
|
1762
1777
|
}
|
|
1763
1778
|
finally {
|
|
1764
1779
|
// Pausing switched the old track off (mediasoup-client pauses that
|
|
@@ -1769,13 +1784,57 @@ export class StreamClient {
|
|
|
1769
1784
|
previous.enabled = true;
|
|
1770
1785
|
}
|
|
1771
1786
|
}
|
|
1787
|
+
catch (err) {
|
|
1788
|
+
if (!swapped) {
|
|
1789
|
+
await this.undoCameraSwap({ producer: current ?? null, given: track, made: sent, shown, source: previousSource, raw: beforeRaw, out: before, paused: wasPaused });
|
|
1790
|
+
}
|
|
1791
|
+
throw err;
|
|
1792
|
+
}
|
|
1772
1793
|
finally {
|
|
1773
|
-
// The processor's previous picture is no longer sent or shown.
|
|
1774
|
-
if (before && before !== this.camOut)
|
|
1794
|
+
// The processor's previous picture is no longer sent or shown -- once the new one is.
|
|
1795
|
+
if (swapped && before && before !== this.camOut)
|
|
1775
1796
|
before.stop();
|
|
1776
1797
|
}
|
|
1777
1798
|
});
|
|
1778
1799
|
}
|
|
1800
|
+
/**
|
|
1801
|
+
* A camera swap that failed (BR-12; the Flutter SDK's V090-02). What is sent is still the old picture -- or nothing,
|
|
1802
|
+
* when the producer was replaced and the new one failed. The client's stream goes back to the old picture, so the
|
|
1803
|
+
* app's preview and a reconnect never use a track that did not go on air. With a processor, the old camera is put
|
|
1804
|
+
* back through it if it dropped the old picture when given the new track. The picture made for the new track is
|
|
1805
|
+
* released; the new track itself is the caller's.
|
|
1806
|
+
*/
|
|
1807
|
+
async undoCameraSwap(undo) {
|
|
1808
|
+
const { producer, given, made, shown, source, raw, out, paused } = undo;
|
|
1809
|
+
if (made !== given)
|
|
1810
|
+
made.stop();
|
|
1811
|
+
const sending = () => producer !== null && !producer.closed && this.cameraProducer() === producer;
|
|
1812
|
+
let picture = shown;
|
|
1813
|
+
if (shown && shown.readyState === 'ended' && source && source !== shown && source.readyState !== 'ended') {
|
|
1814
|
+
// The processor dropped the picture on air: make it again from the old camera, and send that.
|
|
1815
|
+
picture = await this.processCamera(source);
|
|
1816
|
+
if (sending() && producer && producer.track !== picture) {
|
|
1817
|
+
try {
|
|
1818
|
+
await producer.replaceTrack({ track: picture });
|
|
1819
|
+
this.watchTrackEnded(producer);
|
|
1820
|
+
}
|
|
1821
|
+
catch (err) {
|
|
1822
|
+
this.emit('error', new Error(`the camera could not be put back after a failed switch: ${err?.message ?? err}`));
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
// The picture on air is as it was, and so is what stands behind it.
|
|
1828
|
+
this.camRaw = raw;
|
|
1829
|
+
this.camOut = out;
|
|
1830
|
+
}
|
|
1831
|
+
if (!picture || picture.readyState === 'ended')
|
|
1832
|
+
return;
|
|
1833
|
+
// With nothing sending it, a republish reads the camera's state off the track: a paused camera stays paused.
|
|
1834
|
+
if (!sending())
|
|
1835
|
+
picture.enabled = !paused;
|
|
1836
|
+
this.adoptVideoTrack(picture);
|
|
1837
|
+
}
|
|
1779
1838
|
/**
|
|
1780
1839
|
* Give a camera producer whose track was swapped in place the bitrates of
|
|
1781
1840
|
* its new picture. The layout matched, but a bigger picture at the same
|
|
@@ -1849,8 +1908,10 @@ export class StreamClient {
|
|
|
1849
1908
|
* 1280×720), and a layer above the biggest means the biggest. Leave
|
|
1850
1909
|
* `temporalLayer` out to keep the full frame rate. It is the most this
|
|
1851
1910
|
* client is sent: a connection that cannot carry it still gets less. The
|
|
1852
|
-
* choice is kept across reconnects and forgotten when the track ends
|
|
1853
|
-
*
|
|
1911
|
+
* choice is kept across reconnects and forgotten when the track ends; a
|
|
1912
|
+
* call that fails (refused, or the connection lost before the answer)
|
|
1913
|
+
* leaves the previous choice in place. Rejects for a producer whose video
|
|
1914
|
+
* this client is not receiving.
|
|
1854
1915
|
*/
|
|
1855
1916
|
async setPreferredLayers(producerId, spatialLayer, temporalLayer) {
|
|
1856
1917
|
if (!isLayer(spatialLayer) || (temporalLayer !== undefined && !isLayer(temporalLayer))) {
|
|
@@ -1861,12 +1922,19 @@ export class StreamClient {
|
|
|
1861
1922
|
throw new Error(`setPreferredLayers: this client is not receiving a video with producerId ${producerId}`);
|
|
1862
1923
|
}
|
|
1863
1924
|
const layers = temporalLayer === undefined ? { spatialLayer } : { spatialLayer, temporalLayer };
|
|
1864
|
-
|
|
1865
|
-
//
|
|
1866
|
-
this.
|
|
1925
|
+
// Stored before the request, which reads it; a request that fails (refused, or the connection lost before the
|
|
1926
|
+
// answer) leaves the last accepted choice in place (BR-09).
|
|
1927
|
+
const change = this.changeReceive(producerId, { quality: undefined, layers });
|
|
1867
1928
|
// Shown through attachVideo: the lower of this and the size it is shown at.
|
|
1868
1929
|
const adaptive = this.adaptive.current(producerId);
|
|
1869
|
-
|
|
1930
|
+
try {
|
|
1931
|
+
await this.rpc('setPreferredLayers', { consumerId: consumer.id, ...(adaptive ? this.effectiveLayers(producerId, adaptive) : layers) });
|
|
1932
|
+
}
|
|
1933
|
+
catch (err) {
|
|
1934
|
+
change.failed();
|
|
1935
|
+
throw err;
|
|
1936
|
+
}
|
|
1937
|
+
change.accepted();
|
|
1870
1938
|
}
|
|
1871
1939
|
async collectVideoStats(baseline) {
|
|
1872
1940
|
const producer = this.cameraProducer();
|
|
@@ -1885,7 +1953,14 @@ export class StreamClient {
|
|
|
1885
1953
|
pruneBaseline(baseline, (key) => key.startsWith(prefix));
|
|
1886
1954
|
return parseSendStats(report.values(), { quality: this.videoQuality, baseline, keyPrefix: prefix });
|
|
1887
1955
|
}
|
|
1888
|
-
/**
|
|
1956
|
+
/**
|
|
1957
|
+
* The quality this viewer gets of one remote video. '1080p', '720p' and '480p' pick the publisher's biggest layer
|
|
1958
|
+
* at or below that size (a 720p camera cannot give 1080p), and the viewer gets it while the video is shown, however
|
|
1959
|
+
* big it is on screen: someone who picks 1080p expects 1080p, not the smaller copy that fits the tile. A connection
|
|
1960
|
+
* that cannot carry it still gets less until it recovers, and a video shown through attachVideo is still paused
|
|
1961
|
+
* while nobody can see it. 'auto' (the default) goes back to the size it is shown at, or the server's choice.
|
|
1962
|
+
* Kept across reconnects; a call that fails leaves the previous choice in place.
|
|
1963
|
+
*/
|
|
1889
1964
|
async setReceiveQuality(producerId, quality) {
|
|
1890
1965
|
if (!['auto', '1080p', '720p', '480p'].includes(quality))
|
|
1891
1966
|
throw new RangeError('setReceiveQuality: unsupported quality');
|
|
@@ -1903,10 +1978,52 @@ export class StreamClient {
|
|
|
1903
1978
|
throw new RpcError('quality-unavailable', 'this publisher does not describe its video layers; use Auto');
|
|
1904
1979
|
}
|
|
1905
1980
|
}
|
|
1906
|
-
this.
|
|
1907
|
-
this.preferredLayers.set(producerId, layers);
|
|
1981
|
+
const change = this.changeReceive(producerId, { quality, layers });
|
|
1908
1982
|
const adaptive = this.adaptive.current(producerId);
|
|
1909
|
-
|
|
1983
|
+
try {
|
|
1984
|
+
await this.rpc('setPreferredLayers', { consumerId: consumer.id, ...(adaptive ? this.effectiveLayers(producerId, adaptive) : layers) });
|
|
1985
|
+
}
|
|
1986
|
+
catch (err) {
|
|
1987
|
+
change.failed();
|
|
1988
|
+
throw err;
|
|
1989
|
+
}
|
|
1990
|
+
change.accepted();
|
|
1991
|
+
}
|
|
1992
|
+
/** Store `choice` for one video now, before its request; the caller reports how the request ended (BR-09). */
|
|
1993
|
+
changeReceive(producerId, choice) {
|
|
1994
|
+
if (!this.receiveAccepted.has(producerId)) {
|
|
1995
|
+
this.receiveAccepted.set(producerId, { quality: this.receiveQualities.get(producerId), layers: this.preferredLayers.get(producerId) });
|
|
1996
|
+
}
|
|
1997
|
+
const revision = (this.receiveRevision.get(producerId) ?? 0) + 1;
|
|
1998
|
+
this.receiveRevision.set(producerId, revision);
|
|
1999
|
+
this.setReceiveChoice(producerId, choice);
|
|
2000
|
+
return {
|
|
2001
|
+
// Forgotten meanwhile (the video ended, the session closed): a late answer changes nothing.
|
|
2002
|
+
accepted: () => { if (this.receiveRevision.has(producerId))
|
|
2003
|
+
this.receiveAccepted.set(producerId, choice); },
|
|
2004
|
+
failed: () => {
|
|
2005
|
+
const back = this.receiveAccepted.get(producerId);
|
|
2006
|
+
if (this.receiveRevision.get(producerId) === revision && back)
|
|
2007
|
+
this.setReceiveChoice(producerId, back);
|
|
2008
|
+
},
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
setReceiveChoice(producerId, choice) {
|
|
2012
|
+
if (choice.quality === undefined)
|
|
2013
|
+
this.receiveQualities.delete(producerId);
|
|
2014
|
+
else
|
|
2015
|
+
this.receiveQualities.set(producerId, choice.quality);
|
|
2016
|
+
if (choice.layers === undefined)
|
|
2017
|
+
this.preferredLayers.delete(producerId);
|
|
2018
|
+
else
|
|
2019
|
+
this.preferredLayers.set(producerId, choice.layers);
|
|
2020
|
+
}
|
|
2021
|
+
/** The video ended: its choices go with it. */
|
|
2022
|
+
forgetReceiveChoice(producerId) {
|
|
2023
|
+
this.preferredLayers.delete(producerId);
|
|
2024
|
+
this.receiveQualities.delete(producerId);
|
|
2025
|
+
this.receiveAccepted.delete(producerId);
|
|
2026
|
+
this.receiveRevision.delete(producerId);
|
|
1910
2027
|
}
|
|
1911
2028
|
cameraProducer() {
|
|
1912
2029
|
for (const p of this.producers.values()) {
|
|
@@ -2674,26 +2791,35 @@ export class StreamClient {
|
|
|
2674
2791
|
* Adaptive stream (like LiveKit's adaptiveStream and Tencent TRTC's small
|
|
2675
2792
|
* stream with receiveWhenViewVisible): tell the SDK which element shows a
|
|
2676
2793
|
* remote video. It then receives the smallest layer that fills that element
|
|
2677
|
-
*
|
|
2794
|
+
* without stretching it (its size on screen × devicePixelRatio, at most 2) and pauses the
|
|
2678
2795
|
* video at the server while the element is not visible (scrolled away,
|
|
2679
2796
|
* `display: none`, or the tab in the background), so a grid of small tiles
|
|
2680
2797
|
* stops decoding full-size video. Attach the same video to several elements
|
|
2681
2798
|
* and the largest visible one decides. Returns the function that detaches
|
|
2682
|
-
* it; detach when the element goes away. A
|
|
2683
|
-
* `setReceiveQuality`
|
|
2684
|
-
*
|
|
2685
|
-
*
|
|
2799
|
+
* it; detach when the element goes away. A quality chosen with
|
|
2800
|
+
* `setReceiveQuality` replaces the size it is shown at (the video is still
|
|
2801
|
+
* paused while hidden), and a ceiling set with `setPreferredLayers` still
|
|
2802
|
+
* applies. Works with any server; one without `pauseConsumer` gets the
|
|
2803
|
+
* smallest layer instead of a pause.
|
|
2686
2804
|
*/
|
|
2687
2805
|
attachVideo(producerId, element) {
|
|
2688
2806
|
return this.adaptive.attach(producerId, element);
|
|
2689
2807
|
}
|
|
2690
|
-
/**
|
|
2808
|
+
/**
|
|
2809
|
+
* The layers to ask for. A quality chosen with setReceiveQuality is the layer, whatever the size on screen (owner,
|
|
2810
|
+
* 2026-10-20: viewers who picked 1080p got 540p, because it was only a ceiling). A ceiling set with
|
|
2811
|
+
* setPreferredLayers and adaptive stream's size: the lower of the two.
|
|
2812
|
+
*/
|
|
2691
2813
|
effectiveLayers(producerId, want) {
|
|
2814
|
+
// Hidden: paused at the server, or held at the smallest layer where it cannot pause. A chosen quality or a ceiling
|
|
2815
|
+
// applies once the video is shown again, never while nobody can see it (BR-08; the Flutter SDK's V092-01).
|
|
2816
|
+
if (want && !want.visible)
|
|
2817
|
+
return { spatialLayer: 0, temporalLayer: 0 };
|
|
2692
2818
|
let manual = this.preferredLayers.get(producerId);
|
|
2693
2819
|
const quality = this.receiveQualities.get(producerId);
|
|
2694
2820
|
if (quality && quality !== 'auto') {
|
|
2695
2821
|
try {
|
|
2696
|
-
|
|
2822
|
+
return { spatialLayer: receiveSpatialLayer(this.remoteVideoLayers.get(producerId) ?? [], quality) };
|
|
2697
2823
|
}
|
|
2698
2824
|
catch {
|
|
2699
2825
|
manual = undefined;
|
|
@@ -3082,7 +3208,9 @@ export class StreamClient {
|
|
|
3082
3208
|
* captured with the constraints publish was given (size, frame rate, echo cancellation) and its track replaces the
|
|
3083
3209
|
* published one: a camera of the same size swaps with no pause for viewers, a microphone always does. The track it
|
|
3084
3210
|
* replaces is stopped, releasing that device; the client's own stream follows (`localStreamChanged`), and a
|
|
3085
|
-
* reconnect captures the chosen device again. A camera or microphone that is switched off stays off.
|
|
3211
|
+
* reconnect captures the chosen device again. A camera or microphone that is switched off stays off. A swap that
|
|
3212
|
+
* fails releases the new device and keeps the old one on air, put back through a noise suppressor or video effects
|
|
3213
|
+
* if one is set.
|
|
3086
3214
|
*
|
|
3087
3215
|
* For a camera published as a canvas or another processed track, capture the new camera yourself and pass your
|
|
3088
3216
|
* processed track to `replaceVideoTrack`; this rejects rather than replace your picture with the raw camera.
|
|
@@ -3129,15 +3257,38 @@ export class StreamClient {
|
|
|
3129
3257
|
track.stop();
|
|
3130
3258
|
throw new Error('switchDevice: the microphone was unpublished meanwhile');
|
|
3131
3259
|
}
|
|
3132
|
-
|
|
3260
|
+
try {
|
|
3261
|
+
// Releases the new microphone itself if the swap fails.
|
|
3262
|
+
await this.adoptAudioTrack(producer, track);
|
|
3263
|
+
}
|
|
3264
|
+
catch (err) {
|
|
3265
|
+
if (producer.closed || this.micProducer() !== producer)
|
|
3266
|
+
throw new Error('switchDevice: the microphone was unpublished meanwhile');
|
|
3267
|
+
throw err;
|
|
3268
|
+
}
|
|
3133
3269
|
this.publishConstraints = { ...(this.publishConstraints ?? {}), audio };
|
|
3134
3270
|
});
|
|
3135
3271
|
}
|
|
3136
|
-
/**
|
|
3272
|
+
/**
|
|
3273
|
+
* Put `track` (one the SDK captured) on the microphone producer and in the client's own stream; the replaced track is
|
|
3274
|
+
* stopped. A swap that fails releases `track` and the sound made from it, and with a processor puts the microphone
|
|
3275
|
+
* still on air back through it: given the new track, the processor dropped that sound (BR-10, BR-11).
|
|
3276
|
+
*/
|
|
3137
3277
|
async adoptAudioTrack(producer, track) {
|
|
3138
3278
|
const previous = this.micRaw ?? producer.track;
|
|
3139
|
-
const
|
|
3140
|
-
|
|
3279
|
+
const previousRaw = this.micRaw;
|
|
3280
|
+
let sent = track;
|
|
3281
|
+
try {
|
|
3282
|
+
sent = await this.processMic(track);
|
|
3283
|
+
await producer.replaceTrack({ track: sent });
|
|
3284
|
+
}
|
|
3285
|
+
catch (err) {
|
|
3286
|
+
if (sent !== track)
|
|
3287
|
+
sent.stop();
|
|
3288
|
+
track.stop();
|
|
3289
|
+
await this.restoreMic(producer, previousRaw);
|
|
3290
|
+
throw err;
|
|
3291
|
+
}
|
|
3141
3292
|
this.micRaw = sent !== track ? track : null;
|
|
3142
3293
|
const video = this.localStream ? this.localStream.getTracks().filter((t) => t.kind !== 'audio') : [];
|
|
3143
3294
|
this.localStream = new MediaStream([track, ...video]);
|
|
@@ -3146,6 +3297,25 @@ export class StreamClient {
|
|
|
3146
3297
|
previous.stop();
|
|
3147
3298
|
this.watchTrackEnded(producer);
|
|
3148
3299
|
}
|
|
3300
|
+
/** After a failed microphone swap with a processor: send the microphone on air through it again (it dropped that sound). */
|
|
3301
|
+
async restoreMic(producer, raw) {
|
|
3302
|
+
const current = () => !producer.closed && this.micProducer() === producer;
|
|
3303
|
+
if (!this.audioProcessor || !raw || raw.readyState === 'ended' || !current())
|
|
3304
|
+
return;
|
|
3305
|
+
try {
|
|
3306
|
+
const again = await this.processMic(raw);
|
|
3307
|
+
if (!current()) {
|
|
3308
|
+
if (again !== raw)
|
|
3309
|
+
again.stop();
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
await producer.replaceTrack({ track: again });
|
|
3313
|
+
this.micRaw = again !== raw ? raw : null;
|
|
3314
|
+
}
|
|
3315
|
+
catch (err) {
|
|
3316
|
+
this.emit('error', new Error(`the microphone could not be put back after a failed switch: ${err?.message ?? err}`));
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3149
3319
|
/** Notice when the producer's present track stops from outside (unplugged, permission revoked). */
|
|
3150
3320
|
watchTrackEnded(producer) {
|
|
3151
3321
|
// The device's track, not the processed one a processor sends: that is the one a device ends.
|