im-rtc-call-engine 1.0.0 → 2.1.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/dist/browserSignals.d.ts +27 -0
- package/dist/browserSignals.d.ts.map +1 -0
- package/dist/browserSignals.js +34 -0
- package/dist/browserSignals.js.map +1 -0
- package/dist/callGuards.d.ts +35 -0
- package/dist/callGuards.d.ts.map +1 -0
- package/dist/callGuards.js +66 -0
- package/dist/callGuards.js.map +1 -0
- package/dist/callHistory.d.ts +50 -0
- package/dist/callHistory.d.ts.map +1 -0
- package/dist/callHistory.js +100 -0
- package/dist/callHistory.js.map +1 -0
- package/dist/callOptions.d.ts +14 -0
- package/dist/callOptions.d.ts.map +1 -1
- package/dist/callOptions.js +21 -0
- package/dist/callOptions.js.map +1 -1
- package/dist/debugToken.d.ts +29 -0
- package/dist/debugToken.d.ts.map +1 -0
- package/dist/debugToken.js +73 -0
- package/dist/debugToken.js.map +1 -0
- package/dist/engine.d.ts +88 -60
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +141 -178
- package/dist/engine.js.map +1 -1
- package/dist/engineBus.d.ts.map +1 -1
- package/dist/engineBus.js +1 -0
- package/dist/engineBus.js.map +1 -1
- package/dist/engineSession.d.ts +49 -0
- package/dist/engineSession.d.ts.map +1 -0
- package/dist/engineSession.js +97 -0
- package/dist/engineSession.js.map +1 -0
- package/dist/events.d.ts +48 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -1
- package/dist/frameLoop.d.ts +48 -5
- package/dist/frameLoop.d.ts.map +1 -1
- package/dist/frameLoop.js +205 -29
- package/dist/frameLoop.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/media/captureStream.d.ts +9 -0
- package/dist/media/captureStream.d.ts.map +1 -0
- package/dist/media/captureStream.js +20 -0
- package/dist/media/captureStream.js.map +1 -0
- package/dist/media/engineMediaApi.d.ts +24 -3
- package/dist/media/engineMediaApi.d.ts.map +1 -1
- package/dist/media/engineMediaApi.js +63 -27
- package/dist/media/engineMediaApi.js.map +1 -1
- package/dist/media/mediaBridge.d.ts +6 -1
- package/dist/media/mediaBridge.d.ts.map +1 -1
- package/dist/media/mediaBridge.js +18 -1
- package/dist/media/mediaBridge.js.map +1 -1
- package/dist/media/remoteAudio.d.ts +65 -0
- package/dist/media/remoteAudio.d.ts.map +1 -0
- package/dist/media/remoteAudio.js +83 -0
- package/dist/media/remoteAudio.js.map +1 -0
- package/dist/media/videoSender.d.ts +21 -0
- package/dist/media/videoSender.d.ts.map +1 -0
- package/dist/media/videoSender.js +74 -0
- package/dist/media/videoSender.js.map +1 -0
- package/dist/media/viewRegistry.d.ts +11 -3
- package/dist/media/viewRegistry.d.ts.map +1 -1
- package/dist/media/viewRegistry.js +35 -5
- package/dist/media/viewRegistry.js.map +1 -1
- package/dist/media/webrtcAdapter.d.ts +0 -31
- package/dist/media/webrtcAdapter.d.ts.map +1 -1
- package/dist/media/webrtcAdapter.js +10 -88
- package/dist/media/webrtcAdapter.js.map +1 -1
- package/dist/signaling/connection.d.ts +36 -0
- package/dist/signaling/connection.d.ts.map +1 -1
- package/dist/signaling/connection.js +89 -2
- package/dist/signaling/connection.js.map +1 -1
- package/dist/signaling/enums.d.ts +15 -0
- package/dist/signaling/enums.d.ts.map +1 -1
- package/dist/signaling/enums.js +17 -0
- package/dist/signaling/enums.js.map +1 -1
- package/dist/signaling/envelope.d.ts +13 -1
- package/dist/signaling/envelope.d.ts.map +1 -1
- package/dist/signaling/envelope.js +15 -3
- package/dist/signaling/envelope.js.map +1 -1
- package/dist/signaling/frames.call.d.ts +14 -0
- package/dist/signaling/frames.call.d.ts.map +1 -1
- package/dist/signaling/frames.call.js +8 -0
- package/dist/signaling/frames.call.js.map +1 -1
- package/dist/signaling/frames.room.d.ts +6 -4
- package/dist/signaling/frames.room.d.ts.map +1 -1
- package/dist/signaling/frames.room.js +10 -4
- package/dist/signaling/frames.room.js.map +1 -1
- package/dist/signaling/frames.sys.d.ts +5 -1
- package/dist/signaling/frames.sys.d.ts.map +1 -1
- package/dist/signaling/frames.sys.js +5 -1
- package/dist/signaling/frames.sys.js.map +1 -1
- package/dist/signaling/heartbeat.js +1 -1
- package/dist/signaling/heartbeat.js.map +1 -1
- package/dist/signaling/networkProbe.d.ts +31 -0
- package/dist/signaling/networkProbe.d.ts.map +1 -0
- package/dist/signaling/networkProbe.js +47 -0
- package/dist/signaling/networkProbe.js.map +1 -0
- package/dist/signaling/oneShotTimer.d.ts +30 -0
- package/dist/signaling/oneShotTimer.d.ts.map +1 -0
- package/dist/signaling/oneShotTimer.js +44 -0
- package/dist/signaling/oneShotTimer.js.map +1 -0
- package/dist/signaling/reconnector.d.ts +17 -1
- package/dist/signaling/reconnector.d.ts.map +1 -1
- package/dist/signaling/reconnector.js +48 -14
- package/dist/signaling/reconnector.js.map +1 -1
- package/dist/signaling/resumeDeadline.d.ts +1 -1
- package/dist/signaling/resumeDeadline.d.ts.map +1 -1
- package/dist/signaling/resumeDeadline.js +7 -10
- package/dist/signaling/resumeDeadline.js.map +1 -1
- package/dist/state/callMachine.d.ts +7 -0
- package/dist/state/callMachine.d.ts.map +1 -1
- package/dist/state/callMachine.js +16 -7
- package/dist/state/callMachine.js.map +1 -1
- package/dist/state/callRecv.d.ts.map +1 -1
- package/dist/state/callRecv.js +9 -1
- package/dist/state/callRecv.js.map +1 -1
- package/dist/state/engineMachine.d.ts.map +1 -1
- package/dist/state/engineMachine.js +26 -6
- package/dist/state/engineMachine.js.map +1 -1
- package/dist/state/roomMachine.d.ts +10 -2
- package/dist/state/roomMachine.d.ts.map +1 -1
- package/dist/state/roomMachine.js +87 -15
- package/dist/state/roomMachine.js.map +1 -1
- package/dist/state/roomPaging.d.ts +81 -0
- package/dist/state/roomPaging.d.ts.map +1 -0
- package/dist/state/roomPaging.js +202 -0
- package/dist/state/roomPaging.js.map +1 -0
- package/dist/state/roomRecv.d.ts.map +1 -1
- package/dist/state/roomRecv.js +17 -7
- package/dist/state/roomRecv.js.map +1 -1
- package/dist/state/types.d.ts +12 -0
- package/dist/state/types.d.ts.map +1 -1
- package/dist/state/types.js.map +1 -1
- package/dist/state/unsubscribeTimers.d.ts +29 -0
- package/dist/state/unsubscribeTimers.d.ts.map +1 -0
- package/dist/state/unsubscribeTimers.js +55 -0
- package/dist/state/unsubscribeTimers.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,23 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* 探麦克风。失败**只** reject,不再同时发 error 事件(2.0.0 起一次失败只从一个出口报,
|
|
3
|
+
* server `docs/design/ACTION_RESULT_DESIGN.md` R3)。宿主的契约见 CallEngine.probeMicrophone。
|
|
4
|
+
*/
|
|
2
5
|
export async function probeMicrophone(d) {
|
|
3
|
-
|
|
4
|
-
await d.media.probeMicrophone();
|
|
5
|
-
}
|
|
6
|
-
catch (err) {
|
|
7
|
-
// 宿主只监听事件表也该知道「这通电话是因为没权限才没打出去」。
|
|
8
|
-
d.bus.emitError(err);
|
|
9
|
-
throw err;
|
|
10
|
-
}
|
|
6
|
+
await d.media.probeMicrophone();
|
|
11
7
|
}
|
|
12
|
-
/** 探摄像头。与 probeMicrophone
|
|
8
|
+
/** 探摄像头。与 probeMicrophone 同一个契约。 */
|
|
13
9
|
export async function probeCamera(d) {
|
|
14
|
-
|
|
15
|
-
await d.media.probeCamera();
|
|
16
|
-
}
|
|
17
|
-
catch (err) {
|
|
18
|
-
d.bus.emitError(err);
|
|
19
|
-
throw err;
|
|
20
|
-
}
|
|
10
|
+
await d.media.probeCamera();
|
|
21
11
|
}
|
|
22
12
|
/**
|
|
23
13
|
* 发布麦克风。顺序是**先拿轨道再拿 cid**:浏览器不允许自定义 track.id,
|
|
@@ -25,7 +15,7 @@ export async function probeCamera(d) {
|
|
|
25
15
|
*/
|
|
26
16
|
export async function publishMicrophone(d) {
|
|
27
17
|
const info = await d.media.acquireMicrophone();
|
|
28
|
-
await d.loop.
|
|
18
|
+
await d.loop.request({
|
|
29
19
|
kind: 'act',
|
|
30
20
|
op: 'publish',
|
|
31
21
|
args: { cid: info.cid, kind: info.kind, source: info.source, simulcast: false },
|
|
@@ -35,7 +25,7 @@ export async function publishMicrophone(d) {
|
|
|
35
25
|
/** 发布摄像头。`simulcast` **同时喂给媒体面与信令**——只喂一边就是「报了三层、实际发一层」。 */
|
|
36
26
|
export async function publishCamera(d, simulcast) {
|
|
37
27
|
const info = await d.media.acquireCamera(simulcast);
|
|
38
|
-
await d.loop.
|
|
28
|
+
await d.loop.request({
|
|
39
29
|
kind: 'act',
|
|
40
30
|
op: 'publish',
|
|
41
31
|
args: { cid: info.cid, kind: info.kind, source: info.source, simulcast },
|
|
@@ -52,20 +42,66 @@ export async function publishCamera(d, simulcast) {
|
|
|
52
42
|
export async function setMuted(d, cid, muted) {
|
|
53
43
|
await d.media.setMuted(cid, muted);
|
|
54
44
|
const trackId = d.loop.state.room.publishTrackIds[cid];
|
|
55
|
-
if (trackId
|
|
45
|
+
if (trackId !== undefined) {
|
|
46
|
+
await d.loop.request({ kind: 'act', op: 'mute', args: { track_id: trackId, muted } });
|
|
47
|
+
}
|
|
48
|
+
d.bridge.refreshLocalViews();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 「发布过没有」**只问媒体适配器自己的账**,不在门面另记一份:宿主先直接调 `publish*()`
|
|
52
|
+
* 发布过、再调 `open*()` 的话,门面那份账不知道,会误判成「没发布」再发一次(2026-09-15 iOS 踩过)。
|
|
53
|
+
*/
|
|
54
|
+
function publishedCid(d, source) {
|
|
55
|
+
return source === 'microphone' ? d.media.publishedMicrophoneCid() : d.media.publishedCameraCid();
|
|
56
|
+
}
|
|
57
|
+
/** 按类型开:没发布过就发布(摄像头有预览时复用),发布过就取消静音——**不会重复发布**。 */
|
|
58
|
+
export async function openLocal(d, source) {
|
|
59
|
+
const cid = publishedCid(d, source);
|
|
60
|
+
if (cid !== null) {
|
|
61
|
+
await setMuted(d, cid, false);
|
|
56
62
|
return;
|
|
57
|
-
|
|
63
|
+
}
|
|
64
|
+
if (source === 'microphone')
|
|
65
|
+
await publishMicrophone(d);
|
|
66
|
+
else
|
|
67
|
+
await publishCamera(d, true);
|
|
58
68
|
}
|
|
59
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* 按类型关:已发布的那条 `setMuted(cid, true)`,**不 unpublish**;没发布过是空操作。
|
|
71
|
+
*
|
|
72
|
+
* 清理类,**永不 reject**:本端在发帧之前就已经静音,`room.mute` 被拒也不回滚本端(隐私优先),
|
|
73
|
+
* 错误走 error 事件。
|
|
74
|
+
*/
|
|
75
|
+
export async function closeLocal(d, source) {
|
|
76
|
+
const cid = publishedCid(d, source);
|
|
77
|
+
if (cid === null)
|
|
78
|
+
return;
|
|
79
|
+
try {
|
|
80
|
+
await setMuted(d, cid, true);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
d.bus.emitError(err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 报某人画面的层上界。一个 uid 可能有多条视频轨道,逐条报。
|
|
88
|
+
*
|
|
89
|
+
* 提示类,**永不 reject**(界面每次换布局都调,失败宿主也无事可做):失败走 error 事件。
|
|
90
|
+
*/
|
|
60
91
|
export async function setRemoteLayer(d, uid, layer) {
|
|
61
92
|
for (const [trackId, info] of Object.entries(d.loop.state.room.remoteTracks)) {
|
|
62
93
|
if (info.uid !== uid || info.kind !== 'video')
|
|
63
94
|
continue;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
95
|
+
try {
|
|
96
|
+
await d.loop.request({
|
|
97
|
+
kind: 'act',
|
|
98
|
+
op: 'update_layer',
|
|
99
|
+
args: { track_id: trackId, max_layer: layer },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
d.bus.emitError(err);
|
|
104
|
+
}
|
|
69
105
|
}
|
|
70
106
|
}
|
|
71
107
|
//# sourceMappingURL=engineMediaApi.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engineMediaApi.js","sourceRoot":"","sources":["../../src/media/engineMediaApi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"engineMediaApi.js","sourceRoot":"","sources":["../../src/media/engineMediaApi.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MediaAdapter, MediaAdapterEvents } from './mediaAdapter.js';
|
|
2
|
+
import type { SinkFactory } from './remoteAudio.js';
|
|
2
3
|
import type { ViewElement } from './viewRegistry.js';
|
|
3
4
|
/** RemoteTrackOwner 是「这条 track_id 是谁的」的最小查询接口。 */
|
|
4
5
|
export interface RemoteTrackOwner {
|
|
@@ -18,12 +19,16 @@ export interface RemoteTrackOwner {
|
|
|
18
19
|
export declare class MediaBridge {
|
|
19
20
|
readonly adapter: MediaAdapter;
|
|
20
21
|
private readonly views;
|
|
22
|
+
/** 远端音频由引擎自己播,与画面挂在哪无关(`remoteAudio.ts`)。 */
|
|
23
|
+
private readonly audio;
|
|
21
24
|
/** 已经抛过「首帧」的轨道,避免重复抛。 */
|
|
22
25
|
private readonly seenVideo;
|
|
23
26
|
/** 开摄像头之后等新画面上屏的那些人(见 awaitFirstVideoFrame)。 */
|
|
24
27
|
private readonly firstFrames;
|
|
25
28
|
private events;
|
|
26
|
-
constructor(adapter: MediaAdapter);
|
|
29
|
+
constructor(adapter: MediaAdapter, createAudioSink?: SinkFactory);
|
|
30
|
+
/** unlockRemoteAudio 在进房那次用户手势里再试一遍播放(浏览器自动播放限制)。 */
|
|
31
|
+
unlockRemoteAudio(): void;
|
|
27
32
|
/** open 建两条 PeerConnection。 */
|
|
28
33
|
open(events: MediaAdapterEvents): void;
|
|
29
34
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mediaBridge.d.ts","sourceRoot":"","sources":["../../src/media/mediaBridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,yBAAyB;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,gDAAgD;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IACpD,OAAO,CAAC,MAAM,CAAmC;gBAErC,OAAO,EAAE,YAAY;
|
|
1
|
+
{"version":3,"file":"mediaBridge.d.ts","sourceRoot":"","sources":["../../src/media/mediaBridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,6CAA6C;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,yBAAyB;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,gDAAgD;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IACpD,OAAO,CAAC,MAAM,CAAmC;gBAErC,OAAO,EAAE,YAAY,EAAE,eAAe,CAAC,EAAE,WAAW;IAKhE,qDAAqD;IACrD,iBAAiB,IAAI,IAAI;IAIzB,+BAA+B;IAC/B,IAAI,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAKtC;;;;;OAKG;IACH,KAAK,IAAI,IAAI;IAMb,0BAA0B;IAC1B,KAAK,IAAI,IAAI;IAMb;;;;;;;OAOG;IACH,cAAc,CACZ,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,EACvB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACtC,IAAI;IAaP;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IAkBpB;;;;;;;;;;;;;;;;;;OAkBG;IACH,gBAAgB,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,GAAG,IAAI;IAchF;;;;;OAKG;IACH,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAI3E,+CAA+C;IAC/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAKrD,iDAAiD;IACjD,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAW1D;;;;;;OAMG;IACH,iBAAiB,IAAI,IAAI;IAWzB,OAAO,CAAC,KAAK;IAOb,OAAO,CAAC,YAAY;CAGrB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FirstFrameGate } from './firstFrameGate.js';
|
|
2
|
+
import { RemoteAudioPlayer } from './remoteAudio.js';
|
|
2
3
|
import { ViewRegistry } from './viewRegistry.js';
|
|
3
4
|
/**
|
|
4
5
|
* MediaBridge 把媒体适配器与 uid↔元素的挂载登记捆在一起。
|
|
@@ -14,13 +15,20 @@ import { ViewRegistry } from './viewRegistry.js';
|
|
|
14
15
|
export class MediaBridge {
|
|
15
16
|
adapter;
|
|
16
17
|
views = new ViewRegistry();
|
|
18
|
+
/** 远端音频由引擎自己播,与画面挂在哪无关(`remoteAudio.ts`)。 */
|
|
19
|
+
audio;
|
|
17
20
|
/** 已经抛过「首帧」的轨道,避免重复抛。 */
|
|
18
21
|
seenVideo = new Set();
|
|
19
22
|
/** 开摄像头之后等新画面上屏的那些人(见 awaitFirstVideoFrame)。 */
|
|
20
23
|
firstFrames = new FirstFrameGate();
|
|
21
24
|
events = null;
|
|
22
|
-
constructor(adapter) {
|
|
25
|
+
constructor(adapter, createAudioSink) {
|
|
23
26
|
this.adapter = adapter;
|
|
27
|
+
this.audio = new RemoteAudioPlayer(createAudioSink);
|
|
28
|
+
}
|
|
29
|
+
/** unlockRemoteAudio 在进房那次用户手势里再试一遍播放(浏览器自动播放限制)。 */
|
|
30
|
+
unlockRemoteAudio() {
|
|
31
|
+
this.audio.unlock();
|
|
24
32
|
}
|
|
25
33
|
/** open 建两条 PeerConnection。 */
|
|
26
34
|
open(events) {
|
|
@@ -55,6 +63,13 @@ export class MediaBridge {
|
|
|
55
63
|
*/
|
|
56
64
|
addRemoteTrack(trackId, track, uid, onFirstVideo) {
|
|
57
65
|
this.views.addTrack(trackId, track, uid);
|
|
66
|
+
/*
|
|
67
|
+
**音频一到就播,不等认领**:`ontrack` 与 `room.track_published` 谁先到都可能,
|
|
68
|
+
而声音不该等一个信令帧。认领不到 uid 的轨道照样出声——以后服务端换成音频槽位时
|
|
69
|
+
它根本不会有 uid(MEETING_ROOM_DESIGN §9 路 B)。
|
|
70
|
+
*/
|
|
71
|
+
if (track.kind === 'audio')
|
|
72
|
+
this.audio.add(trackId, track);
|
|
58
73
|
if (track.kind !== 'video' || this.seenVideo.has(trackId))
|
|
59
74
|
return;
|
|
60
75
|
this.seenVideo.add(trackId);
|
|
@@ -111,6 +126,7 @@ export class MediaBridge {
|
|
|
111
126
|
if (Object.hasOwn(remoteTracks, trackId))
|
|
112
127
|
continue;
|
|
113
128
|
this.views.removeTrack(trackId);
|
|
129
|
+
this.audio.remove(trackId);
|
|
114
130
|
// 忘掉「首帧抛过了」:同一条 track_id 再回来时该重新抛一次 firstVideoFrame。
|
|
115
131
|
this.seenVideo.delete(trackId);
|
|
116
132
|
}
|
|
@@ -164,6 +180,7 @@ export class MediaBridge {
|
|
|
164
180
|
}
|
|
165
181
|
clear() {
|
|
166
182
|
this.views.clear();
|
|
183
|
+
this.audio.clear();
|
|
167
184
|
this.seenVideo.clear();
|
|
168
185
|
this.firstFrames.clear();
|
|
169
186
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mediaBridge.js","sourceRoot":"","sources":["../../src/media/mediaBridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAOjD;;;;;;;;;;GAUG;AACH,MAAM,OAAO,WAAW;IACb,OAAO,CAAe;IACd,KAAK,GAAG,IAAI,YAAY,EAAE,CAAC;IAC5C,yBAAyB;IACR,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/C,gDAAgD;IAC/B,WAAW,GAAG,IAAI,cAAc,EAAE,CAAC;IAC5C,MAAM,GAA8B,IAAI,CAAC;IAEjD,YAAY,OAAqB;
|
|
1
|
+
{"version":3,"file":"mediaBridge.js","sourceRoot":"","sources":["../../src/media/mediaBridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 远端音频由 **Engine 自己播放**,与画面挂在哪无关。
|
|
3
|
+
*
|
|
4
|
+
* # 为什么这层要存在
|
|
5
|
+
*
|
|
6
|
+
* 原先远端的音视频装在同一条按 uid 的 `MediaStream` 里,挂到 `attachView(uid, el)`
|
|
7
|
+
* 的那个 `<video>` 上才出声。于是**没有格子的人就是彻底静音**:语音版式、页内小窗、
|
|
8
|
+
* 会议翻页之后不在本页的二十几个人,全都听不见。`call-uikit-react` 为此长了一个
|
|
9
|
+
* `RemoteAudioSink` 组件——那是把 Engine 该做的事推给了 Kit,自画 UI 的宿主还得再抄一遍。
|
|
10
|
+
*
|
|
11
|
+
* iOS / Android 从来没有这个问题:它们的远端音频由 WebRTC 原生音频设备直接播放。
|
|
12
|
+
* 这里把 Web 对齐过去(MEETING_ROOM_DESIGN §8 #10 / §9 ②):
|
|
13
|
+
* **`attachView` 从此只管画面,声音一律由引擎播。**
|
|
14
|
+
*
|
|
15
|
+
* # 为什么一条轨道一个 `<audio>`,而不是一个元素收全部
|
|
16
|
+
*
|
|
17
|
+
* `HTMLMediaElement` 绑一条带多路音频轨的 `MediaStream` 时,浏览器只播其中一路——
|
|
18
|
+
* 「汇进一个元素」实际做不到。真要一个出口就得走 `AudioContext`
|
|
19
|
+
* (每条轨 `createMediaStreamSource` 再 connect 到 destination),
|
|
20
|
+
* 那等于把自动播放解锁、采样率、重采样一整套问题揽进来,换不到任何东西。
|
|
21
|
+
* 元素是引擎自己建的、宿主看不见,几个都一样。
|
|
22
|
+
*
|
|
23
|
+
* # 为什么不按 uid 建
|
|
24
|
+
*
|
|
25
|
+
* 认领不到 uid 的轨道**也要出声**(以后服务端换成音频槽位时,轨道根本没有 uid,
|
|
26
|
+
* 见 §9 路 B)。按 track_id 建,这件事天然成立。
|
|
27
|
+
*/
|
|
28
|
+
/** AudioSink 是一个能播一条音频轨的出口。取 `HTMLAudioElement` 的最小交集,便于测试替身。 */
|
|
29
|
+
export interface AudioSink {
|
|
30
|
+
srcObject: MediaProvider | null;
|
|
31
|
+
autoplay: boolean;
|
|
32
|
+
muted: boolean;
|
|
33
|
+
play(): Promise<void>;
|
|
34
|
+
remove(): void;
|
|
35
|
+
}
|
|
36
|
+
/** SinkFactory 建一个出口。默认实现建一个隐藏的 `<audio>`,测试里换成替身。 */
|
|
37
|
+
export type SinkFactory = (track: MediaStreamTrack) => AudioSink | null;
|
|
38
|
+
/** RemoteAudioPlayer 持有全部远端音频的播放出口。 */
|
|
39
|
+
export declare class RemoteAudioPlayer {
|
|
40
|
+
private readonly sinks;
|
|
41
|
+
private readonly createSink;
|
|
42
|
+
constructor(createSink?: SinkFactory);
|
|
43
|
+
/**
|
|
44
|
+
* add 收下一条远端音频轨并立刻播。
|
|
45
|
+
*
|
|
46
|
+
* 重复 add 同一个 track_id 是幂等的——`ontrack` 与认领两条路都可能走到这里。
|
|
47
|
+
*/
|
|
48
|
+
add(trackId: string, track: MediaStreamTrack): void;
|
|
49
|
+
/** remove 摘掉一条(对方停发、退订、离房)。 */
|
|
50
|
+
remove(trackId: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* unlock 再试一次播放,**在一次用户手势的调用栈里调**。
|
|
53
|
+
*
|
|
54
|
+
* 浏览器的自动播放限制会让进房前建出来的出口停在暂停态。进房几乎总是由一次点击
|
|
55
|
+
* 触发(「加入会议」「接听」),所以引擎在 `joinRoom` 那一刻顺手再 `play()` 一遍。
|
|
56
|
+
* 已经在播的调它是空操作。
|
|
57
|
+
*/
|
|
58
|
+
unlock(): void;
|
|
59
|
+
/** clear 关掉全部出口(离房、logout)。 */
|
|
60
|
+
clear(): void;
|
|
61
|
+
/** count 是此刻挂着几个出口,供测试断言。 */
|
|
62
|
+
get count(): number;
|
|
63
|
+
private start;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=remoteAudio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remoteAudio.d.ts","sourceRoot":"","sources":["../../src/media/remoteAudio.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,iEAAiE;AACjE,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,sDAAsD;AACtD,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,gBAAgB,KAAK,SAAS,GAAG,IAAI,CAAC;AAExE,uCAAuC;AACvC,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;gBAE7B,UAAU,GAAE,WAA0B;IAIlD;;;;OAIG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAQnD,+BAA+B;IAC/B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQ7B;;;;;;OAMG;IACH,MAAM,IAAI,IAAI;IAId,+BAA+B;IAC/B,KAAK,IAAI,IAAI;IAIb,6BAA6B;IAC7B,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,OAAO,CAAC,KAAK;CAcd"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { logger } from '../logger.js';
|
|
2
|
+
/** RemoteAudioPlayer 持有全部远端音频的播放出口。 */
|
|
3
|
+
export class RemoteAudioPlayer {
|
|
4
|
+
sinks = new Map();
|
|
5
|
+
createSink;
|
|
6
|
+
constructor(createSink = domAudioSink) {
|
|
7
|
+
this.createSink = createSink;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* add 收下一条远端音频轨并立刻播。
|
|
11
|
+
*
|
|
12
|
+
* 重复 add 同一个 track_id 是幂等的——`ontrack` 与认领两条路都可能走到这里。
|
|
13
|
+
*/
|
|
14
|
+
add(trackId, track) {
|
|
15
|
+
if (this.sinks.has(trackId))
|
|
16
|
+
return;
|
|
17
|
+
const sink = this.createSink(track);
|
|
18
|
+
if (sink === null)
|
|
19
|
+
return;
|
|
20
|
+
this.sinks.set(trackId, sink);
|
|
21
|
+
this.start(sink);
|
|
22
|
+
}
|
|
23
|
+
/** remove 摘掉一条(对方停发、退订、离房)。 */
|
|
24
|
+
remove(trackId) {
|
|
25
|
+
const sink = this.sinks.get(trackId);
|
|
26
|
+
if (sink === undefined)
|
|
27
|
+
return;
|
|
28
|
+
this.sinks.delete(trackId);
|
|
29
|
+
sink.srcObject = null;
|
|
30
|
+
sink.remove();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* unlock 再试一次播放,**在一次用户手势的调用栈里调**。
|
|
34
|
+
*
|
|
35
|
+
* 浏览器的自动播放限制会让进房前建出来的出口停在暂停态。进房几乎总是由一次点击
|
|
36
|
+
* 触发(「加入会议」「接听」),所以引擎在 `joinRoom` 那一刻顺手再 `play()` 一遍。
|
|
37
|
+
* 已经在播的调它是空操作。
|
|
38
|
+
*/
|
|
39
|
+
unlock() {
|
|
40
|
+
for (const [, sink] of this.sinks)
|
|
41
|
+
this.start(sink);
|
|
42
|
+
}
|
|
43
|
+
/** clear 关掉全部出口(离房、logout)。 */
|
|
44
|
+
clear() {
|
|
45
|
+
for (const trackId of [...this.sinks.keys()])
|
|
46
|
+
this.remove(trackId);
|
|
47
|
+
}
|
|
48
|
+
/** count 是此刻挂着几个出口,供测试断言。 */
|
|
49
|
+
get count() {
|
|
50
|
+
return this.sinks.size;
|
|
51
|
+
}
|
|
52
|
+
start(sink) {
|
|
53
|
+
/*
|
|
54
|
+
**播放失败只记一条,不往上抛。**
|
|
55
|
+
|
|
56
|
+
自动播放被挡(还没有用户手势)是最常见的一种,而那时正确的做法是等 `unlock()`,
|
|
57
|
+
不是把一个 Promise 拒绝丢给宿主的 `error` 事件——宿主对它无事可做,
|
|
58
|
+
只会在控制台刷一片没人看的红。
|
|
59
|
+
*/
|
|
60
|
+
sink.play().catch((err) => {
|
|
61
|
+
logger.debug('远端音频暂时播不了(多半是自动播放限制,等进房那次点击解锁)', {
|
|
62
|
+
err: String(err),
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** domAudioSink 是默认实现:一个隐藏的 `<audio>`。没有 DOM(node 环境)时返回 null。 */
|
|
68
|
+
function domAudioSink(track) {
|
|
69
|
+
if (typeof document === 'undefined' || typeof MediaStream === 'undefined')
|
|
70
|
+
return null;
|
|
71
|
+
const el = document.createElement('audio');
|
|
72
|
+
el.autoplay = true;
|
|
73
|
+
/*
|
|
74
|
+
**挂进 document 而不是留一个游离元素**:游离的 `<audio>` 在 iOS Safari 上不出声。
|
|
75
|
+
不设 `display:none`——那在部分浏览器上会让媒体元素停播;`<audio>` 不带 `controls`
|
|
76
|
+
本来就没有可见的盒子,`hidden` 只是让它连布局都不参与。
|
|
77
|
+
*/
|
|
78
|
+
el.hidden = true;
|
|
79
|
+
el.srcObject = new MediaStream([track]);
|
|
80
|
+
document.body.appendChild(el);
|
|
81
|
+
return el;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=remoteAudio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remoteAudio.js","sourceRoot":"","sources":["../../src/media/remoteAudio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA0CtC,uCAAuC;AACvC,MAAM,OAAO,iBAAiB;IACX,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IACrC,UAAU,CAAc;IAEzC,YAAY,aAA0B,YAAY;QAChD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,OAAe,EAAE,KAAuB;QAC1C,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO;QACpC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO;QAC1B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IAED,+BAA+B;IAC/B,MAAM,CAAC,OAAe;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO;QAC/B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACH,MAAM;QACJ,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC;IAED,+BAA+B;IAC/B,KAAK;QACH,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACrE,CAAC;IAED,6BAA6B;IAC7B,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAEO,KAAK,CAAC,IAAe;QAC3B;;;;;;UAME;QACF,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACjC,MAAM,CAAC,KAAK,CAAC,gCAAgC,EAAE;gBAC7C,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC;aACjB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAED,kEAAkE;AAClE,SAAS,YAAY,CAAC,KAAuB;IAC3C,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,OAAO,WAAW,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IACvF,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC3C,EAAE,CAAC,QAAQ,GAAG,IAAI,CAAC;IACnB;;;;MAIE;IACF,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC;IACjB,EAAE,CAAC,SAAS,GAAG,IAAI,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAC9B,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VideoProfile } from './videoProfile.js';
|
|
2
|
+
/**
|
|
3
|
+
* addVideoSender 把一条上行视频挂到 pub 上,返回它的 sender。
|
|
4
|
+
*
|
|
5
|
+
* # 为什么不能用 addTrack
|
|
6
|
+
*
|
|
7
|
+
* `addTrack` 只会产生**一个 encoding**,浏览器里发 simulcast 必须在建
|
|
8
|
+
* transceiver 时就把 `sendEncodings` 给出来——协商之后再 `setParameters`
|
|
9
|
+
* 加层是加不上的(规范不允许改 encoding 的条数)。
|
|
10
|
+
*
|
|
11
|
+
* 这正是之前那个洞:`publishCamera(simulcast = true)` 的这个参数一路传进了
|
|
12
|
+
* `room.publish` 帧、**告诉服务端「我是 simulcast」**,可媒体面走的是裸
|
|
13
|
+
* `addTrack`,实际只发一层。服务端于是只看到空 RID 的单层(`ridToLayer("")`
|
|
14
|
+
* 当成 h),层选择无从谈起:订阅者报 `l` 也只能收到全速率的 h
|
|
15
|
+
* (`selectLayer` 的兜底),弱下行的那一方被自己的全速率流压死。
|
|
16
|
+
*
|
|
17
|
+
* `streams: [stream]` 不能省:msid 的第二段就是 cid,服务端靠它认领 m-line
|
|
18
|
+
* (协议 §3.2)。省掉它服务端永远认不回这条轨道。
|
|
19
|
+
*/
|
|
20
|
+
export declare function addVideoSender(pub: RTCPeerConnection, track: MediaStreamTrack, stream: MediaStream, simulcast: boolean, video: VideoProfile): RTCRtpSender;
|
|
21
|
+
//# sourceMappingURL=videoSender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"videoSender.d.ts","sourceRoot":"","sources":["../../src/media/videoSender.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAWtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,YAAY,GAClB,YAAY,CAiBd"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { logger } from '../logger.js';
|
|
2
|
+
import { simulcastEncodings } from './videoProfile.js';
|
|
3
|
+
/*
|
|
4
|
+
上行视频怎么挂到 pub 上、码率怎么压。
|
|
5
|
+
|
|
6
|
+
从 `webrtcAdapter.ts` 里单拎出来:这两件事只读画质档位、只碰传进来的 PC 与 sender,
|
|
7
|
+
**不碰适配器的任何状态**(预览、开关队列、代数那一套都不相干),
|
|
8
|
+
拆出来之后可以直接对着假 PC 单测,不必再 `Reflect.get` 去掏私有方法。
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* addVideoSender 把一条上行视频挂到 pub 上,返回它的 sender。
|
|
12
|
+
*
|
|
13
|
+
* # 为什么不能用 addTrack
|
|
14
|
+
*
|
|
15
|
+
* `addTrack` 只会产生**一个 encoding**,浏览器里发 simulcast 必须在建
|
|
16
|
+
* transceiver 时就把 `sendEncodings` 给出来——协商之后再 `setParameters`
|
|
17
|
+
* 加层是加不上的(规范不允许改 encoding 的条数)。
|
|
18
|
+
*
|
|
19
|
+
* 这正是之前那个洞:`publishCamera(simulcast = true)` 的这个参数一路传进了
|
|
20
|
+
* `room.publish` 帧、**告诉服务端「我是 simulcast」**,可媒体面走的是裸
|
|
21
|
+
* `addTrack`,实际只发一层。服务端于是只看到空 RID 的单层(`ridToLayer("")`
|
|
22
|
+
* 当成 h),层选择无从谈起:订阅者报 `l` 也只能收到全速率的 h
|
|
23
|
+
* (`selectLayer` 的兜底),弱下行的那一方被自己的全速率流压死。
|
|
24
|
+
*
|
|
25
|
+
* `streams: [stream]` 不能省:msid 的第二段就是 cid,服务端靠它认领 m-line
|
|
26
|
+
* (协议 §3.2)。省掉它服务端永远认不回这条轨道。
|
|
27
|
+
*/
|
|
28
|
+
export function addVideoSender(pub, track, stream, simulcast, video) {
|
|
29
|
+
if (!simulcast) {
|
|
30
|
+
const sender = pub.addTrack(track, stream);
|
|
31
|
+
applyVideoBitrate(sender, video);
|
|
32
|
+
return sender;
|
|
33
|
+
}
|
|
34
|
+
const transceiver = pub.addTransceiver(track, {
|
|
35
|
+
direction: 'sendonly',
|
|
36
|
+
streams: [stream],
|
|
37
|
+
sendEncodings: simulcastEncodings(video),
|
|
38
|
+
});
|
|
39
|
+
logger.info('上行视频已按 simulcast 发布', {
|
|
40
|
+
cid: track.id,
|
|
41
|
+
layers: simulcastEncodings(video).map((e) => e.rid).join(','),
|
|
42
|
+
});
|
|
43
|
+
applyVideoBitrate(transceiver.sender, video);
|
|
44
|
+
return transceiver.sender;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* applyVideoBitrate 给上行视频压一个码率上限。
|
|
48
|
+
*
|
|
49
|
+
* **不设的话浏览器会自己往上飙**:Chrome 对 720p 的默认上限远高于我们给
|
|
50
|
+
* simulcast h 层定的目标值,服务端的带宽预算(`bwe.go` 的 `bitrateHigh`)
|
|
51
|
+
* 就成了一个对不上的数字,降层判断跟着不准。
|
|
52
|
+
*
|
|
53
|
+
* 失败只记日志:码率是画质偏好,`setParameters` 被拒不该让通话打不出去。
|
|
54
|
+
*/
|
|
55
|
+
function applyVideoBitrate(sender, video) {
|
|
56
|
+
const params = sender.getParameters();
|
|
57
|
+
// encodings 可能还是空的(协商之前);补一个默认项,浏览器会认。
|
|
58
|
+
if (params.encodings.length === 0)
|
|
59
|
+
params.encodings = [{}];
|
|
60
|
+
/*
|
|
61
|
+
**simulcast 的三层各有各的码率,不能抹平成同一个值。**
|
|
62
|
+
全设成 h 的目标码率等于让 l / m 两层也按 1.5Mbps 发,
|
|
63
|
+
上行瞬间涨到三倍,而降层根本省不下带宽——降了个寂寞。
|
|
64
|
+
按 rid 对号入座;没有 rid(单层发布)才用整档的上限。
|
|
65
|
+
*/
|
|
66
|
+
const byRid = new Map(simulcastEncodings(video).map((e) => [e.rid, e.maxBitrate]));
|
|
67
|
+
for (const encoding of params.encodings) {
|
|
68
|
+
encoding.maxBitrate = byRid.get(encoding.rid) ?? video.maxBitrateBps;
|
|
69
|
+
}
|
|
70
|
+
void sender.setParameters(params).catch((err) => {
|
|
71
|
+
logger.info('设置上行码率失败,用浏览器默认值', { err: String(err) });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=videoSender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"videoSender.js","sourceRoot":"","sources":["../../src/media/videoSender.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;;EAME;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAsB,EACtB,KAAuB,EACvB,MAAmB,EACnB,SAAkB,EAClB,KAAmB;IAEnB,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC3C,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACjC,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,WAAW,GAAG,GAAG,CAAC,cAAc,CAAC,KAAK,EAAE;QAC5C,SAAS,EAAE,UAAU;QACrB,OAAO,EAAE,CAAC,MAAM,CAAC;QACjB,aAAa,EAAE,kBAAkB,CAAC,KAAK,CAAC;KACzC,CAAC,CAAC;IACH,MAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE;QACjC,GAAG,EAAE,KAAK,CAAC,EAAE;QACb,MAAM,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;KAC9D,CAAC,CAAC;IACH,iBAAiB,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC7C,OAAO,WAAW,CAAC,MAAM,CAAC;AAC5B,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CAAC,MAAoB,EAAE,KAAmB;IAClE,MAAM,MAAM,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;IACtC,uCAAuC;IACvC,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3D;;;;;MAKE;IACF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACnF,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACxC,QAAQ,CAAC,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC;IACvE,CAAC;IACD,KAAK,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACvD,MAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -24,9 +24,15 @@
|
|
|
24
24
|
export interface ViewElement {
|
|
25
25
|
srcObject: MediaProvider | null;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* ViewRegistry 管「哪条轨道属于谁」与「谁挂在哪个元素上」。
|
|
29
|
+
*
|
|
30
|
+
* **只有视频进得了这条流**(2.0.0 起):远端音频由引擎自己播(`remoteAudio.ts`),
|
|
31
|
+
* 挂载与否不影响听不听得见。两样都往这条流里塞的话,挂了 `<video>` 的人会**出两份声音**
|
|
32
|
+
* ——一份来自隐藏的 `<audio>`,一份来自那个 `<video>`。
|
|
33
|
+
*/
|
|
28
34
|
export declare class ViewRegistry {
|
|
29
|
-
/** uid →
|
|
35
|
+
/** uid → 该用户的画面流。 */
|
|
30
36
|
private readonly streams;
|
|
31
37
|
/** uid → 已挂载的元素。 */
|
|
32
38
|
private readonly views;
|
|
@@ -52,6 +58,8 @@ export declare class ViewRegistry {
|
|
|
52
58
|
/**
|
|
53
59
|
* attach 把某个 uid 的画面挂到元素上;`el` 传 null 表示卸载。
|
|
54
60
|
*
|
|
61
|
+
* **只管画面**:挂不挂都不影响听不听得见(远端音频由引擎自己播)。
|
|
62
|
+
*
|
|
55
63
|
* **卸载必须做**:组件卸载时不清 `srcObject`,被卸掉的 <video> 会连着
|
|
56
64
|
* MediaStream 一起被引用,画面停了但解码器还占着(CONVENTIONS §5)。
|
|
57
65
|
*/
|
|
@@ -65,7 +73,7 @@ export declare class ViewRegistry {
|
|
|
65
73
|
claimedTracks(): [string, string][];
|
|
66
74
|
/** viewOf 取某人此刻挂着的元素(没挂为 undefined)。 */
|
|
67
75
|
viewOf(uid: string): ViewElement | undefined;
|
|
68
|
-
/** streamFor
|
|
76
|
+
/** streamFor 取某人的**画面**流,宿主想自己挂载时用。声音不在里面。 */
|
|
69
77
|
streamFor(uid: string): MediaStream | undefined;
|
|
70
78
|
/** clear 清空全部登记(通话结束 / logout)。 */
|
|
71
79
|
clear(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewRegistry.d.ts","sourceRoot":"","sources":["../../src/media/viewRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;CACjC;AAED
|
|
1
|
+
{"version":3,"file":"viewRegistry.d.ts","sourceRoot":"","sources":["../../src/media/viewRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;GAMG;AACH,qBAAa,YAAY;IACvB,qBAAqB;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,oBAAoB;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD,4BAA4B;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAC/D,kDAAkD;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IAEpD;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAgCrE;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAMzC,mCAAmC;IACnC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAelC;;;;;;;OAOG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAWjD;;;;;OAKG;IACH,aAAa,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;IAInC,wCAAwC;IACxC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAI5C,8CAA8C;IAC9C,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAI/C,mCAAmC;IACnC,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,OAAO;CAKhB"}
|
|
@@ -14,9 +14,15 @@
|
|
|
14
14
|
* 迟到时把已经攒着的轨道补挂上去。漏掉这一步的表现是「有声音没画面」,
|
|
15
15
|
* 且只在网络较快时偶发。
|
|
16
16
|
*/
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* ViewRegistry 管「哪条轨道属于谁」与「谁挂在哪个元素上」。
|
|
19
|
+
*
|
|
20
|
+
* **只有视频进得了这条流**(2.0.0 起):远端音频由引擎自己播(`remoteAudio.ts`),
|
|
21
|
+
* 挂载与否不影响听不听得见。两样都往这条流里塞的话,挂了 `<video>` 的人会**出两份声音**
|
|
22
|
+
* ——一份来自隐藏的 `<audio>`,一份来自那个 `<video>`。
|
|
23
|
+
*/
|
|
18
24
|
export class ViewRegistry {
|
|
19
|
-
/** uid →
|
|
25
|
+
/** uid → 该用户的画面流。 */
|
|
20
26
|
streams = new Map();
|
|
21
27
|
/** uid → 已挂载的元素。 */
|
|
22
28
|
views = new Map();
|
|
@@ -36,8 +42,30 @@ export class ViewRegistry {
|
|
|
36
42
|
}
|
|
37
43
|
this.orphans.delete(trackId);
|
|
38
44
|
this.owners.set(trackId, uid);
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
// 音频只记账不进流:它由引擎自己播(见类注释)。记账仍要做——
|
|
46
|
+
// 对账要靠 owners 知道「这条轨道还在不在」。
|
|
47
|
+
if (track.kind !== 'audio') {
|
|
48
|
+
/*
|
|
49
|
+
**同一个人同一类轨道只留最新那条。**
|
|
50
|
+
|
|
51
|
+
会议分页翻走五秒会退订、翻回来重新订阅:`remoteTracks` 里那一条自始至终都在
|
|
52
|
+
(发布者没停发),所以 `syncRemoteTracks` 的对账扫不到它,而 `ontrack`
|
|
53
|
+
会送来一个**全新的** `MediaStreamTrack`。不摘旧的就变成流里同时挂着
|
|
54
|
+
「已经 ended 的旧轨道 + 新轨道」,`<video>` 播的是第一条——
|
|
55
|
+
**画面定格在最后一帧**(2026-09-18 真机在 iOS / Android 上先撞到,同一个根因)。
|
|
56
|
+
|
|
57
|
+
按 kind 摘而不是按 track_id:新旧两条的协议 track_id 是同一个,
|
|
58
|
+
浏览器给的对象却不是。2.0.0 一个人只有一路摄像头,所以「同类只留一条」成立;
|
|
59
|
+
将来加屏幕共享要改成按 source 分。
|
|
60
|
+
*/
|
|
61
|
+
const stream = this.streamOf(uid);
|
|
62
|
+
for (const existing of stream.getTracks()) {
|
|
63
|
+
if (existing.kind === track.kind && existing !== track)
|
|
64
|
+
stream.removeTrack(existing);
|
|
65
|
+
}
|
|
66
|
+
stream.addTrack(track);
|
|
67
|
+
this.refresh(uid);
|
|
68
|
+
}
|
|
41
69
|
}
|
|
42
70
|
/**
|
|
43
71
|
* claim 认领一条之前不知道归属的轨道。
|
|
@@ -72,6 +100,8 @@ export class ViewRegistry {
|
|
|
72
100
|
/**
|
|
73
101
|
* attach 把某个 uid 的画面挂到元素上;`el` 传 null 表示卸载。
|
|
74
102
|
*
|
|
103
|
+
* **只管画面**:挂不挂都不影响听不听得见(远端音频由引擎自己播)。
|
|
104
|
+
*
|
|
75
105
|
* **卸载必须做**:组件卸载时不清 `srcObject`,被卸掉的 <video> 会连着
|
|
76
106
|
* MediaStream 一起被引用,画面停了但解码器还占着(CONVENTIONS §5)。
|
|
77
107
|
*/
|
|
@@ -99,7 +129,7 @@ export class ViewRegistry {
|
|
|
99
129
|
viewOf(uid) {
|
|
100
130
|
return this.views.get(uid);
|
|
101
131
|
}
|
|
102
|
-
/** streamFor
|
|
132
|
+
/** streamFor 取某人的**画面**流,宿主想自己挂载时用。声音不在里面。 */
|
|
103
133
|
streamFor(uid) {
|
|
104
134
|
return this.streams.get(uid);
|
|
105
135
|
}
|