@mentra/miniapp 3.2.0-dev.115 → 3.2.0-dev.120
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/LICENSE +201 -0
- package/dist/background/index.d.ts +3 -1
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -0
- package/dist/background/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/events.d.ts +8 -0
- package/dist/modules/events.d.ts.map +1 -1
- package/dist/modules/events.js.map +1 -1
- package/dist/modules/meeting.d.ts +88 -0
- package/dist/modules/meeting.d.ts.map +1 -0
- package/dist/modules/meeting.js +157 -0
- package/dist/modules/meeting.js.map +1 -0
- package/dist/modules/stream.d.ts +5 -0
- package/dist/modules/stream.d.ts.map +1 -1
- package/dist/modules/stream.js +2 -0
- package/dist/modules/stream.js.map +1 -1
- package/dist/protocol.d.ts +15 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +14 -0
- package/dist/protocol.js.map +1 -1
- package/dist/session.d.ts +14 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +29 -0
- package/dist/session.js.map +1 -1
- package/package.json +3 -3
- package/src/background/index.ts +13 -0
- package/src/index.ts +13 -0
- package/src/modules/events.ts +8 -0
- package/src/modules/meeting.ts +237 -0
- package/src/modules/stream.ts +7 -0
- package/src/protocol.ts +16 -0
- package/src/session.ts +38 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MeetingModule — phone-native meeting (ACS Teams).
|
|
3
|
+
*
|
|
4
|
+
* V1 token pass-through is deliberate technical debt (identity ticket):
|
|
5
|
+
* later, join(meetingUrl, whepUrl) and the host fetches the credential from
|
|
6
|
+
* Porter. Miniapps must not persist the token.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {MiniappErrorCode, MiniappRequestType} from "../protocol"
|
|
10
|
+
import type {MiniappRequestError} from "../session"
|
|
11
|
+
import {MiniappSession} from "../session"
|
|
12
|
+
import type {UnsubscribeFn} from "./events"
|
|
13
|
+
|
|
14
|
+
export const MEETING_HOST_UPDATE_MESSAGE = "Update the Mentra App to use Teams calling"
|
|
15
|
+
|
|
16
|
+
export type MeetingProvider = "acs-teams"
|
|
17
|
+
|
|
18
|
+
export type MeetingPhase = "idle" | "connecting" | "lobby" | "connected" | "disconnected" | "error"
|
|
19
|
+
|
|
20
|
+
export interface MeetingVideoSource {
|
|
21
|
+
type: "whep"
|
|
22
|
+
url: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Advertised ACS outgoing format. Omitted hosts keep 1280×720@15. */
|
|
26
|
+
export interface MeetingOutgoingVideo {
|
|
27
|
+
width: number
|
|
28
|
+
height: number
|
|
29
|
+
fps: number
|
|
30
|
+
maxBitrateBps: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MeetingJoinOptions {
|
|
34
|
+
provider: MeetingProvider
|
|
35
|
+
meetingUrl: string
|
|
36
|
+
videoSource: MeetingVideoSource
|
|
37
|
+
/** V1-only: Porter-minted ACS guest token. Do not persist. */
|
|
38
|
+
token: string
|
|
39
|
+
displayName?: string
|
|
40
|
+
video?: MeetingOutgoingVideo
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type MeetingParticipantState = "idle" | "connecting" | "connected" | "lobby" | "hold" | "disconnected"
|
|
44
|
+
|
|
45
|
+
/** A remote participant as reported by the phone-native meeting client. */
|
|
46
|
+
export interface MeetingParticipant {
|
|
47
|
+
/** Stable provider identifier (ACS raw id). */
|
|
48
|
+
id: string
|
|
49
|
+
displayName: string | null
|
|
50
|
+
state: MeetingParticipantState
|
|
51
|
+
isMuted: boolean
|
|
52
|
+
isSpeaking: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MeetingState {
|
|
56
|
+
state: MeetingPhase
|
|
57
|
+
muted: boolean
|
|
58
|
+
error?: string
|
|
59
|
+
meetingUrl?: string
|
|
60
|
+
provider?: MeetingProvider
|
|
61
|
+
audioSource?: "glasses" | "phone"
|
|
62
|
+
audioSourceReason?:
|
|
63
|
+
| "explicit"
|
|
64
|
+
| "current-mic"
|
|
65
|
+
| "ranking"
|
|
66
|
+
| "fallback-glasses-connected"
|
|
67
|
+
| "fallback-no-glasses"
|
|
68
|
+
activeStream?: "none" | "virtual" | "local"
|
|
69
|
+
audioSafety?: "safe" | "degraded" | "unsafe"
|
|
70
|
+
/**
|
|
71
|
+
* Health of the glasses video the phone is forwarding into the meeting.
|
|
72
|
+
* `live` means a frame reached the meeting client, so it is the only honest
|
|
73
|
+
* "remote participants can see the camera" signal — the WHEP subscription
|
|
74
|
+
* answers seconds earlier. Omitted by hosts that predate the field, which
|
|
75
|
+
* must be read as "unknown", never as "not live".
|
|
76
|
+
*/
|
|
77
|
+
mediaSource?: MeetingMediaSource
|
|
78
|
+
/** Remote roster. Omitted by hosts that predate participant reporting. */
|
|
79
|
+
participants?: MeetingParticipant[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type MeetingMediaSource = "idle" | "connecting" | "live" | "failed"
|
|
83
|
+
|
|
84
|
+
const PARTICIPANT_STATES: ReadonlySet<string> = new Set(["idle", "connecting", "connected", "lobby", "hold", "disconnected"])
|
|
85
|
+
|
|
86
|
+
const MEDIA_SOURCES: ReadonlySet<string> = new Set(["idle", "connecting", "live", "failed"])
|
|
87
|
+
|
|
88
|
+
/** Tolerant parse of a host `mediaSource`. Unknown values read as unknown. */
|
|
89
|
+
export function parseMeetingMediaSource(raw: unknown): MeetingMediaSource | undefined {
|
|
90
|
+
return MEDIA_SOURCES.has(String(raw)) ? (raw as MeetingMediaSource) : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Tolerant parse of a host `participants` payload. Unknown shapes are skipped. */
|
|
94
|
+
export function parseMeetingParticipants(raw: unknown): MeetingParticipant[] | undefined {
|
|
95
|
+
if (!Array.isArray(raw)) return undefined
|
|
96
|
+
const result: MeetingParticipant[] = []
|
|
97
|
+
for (const entry of raw) {
|
|
98
|
+
if (!entry || typeof entry !== "object") continue
|
|
99
|
+
const value = entry as Record<string, unknown>
|
|
100
|
+
if (typeof value.id !== "string" || !value.id) continue
|
|
101
|
+
result.push({
|
|
102
|
+
id: value.id,
|
|
103
|
+
displayName: typeof value.displayName === "string" && value.displayName ? value.displayName : null,
|
|
104
|
+
state: PARTICIPANT_STATES.has(String(value.state)) ? (value.state as MeetingParticipantState) : "idle",
|
|
105
|
+
isMuted: Boolean(value.isMuted),
|
|
106
|
+
isSpeaking: Boolean(value.isSpeaking),
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type MeetingStateHandler = (state: MeetingState) => void
|
|
113
|
+
|
|
114
|
+
function isMiniappRequestError(error: unknown): error is MiniappRequestError {
|
|
115
|
+
return Boolean(error && typeof error === "object" && "code" in error)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function mapHostError(error: unknown): never {
|
|
119
|
+
if (isMiniappRequestError(error) && error.code === MiniappErrorCode.NOT_IMPLEMENTED) {
|
|
120
|
+
throw {code: MiniappErrorCode.NOT_IMPLEMENTED, message: MEETING_HOST_UPDATE_MESSAGE}
|
|
121
|
+
}
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class MeetingModule {
|
|
126
|
+
private _state: MeetingState = {state: "idle", muted: false}
|
|
127
|
+
|
|
128
|
+
constructor(private readonly session: MiniappSession) {}
|
|
129
|
+
|
|
130
|
+
get state(): MeetingState {
|
|
131
|
+
return {...this._state}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Join a Teams meeting via the phone ACS client.
|
|
136
|
+
* Resolves once the host has accepted the join (state may still be connecting/lobby).
|
|
137
|
+
*/
|
|
138
|
+
async join(options: MeetingJoinOptions): Promise<MeetingState> {
|
|
139
|
+
if (options.provider !== "acs-teams") {
|
|
140
|
+
throw {code: MiniappErrorCode.INVALID_ARGUMENT, message: `Unsupported meeting provider: ${options.provider}`}
|
|
141
|
+
}
|
|
142
|
+
if (!options.meetingUrl?.trim()) {
|
|
143
|
+
throw {code: MiniappErrorCode.INVALID_ARGUMENT, message: "meetingUrl is required"}
|
|
144
|
+
}
|
|
145
|
+
if (options.videoSource?.type !== "whep" || !options.videoSource.url?.trim()) {
|
|
146
|
+
throw {code: MiniappErrorCode.INVALID_ARGUMENT, message: "videoSource must be a WHEP URL"}
|
|
147
|
+
}
|
|
148
|
+
if (!options.token?.trim()) {
|
|
149
|
+
throw {code: MiniappErrorCode.INVALID_ARGUMENT, message: "token is required"}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const result = await this.session.sendRequest<MeetingState | null>(
|
|
153
|
+
{
|
|
154
|
+
type: MiniappRequestType.MEETING_JOIN,
|
|
155
|
+
provider: options.provider,
|
|
156
|
+
meetingUrl: options.meetingUrl,
|
|
157
|
+
videoSource: options.videoSource,
|
|
158
|
+
token: options.token,
|
|
159
|
+
displayName: options.displayName,
|
|
160
|
+
...(options.video ? {video: options.video} : {}),
|
|
161
|
+
},
|
|
162
|
+
{timeoutMs: 0},
|
|
163
|
+
)
|
|
164
|
+
if (result) this._applyState(result)
|
|
165
|
+
return this.state
|
|
166
|
+
} catch (error) {
|
|
167
|
+
mapHostError(error)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async leave(): Promise<void> {
|
|
172
|
+
try {
|
|
173
|
+
await this.session.sendRequest<void>({type: MiniappRequestType.MEETING_LEAVE})
|
|
174
|
+
} catch (error) {
|
|
175
|
+
mapHostError(error)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async setMuted(muted: boolean): Promise<void> {
|
|
180
|
+
try {
|
|
181
|
+
const result = await this.session.sendRequest<MeetingState | null>({
|
|
182
|
+
type: MiniappRequestType.MEETING_SET_MUTED,
|
|
183
|
+
muted,
|
|
184
|
+
})
|
|
185
|
+
if (result) this._applyState(result)
|
|
186
|
+
} catch (error) {
|
|
187
|
+
mapHostError(error)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async updateVideoSource(source: MeetingVideoSource): Promise<void> {
|
|
192
|
+
if (source?.type !== "whep" || !source.url?.trim()) {
|
|
193
|
+
throw {code: MiniappErrorCode.INVALID_ARGUMENT, message: "videoSource must be a WHEP URL"}
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await this.session.sendRequest<void>({
|
|
197
|
+
type: MiniappRequestType.MEETING_UPDATE_VIDEO_SOURCE,
|
|
198
|
+
videoSource: source,
|
|
199
|
+
})
|
|
200
|
+
} catch (error) {
|
|
201
|
+
mapHostError(error)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getState(): Promise<MeetingState> {
|
|
206
|
+
try {
|
|
207
|
+
const result = await this.session.sendRequest<MeetingState | null>({
|
|
208
|
+
type: MiniappRequestType.MEETING_GET_STATE,
|
|
209
|
+
})
|
|
210
|
+
if (result) this._applyState(result)
|
|
211
|
+
return this.state
|
|
212
|
+
} catch (error) {
|
|
213
|
+
mapHostError(error)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
onState(handler: MeetingStateHandler): UnsubscribeFn {
|
|
218
|
+
return this.session.on("meetingState", handler)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** @internal — applied by MiniappSession on inbound MEETING_STATE. */
|
|
222
|
+
_applyState(event: MeetingState): void {
|
|
223
|
+
this._state = {
|
|
224
|
+
state: event.state,
|
|
225
|
+
muted: Boolean(event.muted),
|
|
226
|
+
error: event.error,
|
|
227
|
+
meetingUrl: event.meetingUrl,
|
|
228
|
+
provider: event.provider,
|
|
229
|
+
audioSource: event.audioSource,
|
|
230
|
+
audioSourceReason: event.audioSourceReason,
|
|
231
|
+
activeStream: event.activeStream,
|
|
232
|
+
audioSafety: event.audioSafety,
|
|
233
|
+
mediaSource: parseMeetingMediaSource(event.mediaSource),
|
|
234
|
+
participants: parseMeetingParticipants(event.participants),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
package/src/modules/stream.ts
CHANGED
|
@@ -83,6 +83,11 @@ export interface StartStreamOptions {
|
|
|
83
83
|
ingest?: "srt" | "whip" | "rtmp"
|
|
84
84
|
/** Optional Bearer token for direct WHIP Authorization (custom authenticated endpoints). */
|
|
85
85
|
authToken?: string
|
|
86
|
+
/**
|
|
87
|
+
* When false, glasses skip mic capture for this WHIP session. Fixed for the
|
|
88
|
+
* lifetime of the stream. Defaults to true. Older Mentra Apps ignore this.
|
|
89
|
+
*/
|
|
90
|
+
captureAudio?: boolean
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
export interface StreamResult {
|
|
@@ -146,6 +151,7 @@ export class StreamModule {
|
|
|
146
151
|
audio: options.audio,
|
|
147
152
|
sound: options.sound ?? true,
|
|
148
153
|
...(options.authToken ? {authToken: options.authToken} : {}),
|
|
154
|
+
...(typeof options.captureAudio === "boolean" ? {captureAudio: options.captureAudio} : {}),
|
|
149
155
|
})
|
|
150
156
|
}
|
|
151
157
|
return this.session.sendRequest<StreamResult>({
|
|
@@ -155,6 +161,7 @@ export class StreamModule {
|
|
|
155
161
|
audio: options.audio,
|
|
156
162
|
sound: options.sound ?? true,
|
|
157
163
|
ingest: options.ingest,
|
|
164
|
+
...(typeof options.captureAudio === "boolean" ? {captureAudio: options.captureAudio} : {}),
|
|
158
165
|
})
|
|
159
166
|
}
|
|
160
167
|
|
package/src/protocol.ts
CHANGED
|
@@ -205,6 +205,16 @@ export enum MiniappRequestType {
|
|
|
205
205
|
ACTION_INVOKE = "miniapp_action_invoke",
|
|
206
206
|
/** Target → host: the result of a delivered ACTION_CALL, correlated by callId. */
|
|
207
207
|
ACTION_RESULT = "miniapp_action_result",
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Phone-native meeting (ACS Teams). Join/leave/mute live in the MentraOS
|
|
211
|
+
* host so the miniapp never holds the ACS Calling SDK.
|
|
212
|
+
*/
|
|
213
|
+
MEETING_JOIN = "miniapp_meeting_join",
|
|
214
|
+
MEETING_LEAVE = "miniapp_meeting_leave",
|
|
215
|
+
MEETING_SET_MUTED = "miniapp_meeting_set_muted",
|
|
216
|
+
MEETING_UPDATE_VIDEO_SOURCE = "miniapp_meeting_update_video_source",
|
|
217
|
+
MEETING_GET_STATE = "miniapp_meeting_get_state",
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
// ============================================================================
|
|
@@ -261,6 +271,12 @@ export enum MiniappResponseType {
|
|
|
261
271
|
*/
|
|
262
272
|
ACTION_CALL = "miniapp_action_call",
|
|
263
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Push: native meeting state changed. Carries {state, muted?, error?}.
|
|
276
|
+
* See MeetingModule.onState().
|
|
277
|
+
*/
|
|
278
|
+
MEETING_STATE = "miniapp_meeting_state",
|
|
279
|
+
|
|
264
280
|
/**
|
|
265
281
|
* Push: phone is about to tear down the miniapp's session. Gives the SDK
|
|
266
282
|
* a brief window (~50ms grace on the phone side) to fire one last
|
package/src/session.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {SystemModule} from "./modules/system"
|
|
|
45
45
|
import {MiniappsModule} from "./modules/miniapps"
|
|
46
46
|
import {ActionsModule} from "./modules/actions"
|
|
47
47
|
import {BlobModule} from "./modules/blob"
|
|
48
|
+
import {MeetingModule, parseMeetingMediaSource, parseMeetingParticipants} from "./modules/meeting"
|
|
48
49
|
|
|
49
50
|
// ---------------------------------------------------------------------------
|
|
50
51
|
// Public types
|
|
@@ -105,6 +106,13 @@ export interface ConnectAckPayload {
|
|
|
105
106
|
permissions?: PermissionRecord
|
|
106
107
|
/** Miniapp-scoped backend auth. Never a Core or runtime token. */
|
|
107
108
|
auth?: MiniappAuthState
|
|
109
|
+
/** Host-advertised optional features. Absent on older Mentra Apps. */
|
|
110
|
+
hostFeatures?: HostFeatures
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface HostFeatures {
|
|
114
|
+
/** Host honors `startStream({captureAudio})` for the lifetime of a WHIP session. */
|
|
115
|
+
captureAudio?: boolean
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
export interface MiniappAuthState {
|
|
@@ -191,6 +199,7 @@ type SessionEmitterEvents = {
|
|
|
191
199
|
colorScheme: (scheme: MiniappColorScheme) => void
|
|
192
200
|
permissions: (perms: PermissionRecord) => void
|
|
193
201
|
speakerState: (event: import("./modules/speaker").SpeakerStateEvent) => void
|
|
202
|
+
meetingState: (event: import("./modules/meeting").MeetingState) => void
|
|
194
203
|
auth: (auth: MiniappAuthState) => void
|
|
195
204
|
}
|
|
196
205
|
|
|
@@ -211,6 +220,7 @@ export class MiniappSession<TChannels extends object = any> {
|
|
|
211
220
|
*/
|
|
212
221
|
public readonly events: EventManager
|
|
213
222
|
public readonly speaker: SpeakerModule
|
|
223
|
+
public readonly meeting: MeetingModule
|
|
214
224
|
public readonly camera: CameraModule
|
|
215
225
|
public readonly cloud: CloudModule
|
|
216
226
|
public readonly dashboard: DashboardAPI
|
|
@@ -256,6 +266,11 @@ export class MiniappSession<TChannels extends object = any> {
|
|
|
256
266
|
|
|
257
267
|
/** Phone-declared glasses capabilities. Null until CONNECT_ACK arrives. */
|
|
258
268
|
public capabilities: GlassesCapabilities | null = null
|
|
269
|
+
/**
|
|
270
|
+
* Host-advertised optional features. Null until CONNECT_ACK. Older Mentra Apps
|
|
271
|
+
* omit this, so miniapps must not assume `captureAudio` is honored.
|
|
272
|
+
*/
|
|
273
|
+
public hostFeatures: HostFeatures | null = null
|
|
259
274
|
public userId = ""
|
|
260
275
|
public packageName = ""
|
|
261
276
|
public visibility: MiniappVisibility = "foreground"
|
|
@@ -308,6 +323,7 @@ export class MiniappSession<TChannels extends object = any> {
|
|
|
308
323
|
this.auth = new AuthModule(this)
|
|
309
324
|
this.events = new EventManager(this)
|
|
310
325
|
this.speaker = new SpeakerModule(this)
|
|
326
|
+
this.meeting = new MeetingModule(this)
|
|
311
327
|
this.camera = new CameraModule(this)
|
|
312
328
|
this.cloud = new CloudModule(this)
|
|
313
329
|
this.dashboard = new DashboardAPI(this)
|
|
@@ -619,6 +635,7 @@ export class MiniappSession<TChannels extends object = any> {
|
|
|
619
635
|
this.userId = ack.userId ?? ""
|
|
620
636
|
if (ack.packageName) this.packageName = ack.packageName
|
|
621
637
|
this.capabilities = ack.capabilities ?? null
|
|
638
|
+
this.hostFeatures = ack.hostFeatures ?? null
|
|
622
639
|
if (ack.visibility) this.visibility = ack.visibility
|
|
623
640
|
if (ack.colorScheme === "light" || ack.colorScheme === "dark") {
|
|
624
641
|
this.colorScheme = ack.colorScheme
|
|
@@ -667,6 +684,27 @@ export class MiniappSession<TChannels extends object = any> {
|
|
|
667
684
|
return
|
|
668
685
|
}
|
|
669
686
|
|
|
687
|
+
case MiniappResponseType.MEETING_STATE: {
|
|
688
|
+
const state = payload.state as import("./modules/meeting").MeetingPhase | undefined
|
|
689
|
+
if (!state) return
|
|
690
|
+
const event: import("./modules/meeting").MeetingState = {
|
|
691
|
+
state,
|
|
692
|
+
muted: Boolean(payload.muted),
|
|
693
|
+
error: payload.error as string | undefined,
|
|
694
|
+
meetingUrl: payload.meetingUrl as string | undefined,
|
|
695
|
+
provider: payload.provider as import("./modules/meeting").MeetingProvider | undefined,
|
|
696
|
+
audioSource: payload.audioSource as import("./modules/meeting").MeetingState["audioSource"],
|
|
697
|
+
audioSourceReason: payload.audioSourceReason as import("./modules/meeting").MeetingState["audioSourceReason"],
|
|
698
|
+
activeStream: payload.activeStream as import("./modules/meeting").MeetingState["activeStream"],
|
|
699
|
+
audioSafety: payload.audioSafety as import("./modules/meeting").MeetingState["audioSafety"],
|
|
700
|
+
mediaSource: parseMeetingMediaSource(payload.mediaSource),
|
|
701
|
+
participants: parseMeetingParticipants(payload.participants),
|
|
702
|
+
}
|
|
703
|
+
this.meeting._applyState(event)
|
|
704
|
+
this.emitter.emit("meetingState", event)
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
|
|
670
708
|
case MiniappRequestType.PING: {
|
|
671
709
|
// Phone → miniapp keepalive ping. Auto-reply with PONG.
|
|
672
710
|
const pong: object = {type: MiniappResponseType.PONG}
|