lecodes-sdk 2.2.7 → 2.2.8
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/global.d.ts +34 -0
- package/dist/types/inject.d.ts +1 -0
- package/dist/types/plugins/gen/webrtc/sdk/webrtc.RtcMedia.gen.d.ts +55 -0
- package/dist/types/plugins/gen/webrtc/sdk/webrtc.RtcPeerConnection.gen.d.ts +121 -0
- package/dist/types/plugins/gen/webrtc/sdk/webrtc.RtcVideo.gen.d.ts +25 -0
- package/dist/types/plugins/gen/webrtc/sdk/webrtc.d.ts +328 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/chisel.ts +2 -2
- package/src/inject.ts +7 -0
- package/src/plugins/gen/webrtc/contract.d.ts +342 -0
- package/src/plugins/gen/webrtc/sdk/webrtc.RtcMedia.gen.ts +86 -0
- package/src/plugins/gen/webrtc/sdk/webrtc.RtcPeerConnection.gen.ts +184 -0
- package/src/plugins/gen/webrtc/sdk/webrtc.RtcVideo.gen.ts +48 -0
- package/src/plugins/gen/webrtc/sdk/webrtc.ts +760 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
// GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/webrtc/sdk/webrtc.ts — do not edit here.
|
|
2
|
+
// Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
|
|
3
|
+
// The WebRTC API of the browser, over the plugin's wire: RTCPeerConnection, MediaStream,
|
|
4
|
+
// MediaStreamTrack, RTCDataChannel, RTCRtpSender / Receiver / Transceiver, mediaDevices, plus
|
|
5
|
+
// RTCVideoView — the tree node that draws a track (the browser's <video> has no twin in a tree
|
|
6
|
+
// of nodes). Code written against the browser reads the same here; a client of an SFU
|
|
7
|
+
// (mediasoup-client, LiveKit's) runs over these objects.
|
|
8
|
+
//
|
|
9
|
+
// What differs from the browser, because the objects live in the host and a call crosses to it:
|
|
10
|
+
// - what the browser answers at once from its own memory is a promise here when the host must
|
|
11
|
+
// be asked: `track.getSettings()`, `sender.getParameters()`, `channel.getBufferedAmount()`
|
|
12
|
+
// (there is no `bufferedAmount` property);
|
|
13
|
+
// - `pc.getTransceivers()` / `getSenders()` / `getReceivers()` answer from the SDK's copy, which
|
|
14
|
+
// is refreshed after every description is set and on every `track` event — the moments the
|
|
15
|
+
// mids and the directions change;
|
|
16
|
+
// - `createDataChannel` returns at once (the channel is "connecting"; what is sent before the
|
|
17
|
+
// host made it is queued).
|
|
18
|
+
//
|
|
19
|
+
// Hand-written over the generated wire because the wire names ids and the app names objects: the
|
|
20
|
+
// tracks, the senders, the transceivers and the channels are kept here, by the id the host gave
|
|
21
|
+
// them, and every event is turned into the browser's event on the object it concerns.
|
|
22
|
+
|
|
23
|
+
import type { Listener, NativeView } from "../../../../plugin"
|
|
24
|
+
import { RtcMediaBase, NAME as MEDIA_NAME } from "./webrtc.RtcMedia.gen"
|
|
25
|
+
import { RtcPeerConnectionBase, NAME as PC_NAME } from "./webrtc.RtcPeerConnection.gen"
|
|
26
|
+
import { RtcVideoBase, NAME as VIDEO_NAME } from "./webrtc.RtcVideo.gen"
|
|
27
|
+
import type {
|
|
28
|
+
AudioConstraints, AudioOutput, DataChannelInfo, DataChannelInit, DataChannelState, DeviceInfo, Encoding, FacingMode,
|
|
29
|
+
IceCandidate, IceConnectionState, IceGatheringState, IceServer, MediaKind, ObjectFit, OfferOptions,
|
|
30
|
+
PeerConnectionState, RtcConfiguration, RtcVideoEvents, SdpType, SenderInfo, SenderParameters, SessionDescription,
|
|
31
|
+
SignalingState, TrackInfo, TrackSettings, TransceiverDirection, TransceiverInfo, TransceiverInit, VideoConstraints,
|
|
32
|
+
} from "../contract"
|
|
33
|
+
|
|
34
|
+
export type {
|
|
35
|
+
AudioConstraints, AudioOutput, DataChannelInit, DataChannelState, DeviceInfo, Encoding, FacingMode, IceCandidate,
|
|
36
|
+
IceConnectionState, IceGatheringState, IceServer, MediaKind, ObjectFit, OfferOptions, PeerConnectionState,
|
|
37
|
+
RtcConfiguration, RtcVideoEvents, SdpType, SenderParameters, SessionDescription, SignalingState, TrackSettings,
|
|
38
|
+
TransceiverDirection, TransceiverInit, VideoConstraints,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const unsupported = (what: string, channel: string, kind: "service" | "view"): Error =>
|
|
42
|
+
new Error(`${what} isn't available on this host — it needs the "webrtc" plugin (app.json "plugins"), a registered "${channel}" ${kind}.`)
|
|
43
|
+
|
|
44
|
+
// ---- events: the browser's shape, on the objects of this file -------------------------------------
|
|
45
|
+
|
|
46
|
+
/** A listener of one event type; it gets the event, which carries `type` and `target`. */
|
|
47
|
+
export type RTCEventListener<E> = (event: E) => void
|
|
48
|
+
|
|
49
|
+
/** What every object of the API emits from: `addEventListener` / `removeEventListener`, and the
|
|
50
|
+
* `on<type>` properties the subclasses declare. */
|
|
51
|
+
export class RTCEventTarget {
|
|
52
|
+
private readonly _listeners = new Map<string, RTCEventListener<any>[]>()
|
|
53
|
+
|
|
54
|
+
addEventListener<E = any>(type: string, listener: RTCEventListener<E>): void {
|
|
55
|
+
let list = this._listeners.get(type)
|
|
56
|
+
if (!list) this._listeners.set(type, list = [])
|
|
57
|
+
if (!list.includes(listener)) list.push(listener)
|
|
58
|
+
}
|
|
59
|
+
removeEventListener<E = any>(type: string, listener: RTCEventListener<E>): void {
|
|
60
|
+
const list = this._listeners.get(type)
|
|
61
|
+
if (!list) return
|
|
62
|
+
const at = list.indexOf(listener)
|
|
63
|
+
if (at >= 0) list.splice(at, 1)
|
|
64
|
+
}
|
|
65
|
+
/** @internal Deliver `event` (its `type` names the `on<type>` handler too). */
|
|
66
|
+
_dispatch(event: { type: string, [key: string]: any }): void {
|
|
67
|
+
;(event as any).target = this
|
|
68
|
+
const handler = (this as any)["on" + event.type]
|
|
69
|
+
if (typeof handler === "function") handler.call(this, event)
|
|
70
|
+
const list = this._listeners.get(event.type)
|
|
71
|
+
if (list) for (const listener of list.slice()) listener(event)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RTCPeerConnectionIceEvent { type: "icecandidate", candidate: RTCIceCandidate | null, target: RTCPeerConnection }
|
|
76
|
+
export interface RTCTrackEvent { type: "track", track: MediaStreamTrack, streams: MediaStream[], receiver: RTCRtpReceiver, transceiver: RTCRtpTransceiver, target: RTCPeerConnection }
|
|
77
|
+
export interface RTCDataChannelEvent { type: "datachannel", channel: RTCDataChannel, target: RTCPeerConnection }
|
|
78
|
+
export interface RTCMessageEvent { type: "message", data: string | ArrayBuffer, target: RTCDataChannel }
|
|
79
|
+
export interface RTCErrorEvent { type: "error", message: string, target: RTCDataChannel }
|
|
80
|
+
export interface RTCEvent<T extends RTCEventTarget = RTCEventTarget> { type: string, target: T }
|
|
81
|
+
|
|
82
|
+
// ---- the media service: one session, the tracks by id ---------------------------------------------
|
|
83
|
+
|
|
84
|
+
let mediaService: RtcMediaBase | null = null
|
|
85
|
+
/** Every track the app holds, by the host's id: where the host's events find their object. */
|
|
86
|
+
const tracks = new Map<string, MediaStreamTrack>()
|
|
87
|
+
let nextLocalId = 1
|
|
88
|
+
|
|
89
|
+
const media = (): RtcMediaBase => {
|
|
90
|
+
if (mediaService) return mediaService
|
|
91
|
+
if (!RtcMediaBase.supports(MEDIA_NAME)) throw unsupported("mediaDevices", MEDIA_NAME, "service")
|
|
92
|
+
mediaService = new RtcMediaBase()
|
|
93
|
+
mediaService.on("trackEnded", ({ trackId }) => tracks.get(trackId)?._ended())
|
|
94
|
+
mediaService.on("deviceChange", () => mediaDevices._dispatch({ type: "devicechange" }))
|
|
95
|
+
return mediaService
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The track object of a host id: the one the app holds, or a new one for an id first seen now. */
|
|
99
|
+
const trackOf = (info: TrackInfo, remote: boolean): MediaStreamTrack => {
|
|
100
|
+
let track = tracks.get(info.id)
|
|
101
|
+
if (!track) tracks.set(info.id, track = new MediaStreamTrack(info, remote))
|
|
102
|
+
return track
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** One track of audio or video, local (`mediaDevices.getUserMedia`) or received (`pc.ontrack`). */
|
|
106
|
+
export class MediaStreamTrack extends RTCEventTarget {
|
|
107
|
+
readonly id: string
|
|
108
|
+
readonly kind: MediaKind
|
|
109
|
+
readonly label: string
|
|
110
|
+
/** @internal */
|
|
111
|
+
readonly _remote: boolean
|
|
112
|
+
private _enabled: boolean
|
|
113
|
+
private _muted: boolean
|
|
114
|
+
private _state: "live" | "ended" = "live"
|
|
115
|
+
|
|
116
|
+
onended: RTCEventListener<RTCEvent<MediaStreamTrack>> | null = null
|
|
117
|
+
onmute: RTCEventListener<RTCEvent<MediaStreamTrack>> | null = null
|
|
118
|
+
onunmute: RTCEventListener<RTCEvent<MediaStreamTrack>> | null = null
|
|
119
|
+
|
|
120
|
+
/** @internal */
|
|
121
|
+
constructor(info: TrackInfo, remote: boolean) {
|
|
122
|
+
super()
|
|
123
|
+
this.id = info.id
|
|
124
|
+
this.kind = info.kind
|
|
125
|
+
this.label = info.label
|
|
126
|
+
this._remote = remote
|
|
127
|
+
this._enabled = info.enabled
|
|
128
|
+
this._muted = info.muted ?? false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A disabled track sends silence / black frames and stays in every connection. */
|
|
132
|
+
get enabled(): boolean { return this._enabled }
|
|
133
|
+
set enabled(value: boolean) {
|
|
134
|
+
if (this._enabled === value || this._state === "ended") return
|
|
135
|
+
this._enabled = value
|
|
136
|
+
if (!this._remote) media().setTrackEnabled(this.id, value).catch(() => {})
|
|
137
|
+
}
|
|
138
|
+
/** A received track the other side is not sending on. */
|
|
139
|
+
get muted(): boolean { return this._muted }
|
|
140
|
+
get readyState(): "live" | "ended" { return this._state }
|
|
141
|
+
|
|
142
|
+
/** End the track: a local one releases its device (once no track of it is left). */
|
|
143
|
+
stop(): void {
|
|
144
|
+
if (this._state === "ended") return
|
|
145
|
+
this._state = "ended"
|
|
146
|
+
tracks.delete(this.id)
|
|
147
|
+
if (!this._remote && mediaService) mediaService.stopTrack(this.id).catch(() => {})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Re-aim a local video track: the other camera (`facingMode`), another size or rate. */
|
|
151
|
+
applyConstraints(constraints: VideoConstraints = {}): Promise<void> {
|
|
152
|
+
if (this._remote || this.kind !== "video") return Promise.reject(new Error("applyConstraints: a local video track only"))
|
|
153
|
+
return media().applyConstraints(this.id, constraints)
|
|
154
|
+
}
|
|
155
|
+
/** What a local video track captures with (a promise: the host knows). */
|
|
156
|
+
getSettings(): Promise<TrackSettings> {
|
|
157
|
+
if (this._remote) return Promise.resolve({})
|
|
158
|
+
return media().getTrackSettings(this.id)
|
|
159
|
+
}
|
|
160
|
+
/** The same track object (a track is a handle, not a buffer). */
|
|
161
|
+
clone(): MediaStreamTrack { return this }
|
|
162
|
+
|
|
163
|
+
/** @internal The host reports the track gone. */
|
|
164
|
+
_ended(): void {
|
|
165
|
+
if (this._state === "ended") return
|
|
166
|
+
this._state = "ended"
|
|
167
|
+
tracks.delete(this.id)
|
|
168
|
+
this._dispatch({ type: "ended" })
|
|
169
|
+
}
|
|
170
|
+
/** @internal */
|
|
171
|
+
_setMuted(muted: boolean): void {
|
|
172
|
+
if (this._muted === muted) return
|
|
173
|
+
this._muted = muted
|
|
174
|
+
this._dispatch({ type: muted ? "mute" : "unmute" })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A group of tracks: what `getUserMedia` resolves with, what a `track` event names, what
|
|
179
|
+
* `addTrack` associates a track with. The id is a label on the wire. */
|
|
180
|
+
export class MediaStream extends RTCEventTarget {
|
|
181
|
+
readonly id: string
|
|
182
|
+
private readonly _tracks: MediaStreamTrack[]
|
|
183
|
+
|
|
184
|
+
onaddtrack: RTCEventListener<{ type: "addtrack", track: MediaStreamTrack, target: MediaStream }> | null = null
|
|
185
|
+
onremovetrack: RTCEventListener<{ type: "removetrack", track: MediaStreamTrack, target: MediaStream }> | null = null
|
|
186
|
+
|
|
187
|
+
constructor(tracks: MediaStreamTrack[] | MediaStream = [], id?: string) {
|
|
188
|
+
super()
|
|
189
|
+
this.id = id ?? `s${nextLocalId++}`
|
|
190
|
+
this._tracks = tracks instanceof MediaStream ? tracks.getTracks() : tracks.slice()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Whether a track of it is live. */
|
|
194
|
+
get active(): boolean { return this._tracks.some(t => t.readyState === "live") }
|
|
195
|
+
getTracks(): MediaStreamTrack[] { return this._tracks.slice() }
|
|
196
|
+
getAudioTracks(): MediaStreamTrack[] { return this._tracks.filter(t => t.kind === "audio") }
|
|
197
|
+
getVideoTracks(): MediaStreamTrack[] { return this._tracks.filter(t => t.kind === "video") }
|
|
198
|
+
getTrackById(id: string): MediaStreamTrack | null { return this._tracks.find(t => t.id === id) ?? null }
|
|
199
|
+
addTrack(track: MediaStreamTrack): void {
|
|
200
|
+
if (this._tracks.includes(track)) return
|
|
201
|
+
this._tracks.push(track)
|
|
202
|
+
this._dispatch({ type: "addtrack", track })
|
|
203
|
+
}
|
|
204
|
+
removeTrack(track: MediaStreamTrack): void {
|
|
205
|
+
const at = this._tracks.indexOf(track)
|
|
206
|
+
if (at < 0) return
|
|
207
|
+
this._tracks.splice(at, 1)
|
|
208
|
+
this._dispatch({ type: "removetrack", track })
|
|
209
|
+
}
|
|
210
|
+
clone(): MediaStream { return new MediaStream(this._tracks) }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The constraints of `getUserMedia`, the browser's: `true` for the default device, an object to say which. */
|
|
214
|
+
export interface MediaStreamConstraints {
|
|
215
|
+
audio?: boolean | AudioConstraints
|
|
216
|
+
video?: boolean | VideoConstraints
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
class MediaDevices extends RTCEventTarget {
|
|
220
|
+
ondevicechange: RTCEventListener<RTCEvent<MediaDevices>> | null = null
|
|
221
|
+
|
|
222
|
+
/** Whether this host registered the "rtcMedia" service — the devices. */
|
|
223
|
+
get isSupported(): boolean { return RtcMediaBase.supports(MEDIA_NAME) }
|
|
224
|
+
|
|
225
|
+
/** Capture: asks for the permissions it needs, resolves with a stream of the new tracks. Rejects
|
|
226
|
+
* with "denied" / "notFound" / "failed". */
|
|
227
|
+
async getUserMedia(constraints: MediaStreamConstraints = { audio: true, video: true }): Promise<MediaStream> {
|
|
228
|
+
const wire: { audio?: AudioConstraints, video?: VideoConstraints } = {}
|
|
229
|
+
if (constraints.audio) wire.audio = constraints.audio === true ? {} : constraints.audio
|
|
230
|
+
if (constraints.video) wire.video = constraints.video === true ? {} : constraints.video
|
|
231
|
+
if (!wire.audio && !wire.video) throw new Error("getUserMedia: audio or video must be requested")
|
|
232
|
+
const info = await media().getUserMedia(wire)
|
|
233
|
+
return new MediaStream(info.tracks.map(t => trackOf(t, false)), info.id)
|
|
234
|
+
}
|
|
235
|
+
/** The devices the host can capture with (their labels need a granted permission on the web). */
|
|
236
|
+
enumerateDevices(): Promise<DeviceInfo[]> { return media().enumerateDevices() }
|
|
237
|
+
/** Where the call's audio plays on a phone: the earpiece or the loudspeaker. A no-op on the web. */
|
|
238
|
+
setAudioOutput(output: AudioOutput): Promise<void> { return media().setAudioOutput(output) }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** `navigator.mediaDevices` of the browser: the capture and the device list. Host-optional —
|
|
242
|
+
* `mediaDevices.isSupported` before offering a call. */
|
|
243
|
+
// PURE IIFE so an app that never captures tree-shakes the media side away.
|
|
244
|
+
export const mediaDevices: MediaDevices = /*#__PURE__*/ (() => new MediaDevices())()
|
|
245
|
+
|
|
246
|
+
// ---- the peer connection ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
/** The browser's RTCSessionDescription: `{ type, sdp }`, as it is signaled. */
|
|
249
|
+
export class RTCSessionDescription implements SessionDescription {
|
|
250
|
+
readonly type: SdpType
|
|
251
|
+
readonly sdp: string
|
|
252
|
+
constructor(init: SessionDescription) { this.type = init.type; this.sdp = init.sdp }
|
|
253
|
+
toJSON(): SessionDescription { return { type: this.type, sdp: this.sdp } }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The browser's RTCIceCandidate: what `icecandidate` carries and `addIceCandidate` takes. */
|
|
257
|
+
export class RTCIceCandidate implements IceCandidate {
|
|
258
|
+
readonly candidate: string
|
|
259
|
+
readonly sdpMid: string | null
|
|
260
|
+
readonly sdpMLineIndex: number | null
|
|
261
|
+
readonly usernameFragment: string | undefined
|
|
262
|
+
constructor(init: RTCIceCandidateInit) {
|
|
263
|
+
this.candidate = init.candidate ?? ""
|
|
264
|
+
this.sdpMid = init.sdpMid ?? null
|
|
265
|
+
this.sdpMLineIndex = init.sdpMLineIndex ?? null
|
|
266
|
+
this.usernameFragment = init.usernameFragment
|
|
267
|
+
}
|
|
268
|
+
toJSON(): IceCandidate {
|
|
269
|
+
const json: IceCandidate = { candidate: this.candidate, sdpMid: this.sdpMid, sdpMLineIndex: this.sdpMLineIndex }
|
|
270
|
+
if (this.usernameFragment !== undefined) json.usernameFragment = this.usernameFragment
|
|
271
|
+
return json
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export interface RTCIceCandidateInit { candidate?: string, sdpMid?: string | null, sdpMLineIndex?: number | null, usernameFragment?: string }
|
|
275
|
+
|
|
276
|
+
/** The sending half of a transceiver: the track it sends and its encodings. */
|
|
277
|
+
export class RTCRtpSender {
|
|
278
|
+
/** @internal */
|
|
279
|
+
readonly _id: string
|
|
280
|
+
private readonly _pc: RTCPeerConnection
|
|
281
|
+
private _track: MediaStreamTrack | null
|
|
282
|
+
|
|
283
|
+
/** @internal */
|
|
284
|
+
constructor(pc: RTCPeerConnection, info: SenderInfo) {
|
|
285
|
+
this._pc = pc
|
|
286
|
+
this._id = info.id
|
|
287
|
+
this._track = info.trackId ? tracks.get(info.trackId) ?? null : null
|
|
288
|
+
}
|
|
289
|
+
/** What it sends; null = nothing. */
|
|
290
|
+
get track(): MediaStreamTrack | null { return this._track }
|
|
291
|
+
/** Swap the track without renegotiating; null = send nothing. */
|
|
292
|
+
async replaceTrack(track: MediaStreamTrack | null): Promise<void> {
|
|
293
|
+
await this._pc._s.replaceTrack(this._id, track?.id ?? null)
|
|
294
|
+
this._track = track
|
|
295
|
+
}
|
|
296
|
+
/** The encodings (a promise: the host holds them). */
|
|
297
|
+
getParameters(): Promise<SenderParameters> { return this._pc._s.getSenderParameters(this._id) }
|
|
298
|
+
/** The encodings' active / bitrate / scale. */
|
|
299
|
+
setParameters(parameters: SenderParameters): Promise<void> { return this._pc._s.setSenderParameters(this._id, parameters) }
|
|
300
|
+
/** The stats of this sender. */
|
|
301
|
+
getStats(): Promise<Map<string, any>> { return this._pc.getStats(this._track) }
|
|
302
|
+
/** @internal */
|
|
303
|
+
_update(info: SenderInfo): void { this._track = info.trackId ? tracks.get(info.trackId) ?? this._track : null }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The receiving half of a transceiver: its track exists from the start, media arrives into it. */
|
|
307
|
+
export class RTCRtpReceiver {
|
|
308
|
+
readonly track: MediaStreamTrack
|
|
309
|
+
private readonly _pc: RTCPeerConnection
|
|
310
|
+
/** @internal */
|
|
311
|
+
constructor(pc: RTCPeerConnection, track: MediaStreamTrack) { this._pc = pc; this.track = track }
|
|
312
|
+
getStats(): Promise<Map<string, any>> { return this._pc.getStats(this.track) }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** One m-line of the session: a sender and a receiver, a direction, a mid once negotiated. */
|
|
316
|
+
export class RTCRtpTransceiver {
|
|
317
|
+
/** @internal */
|
|
318
|
+
readonly _id: string
|
|
319
|
+
readonly sender: RTCRtpSender
|
|
320
|
+
readonly receiver: RTCRtpReceiver
|
|
321
|
+
private readonly _pc: RTCPeerConnection
|
|
322
|
+
private _mid: string | null
|
|
323
|
+
private _direction: TransceiverDirection
|
|
324
|
+
private _currentDirection: TransceiverDirection | null
|
|
325
|
+
private _stopped: boolean
|
|
326
|
+
|
|
327
|
+
/** @internal */
|
|
328
|
+
constructor(pc: RTCPeerConnection, info: TransceiverInfo) {
|
|
329
|
+
this._pc = pc
|
|
330
|
+
this._id = info.id
|
|
331
|
+
this.sender = new RTCRtpSender(pc, info.sender)
|
|
332
|
+
this.receiver = new RTCRtpReceiver(pc, trackOf(info.receiverTrack, true))
|
|
333
|
+
this._mid = info.mid
|
|
334
|
+
this._direction = info.direction
|
|
335
|
+
this._currentDirection = info.currentDirection
|
|
336
|
+
this._stopped = info.stopped
|
|
337
|
+
}
|
|
338
|
+
/** The m-line's id once a description named it. */
|
|
339
|
+
get mid(): string | null { return this._mid }
|
|
340
|
+
/** The direction wanted; setting it takes effect at the next negotiation. */
|
|
341
|
+
get direction(): TransceiverDirection { return this._direction }
|
|
342
|
+
set direction(value: TransceiverDirection) {
|
|
343
|
+
if (this._direction === value) return
|
|
344
|
+
this._direction = value
|
|
345
|
+
this._pc._s.setTransceiverDirection(this._id, value).catch(() => {})
|
|
346
|
+
}
|
|
347
|
+
/** The direction negotiated; null before the first answer. */
|
|
348
|
+
get currentDirection(): TransceiverDirection | null { return this._currentDirection }
|
|
349
|
+
get stopped(): boolean { return this._stopped }
|
|
350
|
+
/** Stop sending and receiving on it; the next negotiation removes the m-line. */
|
|
351
|
+
stop(): void {
|
|
352
|
+
if (this._stopped) return
|
|
353
|
+
this._stopped = true
|
|
354
|
+
this._direction = "stopped"
|
|
355
|
+
this._pc._s.stopTransceiver(this._id).catch(() => {})
|
|
356
|
+
}
|
|
357
|
+
/** @internal */
|
|
358
|
+
_update(info: TransceiverInfo): void {
|
|
359
|
+
this._mid = info.mid
|
|
360
|
+
this._direction = info.direction
|
|
361
|
+
this._currentDirection = info.currentDirection
|
|
362
|
+
this._stopped = info.stopped
|
|
363
|
+
this.sender._update(info.sender)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** A data channel: strings and bytes, ordered and reliable unless said otherwise. */
|
|
368
|
+
export class RTCDataChannel extends RTCEventTarget {
|
|
369
|
+
readonly label: string
|
|
370
|
+
readonly ordered: boolean
|
|
371
|
+
readonly maxPacketLifeTime: number | null
|
|
372
|
+
readonly maxRetransmits: number | null
|
|
373
|
+
readonly protocol: string
|
|
374
|
+
readonly negotiated: boolean
|
|
375
|
+
/** Bytes arrive as an ArrayBuffer (the browser's "blob" is not a thing here). */
|
|
376
|
+
binaryType: "arraybuffer" = "arraybuffer"
|
|
377
|
+
private readonly _pc: RTCPeerConnection
|
|
378
|
+
/** @internal The host's id; null until the host made the channel. */
|
|
379
|
+
_id: string | null
|
|
380
|
+
private _streamId: number | null
|
|
381
|
+
private _state: DataChannelState
|
|
382
|
+
private _threshold = 0
|
|
383
|
+
/** What was sent before the host made the channel. */
|
|
384
|
+
private _queue: (string | Uint8Array)[] | null = null
|
|
385
|
+
|
|
386
|
+
onopen: RTCEventListener<RTCEvent<RTCDataChannel>> | null = null
|
|
387
|
+
onclose: RTCEventListener<RTCEvent<RTCDataChannel>> | null = null
|
|
388
|
+
onerror: RTCEventListener<RTCErrorEvent> | null = null
|
|
389
|
+
onmessage: RTCEventListener<RTCMessageEvent> | null = null
|
|
390
|
+
onbufferedamountlow: RTCEventListener<RTCEvent<RTCDataChannel>> | null = null
|
|
391
|
+
|
|
392
|
+
/** @internal `info` for a channel the host already made (the other side's); `init` + a label for
|
|
393
|
+
* one being made. */
|
|
394
|
+
constructor(pc: RTCPeerConnection, label: string, init: DataChannelInit, info: DataChannelInfo | null) {
|
|
395
|
+
super()
|
|
396
|
+
this._pc = pc
|
|
397
|
+
this.label = info?.label ?? label
|
|
398
|
+
this.ordered = info?.ordered ?? init.ordered ?? true
|
|
399
|
+
this.maxPacketLifeTime = info?.maxPacketLifeTime ?? init.maxPacketLifeTime ?? null
|
|
400
|
+
this.maxRetransmits = info?.maxRetransmits ?? init.maxRetransmits ?? null
|
|
401
|
+
this.protocol = info?.protocol ?? init.protocol ?? ""
|
|
402
|
+
this.negotiated = info?.negotiated ?? init.negotiated ?? false
|
|
403
|
+
this._id = info?.id ?? null
|
|
404
|
+
this._streamId = info?.streamId ?? init.id ?? null
|
|
405
|
+
this._state = info?.readyState ?? "connecting"
|
|
406
|
+
if (!info) this._queue = []
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The SCTP stream id; null until the transport assigned one. */
|
|
410
|
+
get id(): number | null { return this._streamId }
|
|
411
|
+
get readyState(): DataChannelState { return this._state }
|
|
412
|
+
/** Bytes queued and not yet sent (a promise: the host counts them). */
|
|
413
|
+
getBufferedAmount(): Promise<number> { return this._id ? this._pc._s.getBufferedAmount(this._id) : Promise.resolve(0) }
|
|
414
|
+
get bufferedAmountLowThreshold(): number { return this._threshold }
|
|
415
|
+
set bufferedAmountLowThreshold(value: number) {
|
|
416
|
+
this._threshold = value
|
|
417
|
+
if (this._id) this._pc._s.setBufferedAmountLowThreshold(this._id, value).catch(() => {})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Send a string or bytes. Throws when the channel is closing or closed. */
|
|
421
|
+
send(data: string | ArrayBuffer | ArrayBufferView): void {
|
|
422
|
+
if (this._state === "closing" || this._state === "closed") throw new Error("RTCDataChannel.send: the channel is " + this._state)
|
|
423
|
+
const payload = typeof data === "string" ? data
|
|
424
|
+
: data instanceof ArrayBuffer ? new Uint8Array(data)
|
|
425
|
+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
426
|
+
if (this._queue) { this._queue.push(payload); return }
|
|
427
|
+
this._send(payload)
|
|
428
|
+
}
|
|
429
|
+
private _send(payload: string | Uint8Array): void {
|
|
430
|
+
const id = this._id!
|
|
431
|
+
const sent = typeof payload === "string" ? this._pc._s.sendText(id, payload) : this._pc._s.sendBinary(id, payload)
|
|
432
|
+
sent.catch(e => this._dispatch({ type: "error", message: e instanceof Error ? e.message : String(e) }))
|
|
433
|
+
}
|
|
434
|
+
close(): void {
|
|
435
|
+
if (this._state === "closed" || this._state === "closing") return
|
|
436
|
+
this._state = "closing"
|
|
437
|
+
this._queue = null
|
|
438
|
+
if (this._id) this._pc._s.closeDataChannel(this._id).catch(() => {})
|
|
439
|
+
else this._setState("closed")
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** @internal The host made it: flush what was sent meanwhile. */
|
|
443
|
+
_made(info: DataChannelInfo): void {
|
|
444
|
+
this._id = info.id
|
|
445
|
+
this._streamId = info.streamId
|
|
446
|
+
if (this._threshold) this._pc._s.setBufferedAmountLowThreshold(info.id, this._threshold).catch(() => {})
|
|
447
|
+
const queue = this._queue
|
|
448
|
+
this._queue = null
|
|
449
|
+
if (this._state === "closing") { this._pc._s.closeDataChannel(info.id).catch(() => {}); return }
|
|
450
|
+
if (info.readyState !== this._state) this._setState(info.readyState)
|
|
451
|
+
if (queue) for (const payload of queue) this._send(payload)
|
|
452
|
+
}
|
|
453
|
+
/** @internal */
|
|
454
|
+
_setState(state: DataChannelState): void {
|
|
455
|
+
if (this._state === state) return
|
|
456
|
+
this._state = state
|
|
457
|
+
if (state === "open") this._dispatch({ type: "open" })
|
|
458
|
+
else if (state === "closed") this._dispatch({ type: "close" })
|
|
459
|
+
}
|
|
460
|
+
/** @internal */
|
|
461
|
+
_message(text: string | undefined, binary: Uint8Array | undefined): void {
|
|
462
|
+
const data = text !== undefined ? text : binary ? binary.buffer.slice(binary.byteOffset, binary.byteOffset + binary.byteLength) : ""
|
|
463
|
+
this._dispatch({ type: "message", data })
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** `RTCConfiguration` of the browser, with `iceServers[].urls` as one URL or several. */
|
|
468
|
+
export interface RTCConfiguration extends Omit<RtcConfiguration, "iceServers"> {
|
|
469
|
+
iceServers?: { urls: string | string[], username?: string, credential?: string }[]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const configurationWire = (config: RTCConfiguration): RtcConfiguration => {
|
|
473
|
+
const { iceServers, ...rest } = config
|
|
474
|
+
const wire: RtcConfiguration = { ...rest }
|
|
475
|
+
if (iceServers) {
|
|
476
|
+
wire.iceServers = iceServers.map(s => {
|
|
477
|
+
const server: IceServer = { urls: Array.isArray(s.urls) ? s.urls : [s.urls] }
|
|
478
|
+
if (s.username !== undefined) server.username = s.username
|
|
479
|
+
if (s.credential !== undefined) server.credential = s.credential
|
|
480
|
+
return server
|
|
481
|
+
})
|
|
482
|
+
}
|
|
483
|
+
return wire
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const descriptionOf = (d: SessionDescription | RTCSessionDescription | { type: SdpType, sdp?: string }): SessionDescription =>
|
|
487
|
+
({ type: d.type, sdp: d.sdp ?? "" })
|
|
488
|
+
|
|
489
|
+
/** A connection to one peer: the browser's RTCPeerConnection. The session with the host opens on
|
|
490
|
+
* the first call and closes with `close()`. Signaling — carrying the descriptions and the
|
|
491
|
+
* candidates to the other side — is the app's. */
|
|
492
|
+
export class RTCPeerConnection extends RTCEventTarget {
|
|
493
|
+
/** Whether this host registered the "rtcPeerConnection" service. */
|
|
494
|
+
static get isSupported(): boolean { return RtcPeerConnectionBase.supports(PC_NAME) }
|
|
495
|
+
|
|
496
|
+
/** @internal The session. */
|
|
497
|
+
readonly _s: RtcPeerConnectionBase
|
|
498
|
+
private _configuration: RTCConfiguration
|
|
499
|
+
private _local: RTCSessionDescription | null = null
|
|
500
|
+
private _remote: RTCSessionDescription | null = null
|
|
501
|
+
private _signaling: SignalingState = "stable"
|
|
502
|
+
private _gathering: IceGatheringState = "new"
|
|
503
|
+
private _ice: IceConnectionState = "new"
|
|
504
|
+
private _connection: PeerConnectionState = "new"
|
|
505
|
+
private readonly _transceivers: RTCRtpTransceiver[] = []
|
|
506
|
+
private readonly _channels = new Map<string, RTCDataChannel>()
|
|
507
|
+
private readonly _remoteTracks = new Set<string>()
|
|
508
|
+
/** While a local description is being set, its candidates wait: the browser delivers them
|
|
509
|
+
* after the promise, and the app's signaling expects the description first. */
|
|
510
|
+
private _settingLocal = 0
|
|
511
|
+
private _heldCandidates: (RTCIceCandidate | null)[] = []
|
|
512
|
+
|
|
513
|
+
onnegotiationneeded: RTCEventListener<RTCEvent<RTCPeerConnection>> | null = null
|
|
514
|
+
onicecandidate: RTCEventListener<RTCPeerConnectionIceEvent> | null = null
|
|
515
|
+
onsignalingstatechange: RTCEventListener<RTCEvent<RTCPeerConnection>> | null = null
|
|
516
|
+
onicegatheringstatechange: RTCEventListener<RTCEvent<RTCPeerConnection>> | null = null
|
|
517
|
+
oniceconnectionstatechange: RTCEventListener<RTCEvent<RTCPeerConnection>> | null = null
|
|
518
|
+
onconnectionstatechange: RTCEventListener<RTCEvent<RTCPeerConnection>> | null = null
|
|
519
|
+
ontrack: RTCEventListener<RTCTrackEvent> | null = null
|
|
520
|
+
ondatachannel: RTCEventListener<RTCDataChannelEvent> | null = null
|
|
521
|
+
|
|
522
|
+
constructor(configuration: RTCConfiguration = {}) {
|
|
523
|
+
super()
|
|
524
|
+
if (!RtcPeerConnectionBase.supports(PC_NAME)) throw unsupported("RTCPeerConnection", PC_NAME, "service")
|
|
525
|
+
this._configuration = configuration
|
|
526
|
+
const s = this._s = new RtcPeerConnectionBase(configurationWire(configuration))
|
|
527
|
+
s.on("negotiationNeeded", () => this._dispatch({ type: "negotiationneeded" }))
|
|
528
|
+
s.on("iceCandidate", ({ candidate }) => {
|
|
529
|
+
const ice = candidate ? new RTCIceCandidate(candidate) : null
|
|
530
|
+
if (this._settingLocal > 0) this._heldCandidates.push(ice)
|
|
531
|
+
else this._dispatch({ type: "icecandidate", candidate: ice })
|
|
532
|
+
})
|
|
533
|
+
s.on("signalingStateChange", ({ state }) => { this._signaling = state; this._dispatch({ type: "signalingstatechange" }) })
|
|
534
|
+
s.on("iceGatheringStateChange", ({ state }) => { this._gathering = state; this._dispatch({ type: "icegatheringstatechange" }) })
|
|
535
|
+
s.on("iceConnectionStateChange", ({ state }) => { this._ice = state; this._dispatch({ type: "iceconnectionstatechange" }) })
|
|
536
|
+
s.on("connectionStateChange", ({ state }) => { this._connection = state; this._dispatch({ type: "connectionstatechange" }) })
|
|
537
|
+
s.on("track", ({ track, streamIds, transceiverId }) => { this._onTrack(track, streamIds, transceiverId).catch(() => {}) })
|
|
538
|
+
s.on("trackMute", ({ trackId, muted }) => tracks.get(trackId)?._setMuted(muted))
|
|
539
|
+
s.on("trackEnded", ({ trackId }) => tracks.get(trackId)?._ended())
|
|
540
|
+
s.on("dataChannel", info => {
|
|
541
|
+
const channel = new RTCDataChannel(this, info.label, {}, info)
|
|
542
|
+
this._channels.set(info.id, channel)
|
|
543
|
+
this._dispatch({ type: "datachannel", channel })
|
|
544
|
+
})
|
|
545
|
+
s.on("dataChannelStateChange", ({ id, state }) => {
|
|
546
|
+
const channel = this._channels.get(id)
|
|
547
|
+
if (!channel) return
|
|
548
|
+
channel._setState(state)
|
|
549
|
+
if (state === "closed") this._channels.delete(id)
|
|
550
|
+
})
|
|
551
|
+
s.on("dataChannelMessage", ({ id, text, binary }) => this._channels.get(id)?._message(text, binary))
|
|
552
|
+
s.on("dataChannelError", ({ id, message }) => this._channels.get(id)?._dispatch({ type: "error", message }))
|
|
553
|
+
s.on("dataChannelBufferedAmountLow", ({ id }) => this._channels.get(id)?._dispatch({ type: "bufferedamountlow" }))
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
get localDescription(): RTCSessionDescription | null { return this._local }
|
|
557
|
+
get remoteDescription(): RTCSessionDescription | null { return this._remote }
|
|
558
|
+
get currentLocalDescription(): RTCSessionDescription | null { return this._local }
|
|
559
|
+
get currentRemoteDescription(): RTCSessionDescription | null { return this._remote }
|
|
560
|
+
get signalingState(): SignalingState { return this._signaling }
|
|
561
|
+
get iceGatheringState(): IceGatheringState { return this._gathering }
|
|
562
|
+
get iceConnectionState(): IceConnectionState { return this._ice }
|
|
563
|
+
get connectionState(): PeerConnectionState { return this._connection }
|
|
564
|
+
|
|
565
|
+
async createOffer(options?: OfferOptions): Promise<RTCSessionDescription> {
|
|
566
|
+
return new RTCSessionDescription(await this._s.createOffer(options))
|
|
567
|
+
}
|
|
568
|
+
async createAnswer(): Promise<RTCSessionDescription> {
|
|
569
|
+
return new RTCSessionDescription(await this._s.createAnswer())
|
|
570
|
+
}
|
|
571
|
+
/** Without a description: the implicit form — an answer to a pending remote offer, an offer
|
|
572
|
+
* otherwise. `localDescription` holds what is in force after. */
|
|
573
|
+
async setLocalDescription(description?: SessionDescription | RTCSessionDescription | { type: SdpType, sdp?: string }): Promise<void> {
|
|
574
|
+
this._settingLocal++
|
|
575
|
+
try {
|
|
576
|
+
const inForce = await this._s.setLocalDescription(description ? descriptionOf(description) : undefined)
|
|
577
|
+
this._local = new RTCSessionDescription(inForce)
|
|
578
|
+
await this._syncTransceivers()
|
|
579
|
+
} finally {
|
|
580
|
+
this._settingLocal--
|
|
581
|
+
if (this._settingLocal === 0) {
|
|
582
|
+
const held = this._heldCandidates
|
|
583
|
+
this._heldCandidates = []
|
|
584
|
+
// a task after the promise settled, like the browser's: the caller sends the description first
|
|
585
|
+
if (held.length) setTimeout(() => { for (const ice of held) this._dispatch({ type: "icecandidate", candidate: ice }) }, 0)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async setRemoteDescription(description: SessionDescription | RTCSessionDescription | { type: SdpType, sdp?: string }): Promise<void> {
|
|
590
|
+
const wire = descriptionOf(description)
|
|
591
|
+
await this._s.setRemoteDescription(wire)
|
|
592
|
+
this._remote = wire.type === "rollback" ? null : new RTCSessionDescription(wire)
|
|
593
|
+
await this._syncTransceivers()
|
|
594
|
+
}
|
|
595
|
+
/** A candidate of the other side; none (or an empty one) = the end of its candidates. */
|
|
596
|
+
addIceCandidate(candidate?: RTCIceCandidateInit | RTCIceCandidate | null): Promise<void> {
|
|
597
|
+
if (!candidate || !candidate.candidate) return this._s.addIceCandidate()
|
|
598
|
+
return this._s.addIceCandidate(new RTCIceCandidate(candidate).toJSON())
|
|
599
|
+
}
|
|
600
|
+
/** The next offer carries new ICE credentials. */
|
|
601
|
+
restartIce(): void { this._s.restartIce().catch(() => {}) }
|
|
602
|
+
getConfiguration(): RTCConfiguration { return this._configuration }
|
|
603
|
+
async setConfiguration(configuration: RTCConfiguration): Promise<void> {
|
|
604
|
+
await this._s.setConfiguration(configurationWire(configuration))
|
|
605
|
+
this._configuration = configuration
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Send a local track, in the streams named (the browser's `addTrack(track, ...streams)`). The
|
|
609
|
+
* sender is known once the host made it: a promise, where the browser answers at once. */
|
|
610
|
+
async addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): Promise<RTCRtpSender> {
|
|
611
|
+
const info = await this._s.addTrack(track.id, streams.map(s => s.id))
|
|
612
|
+
await this._syncTransceivers()
|
|
613
|
+
return this._transceivers.find(t => t.sender._id === info.id)?.sender ?? new RTCRtpSender(this, info)
|
|
614
|
+
}
|
|
615
|
+
async removeTrack(sender: RTCRtpSender): Promise<void> {
|
|
616
|
+
await this._s.removeTrack(sender._id)
|
|
617
|
+
sender._update({ id: sender._id, trackId: null })
|
|
618
|
+
}
|
|
619
|
+
/** Add an m-line: for a kind, or for a track to send on it. A promise, where the browser answers at once. */
|
|
620
|
+
async addTransceiver(trackOrKind: MediaStreamTrack | MediaKind, init: Omit<TransceiverInit, "trackId" | "streamIds"> & { streams?: MediaStream[] } = {}): Promise<RTCRtpTransceiver> {
|
|
621
|
+
const { streams, ...rest } = init
|
|
622
|
+
const wire: TransceiverInit = { ...rest }
|
|
623
|
+
if (typeof trackOrKind !== "string") wire.trackId = trackOrKind.id
|
|
624
|
+
if (streams) wire.streamIds = streams.map(s => s.id)
|
|
625
|
+
const info = await this._s.addTransceiver(typeof trackOrKind === "string" ? trackOrKind : trackOrKind.kind, wire)
|
|
626
|
+
return this._transceiverOf(info)
|
|
627
|
+
}
|
|
628
|
+
/** The transceivers as last synced (after every description, on every `track` event). */
|
|
629
|
+
getTransceivers(): RTCRtpTransceiver[] { return this._transceivers.slice() }
|
|
630
|
+
getSenders(): RTCRtpSender[] { return this._transceivers.map(t => t.sender) }
|
|
631
|
+
getReceivers(): RTCRtpReceiver[] { return this._transceivers.map(t => t.receiver) }
|
|
632
|
+
|
|
633
|
+
/** The RTCStatsReport: a Map of the stats id → the stats object (`type`, `timestamp` in ms, the
|
|
634
|
+
* fields of its type), for the whole connection or one track's senders / receivers. */
|
|
635
|
+
async getStats(selector?: MediaStreamTrack | null): Promise<Map<string, any>> {
|
|
636
|
+
const report = await this._s.getStats(selector?.id)
|
|
637
|
+
const map = new Map<string, any>()
|
|
638
|
+
if (report && typeof report === "object" && !Array.isArray(report)) for (const [id, stats] of Object.entries(report)) map.set(id, stats)
|
|
639
|
+
return map
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Open a data channel. The channel is "connecting" until the host and the other side are ready;
|
|
643
|
+
* what is sent before counts as queued. */
|
|
644
|
+
createDataChannel(label: string, init: DataChannelInit = {}): RTCDataChannel {
|
|
645
|
+
const channel = new RTCDataChannel(this, label, init, null)
|
|
646
|
+
this._s.createDataChannel(label, init).then(
|
|
647
|
+
info => { this._channels.set(info.id, channel); channel._made(info) },
|
|
648
|
+
e => { channel._dispatch({ type: "error", message: e instanceof Error ? e.message : String(e) }); channel._setState("closed") },
|
|
649
|
+
)
|
|
650
|
+
return channel
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Close the connection: the session with the host ends, every received track ends, every
|
|
654
|
+
* channel closes. */
|
|
655
|
+
close(): void {
|
|
656
|
+
if (this._connection === "closed") return
|
|
657
|
+
this._connection = "closed"
|
|
658
|
+
this._signaling = "closed"
|
|
659
|
+
this._ice = "closed"
|
|
660
|
+
this._s.close()
|
|
661
|
+
for (const channel of this._channels.values()) channel._setState("closed")
|
|
662
|
+
this._channels.clear()
|
|
663
|
+
for (const id of this._remoteTracks) tracks.get(id)?._ended()
|
|
664
|
+
this._remoteTracks.clear()
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private _transceiverOf(info: TransceiverInfo): RTCRtpTransceiver {
|
|
668
|
+
let transceiver = this._transceivers.find(t => t._id === info.id)
|
|
669
|
+
if (transceiver) transceiver._update(info)
|
|
670
|
+
else this._transceivers.push(transceiver = new RTCRtpTransceiver(this, info))
|
|
671
|
+
this._remoteTracks.add(info.receiverTrack.id)
|
|
672
|
+
return transceiver
|
|
673
|
+
}
|
|
674
|
+
private async _syncTransceivers(): Promise<void> {
|
|
675
|
+
if (this._connection === "closed") return
|
|
676
|
+
const infos = await this._s.getTransceivers()
|
|
677
|
+
for (const info of infos) this._transceiverOf(info)
|
|
678
|
+
}
|
|
679
|
+
private async _onTrack(info: TrackInfo, streamIds: string[], transceiverId: string): Promise<void> {
|
|
680
|
+
const track = trackOf(info, true)
|
|
681
|
+
this._remoteTracks.add(track.id)
|
|
682
|
+
await this._syncTransceivers()
|
|
683
|
+
let transceiver = this._transceivers.find(t => t._id === transceiverId)
|
|
684
|
+
if (!transceiver) {
|
|
685
|
+
// The host told of a transceiver the sync did not list (closed meanwhile): stand one up for the event.
|
|
686
|
+
transceiver = new RTCRtpTransceiver(this, { id: transceiverId, mid: null, direction: "recvonly", currentDirection: null, sender: { id: "", trackId: null }, receiverTrack: info, stopped: false })
|
|
687
|
+
}
|
|
688
|
+
const streams = streamIds.map(id => new MediaStream([track], id))
|
|
689
|
+
this._dispatch({ type: "track", track, streams, receiver: transceiver.receiver, transceiver })
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ---- the picture -----------------------------------------------------------------------------------
|
|
694
|
+
|
|
695
|
+
export interface RTCVideoViewOptions {
|
|
696
|
+
/** The track to draw; none = black. */
|
|
697
|
+
track?: MediaStreamTrack | null
|
|
698
|
+
/** Default "cover". */
|
|
699
|
+
objectFit?: ObjectFit
|
|
700
|
+
/** Flip horizontally — the front camera's preview. Default false. */
|
|
701
|
+
mirror?: boolean
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** A tree node that draws one video track: embed it among a screen's children (sized by its
|
|
705
|
+
* styles) or present it fullscreen (`open()`). `track` may change at any time. */
|
|
706
|
+
export interface RTCVideoView extends NativeView {
|
|
707
|
+
/** The track drawn; null = black. */
|
|
708
|
+
track: MediaStreamTrack | null
|
|
709
|
+
objectFit: ObjectFit
|
|
710
|
+
mirror: boolean
|
|
711
|
+
/** `resize`: the first frame, and every change of the picture's size. */
|
|
712
|
+
on<K extends keyof RtcVideoEvents & string>(event: K, callback: Listener<RtcVideoEvents, K>): this
|
|
713
|
+
off<K extends keyof RtcVideoEvents & string>(event: K, callback: Listener<RtcVideoEvents, K>): this
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
class RTCVideoViewElement extends RtcVideoBase {
|
|
717
|
+
private _track: MediaStreamTrack | null
|
|
718
|
+
private _objectFit: ObjectFit
|
|
719
|
+
private _mirror: boolean
|
|
720
|
+
|
|
721
|
+
constructor(options: RTCVideoViewOptions) {
|
|
722
|
+
super({ ...(options.track ? { trackId: options.track.id } : {}), objectFit: options.objectFit ?? "cover", mirror: options.mirror ?? false })
|
|
723
|
+
this._track = options.track ?? null
|
|
724
|
+
this._objectFit = options.objectFit ?? "cover"
|
|
725
|
+
this._mirror = options.mirror ?? false
|
|
726
|
+
}
|
|
727
|
+
get track(): MediaStreamTrack | null { return this._track }
|
|
728
|
+
set track(track: MediaStreamTrack | null) {
|
|
729
|
+
if (this._track === track) return
|
|
730
|
+
this._track = track
|
|
731
|
+
this.setTrack(track?.id ?? null).catch(() => {})
|
|
732
|
+
}
|
|
733
|
+
get objectFit(): ObjectFit { return this._objectFit }
|
|
734
|
+
set objectFit(fit: ObjectFit) {
|
|
735
|
+
if (this._objectFit === fit) return
|
|
736
|
+
this._objectFit = fit
|
|
737
|
+
this.setObjectFit(fit).catch(() => {})
|
|
738
|
+
}
|
|
739
|
+
get mirror(): boolean { return this._mirror }
|
|
740
|
+
set mirror(mirror: boolean) {
|
|
741
|
+
if (this._mirror === mirror) return
|
|
742
|
+
this._mirror = mirror
|
|
743
|
+
this.setMirror(mirror).catch(() => {})
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Create a video view. `RTCVideoView.isSupported` reports whether this host has the capability —
|
|
749
|
+
* check it before offering the feature (a view no host registered mounts nothing).
|
|
750
|
+
*/
|
|
751
|
+
// PURE IIFE so an app that never shows a track tree-shakes the view away.
|
|
752
|
+
export const RTCVideoView: {
|
|
753
|
+
(options?: RTCVideoViewOptions): RTCVideoView
|
|
754
|
+
/** Whether this host registered an "rtcVideo" view. */
|
|
755
|
+
readonly isSupported: boolean
|
|
756
|
+
} = /*#__PURE__*/ (() => {
|
|
757
|
+
const factory = (options: RTCVideoViewOptions = {}): RTCVideoView => new RTCVideoViewElement(options) as unknown as RTCVideoView
|
|
758
|
+
Object.defineProperty(factory, "isSupported", { get: (): boolean => RtcVideoBase.supports(VIDEO_NAME) })
|
|
759
|
+
return factory as any
|
|
760
|
+
})()
|