@voltkit-rtc/client 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +147 -3
- package/dist/cjs/chat.d.ts +2 -0
- package/dist/cjs/chat.d.ts.map +1 -1
- package/dist/cjs/chat.js +23 -4
- package/dist/cjs/chat.js.map +1 -1
- package/dist/cjs/device.d.ts +85 -0
- package/dist/cjs/device.d.ts.map +1 -0
- package/dist/cjs/device.js +134 -0
- package/dist/cjs/device.js.map +1 -0
- package/dist/cjs/index.d.ts +184 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +546 -32
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/quality.d.ts +74 -0
- package/dist/cjs/quality.d.ts.map +1 -0
- package/dist/cjs/quality.js +115 -0
- package/dist/cjs/quality.js.map +1 -0
- package/dist/cjs/stats.d.ts +151 -0
- package/dist/cjs/stats.d.ts.map +1 -0
- package/dist/cjs/stats.js +243 -0
- package/dist/cjs/stats.js.map +1 -0
- package/dist/esm/chat.d.ts +2 -0
- package/dist/esm/chat.d.ts.map +1 -1
- package/dist/esm/chat.js +23 -4
- package/dist/esm/chat.js.map +1 -1
- package/dist/esm/device.d.ts +85 -0
- package/dist/esm/device.d.ts.map +1 -0
- package/dist/esm/device.js +122 -0
- package/dist/esm/device.js.map +1 -0
- package/dist/esm/index.d.ts +184 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +541 -31
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/quality.d.ts +74 -0
- package/dist/esm/quality.d.ts.map +1 -0
- package/dist/esm/quality.js +107 -0
- package/dist/esm/quality.js.map +1 -0
- package/dist/esm/stats.d.ts +151 -0
- package/dist/esm/stats.d.ts.map +1 -0
- package/dist/esm/stats.js +232 -0
- package/dist/esm/stats.js.map +1 -0
- package/package.json +3 -2
- package/src/chat.ts +21 -4
- package/src/device.ts +168 -0
- package/src/index.ts +627 -29
- package/src/quality.ts +126 -0
- package/src/stats.ts +328 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
|
|
2
|
+
const optNum = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined);
|
|
3
|
+
const optStr = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
|
|
4
|
+
const oneDecimal = (value) => Math.round(value * 10) / 10;
|
|
5
|
+
/**
|
|
6
|
+
* Bits per second from `bytes` at `timestamp` (ms), against what `key` had
|
|
7
|
+
* last time, and remembers this reading for the next call. 0 on the first
|
|
8
|
+
* reading, and whenever the counter went backwards (a new stream).
|
|
9
|
+
*/
|
|
10
|
+
export function bitrateSince(baseline, key, bytes, timestamp) {
|
|
11
|
+
const previous = baseline.get(key);
|
|
12
|
+
baseline.set(key, { bytes, timestamp });
|
|
13
|
+
if (!previous)
|
|
14
|
+
return 0;
|
|
15
|
+
const seconds = (timestamp - previous.timestamp) / 1000;
|
|
16
|
+
const delta = bytes - previous.bytes;
|
|
17
|
+
if (seconds <= 0 || delta < 0)
|
|
18
|
+
return 0;
|
|
19
|
+
return Math.round((delta * 8) / seconds);
|
|
20
|
+
}
|
|
21
|
+
/** Forget every key `keep` turns down: the streams they counted are gone. */
|
|
22
|
+
export function pruneBaseline(baseline, keep) {
|
|
23
|
+
for (const key of [...baseline.keys()]) {
|
|
24
|
+
if (!keep(key))
|
|
25
|
+
baseline.delete(key);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const isVideo = (entry) => entry.kind === 'video' || entry.mediaType === 'video';
|
|
29
|
+
/** `VP8` from the codec entry an RTP stream points at. */
|
|
30
|
+
function codecOf(byId, entry) {
|
|
31
|
+
const codec = entry && typeof entry.codecId === 'string' ? byId.get(entry.codecId) : undefined;
|
|
32
|
+
const mime = optStr(codec?.mimeType);
|
|
33
|
+
return mime ? mime.replace(/^video\//i, '') : undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The upload estimate on the candidate pair in use. */
|
|
36
|
+
function availableOutgoing(entries, byId) {
|
|
37
|
+
for (const transport of entries.filter((e) => e.type === 'transport')) {
|
|
38
|
+
const pair = typeof transport.selectedCandidatePairId === 'string' ? byId.get(transport.selectedCandidatePairId) : undefined;
|
|
39
|
+
const bitrate = optNum(pair?.availableOutgoingBitrate);
|
|
40
|
+
if (bitrate !== undefined)
|
|
41
|
+
return bitrate;
|
|
42
|
+
}
|
|
43
|
+
const pairs = entries.filter((e) => e.type === 'candidate-pair' && optNum(e.availableOutgoingBitrate) !== undefined);
|
|
44
|
+
const chosen = pairs.find((p) => p.nominated === true && p.state === 'succeeded') ?? pairs.find((p) => p.nominated === true) ?? pairs[0];
|
|
45
|
+
return optNum(chosen?.availableOutgoingBitrate);
|
|
46
|
+
}
|
|
47
|
+
const area = (layer) => layer.width * layer.height;
|
|
48
|
+
/** r0, r1, r2 in order; layers without a rid by size. */
|
|
49
|
+
function layerOrder(a, b) {
|
|
50
|
+
if (a.rid !== undefined && b.rid !== undefined && a.rid !== b.rid) {
|
|
51
|
+
return a.rid.localeCompare(b.rid, 'en', { numeric: true });
|
|
52
|
+
}
|
|
53
|
+
return area(a) - area(b);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The camera's send statistics from a sender's report. `keyPrefix` names the
|
|
57
|
+
* producer, so a new producer's counters never meet an old one's.
|
|
58
|
+
*/
|
|
59
|
+
export function parseSendStats(report, options) {
|
|
60
|
+
const entries = [...report].filter((e) => typeof e === 'object' && e !== null);
|
|
61
|
+
const byId = new Map();
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (typeof entry.id === 'string')
|
|
64
|
+
byId.set(entry.id, entry);
|
|
65
|
+
}
|
|
66
|
+
const outbound = entries.filter((e) => e.type === 'outbound-rtp' && isVideo(e));
|
|
67
|
+
const layers = outbound.map((entry) => {
|
|
68
|
+
const rid = optStr(entry.rid);
|
|
69
|
+
const key = `${options.keyPrefix}${rid ?? String(entry.ssrc ?? entry.id ?? '')}`;
|
|
70
|
+
const bitrate = bitrateSince(options.baseline, key, num(entry.bytesSent), num(entry.timestamp));
|
|
71
|
+
const fps = oneDecimal(num(entry.framesPerSecond));
|
|
72
|
+
const reason = optStr(entry.qualityLimitationReason);
|
|
73
|
+
return {
|
|
74
|
+
...(rid !== undefined ? { rid } : {}),
|
|
75
|
+
width: num(entry.frameWidth),
|
|
76
|
+
height: num(entry.frameHeight),
|
|
77
|
+
fps,
|
|
78
|
+
bitrate,
|
|
79
|
+
active: entry.active !== false && (fps > 0 || bitrate > 0),
|
|
80
|
+
...(reason !== undefined ? { qualityLimitation: reason } : {}),
|
|
81
|
+
entry,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
layers.sort(layerOrder);
|
|
85
|
+
// The biggest layer going out is what the best-connected viewer gets. With
|
|
86
|
+
// nothing going out (paused, or the very first frames), the biggest there is.
|
|
87
|
+
const pool = layers.some((l) => l.active) ? layers.filter((l) => l.active) : layers;
|
|
88
|
+
const top = pool.reduce((best, l) => (!best || area(l) > area(best) ? l : best), undefined);
|
|
89
|
+
const codec = codecOf(byId, top?.entry) ?? outbound.map((e) => codecOf(byId, e)).find(Boolean);
|
|
90
|
+
const available = availableOutgoing(entries, byId);
|
|
91
|
+
return {
|
|
92
|
+
...(codec !== undefined ? { codec } : {}),
|
|
93
|
+
quality: options.quality,
|
|
94
|
+
width: top?.width ?? 0,
|
|
95
|
+
height: top?.height ?? 0,
|
|
96
|
+
fps: top?.fps ?? 0,
|
|
97
|
+
bitrate: top?.bitrate ?? 0,
|
|
98
|
+
...(top?.qualityLimitation !== undefined ? { qualityLimitation: top.qualityLimitation } : {}),
|
|
99
|
+
...(available !== undefined ? { availableOutgoingBitrate: available } : {}),
|
|
100
|
+
layers: layers.map(({ entry: _entry, ...layer }) => layer),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** One consumed video's receive statistics from a receiver's report; null when it has none. */
|
|
104
|
+
export function parseReceiveStats(report, options) {
|
|
105
|
+
const entries = [...report].filter((e) => typeof e === 'object' && e !== null);
|
|
106
|
+
const byId = new Map();
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (typeof entry.id === 'string')
|
|
109
|
+
byId.set(entry.id, entry);
|
|
110
|
+
}
|
|
111
|
+
const inbound = entries
|
|
112
|
+
.filter((e) => e.type === 'inbound-rtp' && isVideo(e))
|
|
113
|
+
.sort((a, b) => num(b.bytesReceived) - num(a.bytesReceived))[0];
|
|
114
|
+
if (!inbound)
|
|
115
|
+
return null;
|
|
116
|
+
const codec = codecOf(byId, inbound);
|
|
117
|
+
const jitter = optNum(inbound.jitter);
|
|
118
|
+
const freezeCount = optNum(inbound.freezeCount);
|
|
119
|
+
return {
|
|
120
|
+
producerId: options.producerId,
|
|
121
|
+
...(codec !== undefined ? { codec } : {}),
|
|
122
|
+
width: num(inbound.frameWidth),
|
|
123
|
+
height: num(inbound.frameHeight),
|
|
124
|
+
fps: oneDecimal(num(inbound.framesPerSecond)),
|
|
125
|
+
bitrate: bitrateSince(options.baseline, options.key, num(inbound.bytesReceived), num(inbound.timestamp)),
|
|
126
|
+
packetsLost: num(inbound.packetsLost),
|
|
127
|
+
...(jitter !== undefined ? { jitter } : {}),
|
|
128
|
+
...(freezeCount !== undefined ? { freezeCount } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The reason across the layers going out (all of them when none is): the
|
|
133
|
+
* device first, since it costs the most picture, then the upload, then
|
|
134
|
+
* anything else.
|
|
135
|
+
*/
|
|
136
|
+
export function limitReasonOf(layers) {
|
|
137
|
+
const pool = layers.some((l) => l.active) ? layers.filter((l) => l.active) : layers;
|
|
138
|
+
const reasons = new Set(pool.map((l) => l.qualityLimitation));
|
|
139
|
+
if (reasons.has('cpu'))
|
|
140
|
+
return 'cpu';
|
|
141
|
+
if (reasons.has('bandwidth'))
|
|
142
|
+
return 'bandwidth';
|
|
143
|
+
if (reasons.has('other'))
|
|
144
|
+
return 'other';
|
|
145
|
+
return 'none';
|
|
146
|
+
}
|
|
147
|
+
/** Samples a new reason must hold for before it is reported: about 4 s at one every 2 s. */
|
|
148
|
+
export const LIMIT_SAMPLES_TO_REPORT = 2;
|
|
149
|
+
export const initialLimitTracker = () => ({ reported: null, pending: null, streak: 0 });
|
|
150
|
+
/**
|
|
151
|
+
* Feed one sample's reason. Returns the reason to report, if any: one that
|
|
152
|
+
* differs from the last reported and held for LIMIT_SAMPLES_TO_REPORT samples
|
|
153
|
+
* in a row, so a brief spike stays quiet. Before anything is reported the
|
|
154
|
+
* state counts as 'none', so 'none' is never the first event.
|
|
155
|
+
*/
|
|
156
|
+
export function trackLimit(tracker, reason) {
|
|
157
|
+
const current = tracker.reported ?? 'none';
|
|
158
|
+
if (reason === current)
|
|
159
|
+
return { tracker: { reported: tracker.reported, pending: null, streak: 0 }, report: null };
|
|
160
|
+
const streak = reason === tracker.pending ? tracker.streak + 1 : 1;
|
|
161
|
+
if (streak >= LIMIT_SAMPLES_TO_REPORT)
|
|
162
|
+
return { tracker: { reported: reason, pending: null, streak: 0 }, report: reason };
|
|
163
|
+
return { tracker: { reported: tracker.reported, pending: reason, streak }, report: null };
|
|
164
|
+
}
|
|
165
|
+
/** How often the camera is sampled while someone listens for `videoLimited`. */
|
|
166
|
+
export const LIMIT_SAMPLE_MS = 2000;
|
|
167
|
+
/**
|
|
168
|
+
* Samples the camera's send statistics on a timer while it is wanted and
|
|
169
|
+
* reports changes in what holds the video back. The stats it reads keep their
|
|
170
|
+
* own baseline, so its sampling never disturbs the bitrates an app reads with
|
|
171
|
+
* `getVideoStats()`.
|
|
172
|
+
*/
|
|
173
|
+
export class VideoLimitWatch {
|
|
174
|
+
sample;
|
|
175
|
+
report;
|
|
176
|
+
intervalMs;
|
|
177
|
+
timer = null;
|
|
178
|
+
tracker = initialLimitTracker();
|
|
179
|
+
sampling = false;
|
|
180
|
+
baseline = new Map();
|
|
181
|
+
constructor(sample, report, intervalMs = LIMIT_SAMPLE_MS) {
|
|
182
|
+
this.sample = sample;
|
|
183
|
+
this.report = report;
|
|
184
|
+
this.intervalMs = intervalMs;
|
|
185
|
+
}
|
|
186
|
+
get running() {
|
|
187
|
+
return this.timer !== null;
|
|
188
|
+
}
|
|
189
|
+
/** Start or stop sampling. Safe to call as often as anything changes. */
|
|
190
|
+
set(wanted) {
|
|
191
|
+
if (wanted && !this.timer) {
|
|
192
|
+
this.timer = setInterval(() => void this.tick(), this.intervalMs);
|
|
193
|
+
}
|
|
194
|
+
else if (!wanted && this.timer) {
|
|
195
|
+
clearInterval(this.timer);
|
|
196
|
+
this.timer = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Stop, and start over as if nothing had been reported. Returns the event
|
|
201
|
+
* that clears a limitation still being reported, for when publishing stops.
|
|
202
|
+
*/
|
|
203
|
+
reset() {
|
|
204
|
+
this.set(false);
|
|
205
|
+
const was = this.tracker.reported;
|
|
206
|
+
this.tracker = initialLimitTracker();
|
|
207
|
+
this.baseline.clear();
|
|
208
|
+
return was && was !== 'none' ? { reason: 'none', width: 0, height: 0, fps: 0, bitrate: 0 } : null;
|
|
209
|
+
}
|
|
210
|
+
async tick() {
|
|
211
|
+
// A slow getStats must not pile samples up behind it.
|
|
212
|
+
if (this.sampling)
|
|
213
|
+
return;
|
|
214
|
+
this.sampling = true;
|
|
215
|
+
try {
|
|
216
|
+
const stats = await this.sample(this.baseline);
|
|
217
|
+
if (!stats || !this.timer)
|
|
218
|
+
return;
|
|
219
|
+
const { tracker, report } = trackLimit(this.tracker, limitReasonOf(stats.layers));
|
|
220
|
+
this.tracker = tracker;
|
|
221
|
+
if (report)
|
|
222
|
+
this.report({ reason: report, width: stats.width, height: stats.height, fps: stats.fps, bitrate: stats.bitrate });
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Statistics are best effort; the next sample tries again.
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
this.sampling = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/stats.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltkit-rtc/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Browser SDK for VoltKit RTC: realtime audio/video rooms, chat and live broadcast publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltkit",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "rm -rf dist && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node ../scripts/dual-package.mjs .",
|
|
51
51
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
|
-
"prepublishOnly": "npm run build"
|
|
52
|
+
"prepublishOnly": "npm run build",
|
|
53
|
+
"test": "npm run build && node --test test/"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
56
|
"mediasoup-client": "^3.22.0"
|
package/src/chat.ts
CHANGED
|
@@ -122,6 +122,8 @@ export class ChatClient {
|
|
|
122
122
|
private closedByUs = false;
|
|
123
123
|
private reconnecting = false;
|
|
124
124
|
private reconnectAttempt = 0;
|
|
125
|
+
/** Set by the first successful join. Only a session that has joined is reconnected. */
|
|
126
|
+
private joinedOnce = false;
|
|
125
127
|
|
|
126
128
|
constructor(private readonly wsUrl: string, token: string) {
|
|
127
129
|
this.token = token;
|
|
@@ -153,8 +155,22 @@ export class ChatClient {
|
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
async connect(): Promise<ChatJoinResult> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
let result: ChatJoinResult;
|
|
159
|
+
try {
|
|
160
|
+
await this.openSocket();
|
|
161
|
+
result = (await this.rpc('join', { token: this.token })) as ChatJoinResult;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
// Never joined: the app has the error. Left open, the socket would be
|
|
164
|
+
// closed by the server's join deadline, and that would start
|
|
165
|
+
// reconnecting a session that never began.
|
|
166
|
+
if (!this.joinedOnce) this.detachSocket();
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
// Joined: the next drop gets every reconnect attempt again. Not when the
|
|
170
|
+
// socket opens, or a join refused with a code worth retrying never uses
|
|
171
|
+
// them up and is retried about twice a second, forever.
|
|
172
|
+
this.reconnectAttempt = 0;
|
|
173
|
+
this.joinedOnce = true;
|
|
158
174
|
this.joinResult = result;
|
|
159
175
|
this.emit('joined', result);
|
|
160
176
|
return result;
|
|
@@ -170,7 +186,6 @@ export class ChatClient {
|
|
|
170
186
|
|
|
171
187
|
const onError = () => reject(new Error('websocket failed to open'));
|
|
172
188
|
ws.addEventListener('open', () => {
|
|
173
|
-
this.reconnectAttempt = 0;
|
|
174
189
|
this.emit('connected');
|
|
175
190
|
resolve();
|
|
176
191
|
}, { once: true });
|
|
@@ -191,7 +206,9 @@ export class ChatClient {
|
|
|
191
206
|
this.emit('closed', 'ended');
|
|
192
207
|
return;
|
|
193
208
|
}
|
|
194
|
-
|
|
209
|
+
// Before a first join the app is still waiting on connect(), which
|
|
210
|
+
// reports the failure; there is no session to restore.
|
|
211
|
+
if (!this.closedByUs && this.joinedOnce) void this.scheduleReconnect();
|
|
195
212
|
});
|
|
196
213
|
});
|
|
197
214
|
}
|
package/src/device.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What kind of device is publishing, and which video codec it should send.
|
|
3
|
+
*
|
|
4
|
+
* Phones and tablets get different defaults from computers: each simulcast
|
|
5
|
+
* layer is a separate encode, and a phone encoding VP8 does it in software.
|
|
6
|
+
* Under that load Chrome shrinks the picture, and with three layers asked for
|
|
7
|
+
* it then sends only two and drops the TOP one: viewers got 180×320 from a
|
|
8
|
+
* 720p phone. Two layers in hardware H.264 keep the full picture.
|
|
9
|
+
*
|
|
10
|
+
* Everything here reads the browser through small injectable parameters, so
|
|
11
|
+
* tests can stand in for any device.
|
|
12
|
+
*/
|
|
13
|
+
import type { types as ms } from 'mediasoup-client';
|
|
14
|
+
|
|
15
|
+
type RtpCodecCapability = ms.RtpCodecCapability;
|
|
16
|
+
|
|
17
|
+
/** What the SDK reads about the browser to choose its defaults. */
|
|
18
|
+
export interface DeviceEnv {
|
|
19
|
+
userAgent: string;
|
|
20
|
+
/** `navigator.userAgentData.mobile`, where the browser has it (Chromium). */
|
|
21
|
+
uaMobile?: boolean;
|
|
22
|
+
maxTouchPoints: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The browser this runs in; an empty desktop-like one outside a browser. */
|
|
26
|
+
export function currentEnv(): DeviceEnv {
|
|
27
|
+
if (typeof navigator === 'undefined') return { userAgent: '', maxTouchPoints: 0 };
|
|
28
|
+
const nav = navigator as Navigator & { userAgentData?: { mobile?: unknown } };
|
|
29
|
+
const mobile = nav.userAgentData?.mobile;
|
|
30
|
+
return {
|
|
31
|
+
userAgent: typeof nav.userAgent === 'string' ? nav.userAgent : '',
|
|
32
|
+
...(typeof mobile === 'boolean' ? { uaMobile: mobile } : {}),
|
|
33
|
+
maxTouchPoints: typeof nav.maxTouchPoints === 'number' ? nav.maxTouchPoints : 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// iPadOS asks for desktop sites, so an iPad says "Macintosh". No Mac has a
|
|
38
|
+
// touch screen, so one that reports touch points is an iPad.
|
|
39
|
+
const isIPadOS = (env: DeviceEnv): boolean => /Macintosh/.test(env.userAgent) && env.maxTouchPoints > 1;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A phone or a tablet. `userAgentData.mobile` is false on Android tablets, so
|
|
43
|
+
* the user agent is checked as well.
|
|
44
|
+
*/
|
|
45
|
+
export function isPhoneOrTablet(env: DeviceEnv = currentEnv()): boolean {
|
|
46
|
+
if (env.uaMobile === true) return true;
|
|
47
|
+
if (/Android|iPhone|iPad|iPod|Mobile/.test(env.userAgent)) return true;
|
|
48
|
+
return isIPadOS(env);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An iPhone, iPad or iPod: every one of them encodes H.264 in hardware. */
|
|
52
|
+
export function isAppleMobile(env: DeviceEnv = currentEnv()): boolean {
|
|
53
|
+
return /iPhone|iPad|iPod/.test(env.userAgent) || isIPadOS(env);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Two layers on phones and tablets, three elsewhere. */
|
|
57
|
+
export function defaultLayerCount(env: DeviceEnv = currentEnv()): 2 | 3 {
|
|
58
|
+
return isPhoneOrTablet(env) ? 2 : 3;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `navigator.mediaCapabilities.encodingInfo`, or a stand-in for tests. */
|
|
62
|
+
export type EncodingInfoProbe = (configuration: MediaEncodingConfiguration) => Promise<MediaCapabilitiesEncodingInfo>;
|
|
63
|
+
|
|
64
|
+
/** The browser's own probe, when it has one. */
|
|
65
|
+
export function currentProbe(): EncodingInfoProbe | undefined {
|
|
66
|
+
if (typeof navigator === 'undefined') return undefined;
|
|
67
|
+
const capabilities = (navigator as Navigator & { mediaCapabilities?: MediaCapabilities }).mediaCapabilities;
|
|
68
|
+
if (!capabilities || typeof capabilities.encodingInfo !== 'function') return undefined;
|
|
69
|
+
return (configuration) => capabilities.encodingInfo(configuration);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The H.264 the media server takes: constrained baseline, packetization mode 1. */
|
|
73
|
+
export const H264_CONTENT_TYPE = 'video/H264;profile-level-id=42e01f;packetization-mode=1';
|
|
74
|
+
|
|
75
|
+
const PROBE_TIMEOUT_MS = 1500;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether this browser encodes H.264 at this size and bitrate in hardware:
|
|
79
|
+
* true or false when it says so, null when it cannot say (no
|
|
80
|
+
* `mediaCapabilities`, no `webrtc` support in it, an error, or no answer in
|
|
81
|
+
* time). Publishing never waits on it for more than a moment.
|
|
82
|
+
*/
|
|
83
|
+
export async function h264HardwareEncode(
|
|
84
|
+
video: { width: number; height: number; bitrate: number },
|
|
85
|
+
probe: EncodingInfoProbe | undefined = currentProbe(),
|
|
86
|
+
timeoutMs = PROBE_TIMEOUT_MS,
|
|
87
|
+
): Promise<boolean | null> {
|
|
88
|
+
if (!probe) return null;
|
|
89
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
90
|
+
try {
|
|
91
|
+
const answer = probe({
|
|
92
|
+
type: 'webrtc',
|
|
93
|
+
video: {
|
|
94
|
+
contentType: H264_CONTENT_TYPE,
|
|
95
|
+
width: Math.round(video.width),
|
|
96
|
+
height: Math.round(video.height),
|
|
97
|
+
bitrate: video.bitrate,
|
|
98
|
+
framerate: 30,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const late = new Promise<null>((resolve) => {
|
|
102
|
+
timer = setTimeout(() => resolve(null), timeoutMs);
|
|
103
|
+
});
|
|
104
|
+
const info = await Promise.race([answer, late]);
|
|
105
|
+
if (!info) return null;
|
|
106
|
+
return Boolean(info.supported && info.powerEfficient);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
} finally {
|
|
110
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** What `PublishOptions.codec` may say. */
|
|
115
|
+
export type CodecPreference = 'auto' | 'vp8' | 'h264';
|
|
116
|
+
|
|
117
|
+
/** 'vp8' or 'h264' in any case; anything else, including nothing, is 'auto'. */
|
|
118
|
+
export function normalizeCodec(codec: unknown): CodecPreference {
|
|
119
|
+
const name = typeof codec === 'string' ? codec.toLowerCase() : '';
|
|
120
|
+
return name === 'vp8' || name === 'h264' ? name : 'auto';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface CodecChoiceInput {
|
|
124
|
+
requested: CodecPreference;
|
|
125
|
+
phoneOrTablet: boolean;
|
|
126
|
+
appleMobile: boolean;
|
|
127
|
+
/** This browser can send H.264 to this server (it is in the device's send capabilities). */
|
|
128
|
+
h264Available: boolean;
|
|
129
|
+
/** What the hardware probe said; null when it could not say. Only asked for 'auto' on phones and tablets. */
|
|
130
|
+
h264Hardware: boolean | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface CodecChoice {
|
|
134
|
+
codec: 'vp8' | 'h264';
|
|
135
|
+
/** Set when the codec asked for cannot be used: report it, and publish anyway. */
|
|
136
|
+
warning?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The codec to publish with.
|
|
141
|
+
*
|
|
142
|
+
* 'auto' is H.264 on a phone or tablet that encodes it in hardware, and VP8
|
|
143
|
+
* everywhere else: computers encode VP8 well, and a few desktop browsers
|
|
144
|
+
* cannot decode H.264. When the probe cannot say, an Apple device is taken to
|
|
145
|
+
* have hardware H.264 (they all do) and anything else is not.
|
|
146
|
+
*/
|
|
147
|
+
export function chooseCodec(input: CodecChoiceInput): CodecChoice {
|
|
148
|
+
if (input.requested === 'h264') {
|
|
149
|
+
return input.h264Available
|
|
150
|
+
? { codec: 'h264' }
|
|
151
|
+
: { codec: 'vp8', warning: 'H.264 was asked for, but this browser cannot send it to this server: publishing VP8 instead' };
|
|
152
|
+
}
|
|
153
|
+
if (input.requested === 'vp8') return { codec: 'vp8' };
|
|
154
|
+
if (!input.phoneOrTablet || !input.h264Available) return { codec: 'vp8' };
|
|
155
|
+
return { codec: (input.h264Hardware ?? input.appleMobile) ? 'h264' : 'vp8' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The device's own capability entry for a codec, to hand to `produce()`.
|
|
160
|
+
* Only ever pass `produce()` an entry from the device's send capabilities:
|
|
161
|
+
* mediasoup-client adds the transceiver before it checks the codec, so one it
|
|
162
|
+
* cannot match leaves a transceiver behind that breaks every later produce.
|
|
163
|
+
*/
|
|
164
|
+
export function findCodec(codecs: readonly RtpCodecCapability[] | undefined, codec: 'vp8' | 'h264'): RtpCodecCapability | undefined {
|
|
165
|
+
if (!codecs) return undefined;
|
|
166
|
+
if (codec === 'vp8') return codecs.find((c) => c.mimeType.toLowerCase() === 'video/vp8');
|
|
167
|
+
return codecs.find((c) => c.mimeType.toLowerCase() === 'video/h264' && Number(c.parameters?.['packetization-mode'] ?? 0) === 1);
|
|
168
|
+
}
|