@camera.ui/browser 0.1.14 → 0.1.16
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/dist/index.d.ts +15 -202
- package/dist/index.js +323 -40
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -209,6 +209,8 @@ export declare interface CameraStream {
|
|
|
209
209
|
readonly status: Ref<StreamStatus>;
|
|
210
210
|
readonly isPlaying: Ref<boolean>;
|
|
211
211
|
readonly stalled: Ref<boolean>;
|
|
212
|
+
readonly connectionPath: Ref<StreamConnectionPath | undefined>;
|
|
213
|
+
readonly webrtcUnreachable: Ref<boolean>;
|
|
212
214
|
readonly activeMode: Ref<ActiveStreamingMode>;
|
|
213
215
|
readonly activeResolution: Ref<StreamingRole>;
|
|
214
216
|
readonly degradedFrom: Ref<StreamingRole | null>;
|
|
@@ -769,6 +771,11 @@ export declare class NamespaceManager {
|
|
|
769
771
|
}
|
|
770
772
|
}
|
|
771
773
|
|
|
774
|
+
export declare interface NativeFullscreen {
|
|
775
|
+
enter: () => Promise<boolean>;
|
|
776
|
+
exit: () => Promise<void>;
|
|
777
|
+
}
|
|
778
|
+
|
|
772
779
|
export { NatsStatus }
|
|
773
780
|
|
|
774
781
|
declare interface NotificationManagerNamespaces {
|
|
@@ -1129,6 +1136,8 @@ export declare interface ReactiveStream {
|
|
|
1129
1136
|
readonly hasBackchannel: Ref<boolean>;
|
|
1130
1137
|
readonly isPlaying: Ref<boolean>;
|
|
1131
1138
|
readonly stalled?: Ref<boolean>;
|
|
1139
|
+
readonly connectionPath?: Readonly<Ref<StreamConnectionPath | undefined>>;
|
|
1140
|
+
readonly webrtcUnreachable?: Readonly<Ref<boolean>>;
|
|
1132
1141
|
readonly error: Ref<Error | undefined>;
|
|
1133
1142
|
readonly probeInfo: Ref<ProbeStream | undefined>;
|
|
1134
1143
|
readonly muted: Ref<boolean>;
|
|
@@ -1294,6 +1303,8 @@ export declare interface SensorRegistryRPC {
|
|
|
1294
1303
|
|
|
1295
1304
|
export declare function setFullscreenRoot(el: HTMLElement | null): void;
|
|
1296
1305
|
|
|
1306
|
+
export declare function setNativeFullscreen(native: NativeFullscreen | null): void;
|
|
1307
|
+
|
|
1297
1308
|
/* Excluded from this release type: SirenControlProperties */
|
|
1298
1309
|
|
|
1299
1310
|
/* Excluded from this release type: SirenProperty */
|
|
@@ -1345,8 +1356,9 @@ export declare const STREAM_CONFIG: {
|
|
|
1345
1356
|
readonly RECONNECT_DELAY: 1000;
|
|
1346
1357
|
readonly CONNECT_TIMEOUT: 10000;
|
|
1347
1358
|
readonly WS_CONNECT_TIMEOUT: 5000;
|
|
1359
|
+
readonly DISCONNECT_GRACE: 5000;
|
|
1348
1360
|
readonly ICE_SERVERS: readonly [{
|
|
1349
|
-
readonly urls: "stun:stun.
|
|
1361
|
+
readonly urls: "stun:stun.cloudflare.com:3478";
|
|
1350
1362
|
}];
|
|
1351
1363
|
};
|
|
1352
1364
|
readonly MSE: {
|
|
@@ -1361,6 +1373,8 @@ export declare const STREAM_CONFIG: {
|
|
|
1361
1373
|
};
|
|
1362
1374
|
};
|
|
1363
1375
|
|
|
1376
|
+
export declare type StreamConnectionPath = 'direct' | 'relay' | 'tunnel';
|
|
1377
|
+
|
|
1364
1378
|
declare type StreamDegradeReason = 'codec';
|
|
1365
1379
|
|
|
1366
1380
|
export declare interface StreamOptions {
|
|
@@ -1705,204 +1719,3 @@ declare module 'fastify' {
|
|
|
1705
1719
|
uploadToDisk?: boolean;
|
|
1706
1720
|
}
|
|
1707
1721
|
}
|
|
1708
|
-
|
|
1709
|
-
/**
|
|
1710
|
-
* Copyright (c) 2017, Daniel Imms (MIT License).
|
|
1711
|
-
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
|
1712
|
-
*/
|
|
1713
|
-
|
|
1714
|
-
declare module '@scrypted/node-pty' {
|
|
1715
|
-
/**
|
|
1716
|
-
* Forks a process as a pseudoterminal.
|
|
1717
|
-
* @param file The file to launch.
|
|
1718
|
-
* @param args The file's arguments as argv (string[]) or in a pre-escaped CommandLine format
|
|
1719
|
-
* (string). Note that the CommandLine option is only available on Windows and is expected to be
|
|
1720
|
-
* escaped properly.
|
|
1721
|
-
* @param options The options of the terminal.
|
|
1722
|
-
* @see CommandLineToArgvW https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
|
|
1723
|
-
* @see Parsing C++ Comamnd-Line Arguments https://msdn.microsoft.com/en-us/library/17w5ykft.aspx
|
|
1724
|
-
* @see GetCommandLine https://msdn.microsoft.com/en-us/library/windows/desktop/ms683156.aspx
|
|
1725
|
-
*/
|
|
1726
|
-
export function spawn(file: string, args: string[] | string, options: IPtyForkOptions | IWindowsPtyForkOptions): IPty;
|
|
1727
|
-
|
|
1728
|
-
export interface IBasePtyForkOptions {
|
|
1729
|
-
/**
|
|
1730
|
-
* Name of the terminal to be set in environment ($TERM variable).
|
|
1731
|
-
*/
|
|
1732
|
-
name?: string;
|
|
1733
|
-
|
|
1734
|
-
/**
|
|
1735
|
-
* Number of intial cols of the pty.
|
|
1736
|
-
*/
|
|
1737
|
-
cols?: number;
|
|
1738
|
-
|
|
1739
|
-
/**
|
|
1740
|
-
* Number of initial rows of the pty.
|
|
1741
|
-
*/
|
|
1742
|
-
rows?: number;
|
|
1743
|
-
|
|
1744
|
-
/**
|
|
1745
|
-
* Working directory to be set for the child program.
|
|
1746
|
-
*/
|
|
1747
|
-
cwd?: string;
|
|
1748
|
-
|
|
1749
|
-
/**
|
|
1750
|
-
* Environment to be set for the child program.
|
|
1751
|
-
*/
|
|
1752
|
-
env?: Record<string, string | undefined>;
|
|
1753
|
-
|
|
1754
|
-
/**
|
|
1755
|
-
* String encoding of the underlying pty.
|
|
1756
|
-
* If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding.
|
|
1757
|
-
* If unset, incoming data will be delivered as raw bytes (Buffer type).
|
|
1758
|
-
* By default 'utf8' is assumed, to unset it explicitly set it to `null`.
|
|
1759
|
-
*/
|
|
1760
|
-
encoding?: string | null;
|
|
1761
|
-
|
|
1762
|
-
/**
|
|
1763
|
-
* (EXPERIMENTAL)
|
|
1764
|
-
* Whether to enable flow control handling (false by default). If enabled a message of `flowControlPause`
|
|
1765
|
-
* will pause the socket and thus blocking the child program execution due to buffer back pressure.
|
|
1766
|
-
* A message of `flowControlResume` will resume the socket into flow mode.
|
|
1767
|
-
* For performance reasons only a single message as a whole will match (no message part matching).
|
|
1768
|
-
* If flow control is enabled the `flowControlPause` and `flowControlResume` messages are not forwarded to
|
|
1769
|
-
* the underlying pseudoterminal.
|
|
1770
|
-
*/
|
|
1771
|
-
handleFlowControl?: boolean;
|
|
1772
|
-
|
|
1773
|
-
/**
|
|
1774
|
-
* (EXPERIMENTAL)
|
|
1775
|
-
* The string that should pause the pty when `handleFlowControl` is true. Default is XOFF ('\x13').
|
|
1776
|
-
*/
|
|
1777
|
-
flowControlPause?: string;
|
|
1778
|
-
|
|
1779
|
-
/**
|
|
1780
|
-
* (EXPERIMENTAL)
|
|
1781
|
-
* The string that should resume the pty when `handleFlowControl` is true. Default is XON ('\x11').
|
|
1782
|
-
*/
|
|
1783
|
-
flowControlResume?: string;
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
export interface IPtyForkOptions extends IBasePtyForkOptions {
|
|
1787
|
-
/**
|
|
1788
|
-
* Security warning: use this option with great caution,
|
|
1789
|
-
* as opened file descriptors with higher privileges might leak to the child program.
|
|
1790
|
-
*/
|
|
1791
|
-
uid?: number;
|
|
1792
|
-
gid?: number;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
|
|
1796
|
-
/**
|
|
1797
|
-
* Whether to use the ConPTY system on Windows. When this is not set, ConPTY will be used when
|
|
1798
|
-
* the Windows build number is >= 18309 (instead of winpty). Note that ConPTY is available from
|
|
1799
|
-
* build 17134 but is too unstable to enable by default.
|
|
1800
|
-
*
|
|
1801
|
-
* This setting does nothing on non-Windows.
|
|
1802
|
-
*/
|
|
1803
|
-
useConpty?: boolean;
|
|
1804
|
-
|
|
1805
|
-
/**
|
|
1806
|
-
* Whether to use PSEUDOCONSOLE_INHERIT_CURSOR in conpty.
|
|
1807
|
-
* @see https://docs.microsoft.com/en-us/windows/console/createpseudoconsole
|
|
1808
|
-
*/
|
|
1809
|
-
conptyInheritCursor?: boolean;
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
/**
|
|
1813
|
-
* An interface representing a pseudoterminal, on Windows this is emulated via the winpty library.
|
|
1814
|
-
*/
|
|
1815
|
-
export interface IPty {
|
|
1816
|
-
/**
|
|
1817
|
-
* The process ID of the outer process.
|
|
1818
|
-
*/
|
|
1819
|
-
readonly pid: number;
|
|
1820
|
-
|
|
1821
|
-
/**
|
|
1822
|
-
* The column size in characters.
|
|
1823
|
-
*/
|
|
1824
|
-
readonly cols: number;
|
|
1825
|
-
|
|
1826
|
-
/**
|
|
1827
|
-
* The row size in characters.
|
|
1828
|
-
*/
|
|
1829
|
-
readonly rows: number;
|
|
1830
|
-
|
|
1831
|
-
/**
|
|
1832
|
-
* The title of the active process.
|
|
1833
|
-
*/
|
|
1834
|
-
readonly process: string;
|
|
1835
|
-
|
|
1836
|
-
/**
|
|
1837
|
-
* (EXPERIMENTAL)
|
|
1838
|
-
* Whether to handle flow control. Useful to disable/re-enable flow control during runtime.
|
|
1839
|
-
* Use this for binary data that is likely to contain the `flowControlPause` string by accident.
|
|
1840
|
-
*/
|
|
1841
|
-
handleFlowControl: boolean;
|
|
1842
|
-
|
|
1843
|
-
/**
|
|
1844
|
-
* Adds an event listener for when a data event fires. This happens when data is returned from
|
|
1845
|
-
* the pty.
|
|
1846
|
-
* @returns an `IDisposable` to stop listening.
|
|
1847
|
-
*/
|
|
1848
|
-
readonly onData: IEvent<string>;
|
|
1849
|
-
|
|
1850
|
-
/**
|
|
1851
|
-
* Adds an event listener for when an exit event fires. This happens when the pty exits.
|
|
1852
|
-
* @returns an `IDisposable` to stop listening.
|
|
1853
|
-
*/
|
|
1854
|
-
readonly onExit: IEvent<{ exitCode: number; signal?: number }>;
|
|
1855
|
-
|
|
1856
|
-
/**
|
|
1857
|
-
* Resizes the dimensions of the pty.
|
|
1858
|
-
* @param columns The number of columns to use.
|
|
1859
|
-
* @param rows The number of rows to use.
|
|
1860
|
-
*/
|
|
1861
|
-
resize(columns: number, rows: number): void;
|
|
1862
|
-
|
|
1863
|
-
/**
|
|
1864
|
-
* Clears the pty's internal representation of its buffer. This is a no-op
|
|
1865
|
-
* unless on Windows/ConPTY. This is useful if the buffer is cleared on the
|
|
1866
|
-
* frontend in order to synchronize state with the backend to avoid ConPTY
|
|
1867
|
-
* possibly reprinting the screen.
|
|
1868
|
-
*/
|
|
1869
|
-
clear(): void;
|
|
1870
|
-
|
|
1871
|
-
/**
|
|
1872
|
-
* Writes data to the pty.
|
|
1873
|
-
* @param data The data to write.
|
|
1874
|
-
*/
|
|
1875
|
-
write(data: string): void;
|
|
1876
|
-
|
|
1877
|
-
/**
|
|
1878
|
-
* Kills the pty.
|
|
1879
|
-
* @param signal The signal to use, defaults to SIGHUP. This parameter is not supported on
|
|
1880
|
-
* Windows.
|
|
1881
|
-
* @throws Will throw when signal is used on Windows.
|
|
1882
|
-
*/
|
|
1883
|
-
kill(signal?: string): void;
|
|
1884
|
-
|
|
1885
|
-
/**
|
|
1886
|
-
* Pauses the pty for customizable flow control.
|
|
1887
|
-
*/
|
|
1888
|
-
pause(): void;
|
|
1889
|
-
|
|
1890
|
-
/**
|
|
1891
|
-
* Resumes the pty for customizable flow control.
|
|
1892
|
-
*/
|
|
1893
|
-
resume(): void;
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
/**
|
|
1897
|
-
* An object that can be disposed via a dispose function.
|
|
1898
|
-
*/
|
|
1899
|
-
export interface IDisposable {
|
|
1900
|
-
dispose(): void;
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
/**
|
|
1904
|
-
* An event that can be listened to.
|
|
1905
|
-
* @returns an `IDisposable` to stop listening.
|
|
1906
|
-
*/
|
|
1907
|
-
export type IEvent<T> = (listener: (e: T) => any) => IDisposable;
|
|
1908
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -484,7 +484,7 @@ function makeAbortError() {
|
|
|
484
484
|
return err;
|
|
485
485
|
}
|
|
486
486
|
// src/composables/bulkLane.ts
|
|
487
|
-
var PROBE_TIMEOUT_MS = 2e3;
|
|
487
|
+
var PROBE_TIMEOUT_MS$1 = 2e3;
|
|
488
488
|
var RECHECK_AFTER_MS = 6e4;
|
|
489
489
|
var deadMarks = /* @__PURE__ */ new WeakMap();
|
|
490
490
|
var runningProbes = /* @__PURE__ */ new WeakMap();
|
|
@@ -521,7 +521,7 @@ function probeBulkClient(client) {
|
|
|
521
521
|
}
|
|
522
522
|
async function answersPing(client) {
|
|
523
523
|
try {
|
|
524
|
-
await client.flush(PROBE_TIMEOUT_MS);
|
|
524
|
+
await client.flush(PROBE_TIMEOUT_MS$1);
|
|
525
525
|
return true;
|
|
526
526
|
} catch {
|
|
527
527
|
return false;
|
|
@@ -3668,7 +3668,8 @@ var STREAM_CONFIG = {
|
|
|
3668
3668
|
RECONNECT_DELAY: 1e3,
|
|
3669
3669
|
CONNECT_TIMEOUT: 1e4,
|
|
3670
3670
|
WS_CONNECT_TIMEOUT: 5e3,
|
|
3671
|
-
|
|
3671
|
+
DISCONNECT_GRACE: 5e3,
|
|
3672
|
+
ICE_SERVERS: [{ urls: "stun:stun.cloudflare.com:3478" }]
|
|
3672
3673
|
},
|
|
3673
3674
|
MSE: {
|
|
3674
3675
|
BUFFER_SIZE: 2097152,
|
|
@@ -3999,6 +4000,9 @@ function checkWebRTCCompatibility(probe) {
|
|
|
3999
4000
|
function hasManagedMediaSource() {
|
|
4000
4001
|
return "ManagedMediaSource" in window;
|
|
4001
4002
|
}
|
|
4003
|
+
function hasMediaSource() {
|
|
4004
|
+
return "MediaSource" in window || hasManagedMediaSource();
|
|
4005
|
+
}
|
|
4002
4006
|
function getMediaSourceConstructor() {
|
|
4003
4007
|
if (hasManagedMediaSource()) return window.ManagedMediaSource;
|
|
4004
4008
|
if ("MediaSource" in window) return window.MediaSource;
|
|
@@ -4281,6 +4285,101 @@ async function playVideo(videoElement) {
|
|
|
4281
4285
|
}
|
|
4282
4286
|
}
|
|
4283
4287
|
}
|
|
4288
|
+
// src/streaming/relayFirst.ts
|
|
4289
|
+
var PROBE_STUN_SERVERS = [{ urls: "stun:stun.cloudflare.com:3478" }, { urls: "stun:stun.l.google.com:19302" }];
|
|
4290
|
+
var PROBE_SETTLE_MS = 1500;
|
|
4291
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
4292
|
+
var PROBE_REUSE_MS = 6e4;
|
|
4293
|
+
var RELAY_WAIT_MS = 3e3;
|
|
4294
|
+
var RELAY_HEAD_START_MS = 500;
|
|
4295
|
+
var IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
4296
|
+
var lastProbe;
|
|
4297
|
+
function relayFirstAddress(iceServers) {
|
|
4298
|
+
if (!usesWebKitNetworking() || !iceServers?.some((server) => urlsOf(server).some(isIPv6OnlyStun))) return Promise.resolve(void 0);
|
|
4299
|
+
if (!lastProbe || Date.now() - lastProbe.startedAt > PROBE_REUSE_MS) lastProbe = {
|
|
4300
|
+
startedAt: Date.now(),
|
|
4301
|
+
address: probeSingleIPv4Target()
|
|
4302
|
+
};
|
|
4303
|
+
return lastProbe.address;
|
|
4304
|
+
}
|
|
4305
|
+
function relayFirstIceServers(iceServers) {
|
|
4306
|
+
return (iceServers ?? []).map((server) => ({
|
|
4307
|
+
...server,
|
|
4308
|
+
urls: urlsOf(server).filter((url) => !/^stuns?:/i.test(url) || isIPv6OnlyStun(url))
|
|
4309
|
+
})).filter((server) => server.urls.length > 0);
|
|
4310
|
+
}
|
|
4311
|
+
function relayPermitCandidate(address) {
|
|
4312
|
+
return `candidate:1 1 udp 1 ${address} 9 typ srflx raddr 0.0.0.0 rport 0`;
|
|
4313
|
+
}
|
|
4314
|
+
function orderRelayFirst(add) {
|
|
4315
|
+
const held = [];
|
|
4316
|
+
let relaySeen = false;
|
|
4317
|
+
let open = false;
|
|
4318
|
+
function release() {
|
|
4319
|
+
if (open) return;
|
|
4320
|
+
open = true;
|
|
4321
|
+
clearTimeout(wait);
|
|
4322
|
+
for (const candidate of held.splice(0)) add(candidate).catch(() => {});
|
|
4323
|
+
}
|
|
4324
|
+
const wait = setTimeout(release, RELAY_WAIT_MS);
|
|
4325
|
+
return async (candidate) => {
|
|
4326
|
+
if (open || !isUdpIPv4(candidate)) return add(candidate);
|
|
4327
|
+
if (relaySeen || !/ typ relay\b/.test(candidate)) {
|
|
4328
|
+
held.push(candidate);
|
|
4329
|
+
return;
|
|
4330
|
+
}
|
|
4331
|
+
relaySeen = true;
|
|
4332
|
+
setTimeout(release, RELAY_HEAD_START_MS);
|
|
4333
|
+
return add(candidate);
|
|
4334
|
+
};
|
|
4335
|
+
}
|
|
4336
|
+
function probeSingleIPv4Target() {
|
|
4337
|
+
return new Promise((resolve) => {
|
|
4338
|
+
let pc;
|
|
4339
|
+
try {
|
|
4340
|
+
pc = new RTCPeerConnection({ iceServers: PROBE_STUN_SERVERS });
|
|
4341
|
+
} catch {
|
|
4342
|
+
resolve(void 0);
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
let address;
|
|
4346
|
+
let settle;
|
|
4347
|
+
const timeout = setTimeout(() => finish(void 0), PROBE_TIMEOUT_MS);
|
|
4348
|
+
function finish(result) {
|
|
4349
|
+
clearTimeout(timeout);
|
|
4350
|
+
clearTimeout(settle);
|
|
4351
|
+
pc.onicecandidate = null;
|
|
4352
|
+
pc.close();
|
|
4353
|
+
resolve(result);
|
|
4354
|
+
}
|
|
4355
|
+
pc.onicecandidate = (event) => {
|
|
4356
|
+
if (!event.candidate) {
|
|
4357
|
+
finish(void 0);
|
|
4358
|
+
return;
|
|
4359
|
+
}
|
|
4360
|
+
const found = event.candidate.address ?? event.candidate.candidate.split(" ")[4];
|
|
4361
|
+
if (address || event.candidate.type !== "srflx" || !found || !IPV4.test(found)) return;
|
|
4362
|
+
address = found;
|
|
4363
|
+
settle = setTimeout(() => finish(address), PROBE_SETTLE_MS);
|
|
4364
|
+
};
|
|
4365
|
+
pc.createDataChannel("probe");
|
|
4366
|
+
pc.createOffer().then((offer) => pc.setLocalDescription(offer)).catch(() => finish(void 0));
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
function usesWebKitNetworking() {
|
|
4370
|
+
if (typeof navigator === "undefined") return false;
|
|
4371
|
+
return /AppleWebKit/i.test(navigator.userAgent) && !/Chrome|Chromium|Android|Edg/i.test(navigator.userAgent);
|
|
4372
|
+
}
|
|
4373
|
+
function urlsOf(server) {
|
|
4374
|
+
return Array.isArray(server.urls) ? server.urls : [server.urls];
|
|
4375
|
+
}
|
|
4376
|
+
function isIPv6OnlyStun(url) {
|
|
4377
|
+
return /^stuns?:\[/i.test(url);
|
|
4378
|
+
}
|
|
4379
|
+
function isUdpIPv4(candidate) {
|
|
4380
|
+
const fields = candidate.split(" ");
|
|
4381
|
+
return fields[2]?.toLowerCase() === "udp" && IPV4.test(fields[4] ?? "");
|
|
4382
|
+
}
|
|
4284
4383
|
// src/streaming/webrtc.ts
|
|
4285
4384
|
function newPeerConnection(iceServers = [...STREAM_CONFIG.WEBRTC.ICE_SERVERS]) {
|
|
4286
4385
|
try {
|
|
@@ -4320,12 +4419,40 @@ async function handleIceCandidate(pc, signal, candidate, filterUdp) {
|
|
|
4320
4419
|
sdpMid: "0"
|
|
4321
4420
|
});
|
|
4322
4421
|
}
|
|
4422
|
+
function createRelayFirst(state, signal, filterUdp, probe) {
|
|
4423
|
+
const add = (candidate) => handleIceCandidate(state.pc, signal, candidate, filterUdp);
|
|
4424
|
+
let address;
|
|
4425
|
+
let handle = add;
|
|
4426
|
+
return {
|
|
4427
|
+
async settle() {
|
|
4428
|
+
address = probe && !filterUdp ? await probe : void 0;
|
|
4429
|
+
if (address) handle = orderRelayFirst(add);
|
|
4430
|
+
},
|
|
4431
|
+
iceServers: (servers) => address ? relayFirstIceServers(servers) : servers,
|
|
4432
|
+
permit(send) {
|
|
4433
|
+
if (address && !signal.aborted) send(relayPermitCandidate(address));
|
|
4434
|
+
},
|
|
4435
|
+
handleCandidate: (candidate) => handle(candidate)
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4323
4438
|
async function setMicTrack(micTransceiver, signal, track) {
|
|
4324
4439
|
if (signal.aborted || !micTransceiver) return;
|
|
4325
4440
|
try {
|
|
4326
4441
|
await micTransceiver.sender.replaceTrack(track);
|
|
4327
4442
|
} catch {}
|
|
4328
4443
|
}
|
|
4444
|
+
async function readPeerPath(pc) {
|
|
4445
|
+
const reports = /* @__PURE__ */ new Map();
|
|
4446
|
+
let pairId;
|
|
4447
|
+
(await pc.getStats()).forEach((report, id) => {
|
|
4448
|
+
reports.set(id, report);
|
|
4449
|
+
if (report.type === "transport" && report.selectedCandidatePairId) pairId = report.selectedCandidatePairId;
|
|
4450
|
+
else if (report.type === "candidate-pair" && report.selected) pairId ??= id;
|
|
4451
|
+
});
|
|
4452
|
+
const pair = pairId ? reports.get(pairId) : void 0;
|
|
4453
|
+
if (!pair) return void 0;
|
|
4454
|
+
return [reports.get(pair.localCandidateId)?.candidateType, reports.get(pair.remoteCandidateId)?.candidateType].includes("relay") ? "relay" : "direct";
|
|
4455
|
+
}
|
|
4329
4456
|
function closePeerConnection(state) {
|
|
4330
4457
|
if (state.micTransceiver) {
|
|
4331
4458
|
state.micTransceiver.sender.replaceTrack(null).catch(() => {});
|
|
@@ -4339,21 +4466,38 @@ function closePeerConnection(state) {
|
|
|
4339
4466
|
state.isConnected = false;
|
|
4340
4467
|
}
|
|
4341
4468
|
function createWebRTCHandler(options) {
|
|
4342
|
-
const { mode, iceServers, onConnected, onDisconnected, onFailed, onCandidate, signal } = options;
|
|
4469
|
+
const { mode, iceServers, relayFirst, onConnected, onReconnected, onDisconnected, onFailed, onCandidate, onPath, signal } = options;
|
|
4343
4470
|
const state = {
|
|
4344
4471
|
pc: null,
|
|
4345
4472
|
micTransceiver: null,
|
|
4346
4473
|
isConnected: false
|
|
4347
4474
|
};
|
|
4348
4475
|
const filterUdp = mode === "webrtc/tcp";
|
|
4476
|
+
const relay = createRelayFirst(state, signal, filterUdp, relayFirst);
|
|
4477
|
+
async function handleAnswer(sdp) {
|
|
4478
|
+
await handleSdpAnswer(state.pc, signal, sdp);
|
|
4479
|
+
relay.permit(onCandidate);
|
|
4480
|
+
}
|
|
4349
4481
|
function createPeerConnection() {
|
|
4350
|
-
const peerConnection = createBasePeerConnection(signal, onCandidate, filterUdp, iceServers);
|
|
4482
|
+
const peerConnection = createBasePeerConnection(signal, onCandidate, filterUdp, relay.iceServers(iceServers));
|
|
4483
|
+
let stream;
|
|
4484
|
+
function reportPath() {
|
|
4485
|
+
readPeerPath(peerConnection).then((path) => {
|
|
4486
|
+
if (path && !signal.aborted && state.pc === peerConnection) onPath(path);
|
|
4487
|
+
}, () => {});
|
|
4488
|
+
}
|
|
4351
4489
|
peerConnection.onconnectionstatechange = () => {
|
|
4352
4490
|
if (signal.aborted) return;
|
|
4353
4491
|
const connectionState = peerConnection.connectionState;
|
|
4354
4492
|
if (connectionState === "connected") {
|
|
4355
4493
|
state.isConnected = true;
|
|
4356
|
-
|
|
4494
|
+
reportPath();
|
|
4495
|
+
if (stream) {
|
|
4496
|
+
onReconnected();
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
4499
|
+
stream = new MediaStream(peerConnection.getTransceivers().filter((tr) => tr.currentDirection === "recvonly").map((tr) => tr.receiver.track));
|
|
4500
|
+
peerConnection.getReceivers()[0]?.transport?.iceTransport?.addEventListener("selectedcandidatepairchange", reportPath);
|
|
4357
4501
|
onConnected(stream);
|
|
4358
4502
|
} else if (connectionState === "failed") {
|
|
4359
4503
|
state.isConnected = false;
|
|
@@ -4363,11 +4507,16 @@ function createWebRTCHandler(options) {
|
|
|
4363
4507
|
onDisconnected();
|
|
4364
4508
|
}
|
|
4365
4509
|
};
|
|
4510
|
+
peerConnection.oniceconnectionstatechange = () => {
|
|
4511
|
+
if (!signal.aborted && state.isConnected) reportPath();
|
|
4512
|
+
};
|
|
4366
4513
|
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
4367
4514
|
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
|
4368
4515
|
return peerConnection;
|
|
4369
4516
|
}
|
|
4370
4517
|
async function createOffer() {
|
|
4518
|
+
if (signal.aborted) return void 0;
|
|
4519
|
+
await relay.settle();
|
|
4371
4520
|
if (signal.aborted) return void 0;
|
|
4372
4521
|
state.pc = createPeerConnection();
|
|
4373
4522
|
const offer = await state.pc.createOffer();
|
|
@@ -4386,13 +4535,13 @@ function createWebRTCHandler(options) {
|
|
|
4386
4535
|
return state.isConnected;
|
|
4387
4536
|
},
|
|
4388
4537
|
createOffer,
|
|
4389
|
-
handleAnswer
|
|
4390
|
-
handleCandidate:
|
|
4538
|
+
handleAnswer,
|
|
4539
|
+
handleCandidate: relay.handleCandidate,
|
|
4391
4540
|
close
|
|
4392
4541
|
};
|
|
4393
4542
|
}
|
|
4394
4543
|
function createBackchannelHandler(options) {
|
|
4395
|
-
const { filterUdp, iceServers, onConnected, onFailed, onCandidate, signal } = options;
|
|
4544
|
+
const { filterUdp, iceServers, relayFirst, onConnected, onFailed, onCandidate, signal } = options;
|
|
4396
4545
|
const state = {
|
|
4397
4546
|
pc: null,
|
|
4398
4547
|
micTransceiver: null,
|
|
@@ -4400,6 +4549,8 @@ function createBackchannelHandler(options) {
|
|
|
4400
4549
|
};
|
|
4401
4550
|
let answered = false;
|
|
4402
4551
|
const heldCandidates = [];
|
|
4552
|
+
let disconnectGrace;
|
|
4553
|
+
const relay = createRelayFirst(state, signal, filterUdp, relayFirst);
|
|
4403
4554
|
function sendCandidate(candidate) {
|
|
4404
4555
|
if (answered) onCandidate(candidate);
|
|
4405
4556
|
else heldCandidates.push(candidate);
|
|
@@ -4408,21 +4559,26 @@ function createBackchannelHandler(options) {
|
|
|
4408
4559
|
if (signal.aborted || !state.pc) return;
|
|
4409
4560
|
answered = true;
|
|
4410
4561
|
for (const candidate of heldCandidates.splice(0)) onCandidate(candidate);
|
|
4562
|
+
relay.permit(onCandidate);
|
|
4411
4563
|
await state.pc.setRemoteDescription({
|
|
4412
4564
|
type: "answer",
|
|
4413
4565
|
sdp
|
|
4414
4566
|
});
|
|
4415
4567
|
}
|
|
4416
4568
|
function createPeerConnection() {
|
|
4417
|
-
const peerConnection = createBasePeerConnection(signal, sendCandidate, filterUdp, iceServers);
|
|
4569
|
+
const peerConnection = createBasePeerConnection(signal, sendCandidate, filterUdp, relay.iceServers(iceServers));
|
|
4418
4570
|
peerConnection.onconnectionstatechange = () => {
|
|
4419
4571
|
if (signal.aborted) return;
|
|
4420
4572
|
const connectionState = peerConnection.connectionState;
|
|
4421
4573
|
if (connectionState === "connected") {
|
|
4574
|
+
clearTimeout(disconnectGrace);
|
|
4422
4575
|
state.isConnected = true;
|
|
4423
4576
|
onConnected();
|
|
4424
|
-
} else if (connectionState === "disconnected")
|
|
4425
|
-
|
|
4577
|
+
} else if (connectionState === "disconnected") {
|
|
4578
|
+
state.isConnected = false;
|
|
4579
|
+
disconnectGrace = setTimeout(onFailed, STREAM_CONFIG.WEBRTC.DISCONNECT_GRACE);
|
|
4580
|
+
} else if (connectionState === "failed") {
|
|
4581
|
+
clearTimeout(disconnectGrace);
|
|
4426
4582
|
state.isConnected = false;
|
|
4427
4583
|
onFailed();
|
|
4428
4584
|
}
|
|
@@ -4431,6 +4587,8 @@ function createBackchannelHandler(options) {
|
|
|
4431
4587
|
return peerConnection;
|
|
4432
4588
|
}
|
|
4433
4589
|
async function createOffer() {
|
|
4590
|
+
if (signal.aborted) return void 0;
|
|
4591
|
+
await relay.settle();
|
|
4434
4592
|
if (signal.aborted) return void 0;
|
|
4435
4593
|
state.pc = createPeerConnection();
|
|
4436
4594
|
const offer = await state.pc.createOffer();
|
|
@@ -4438,6 +4596,7 @@ function createBackchannelHandler(options) {
|
|
|
4438
4596
|
return offer.sdp;
|
|
4439
4597
|
}
|
|
4440
4598
|
function close() {
|
|
4599
|
+
clearTimeout(disconnectGrace);
|
|
4441
4600
|
closePeerConnection(state);
|
|
4442
4601
|
}
|
|
4443
4602
|
signal.addEventListener("abort", close, { once: true });
|
|
@@ -4447,7 +4606,7 @@ function createBackchannelHandler(options) {
|
|
|
4447
4606
|
},
|
|
4448
4607
|
createOffer,
|
|
4449
4608
|
handleAnswer,
|
|
4450
|
-
handleCandidate:
|
|
4609
|
+
handleCandidate: relay.handleCandidate,
|
|
4451
4610
|
setMicrophoneTrack: (track) => setMicTrack(state.micTransceiver, signal, track),
|
|
4452
4611
|
close
|
|
4453
4612
|
};
|
|
@@ -4472,6 +4631,11 @@ var STALL_AFTER = 3e3;
|
|
|
4472
4631
|
var STALL_RECONNECT_AFTER = 15e3;
|
|
4473
4632
|
var HAVE_CURRENT_DATA$1 = 2;
|
|
4474
4633
|
var PROBE_WAIT_MS = 2e3;
|
|
4634
|
+
var WEBRTC_RETRY_DELAYS = [
|
|
4635
|
+
3e4,
|
|
4636
|
+
12e4,
|
|
4637
|
+
6e5
|
|
4638
|
+
];
|
|
4475
4639
|
function isPageHidden() {
|
|
4476
4640
|
return typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
4477
4641
|
}
|
|
@@ -4495,6 +4659,8 @@ var StreamConnection = class {
|
|
|
4495
4659
|
hasBackchannel;
|
|
4496
4660
|
isPlaying;
|
|
4497
4661
|
stalled;
|
|
4662
|
+
connectionPath;
|
|
4663
|
+
webrtcUnreachable;
|
|
4498
4664
|
error;
|
|
4499
4665
|
probeInfo;
|
|
4500
4666
|
muted;
|
|
@@ -4512,6 +4678,7 @@ var StreamConnection = class {
|
|
|
4512
4678
|
iceServers;
|
|
4513
4679
|
scope = effectScope(true);
|
|
4514
4680
|
stopsWhenHidden = suspendsWhenHidden();
|
|
4681
|
+
webrtcPath = ref();
|
|
4515
4682
|
connectionGeneration = 0;
|
|
4516
4683
|
abortController = new AbortController();
|
|
4517
4684
|
probedSourceId;
|
|
@@ -4548,6 +4715,9 @@ var StreamConnection = class {
|
|
|
4548
4715
|
lastMediaTime = 0;
|
|
4549
4716
|
lastFrameCount;
|
|
4550
4717
|
frameCounterLive = false;
|
|
4718
|
+
webrtcRetries = 0;
|
|
4719
|
+
webrtcRetryDue = false;
|
|
4720
|
+
webrtcPlayingSince;
|
|
4551
4721
|
offTabVisible;
|
|
4552
4722
|
offTabPaused;
|
|
4553
4723
|
onVideoPauseBound;
|
|
@@ -4559,6 +4729,10 @@ var StreamConnection = class {
|
|
|
4559
4729
|
stopConnectTimeout;
|
|
4560
4730
|
startMseConnectTimeout;
|
|
4561
4731
|
stopMseConnectTimeout;
|
|
4732
|
+
startDisconnectGrace;
|
|
4733
|
+
stopDisconnectGrace;
|
|
4734
|
+
startWebRTCRetry;
|
|
4735
|
+
stopWebRTCRetry;
|
|
4562
4736
|
constructor(options) {
|
|
4563
4737
|
this.options = options;
|
|
4564
4738
|
const ctx = useCameraUi();
|
|
@@ -4578,6 +4752,7 @@ var StreamConnection = class {
|
|
|
4578
4752
|
this.hasBackchannel = ref(false);
|
|
4579
4753
|
this.isPlaying = ref(false);
|
|
4580
4754
|
this.stalled = ref(false);
|
|
4755
|
+
this.webrtcUnreachable = ref(false);
|
|
4581
4756
|
this.error = ref();
|
|
4582
4757
|
this.probeInfo = shallowRef();
|
|
4583
4758
|
this.muted = ref(true);
|
|
@@ -4590,6 +4765,10 @@ var StreamConnection = class {
|
|
|
4590
4765
|
this.videoElement = computed(() => toValue(options.videoElement));
|
|
4591
4766
|
this.containerElement = computed(() => toValue(options.containerElement));
|
|
4592
4767
|
this.isReady = computed(() => !!this.camera.value && !!this.videoElement.value && !!this.target.value);
|
|
4768
|
+
this.connectionPath = computed(() => {
|
|
4769
|
+
if (this.status.value !== "connected") return void 0;
|
|
4770
|
+
return this.activeMode.value === "mse" || this.activeMode.value === "mjpeg" ? "tunnel" : this.webrtcPath.value;
|
|
4771
|
+
});
|
|
4593
4772
|
this.scope.run(() => {
|
|
4594
4773
|
const { onTabPaused, onTabVisible } = useTabVisibility();
|
|
4595
4774
|
const hiddenStopDelay = this.stopsWhenHidden ? 0 : DEFAULT_TAB_PAUSE_MS;
|
|
@@ -4605,10 +4784,8 @@ var StreamConnection = class {
|
|
|
4605
4784
|
}, STREAM_CONFIG.WEBRTC.WS_CONNECT_TIMEOUT, { immediate: false });
|
|
4606
4785
|
const connectTimeout = useTimeoutFn(() => {
|
|
4607
4786
|
if (this.abortController.signal.aborted) return;
|
|
4608
|
-
if (this.webrtcHandler && !this.webrtcHandler.isConnected)
|
|
4609
|
-
|
|
4610
|
-
else this.retryAfterNoMedia();
|
|
4611
|
-
} else if (this.webrtcHandler && this.awaitingWebRTCFrame || this.mjpegHandler && !this.isPlaying.value || this.status.value === "connected" && !this.isPlaying.value) {
|
|
4787
|
+
if (this.webrtcHandler && !this.webrtcHandler.isConnected) this.handleWebRTCFailed(true);
|
|
4788
|
+
else if (this.webrtcHandler && this.awaitingWebRTCFrame || this.mjpegHandler && !this.isPlaying.value || this.status.value === "connected" && !this.isPlaying.value) {
|
|
4612
4789
|
if (isPageHidden()) {
|
|
4613
4790
|
this.startConnectTimeout();
|
|
4614
4791
|
return;
|
|
@@ -4620,12 +4797,18 @@ var StreamConnection = class {
|
|
|
4620
4797
|
if (this.abortController.signal.aborted || this.status.value === "connected" || this.status.value === "closed") return;
|
|
4621
4798
|
if (this.mseHandler && !this.mseHandler.isReady) this.retryAfterNoMedia();
|
|
4622
4799
|
}, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
|
|
4800
|
+
const disconnectGrace = useTimeoutFn(() => this.handleWebRTCLost(), STREAM_CONFIG.WEBRTC.DISCONNECT_GRACE, { immediate: false });
|
|
4801
|
+
const webrtcRetry = useTimeoutFn(() => this.retryWebRTC(), () => WEBRTC_RETRY_DELAYS[Math.min(this.webrtcRetries, WEBRTC_RETRY_DELAYS.length - 1)], { immediate: false });
|
|
4623
4802
|
this.startWsConnectTimeout = wsConnectTimeout.start;
|
|
4624
4803
|
this.stopWsConnectTimeout = wsConnectTimeout.stop;
|
|
4625
4804
|
this.startConnectTimeout = connectTimeout.start;
|
|
4626
4805
|
this.stopConnectTimeout = connectTimeout.stop;
|
|
4627
4806
|
this.startMseConnectTimeout = mseConnectTimeout.start;
|
|
4628
4807
|
this.stopMseConnectTimeout = mseConnectTimeout.stop;
|
|
4808
|
+
this.startDisconnectGrace = disconnectGrace.start;
|
|
4809
|
+
this.stopDisconnectGrace = disconnectGrace.stop;
|
|
4810
|
+
this.startWebRTCRetry = webrtcRetry.start;
|
|
4811
|
+
this.stopWebRTCRetry = webrtcRetry.stop;
|
|
4629
4812
|
this.setupWatchers();
|
|
4630
4813
|
this.offTabPaused = onTabPaused(() => {
|
|
4631
4814
|
log.debug(`onTabPaused fired — status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
|
|
@@ -4658,6 +4841,11 @@ var StreamConnection = class {
|
|
|
4658
4841
|
this.restart();
|
|
4659
4842
|
return;
|
|
4660
4843
|
}
|
|
4844
|
+
if (this.webrtcRetryDue) {
|
|
4845
|
+
log.debug("onTabVisible: trying WebRTC again");
|
|
4846
|
+
this.retryWebRTC();
|
|
4847
|
+
return;
|
|
4848
|
+
}
|
|
4661
4849
|
log.debug("onTabVisible — not paused by visibility, no-op");
|
|
4662
4850
|
});
|
|
4663
4851
|
if (autoStart) whenever(this.isReady, () => {
|
|
@@ -4710,6 +4898,8 @@ var StreamConnection = class {
|
|
|
4710
4898
|
this.status.value = "connecting";
|
|
4711
4899
|
this.error.value = void 0;
|
|
4712
4900
|
this.isPlaying.value = false;
|
|
4901
|
+
this.webrtcUnreachable.value = false;
|
|
4902
|
+
if (this.requestedMode.value !== "mse") relayFirstAddress(this.iceServers?.value);
|
|
4713
4903
|
try {
|
|
4714
4904
|
const init = this.initializeSource();
|
|
4715
4905
|
if (init === "unsupported") {
|
|
@@ -4993,6 +5183,10 @@ var StreamConnection = class {
|
|
|
4993
5183
|
if (active) this.startHealthCheck();
|
|
4994
5184
|
}, { immediate: true });
|
|
4995
5185
|
this.stopWatchers.push(stopHealthWatch);
|
|
5186
|
+
const stopPathWatch = watch(this.connectionPath, (path) => {
|
|
5187
|
+
if (path) log.debug(`connection path: ${path}`);
|
|
5188
|
+
});
|
|
5189
|
+
this.stopWatchers.push(stopPathWatch);
|
|
4996
5190
|
const stopCameraWatch = watch([
|
|
4997
5191
|
this.camera,
|
|
4998
5192
|
this.videoElement,
|
|
@@ -5129,8 +5323,7 @@ var StreamConnection = class {
|
|
|
5129
5323
|
handleNoFramesWhileConnected() {
|
|
5130
5324
|
if (this.sourceLikelyH265() && !this.webrtcTookOver()) reportH265DecodeFailure(this.activeMode.value === "mse" ? "mse" : "webrtc");
|
|
5131
5325
|
if (this.requestedMode.value === "auto" && this.activeMode.value !== "mse" && !this.mjpegHandler && !this.mseRejected) {
|
|
5132
|
-
this.
|
|
5133
|
-
this.webrtcHandler = void 0;
|
|
5326
|
+
this.closeWebRTC();
|
|
5134
5327
|
this.awaitingWebRTCFrame = false;
|
|
5135
5328
|
this.activeMode.value = "mse";
|
|
5136
5329
|
this.isPlaying.value = false;
|
|
@@ -5333,7 +5526,7 @@ var StreamConnection = class {
|
|
|
5333
5526
|
if (peer && peer === this.webrtcHandler && !peer.isConnected) {
|
|
5334
5527
|
this.lastError = err;
|
|
5335
5528
|
this.stopConnectTimeout();
|
|
5336
|
-
if (this.requestedMode.value === "auto") this.fallBackToMSE();
|
|
5529
|
+
if (this.requestedMode.value === "auto") this.fallBackToMSE(false);
|
|
5337
5530
|
else this.retryLater(err);
|
|
5338
5531
|
return;
|
|
5339
5532
|
}
|
|
@@ -5370,14 +5563,19 @@ var StreamConnection = class {
|
|
|
5370
5563
|
const handler = createWebRTCHandler({
|
|
5371
5564
|
mode,
|
|
5372
5565
|
iceServers: this.iceServers?.value,
|
|
5566
|
+
relayFirst: relayFirstAddress(this.iceServers?.value),
|
|
5373
5567
|
signal: this.abortController.signal,
|
|
5374
5568
|
onConnected: (stream) => this.handleWebRTCConnected(stream),
|
|
5569
|
+
onReconnected: () => this.handleWebRTCReconnected(),
|
|
5375
5570
|
onDisconnected: () => this.handleWebRTCDisconnected(),
|
|
5376
|
-
onFailed: () => this.handleWebRTCFailed(),
|
|
5571
|
+
onFailed: () => this.handleWebRTCFailed(true),
|
|
5377
5572
|
onCandidate: (candidate) => this.sendWsMessage({
|
|
5378
5573
|
type: "webrtc/candidate",
|
|
5379
5574
|
value: candidate
|
|
5380
|
-
})
|
|
5575
|
+
}),
|
|
5576
|
+
onPath: (path) => {
|
|
5577
|
+
this.webrtcPath.value = path;
|
|
5578
|
+
}
|
|
5381
5579
|
});
|
|
5382
5580
|
this.webrtcHandler = handler;
|
|
5383
5581
|
const offer = await handler.createOffer();
|
|
@@ -5385,12 +5583,14 @@ var StreamConnection = class {
|
|
|
5385
5583
|
}
|
|
5386
5584
|
handleWebRTCConnected(stream) {
|
|
5387
5585
|
if (this.abortController.signal.aborted) return;
|
|
5586
|
+
this.stopDisconnectGrace();
|
|
5388
5587
|
const video = this.videoElement.value;
|
|
5389
5588
|
if (!video) return;
|
|
5390
5589
|
if (this.hasVideo.value && stream.getVideoTracks().length === 0) {
|
|
5391
|
-
this.handleWebRTCFailed();
|
|
5590
|
+
this.handleWebRTCFailed(false);
|
|
5392
5591
|
return;
|
|
5393
5592
|
}
|
|
5593
|
+
this.webrtcUnreachable.value = false;
|
|
5394
5594
|
this.noteDeliveredAudio(stream.getAudioTracks().length > 0);
|
|
5395
5595
|
this.activeMode.value = this.requestedMode.value === "auto" ? "webrtc" : this.requestedMode.value;
|
|
5396
5596
|
this.lastMediaStream = stream;
|
|
@@ -5407,26 +5607,35 @@ var StreamConnection = class {
|
|
|
5407
5607
|
if (this.requestedMode.value === "auto" && this.mseHandler) this.dropMSE();
|
|
5408
5608
|
}
|
|
5409
5609
|
handleWebRTCDisconnected() {
|
|
5410
|
-
if (this.status.value
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5610
|
+
if (this.status.value === "closed" || this.abortController.signal.aborted) return;
|
|
5611
|
+
log.debug("WebRTC disconnected, waiting for it to come back");
|
|
5612
|
+
if (this.activeMode.value !== "mse") this.status.value = "reconnecting";
|
|
5613
|
+
this.startDisconnectGrace();
|
|
5614
|
+
}
|
|
5615
|
+
handleWebRTCReconnected() {
|
|
5616
|
+
if (this.abortController.signal.aborted) return;
|
|
5617
|
+
log.debug("WebRTC connected again");
|
|
5618
|
+
this.stopDisconnectGrace();
|
|
5619
|
+
this.status.value = "connected";
|
|
5620
|
+
}
|
|
5621
|
+
handleWebRTCLost() {
|
|
5622
|
+
log.debug("WebRTC did not come back");
|
|
5623
|
+
if (this.requestedMode.value !== "auto" || this.mseRejected || !hasMediaSource()) {
|
|
5624
|
+
this.retryLater();
|
|
5625
|
+
return;
|
|
5419
5626
|
}
|
|
5627
|
+
if (this.activeMode.value !== "mse") this.isPlaying.value = false;
|
|
5628
|
+
this.fallBackToMSE(true);
|
|
5420
5629
|
}
|
|
5421
|
-
handleWebRTCFailed() {
|
|
5630
|
+
handleWebRTCFailed(retryWebRTC) {
|
|
5422
5631
|
if (this.abortController.signal.aborted) return;
|
|
5423
5632
|
this.stopConnectTimeout();
|
|
5424
|
-
if (this.requestedMode.value === "auto") this.fallBackToMSE();
|
|
5633
|
+
if (this.requestedMode.value === "auto") this.fallBackToMSE(retryWebRTC);
|
|
5425
5634
|
else this.retryLater(this.lastError ?? /* @__PURE__ */ new Error("WebRTC connection failed"));
|
|
5426
5635
|
}
|
|
5427
|
-
fallBackToMSE() {
|
|
5428
|
-
this.
|
|
5429
|
-
this.
|
|
5636
|
+
fallBackToMSE(retryWebRTC) {
|
|
5637
|
+
this.closeWebRTC();
|
|
5638
|
+
this.webrtcUnreachable.value = retryWebRTC;
|
|
5430
5639
|
this.awaitingWebRTCFrame = false;
|
|
5431
5640
|
if (this.mseRejected) {
|
|
5432
5641
|
this.retryLater(this.lastError);
|
|
@@ -5436,6 +5645,33 @@ var StreamConnection = class {
|
|
|
5436
5645
|
if (this.mseHandler?.isReady) this.handleMSEReady();
|
|
5437
5646
|
else this.startMSE();
|
|
5438
5647
|
this.resumeBackchannel();
|
|
5648
|
+
if (retryWebRTC) this.scheduleWebRTCRetry();
|
|
5649
|
+
}
|
|
5650
|
+
scheduleWebRTCRetry() {
|
|
5651
|
+
if (this.webrtcPlayingSince !== void 0 && performance.now() - this.webrtcPlayingSince >= RETRY_RESET_AFTER) this.webrtcRetries = 0;
|
|
5652
|
+
this.webrtcPlayingSince = void 0;
|
|
5653
|
+
this.startWebRTCRetry();
|
|
5654
|
+
}
|
|
5655
|
+
retryWebRTC() {
|
|
5656
|
+
if (isPageHidden()) {
|
|
5657
|
+
this.webrtcRetryDue = true;
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
5660
|
+
this.webrtcRetryDue = false;
|
|
5661
|
+
this.webrtcRetries++;
|
|
5662
|
+
const signalingBusy = !!this.offerPeer || !!this.backchannelHandler;
|
|
5663
|
+
if (this.requestedMode.value !== "auto" || this.activeMode.value !== "mse" || this.status.value !== "connected" || this.webrtcHandler || signalingBusy) {
|
|
5664
|
+
this.startWebRTCRetry();
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
5667
|
+
log.debug("trying WebRTC again while MSE plays");
|
|
5668
|
+
this.startWebRTC("webrtc");
|
|
5669
|
+
}
|
|
5670
|
+
closeWebRTC() {
|
|
5671
|
+
this.stopDisconnectGrace();
|
|
5672
|
+
this.webrtcHandler?.close();
|
|
5673
|
+
this.webrtcHandler = void 0;
|
|
5674
|
+
this.webrtcPath.value = void 0;
|
|
5439
5675
|
}
|
|
5440
5676
|
startMSE() {
|
|
5441
5677
|
if (this.mseHandler) return;
|
|
@@ -5599,6 +5835,7 @@ var StreamConnection = class {
|
|
|
5599
5835
|
if (!this.webrtcHandler || this.webrtcHandler.isConnected) {
|
|
5600
5836
|
this.stopConnectTimeout();
|
|
5601
5837
|
this.awaitingWebRTCFrame = false;
|
|
5838
|
+
if (this.webrtcHandler) this.webrtcPlayingSince = performance.now();
|
|
5602
5839
|
this.resumeBackchannel();
|
|
5603
5840
|
}
|
|
5604
5841
|
this.error.value = void 0;
|
|
@@ -5680,12 +5917,15 @@ var StreamConnection = class {
|
|
|
5680
5917
|
this.stopConnectTimeout();
|
|
5681
5918
|
this.stopWsConnectTimeout();
|
|
5682
5919
|
this.stopMseConnectTimeout();
|
|
5920
|
+
this.stopWebRTCRetry();
|
|
5921
|
+
this.webrtcRetryDue = false;
|
|
5922
|
+
this.webrtcRetries = 0;
|
|
5923
|
+
this.webrtcPlayingSince = void 0;
|
|
5683
5924
|
this.stopHealthCheck();
|
|
5684
5925
|
this.cancelFirstFrameWatch?.();
|
|
5685
5926
|
this.awaitingWebRTCFrame = false;
|
|
5686
5927
|
this.lastMediaStream = null;
|
|
5687
|
-
this.
|
|
5688
|
-
this.webrtcHandler = void 0;
|
|
5928
|
+
this.closeWebRTC();
|
|
5689
5929
|
this.mseHandler?.close();
|
|
5690
5930
|
this.mseHandler = void 0;
|
|
5691
5931
|
this.mjpegHandler?.close();
|
|
@@ -5709,6 +5949,7 @@ var StreamConnection = class {
|
|
|
5709
5949
|
const handler = createBackchannelHandler({
|
|
5710
5950
|
filterUdp: this.requestedMode.value === "webrtc/tcp",
|
|
5711
5951
|
iceServers: this.iceServers?.value,
|
|
5952
|
+
relayFirst: relayFirstAddress(this.iceServers?.value),
|
|
5712
5953
|
signal: this.abortController.signal,
|
|
5713
5954
|
onCandidate: (candidate) => this.sendWsMessage({
|
|
5714
5955
|
type: "webrtc/candidate",
|
|
@@ -5920,16 +6161,46 @@ function restartConnectedStreams() {
|
|
|
5920
6161
|
return streamManager.restartConnected();
|
|
5921
6162
|
}
|
|
5922
6163
|
// src/composables/useFullscreen.ts
|
|
6164
|
+
var documentFullscreen = {
|
|
6165
|
+
async enter() {
|
|
6166
|
+
if (!document.fullscreenEnabled || document.fullscreenElement) return false;
|
|
6167
|
+
await document.documentElement.requestFullscreen({ navigationUI: "hide" });
|
|
6168
|
+
return true;
|
|
6169
|
+
},
|
|
6170
|
+
async exit() {
|
|
6171
|
+
if (document.fullscreenElement) await document.exitFullscreen();
|
|
6172
|
+
}
|
|
6173
|
+
};
|
|
5923
6174
|
var fsRegistry = /* @__PURE__ */ new WeakMap();
|
|
5924
6175
|
var fullscreenRoot = null;
|
|
6176
|
+
var nativeFullscreen = documentFullscreen;
|
|
6177
|
+
var nativeOwned = false;
|
|
5925
6178
|
function setFullscreenRoot(el) {
|
|
5926
6179
|
fullscreenRoot = el;
|
|
5927
6180
|
}
|
|
6181
|
+
function setNativeFullscreen(native) {
|
|
6182
|
+
nativeFullscreen = native;
|
|
6183
|
+
}
|
|
5928
6184
|
var fullscreenStack = [];
|
|
5929
6185
|
var topmostFullscreenWrapper = shallowRef(null);
|
|
5930
6186
|
function refreshTopmost() {
|
|
5931
6187
|
topmostFullscreenWrapper.value = fullscreenStack[fullscreenStack.length - 1] ?? null;
|
|
5932
6188
|
}
|
|
6189
|
+
function enterNative() {
|
|
6190
|
+
if (!nativeFullscreen || nativeOwned) return;
|
|
6191
|
+
nativeOwned = true;
|
|
6192
|
+
nativeFullscreen.enter().then((taken) => {
|
|
6193
|
+
nativeOwned = taken;
|
|
6194
|
+
exitNative();
|
|
6195
|
+
}, () => {
|
|
6196
|
+
nativeOwned = false;
|
|
6197
|
+
});
|
|
6198
|
+
}
|
|
6199
|
+
function exitNative() {
|
|
6200
|
+
if (!nativeOwned || fullscreenStack.length > 0) return;
|
|
6201
|
+
nativeOwned = false;
|
|
6202
|
+
nativeFullscreen?.exit();
|
|
6203
|
+
}
|
|
5933
6204
|
function useTopmostFullscreenElement() {
|
|
5934
6205
|
return computed(() => topmostFullscreenWrapper.value);
|
|
5935
6206
|
}
|
|
@@ -5943,9 +6214,13 @@ function useCuiFullscreen(target, options = {}) {
|
|
|
5943
6214
|
function onEscape(e) {
|
|
5944
6215
|
if (e.key === "Escape" && isFullscreen.value) exit();
|
|
5945
6216
|
}
|
|
6217
|
+
function onNativeChange() {
|
|
6218
|
+
if (nativeOwned && nativeFullscreen === documentFullscreen && !document.fullscreenElement) exit();
|
|
6219
|
+
}
|
|
5946
6220
|
async function enter() {
|
|
5947
6221
|
const el = currentEl.value;
|
|
5948
6222
|
if (!el || isFullscreen.value) return;
|
|
6223
|
+
enterNative();
|
|
5949
6224
|
const wrapper = document.createElement("div");
|
|
5950
6225
|
wrapper.className = `cui-fullscreen cui-fullscreen-${mode}`;
|
|
5951
6226
|
applyWrapperStyles(wrapper, mode);
|
|
@@ -5970,6 +6245,7 @@ function useCuiFullscreen(target, options = {}) {
|
|
|
5970
6245
|
behavior: "instant"
|
|
5971
6246
|
});
|
|
5972
6247
|
document.addEventListener("keydown", onEscape);
|
|
6248
|
+
document.addEventListener("fullscreenchange", onNativeChange);
|
|
5973
6249
|
fullscreenStack.push(wrapper);
|
|
5974
6250
|
refreshTopmost();
|
|
5975
6251
|
isFullscreen.value = true;
|
|
@@ -5991,9 +6267,11 @@ function useCuiFullscreen(target, options = {}) {
|
|
|
5991
6267
|
behavior: "instant"
|
|
5992
6268
|
});
|
|
5993
6269
|
document.removeEventListener("keydown", onEscape);
|
|
6270
|
+
document.removeEventListener("fullscreenchange", onNativeChange);
|
|
5994
6271
|
const stackIndex = fullscreenStack.indexOf(state.wrapper);
|
|
5995
6272
|
if (stackIndex !== -1) fullscreenStack.splice(stackIndex, 1);
|
|
5996
6273
|
refreshTopmost();
|
|
6274
|
+
exitNative();
|
|
5997
6275
|
isFullscreen.value = false;
|
|
5998
6276
|
}
|
|
5999
6277
|
async function toggle() {
|
|
@@ -6003,6 +6281,7 @@ function useCuiFullscreen(target, options = {}) {
|
|
|
6003
6281
|
tryOnScopeDispose(() => {
|
|
6004
6282
|
if (isFullscreen.value && currentEl.value) exit();
|
|
6005
6283
|
document.removeEventListener("keydown", onEscape);
|
|
6284
|
+
document.removeEventListener("fullscreenchange", onNativeChange);
|
|
6006
6285
|
});
|
|
6007
6286
|
return {
|
|
6008
6287
|
isFullscreen: computed(() => isFullscreen.value),
|
|
@@ -6114,6 +6393,8 @@ function useCameraStream(options) {
|
|
|
6114
6393
|
const status = computed(() => currentStream.value?.status.value ?? "idle");
|
|
6115
6394
|
const isPlaying = computed(() => currentStream.value?.isPlaying.value ?? false);
|
|
6116
6395
|
const stalled = computed(() => currentStream.value?.stalled?.value ?? false);
|
|
6396
|
+
const connectionPath = computed(() => currentStream.value?.connectionPath?.value);
|
|
6397
|
+
const webrtcUnreachable = computed(() => currentStream.value?.webrtcUnreachable?.value ?? false);
|
|
6117
6398
|
const activeMode = computed(() => currentStream.value?.activeMode.value ?? "webrtc");
|
|
6118
6399
|
const activeResolution = computed(() => currentStream.value?.activeResolution.value ?? "low-resolution");
|
|
6119
6400
|
const degradedFrom = computed(() => currentStream.value?.degradedFrom.value ?? null);
|
|
@@ -6665,6 +6946,8 @@ function useCameraStream(options) {
|
|
|
6665
6946
|
status,
|
|
6666
6947
|
isPlaying,
|
|
6667
6948
|
stalled,
|
|
6949
|
+
connectionPath,
|
|
6950
|
+
webrtcUnreachable,
|
|
6668
6951
|
activeMode,
|
|
6669
6952
|
activeResolution,
|
|
6670
6953
|
degradedFrom,
|
|
@@ -6935,4 +7218,4 @@ function useTerminalSessions() {
|
|
|
6935
7218
|
function terminalProxy(r) {
|
|
6936
7219
|
return r.createProxy(NamespaceManager.terminalManagerNamespaces().terminalManagerRpc);
|
|
6937
7220
|
}
|
|
6938
|
-
export { CAMERA_UI_INJECTION_KEY, NamespaceManager, STREAM_CONFIG, acquireSensorManager, clearCameraCache, clearOAuthCache, clearPluginCache, clearSensorCache, clearSnapshotCache, clearStorageCache, createActivityMode, createCameraUiPlugin, createReactiveCameraDevice, createSensorManager, getSnapshotUrl, isReactiveAudioSensor, isReactiveBatteryInfo, isReactiveCarbonDioxideInfo, isReactiveCarbonMonoxideSensor, isReactiveClassifierSensor, isReactiveColdSensor, isReactiveContactSensor, isReactiveDoorbellTrigger, isReactiveFaceSensor, isReactiveGarageControl, isReactiveGasSensor, isReactiveHeatSensor, isReactiveHumidityInfo, isReactiveIlluminanceInfo, isReactiveLeakSensor, isReactiveLicensePlateSensor, isReactiveLightControl, isReactiveLockControl, isReactiveMotionSensor, isReactiveObjectSensor, isReactiveOccupancySensor, isReactivePTZControl, isReactivePowerSensor, isReactiveProblemSensor, isReactiveSecuritySystem, isReactiveSirenControl, isReactiveSmokeSensor, isReactiveSwitchControl, isReactiveTamperSensor, isReactiveTemperatureInfo, isReactiveVibrationSensor, refreshClientSubscriptions, releaseSensorManager, resetClientState, restartConnectedStreams, rpcCall, setFullscreenRoot, useAllSensors, useAudioSensor, useCameraById, useCameraStorage, useCameraStream, useCameraUi, useClassifierSensors, useCoreManager, useCuiFullscreen, useDeviceManager, useFaceSensor, useLicensePlateSensor, useMotionSensor, useOAuth, useObjectSensor, usePTZControl, usePlugin, usePluginStorage, useRpcCall, useRpcSubscription, useSensorById, useSensorByType, useSensorStorage, useSensors, useSensorsByType, useSnapshot, useTabVisibility, useTerminal, useTerminalSessions, useTopmostFullscreenElement };
|
|
7221
|
+
export { CAMERA_UI_INJECTION_KEY, NamespaceManager, STREAM_CONFIG, acquireSensorManager, clearCameraCache, clearOAuthCache, clearPluginCache, clearSensorCache, clearSnapshotCache, clearStorageCache, createActivityMode, createCameraUiPlugin, createReactiveCameraDevice, createSensorManager, getSnapshotUrl, isReactiveAudioSensor, isReactiveBatteryInfo, isReactiveCarbonDioxideInfo, isReactiveCarbonMonoxideSensor, isReactiveClassifierSensor, isReactiveColdSensor, isReactiveContactSensor, isReactiveDoorbellTrigger, isReactiveFaceSensor, isReactiveGarageControl, isReactiveGasSensor, isReactiveHeatSensor, isReactiveHumidityInfo, isReactiveIlluminanceInfo, isReactiveLeakSensor, isReactiveLicensePlateSensor, isReactiveLightControl, isReactiveLockControl, isReactiveMotionSensor, isReactiveObjectSensor, isReactiveOccupancySensor, isReactivePTZControl, isReactivePowerSensor, isReactiveProblemSensor, isReactiveSecuritySystem, isReactiveSirenControl, isReactiveSmokeSensor, isReactiveSwitchControl, isReactiveTamperSensor, isReactiveTemperatureInfo, isReactiveVibrationSensor, refreshClientSubscriptions, releaseSensorManager, resetClientState, restartConnectedStreams, rpcCall, setFullscreenRoot, setNativeFullscreen, useAllSensors, useAudioSensor, useCameraById, useCameraStorage, useCameraStream, useCameraUi, useClassifierSensors, useCoreManager, useCuiFullscreen, useDeviceManager, useFaceSensor, useLicensePlateSensor, useMotionSensor, useOAuth, useObjectSensor, usePTZControl, usePlugin, usePluginStorage, useRpcCall, useRpcSubscription, useSensorById, useSensorByType, useSensorStorage, useSensors, useSensorsByType, useSnapshot, useTabVisibility, useTerminal, useTerminalSessions, useTopmostFullscreenElement };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camera.ui/browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "camera.ui browser client",
|
|
5
5
|
"author": "seydx (https://github.com/cameraui/clients)",
|
|
6
6
|
"type": "module",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@camera.ui/logger": "file:../../packages/logger",
|
|
41
41
|
"@camera.ui/rpc": "^1.1.2",
|
|
42
|
-
"@camera.ui/sdk": "~1.4.
|
|
42
|
+
"@camera.ui/sdk": "~1.4.9",
|
|
43
43
|
"@camera.ui/transport": "file:../../packages/transport",
|
|
44
44
|
"@eneris/push-receiver": "^4.4.0",
|
|
45
45
|
"@microsoft/api-extractor": "^7.60.1",
|
|
46
46
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
47
47
|
"@types/webrtc": "^0.0.47",
|
|
48
48
|
"@typescript-eslint/parser": "^8.72.2",
|
|
49
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
49
|
+
"@vitejs/plugin-vue": "^6.0.11",
|
|
50
50
|
"@vue/eslint-config-prettier": "^10.2.0",
|
|
51
51
|
"@vue/eslint-config-typescript": "^14.9.0",
|
|
52
52
|
"@vue/language-core": "^3.3.14",
|
|
@@ -54,15 +54,15 @@
|
|
|
54
54
|
"@vueuse/core": "^15.0.0",
|
|
55
55
|
"@webgpu/types": "^0.1.74",
|
|
56
56
|
"eslint": "9.39.2",
|
|
57
|
-
"eslint-plugin-vue": "^10.12.
|
|
57
|
+
"eslint-plugin-vue": "^10.12.1",
|
|
58
58
|
"globals": "^17.14.0",
|
|
59
|
-
"prettier": "^3.
|
|
59
|
+
"prettier": "^3.10.1",
|
|
60
60
|
"rimraf": "^6.1.4",
|
|
61
61
|
"typescript": "5.9.3",
|
|
62
62
|
"typescript-eslint": "^8.72.2",
|
|
63
63
|
"unplugin-dts": "^1.1.2",
|
|
64
64
|
"updates": "^20.0.1",
|
|
65
|
-
"vite": "^8.3.
|
|
65
|
+
"vite": "^8.3.8",
|
|
66
66
|
"vitest": "^5.1.0",
|
|
67
67
|
"vue": "^3.5.45",
|
|
68
68
|
"vue-tsc": "3.3.11"
|