@burdenoff/website-sdk 2026.1105.1 → 2026.1105.2

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/README.md CHANGED
@@ -420,6 +420,44 @@ publishes audio, video or data, and it sends the avatar nothing to say.
420
420
  site's main bundle and the view's chunk carry none of it. On alpha, a browser
421
421
  WebSocket cannot carry Cloudflare Access service-token headers, so the
422
422
  stream needs the Access cookie for the gateway host.
423
+ - **The answers after the first (BOFF-9797 Stage D, dark).** Only where the
424
+ create's 201 carries `followUps: {maxAnswers, idleSeconds,
425
+ maxAnswerWaitSeconds}` (the avatar service adds it only under its
426
+ `AVATAR_WEB_MULTI_ANSWER`, read strictly into `AvatarSession.followUps`;
427
+ absent or unreadable, the session reads its one answer exactly as before),
428
+ the visitor agreed to consent copy whose words describe them (`followUps:
429
+ true`: `web-consent-2026-11-19-d` with the bot check, `web-consent-2026-11-20-d`
430
+ without, DRAFTS no server names yet), and the tab has its own box. The box
431
+ then stays under the live avatar, and a question asked there (typed, or by
432
+ the mic where it is offered) is the next one of the same conversation, sent
433
+ with `surface: "AVATAR"`; the same session reads its answer. The view tells
434
+ the session through the gateway's `POST /api/web-avatar/v1/sessions/{id}/turn`
435
+ (`{secret, kind, nonce, messageId?, idempotencyKey?}`, a simple request like
436
+ the end, `requestTurn`): `asking` as Send is pressed, `listening` while the
437
+ mic records (once the microphone is open and its stream ready, and as it
438
+ stops), `answer` with the answer's id once it is written, and `interrupt`
439
+ when the mic is tapped while the avatar speaks. The session reads one answer
440
+ at a time and refuses a later one `busy` while the one before plays, so an
441
+ answer written while the avatar speaks is sent once that answer has played
442
+ out here, and a `busy` is asked again with the same key after the refusal's
443
+ `retryAfterSeconds` (doubling up to 8 s, at most 24 times, never past the
444
+ session's end; this also covers a room that tells no voice). An answer that
445
+ got no reply is asked again once with the same key. An answer the session
446
+ does not read stays in Chat, said in one line (`no_time`: "There isn't
447
+ enough time left in this session to read that answer. It's in Chat.";
448
+ `not_eligible`: "This answer stays in Chat as text. …"; also `max_answers`,
449
+ `answer_claimed`, `limits`, `paused`, `busy` past its retries), and the box
450
+ keeps asking while answers are left. An answer this browser can tell will
451
+ not be read (a failed answer, or one its checks refuse) is sent all the same
452
+ with this browser's line shown: only the session's refusal ends the wait an
453
+ `asking` mark holds open. When an answer has played out is the worker's voice going quiet for
454
+ 2.5 s (the room's new `speaking` event, from the media server's active
455
+ speakers); the status line then says "The avatar ends after about thirty
456
+ seconds without a question.", and the server ends the session after its
457
+ idle period (the room closing ends it here, as any server end). Under
458
+ `endUnattended`, a session out of sight ends once its answer has played out
459
+ and nothing is on its way. Stop, an interrupt and a correction never create a
460
+ session or ask for an answer twice.
423
461
  - **One answer: the first, to a suggested question.** The avatar service
424
462
  accepts a conversation only when it holds exactly two messages: the first user
425
463
  message (a suggested question) and the first assistant answer, completed. The
@@ -628,7 +666,8 @@ Testing hooks (`data-boff-explore`): `avatar-live` (with `data-phase`),
628
666
  `avatar-live-start`, `avatar-live-not-now`, `avatar-live-stop`,
629
667
  `avatar-live-video`, `avatar-live-audio`, `avatar-live-badge`,
630
668
  `avatar-live-captions`, `avatar-live-countdown`, `avatar-live-sound`,
631
- `avatar-live-continue`, `avatar-live-back`, `avatar-live-bot-check`.
669
+ `avatar-live-continue`, `avatar-live-back`, `avatar-live-bot-check`,
670
+ `avatar-live-idle` (Stage D: the idle line under a refusal's).
632
671
 
633
672
  #### Site assistant dock (`SiteAssistantDock`, every product site)
634
673
 
@@ -1,5 +1,5 @@
1
- import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CXqaLlNB.mjs';
2
- export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CXqaLlNB.mjs';
1
+ import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CTUJNvQu.mjs';
2
+ export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CTUJNvQu.mjs';
3
3
 
4
4
  /**
5
5
  * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice
@@ -1,5 +1,5 @@
1
- import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CXqaLlNB.js';
2
- export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CXqaLlNB.js';
1
+ import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CTUJNvQu.js';
2
+ export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CTUJNvQu.js';
3
3
 
4
4
  /**
5
5
  * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice
@@ -61,6 +61,16 @@ async function joinReceiveOnly(options) {
61
61
  sound: !room.canPlaybackAudio
62
62
  });
63
63
  };
64
+ let workerSpeaking = false;
65
+ const reportSpeakers = (speakers) => {
66
+ if (leaving) return;
67
+ const speaking = speakers.some(
68
+ (speaker) => speaker.identity === workerIdentity
69
+ );
70
+ if (speaking === workerSpeaking) return;
71
+ workerSpeaking = speaking;
72
+ onEvent({ type: "speaking", speaking });
73
+ };
64
74
  room.on(livekitClient.RoomEvent.TrackPublished, consider).on(
65
75
  livekitClient.RoomEvent.TrackSubscribed,
66
76
  (track, publication, participant) => {
@@ -93,7 +103,7 @@ async function joinReceiveOnly(options) {
93
103
  } else {
94
104
  voiceGone(participant);
95
105
  }
96
- }).on(livekitClient.RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.Disconnected, (reason) => {
106
+ }).on(livekitClient.RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.ActiveSpeakersChanged, reportSpeakers).on(livekitClient.RoomEvent.Disconnected, (reason) => {
97
107
  if (leaving) return;
98
108
  onEvent({
99
109
  type: "disconnected",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/avatar-live/receive-only.ts"],"names":["DisconnectReason","Track","Room","RoomEvent"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * BOFF-9797. The live avatar's room, joined to watch and listen only.\n *\n * A visitor's browser on a product website holds a subscribe-only room token: it can receive the avatar worker's\n * video and voice and do nothing else. This adapter keeps to that from its side too:\n *\n * - **Nothing is subscribed automatically** (`autoSubscribe: false`). Only the tracks of `workerIdentity`, the avatar\n * worker the create answer named, are subscribed, and only their video and audio. Any other participant's track\n * (the server's own speaker included) is never asked for, and is dropped again should the server send it anyway.\n * - **Nothing is published.** No microphone, no camera, no data, no RPC method: this file never calls a publishing\n * API, and the token would refuse one.\n * - **Safari and iOS** hold sound back until a gesture. The room reports it (`playback-blocked`, with `sound` when the\n * sound is held back and not only the video) and the panel offers \"Tap to turn on sound\", whose tap calls\n * `enablePlayback`.\n * - **The end of the speech** is told (`speech-ended`) from what any viewer of the room sees: the other voice in it, the\n * server's speaker that the worker follows, leaving the room or unpublishing its voice. Its track is never subscribed.\n *\n * Written for the website from the media library's public API: nothing here is copied from the product apps.\n */\nimport {\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n type RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n} from \"livekit-client\";\n\nimport type { ReceiveOnlyJoinOptions, ReceiveOnlySession } from \"./types\";\n\n/** The reasons a room closes because the server closed it, rather than because the connection was lost. */\nconst CLOSED_BY_SERVER: ReadonlySet<DisconnectReason> = new Set([\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.ROOM_CLOSED,\n DisconnectReason.PARTICIPANT_REMOVED,\n]);\n\nfunction isMedia(publication: RemoteTrackPublication): boolean {\n return (\n publication.kind === Track.Kind.Video ||\n publication.kind === Track.Kind.Audio\n );\n}\n\n/**\n * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice\n * in `options.video` and `options.audio`. Resolves once connected; rejects when the room cannot be joined (the panel\n * then ends the session).\n */\nexport async function joinReceiveOnly(\n options: ReceiveOnlyJoinOptions,\n): Promise<ReceiveOnlySession> {\n const { url, token, workerIdentity, video, audio, onEvent } = options;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(\"live avatar: the room address is not a URL\");\n }\n if (parsed.protocol !== \"wss:\") {\n throw new Error(\"live avatar: the room address must be wss:\");\n }\n if (!token || !workerIdentity) {\n throw new Error(\"live avatar: the room token or speaker is missing\");\n }\n\n const room = new Room({\n // One small video, always shown while the session lasts: no quality switching, no layer pausing.\n adaptiveStream: false,\n dynacast: false,\n // Closing the tab or leaving the page leaves the room at once. Belt and braces with the panel's own page-hide\n // handler, which also asks the server to end the session: either may run first, and both are safe to repeat.\n disconnectOnPageLeave: true,\n });\n let leaving = false;\n\n const isSpeaker = (participant: RemoteParticipant) =>\n participant.identity === workerIdentity;\n\n /**\n * Everyone else in the room seen publishing a voice: the server's speaker, whose speech the worker voices. Noted\n * only, never subscribed to, so that the end of that speech can be told (`speech-ended`).\n */\n const voiced = new Set<string>();\n let speechEnded = false;\n\n /** Subscribe to `publication` when it is the speaker's video or audio; leave everything else alone. */\n const consider = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant)) {\n if (publication.kind === Track.Kind.Audio) {\n voiced.add(participant.identity);\n }\n return;\n }\n if (!isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n /** A voice seen published has gone (its track, or its participant): the speech has ended at its source. Once. */\n const voiceGone = (participant: RemoteParticipant) => {\n if (leaving || speechEnded || !voiced.has(participant.identity)) return;\n speechEnded = true;\n onEvent({ type: \"speech-ended\" });\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\n sound: !room.canPlaybackAudio,\n });\n };\n\n room\n .on(RoomEvent.TrackPublished, consider)\n .on(\n RoomEvent.TrackSubscribed,\n (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant) || !isMedia(publication)) {\n // Never asked for: drop it rather than play it.\n publication.setSubscribed(false);\n return;\n }\n if (track.kind === Track.Kind.Video) {\n track.attach(video);\n onEvent({ type: \"speaker-track\", kind: \"video\" });\n } else {\n track.attach(audio);\n onEvent({ type: \"speaker-track\", kind: \"audio\" });\n }\n },\n )\n .on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n track.detach();\n })\n .on(\n RoomEvent.TrackUnpublished,\n (publication: RemoteTrackPublication, participant: RemoteParticipant) => {\n if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {\n voiceGone(participant);\n }\n },\n )\n .on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (leaving) return;\n if (isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\n } else {\n voiceGone(participant);\n }\n })\n .on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n if (leaving) return;\n onEvent({\n type: \"disconnected\",\n byServer: reason !== undefined && CLOSED_BY_SERVER.has(reason),\n });\n });\n\n const detachAll = () => {\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n publication.track?.detach();\n }\n }\n video.srcObject = null;\n audio.srcObject = null;\n };\n\n try {\n await room.connect(url, token, { autoSubscribe: false });\n } catch (error) {\n leaving = true;\n room.removeAllListeners();\n detachAll();\n throw error;\n }\n\n // Whoever was in the room before this viewer joined: their tracks were published before the listener above.\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n consider(publication, participant);\n }\n }\n reportPlayback();\n\n return {\n enablePlayback: async () => {\n // Both started inside the visitor's gesture, before anything is awaited: Safari allows playback only there.\n // A refusal leaves the room as it was, and the report below keeps the button offered.\n await Promise.allSettled([room.startAudio(), room.startVideo()]);\n reportPlayback();\n },\n leave: async () => {\n if (leaving) return;\n leaving = true;\n detachAll();\n try {\n await room.disconnect();\n } finally {\n room.removeAllListeners();\n }\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/avatar-live/receive-only.ts"],"names":["DisconnectReason","Track","Room","RoomEvent"],"mappings":";;;;;AAmCA,IAAM,gBAAA,uBAAsD,GAAA,CAAI;AAAA,EAC9DA,8BAAA,CAAiB,YAAA;AAAA,EACjBA,8BAAA,CAAiB,WAAA;AAAA,EACjBA,8BAAA,CAAiB;AACnB,CAAC,CAAA;AAED,SAAS,QAAQ,WAAA,EAA8C;AAC7D,EAAA,OACE,WAAA,CAAY,SAASC,mBAAA,CAAM,IAAA,CAAK,SAChC,WAAA,CAAY,IAAA,KAASA,oBAAM,IAAA,CAAK,KAAA;AAEpC;AAOA,eAAsB,gBACpB,OAAA,EAC6B;AAC7B,EAAA,MAAM,EAAE,GAAA,EAAK,KAAA,EAAO,gBAAgB,KAAA,EAAO,KAAA,EAAO,SAAQ,GAAI,OAAA;AAC9D,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAI,IAAI,GAAG,CAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAAA,EAC9D;AACA,EAAA,IAAI,MAAA,CAAO,aAAa,MAAA,EAAQ;AAC9B,IAAA,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAAA,EAC9D;AACA,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,cAAA,EAAgB;AAC7B,IAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,EACrE;AAEA,EAAA,MAAM,IAAA,GAAO,IAAIC,kBAAA,CAAK;AAAA;AAAA,IAEpB,cAAA,EAAgB,KAAA;AAAA,IAChB,QAAA,EAAU,KAAA;AAAA;AAAA;AAAA,IAGV,qBAAA,EAAuB;AAAA,GACxB,CAAA;AACD,EAAA,IAAI,OAAA,GAAU,KAAA;AAEd,EAAA,MAAM,SAAA,GAAY,CAAC,WAAA,KACjB,WAAA,CAAY,QAAA,KAAa,cAAA;AAM3B,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAY;AAC/B,EAAA,IAAI,WAAA,GAAc,KAAA;AAGlB,EAAA,MAAM,QAAA,GAAW,CACf,WAAA,EACA,WAAA,KACG;AACH,IAAA,IAAI,OAAA,EAAS;AACb,IAAA,IAAI,CAAC,SAAA,CAAU,WAAW,CAAA,EAAG;AAC3B,MAAA,IAAI,WAAA,CAAY,IAAA,KAASD,mBAAA,CAAM,IAAA,CAAK,KAAA,EAAO;AACzC,QAAA,MAAA,CAAO,GAAA,CAAI,YAAY,QAAQ,CAAA;AAAA,MACjC;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAC,OAAA,CAAQ,WAAW,CAAA,EAAG;AAC3B,IAAA,IAAI,CAAC,WAAA,CAAY,YAAA,EAAc,WAAA,CAAY,cAAc,IAAI,CAAA;AAAA,EAC/D,CAAA;AAGA,EAAA,MAAM,SAAA,GAAY,CAAC,WAAA,KAAmC;AACpD,IAAA,IAAI,WAAW,WAAA,IAAe,CAAC,OAAO,GAAA,CAAI,WAAA,CAAY,QAAQ,CAAA,EAAG;AACjE,IAAA,WAAA,GAAc,IAAA;AACd,IAAA,OAAA,CAAQ,EAAE,IAAA,EAAM,cAAA,EAAgB,CAAA;AAAA,EAClC,CAAA;AAEA,EAAA,MAAM,iBAAiB,MAAM;AAC3B,IAAA,IAAI,OAAA,EAAS;AACb,IAAA,OAAA,CAAQ;AAAA,MACN,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EAAS,CAAC,IAAA,CAAK,gBAAA,IAAoB,CAAC,IAAA,CAAK,gBAAA;AAAA,MACzC,KAAA,EAAO,CAAC,IAAA,CAAK;AAAA,KACd,CAAA;AAAA,EACH,CAAA;AAGA,EAAA,IAAI,cAAA,GAAiB,KAAA;AACrB,EAAA,MAAM,cAAA,GAAiB,CAAC,QAAA,KAA4B;AAClD,IAAA,IAAI,OAAA,EAAS;AACb,IAAA,MAAM,WAAW,QAAA,CAAS,IAAA;AAAA,MACxB,CAAC,OAAA,KAAY,OAAA,CAAQ,QAAA,KAAa;AAAA,KACpC;AACA,IAAA,IAAI,aAAa,cAAA,EAAgB;AACjC,IAAA,cAAA,GAAiB,QAAA;AACjB,IAAA,OAAA,CAAQ,EAAE,IAAA,EAAM,UAAA,EAAY,QAAA,EAAU,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,IAAA,CACG,EAAA,CAAGE,uBAAA,CAAU,cAAA,EAAgB,QAAQ,CAAA,CACrC,EAAA;AAAA,IACCA,uBAAA,CAAU,eAAA;AAAA,IACV,CACE,KAAA,EACA,WAAA,EACA,WAAA,KACG;AACH,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,IAAI,CAAC,SAAA,CAAU,WAAW,KAAK,CAAC,OAAA,CAAQ,WAAW,CAAA,EAAG;AAEpD,QAAA,WAAA,CAAY,cAAc,KAAK,CAAA;AAC/B,QAAA;AAAA,MACF;AACA,MAAA,IAAI,KAAA,CAAM,IAAA,KAASF,mBAAA,CAAM,IAAA,CAAK,KAAA,EAAO;AACnC,QAAA,KAAA,CAAM,OAAO,KAAK,CAAA;AAClB,QAAA,OAAA,CAAQ,EAAE,IAAA,EAAM,eAAA,EAAiB,IAAA,EAAM,SAAS,CAAA;AAAA,MAClD,CAAA,MAAO;AACL,QAAA,KAAA,CAAM,OAAO,KAAK,CAAA;AAClB,QAAA,OAAA,CAAQ,EAAE,IAAA,EAAM,eAAA,EAAiB,IAAA,EAAM,SAAS,CAAA;AAAA,MAClD;AAAA,IACF;AAAA,GACF,CACC,EAAA,CAAGE,uBAAA,CAAU,iBAAA,EAAmB,CAAC,KAAA,KAAuB;AACvD,IAAA,KAAA,CAAM,MAAA,EAAO;AAAA,EACf,CAAC,CAAA,CACA,EAAA;AAAA,IACCA,uBAAA,CAAU,gBAAA;AAAA,IACV,CAAC,aAAqC,WAAA,KAAmC;AACvE,MAAA,IAAI,CAAC,UAAU,WAAW,CAAA,IAAK,YAAY,IAAA,KAASF,mBAAA,CAAM,KAAK,KAAA,EAAO;AACpE,QAAA,SAAA,CAAU,WAAW,CAAA;AAAA,MACvB;AAAA,IACF;AAAA,GACF,CACC,EAAA,CAAGE,uBAAA,CAAU,uBAAA,EAAyB,CAAC,WAAA,KAAmC;AACzE,IAAA,IAAI,OAAA,EAAS;AACb,IAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,MAAA,OAAA,CAAQ,EAAE,IAAA,EAAM,cAAA,EAAgB,CAAA;AAAA,IAClC,CAAA,MAAO;AACL,MAAA,SAAA,CAAU,WAAW,CAAA;AAAA,IACvB;AAAA,EACF,CAAC,EACA,EAAA,CAAGA,uBAAA,CAAU,4BAA4B,cAAc,CAAA,CACvD,GAAGA,uBAAA,CAAU,0BAAA,EAA4B,cAAc,CAAA,CACvD,EAAA,CAAGA,wBAAU,qBAAA,EAAuB,cAAc,EAClD,EAAA,CAAGA,uBAAA,CAAU,YAAA,EAAc,CAAC,MAAA,KAA8B;AACzD,IAAA,IAAI,OAAA,EAAS;AACb,IAAA,OAAA,CAAQ;AAAA,MACN,IAAA,EAAM,cAAA;AAAA,MACN,QAAA,EAAU,MAAA,KAAW,MAAA,IAAa,gBAAA,CAAiB,IAAI,MAAM;AAAA,KAC9D,CAAA;AAAA,EACH,CAAC,CAAA;AAEH,EAAA,MAAM,YAAY,MAAM;AACtB,IAAA,KAAA,MAAW,WAAA,IAAe,IAAA,CAAK,kBAAA,CAAmB,MAAA,EAAO,EAAG;AAC1D,MAAA,KAAA,MAAW,WAAA,IAAe,WAAA,CAAY,iBAAA,CAAkB,MAAA,EAAO,EAAG;AAChE,QAAA,WAAA,CAAY,OAAO,MAAA,EAAO;AAAA,MAC5B;AAAA,IACF;AACA,IAAA,KAAA,CAAM,SAAA,GAAY,IAAA;AAClB,IAAA,KAAA,CAAM,SAAA,GAAY,IAAA;AAAA,EACpB,CAAA;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,KAAK,OAAA,CAAQ,GAAA,EAAK,OAAO,EAAE,aAAA,EAAe,OAAO,CAAA;AAAA,EACzD,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,SAAA,EAAU;AACV,IAAA,MAAM,KAAA;AAAA,EACR;AAGA,EAAA,KAAA,MAAW,WAAA,IAAe,IAAA,CAAK,kBAAA,CAAmB,MAAA,EAAO,EAAG;AAC1D,IAAA,KAAA,MAAW,WAAA,IAAe,WAAA,CAAY,iBAAA,CAAkB,MAAA,EAAO,EAAG;AAChE,MAAA,QAAA,CAAS,aAAa,WAAW,CAAA;AAAA,IACnC;AAAA,EACF;AACA,EAAA,cAAA,EAAe;AAEf,EAAA,OAAO;AAAA,IACL,gBAAgB,YAAY;AAG1B,MAAA,MAAM,OAAA,CAAQ,WAAW,CAAC,IAAA,CAAK,YAAW,EAAG,IAAA,CAAK,UAAA,EAAY,CAAC,CAAA;AAC/D,MAAA,cAAA,EAAe;AAAA,IACjB,CAAA;AAAA,IACA,OAAO,YAAY;AACjB,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,SAAA,EAAU;AACV,MAAA,IAAI;AACF,QAAA,MAAM,KAAK,UAAA,EAAW;AAAA,MACxB,CAAA,SAAE;AACA,QAAA,IAAA,CAAK,kBAAA,EAAmB;AAAA,MAC1B;AAAA,IACF;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * BOFF-9797. The live avatar's room, joined to watch and listen only.\n *\n * A visitor's browser on a product website holds a subscribe-only room token: it can receive the avatar worker's\n * video and voice and do nothing else. This adapter keeps to that from its side too:\n *\n * - **Nothing is subscribed automatically** (`autoSubscribe: false`). Only the tracks of `workerIdentity`, the avatar\n * worker the create answer named, are subscribed, and only their video and audio. Any other participant's track\n * (the server's own speaker included) is never asked for, and is dropped again should the server send it anyway.\n * - **Nothing is published.** No microphone, no camera, no data, no RPC method: this file never calls a publishing\n * API, and the token would refuse one.\n * - **Safari and iOS** hold sound back until a gesture. The room reports it (`playback-blocked`, with `sound` when the\n * sound is held back and not only the video) and the panel offers \"Tap to turn on sound\", whose tap calls\n * `enablePlayback`.\n * - **The end of the speech** is told (`speech-ended`) from what any viewer of the room sees: the other voice in it, the\n * server's speaker that the worker follows, leaving the room or unpublishing its voice. Its track is never subscribed.\n * - **Whether the worker's voice is heard** is told (`speaking`) each time it changes, from the room server's active\n * speakers, which it reports to a speaker's subscribers.\n *\n * Written for the website from the media library's public API: nothing here is copied from the product apps.\n */\nimport {\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n type Participant,\n type RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n} from \"livekit-client\";\n\nimport type { ReceiveOnlyJoinOptions, ReceiveOnlySession } from \"./types\";\n\n/** The reasons a room closes because the server closed it, rather than because the connection was lost. */\nconst CLOSED_BY_SERVER: ReadonlySet<DisconnectReason> = new Set([\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.ROOM_CLOSED,\n DisconnectReason.PARTICIPANT_REMOVED,\n]);\n\nfunction isMedia(publication: RemoteTrackPublication): boolean {\n return (\n publication.kind === Track.Kind.Video ||\n publication.kind === Track.Kind.Audio\n );\n}\n\n/**\n * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice\n * in `options.video` and `options.audio`. Resolves once connected; rejects when the room cannot be joined (the panel\n * then ends the session).\n */\nexport async function joinReceiveOnly(\n options: ReceiveOnlyJoinOptions,\n): Promise<ReceiveOnlySession> {\n const { url, token, workerIdentity, video, audio, onEvent } = options;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(\"live avatar: the room address is not a URL\");\n }\n if (parsed.protocol !== \"wss:\") {\n throw new Error(\"live avatar: the room address must be wss:\");\n }\n if (!token || !workerIdentity) {\n throw new Error(\"live avatar: the room token or speaker is missing\");\n }\n\n const room = new Room({\n // One small video, always shown while the session lasts: no quality switching, no layer pausing.\n adaptiveStream: false,\n dynacast: false,\n // Closing the tab or leaving the page leaves the room at once. Belt and braces with the panel's own page-hide\n // handler, which also asks the server to end the session: either may run first, and both are safe to repeat.\n disconnectOnPageLeave: true,\n });\n let leaving = false;\n\n const isSpeaker = (participant: RemoteParticipant) =>\n participant.identity === workerIdentity;\n\n /**\n * Everyone else in the room seen publishing a voice: the server's speaker, whose speech the worker voices. Noted\n * only, never subscribed to, so that the end of that speech can be told (`speech-ended`).\n */\n const voiced = new Set<string>();\n let speechEnded = false;\n\n /** Subscribe to `publication` when it is the speaker's video or audio; leave everything else alone. */\n const consider = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant)) {\n if (publication.kind === Track.Kind.Audio) {\n voiced.add(participant.identity);\n }\n return;\n }\n if (!isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n /** A voice seen published has gone (its track, or its participant): the speech has ended at its source. Once. */\n const voiceGone = (participant: RemoteParticipant) => {\n if (leaving || speechEnded || !voiced.has(participant.identity)) return;\n speechEnded = true;\n onEvent({ type: \"speech-ended\" });\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\n sound: !room.canPlaybackAudio,\n });\n };\n\n /** Whether the worker's voice is heard, as last told: `speaking` goes out only when it changes. */\n let workerSpeaking = false;\n const reportSpeakers = (speakers: Participant[]) => {\n if (leaving) return;\n const speaking = speakers.some(\n (speaker) => speaker.identity === workerIdentity,\n );\n if (speaking === workerSpeaking) return;\n workerSpeaking = speaking;\n onEvent({ type: \"speaking\", speaking });\n };\n\n room\n .on(RoomEvent.TrackPublished, consider)\n .on(\n RoomEvent.TrackSubscribed,\n (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant) || !isMedia(publication)) {\n // Never asked for: drop it rather than play it.\n publication.setSubscribed(false);\n return;\n }\n if (track.kind === Track.Kind.Video) {\n track.attach(video);\n onEvent({ type: \"speaker-track\", kind: \"video\" });\n } else {\n track.attach(audio);\n onEvent({ type: \"speaker-track\", kind: \"audio\" });\n }\n },\n )\n .on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n track.detach();\n })\n .on(\n RoomEvent.TrackUnpublished,\n (publication: RemoteTrackPublication, participant: RemoteParticipant) => {\n if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {\n voiceGone(participant);\n }\n },\n )\n .on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (leaving) return;\n if (isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\n } else {\n voiceGone(participant);\n }\n })\n .on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.ActiveSpeakersChanged, reportSpeakers)\n .on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n if (leaving) return;\n onEvent({\n type: \"disconnected\",\n byServer: reason !== undefined && CLOSED_BY_SERVER.has(reason),\n });\n });\n\n const detachAll = () => {\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n publication.track?.detach();\n }\n }\n video.srcObject = null;\n audio.srcObject = null;\n };\n\n try {\n await room.connect(url, token, { autoSubscribe: false });\n } catch (error) {\n leaving = true;\n room.removeAllListeners();\n detachAll();\n throw error;\n }\n\n // Whoever was in the room before this viewer joined: their tracks were published before the listener above.\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n consider(publication, participant);\n }\n }\n reportPlayback();\n\n return {\n enablePlayback: async () => {\n // Both started inside the visitor's gesture, before anything is awaited: Safari allows playback only there.\n // A refusal leaves the room as it was, and the report below keeps the button offered.\n await Promise.allSettled([room.startAudio(), room.startVideo()]);\n reportPlayback();\n },\n leave: async () => {\n if (leaving) return;\n leaving = true;\n detachAll();\n try {\n await room.disconnect();\n } finally {\n room.removeAllListeners();\n }\n },\n };\n}\n"]}
@@ -59,6 +59,16 @@ async function joinReceiveOnly(options) {
59
59
  sound: !room.canPlaybackAudio
60
60
  });
61
61
  };
62
+ let workerSpeaking = false;
63
+ const reportSpeakers = (speakers) => {
64
+ if (leaving) return;
65
+ const speaking = speakers.some(
66
+ (speaker) => speaker.identity === workerIdentity
67
+ );
68
+ if (speaking === workerSpeaking) return;
69
+ workerSpeaking = speaking;
70
+ onEvent({ type: "speaking", speaking });
71
+ };
62
72
  room.on(RoomEvent.TrackPublished, consider).on(
63
73
  RoomEvent.TrackSubscribed,
64
74
  (track, publication, participant) => {
@@ -91,7 +101,7 @@ async function joinReceiveOnly(options) {
91
101
  } else {
92
102
  voiceGone(participant);
93
103
  }
94
- }).on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(RoomEvent.Disconnected, (reason) => {
104
+ }).on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(RoomEvent.ActiveSpeakersChanged, reportSpeakers).on(RoomEvent.Disconnected, (reason) => {
95
105
  if (leaving) return;
96
106
  onEvent({
97
107
  type: "disconnected",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/avatar-live/receive-only.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * BOFF-9797. The live avatar's room, joined to watch and listen only.\n *\n * A visitor's browser on a product website holds a subscribe-only room token: it can receive the avatar worker's\n * video and voice and do nothing else. This adapter keeps to that from its side too:\n *\n * - **Nothing is subscribed automatically** (`autoSubscribe: false`). Only the tracks of `workerIdentity`, the avatar\n * worker the create answer named, are subscribed, and only their video and audio. Any other participant's track\n * (the server's own speaker included) is never asked for, and is dropped again should the server send it anyway.\n * - **Nothing is published.** No microphone, no camera, no data, no RPC method: this file never calls a publishing\n * API, and the token would refuse one.\n * - **Safari and iOS** hold sound back until a gesture. The room reports it (`playback-blocked`, with `sound` when the\n * sound is held back and not only the video) and the panel offers \"Tap to turn on sound\", whose tap calls\n * `enablePlayback`.\n * - **The end of the speech** is told (`speech-ended`) from what any viewer of the room sees: the other voice in it, the\n * server's speaker that the worker follows, leaving the room or unpublishing its voice. Its track is never subscribed.\n *\n * Written for the website from the media library's public API: nothing here is copied from the product apps.\n */\nimport {\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n type RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n} from \"livekit-client\";\n\nimport type { ReceiveOnlyJoinOptions, ReceiveOnlySession } from \"./types\";\n\n/** The reasons a room closes because the server closed it, rather than because the connection was lost. */\nconst CLOSED_BY_SERVER: ReadonlySet<DisconnectReason> = new Set([\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.ROOM_CLOSED,\n DisconnectReason.PARTICIPANT_REMOVED,\n]);\n\nfunction isMedia(publication: RemoteTrackPublication): boolean {\n return (\n publication.kind === Track.Kind.Video ||\n publication.kind === Track.Kind.Audio\n );\n}\n\n/**\n * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice\n * in `options.video` and `options.audio`. Resolves once connected; rejects when the room cannot be joined (the panel\n * then ends the session).\n */\nexport async function joinReceiveOnly(\n options: ReceiveOnlyJoinOptions,\n): Promise<ReceiveOnlySession> {\n const { url, token, workerIdentity, video, audio, onEvent } = options;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(\"live avatar: the room address is not a URL\");\n }\n if (parsed.protocol !== \"wss:\") {\n throw new Error(\"live avatar: the room address must be wss:\");\n }\n if (!token || !workerIdentity) {\n throw new Error(\"live avatar: the room token or speaker is missing\");\n }\n\n const room = new Room({\n // One small video, always shown while the session lasts: no quality switching, no layer pausing.\n adaptiveStream: false,\n dynacast: false,\n // Closing the tab or leaving the page leaves the room at once. Belt and braces with the panel's own page-hide\n // handler, which also asks the server to end the session: either may run first, and both are safe to repeat.\n disconnectOnPageLeave: true,\n });\n let leaving = false;\n\n const isSpeaker = (participant: RemoteParticipant) =>\n participant.identity === workerIdentity;\n\n /**\n * Everyone else in the room seen publishing a voice: the server's speaker, whose speech the worker voices. Noted\n * only, never subscribed to, so that the end of that speech can be told (`speech-ended`).\n */\n const voiced = new Set<string>();\n let speechEnded = false;\n\n /** Subscribe to `publication` when it is the speaker's video or audio; leave everything else alone. */\n const consider = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant)) {\n if (publication.kind === Track.Kind.Audio) {\n voiced.add(participant.identity);\n }\n return;\n }\n if (!isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n /** A voice seen published has gone (its track, or its participant): the speech has ended at its source. Once. */\n const voiceGone = (participant: RemoteParticipant) => {\n if (leaving || speechEnded || !voiced.has(participant.identity)) return;\n speechEnded = true;\n onEvent({ type: \"speech-ended\" });\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\n sound: !room.canPlaybackAudio,\n });\n };\n\n room\n .on(RoomEvent.TrackPublished, consider)\n .on(\n RoomEvent.TrackSubscribed,\n (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant) || !isMedia(publication)) {\n // Never asked for: drop it rather than play it.\n publication.setSubscribed(false);\n return;\n }\n if (track.kind === Track.Kind.Video) {\n track.attach(video);\n onEvent({ type: \"speaker-track\", kind: \"video\" });\n } else {\n track.attach(audio);\n onEvent({ type: \"speaker-track\", kind: \"audio\" });\n }\n },\n )\n .on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n track.detach();\n })\n .on(\n RoomEvent.TrackUnpublished,\n (publication: RemoteTrackPublication, participant: RemoteParticipant) => {\n if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {\n voiceGone(participant);\n }\n },\n )\n .on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (leaving) return;\n if (isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\n } else {\n voiceGone(participant);\n }\n })\n .on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n if (leaving) return;\n onEvent({\n type: \"disconnected\",\n byServer: reason !== undefined && CLOSED_BY_SERVER.has(reason),\n });\n });\n\n const detachAll = () => {\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n publication.track?.detach();\n }\n }\n video.srcObject = null;\n audio.srcObject = null;\n };\n\n try {\n await room.connect(url, token, { autoSubscribe: false });\n } catch (error) {\n leaving = true;\n room.removeAllListeners();\n detachAll();\n throw error;\n }\n\n // Whoever was in the room before this viewer joined: their tracks were published before the listener above.\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n consider(publication, participant);\n }\n }\n reportPlayback();\n\n return {\n enablePlayback: async () => {\n // Both started inside the visitor's gesture, before anything is awaited: Safari allows playback only there.\n // A refusal leaves the room as it was, and the report below keeps the button offered.\n await Promise.allSettled([room.startAudio(), room.startVideo()]);\n reportPlayback();\n },\n leave: async () => {\n if (leaving) return;\n leaving = true;\n detachAll();\n try {\n await room.disconnect();\n } finally {\n room.removeAllListeners();\n }\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/avatar-live/receive-only.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * BOFF-9797. The live avatar's room, joined to watch and listen only.\n *\n * A visitor's browser on a product website holds a subscribe-only room token: it can receive the avatar worker's\n * video and voice and do nothing else. This adapter keeps to that from its side too:\n *\n * - **Nothing is subscribed automatically** (`autoSubscribe: false`). Only the tracks of `workerIdentity`, the avatar\n * worker the create answer named, are subscribed, and only their video and audio. Any other participant's track\n * (the server's own speaker included) is never asked for, and is dropped again should the server send it anyway.\n * - **Nothing is published.** No microphone, no camera, no data, no RPC method: this file never calls a publishing\n * API, and the token would refuse one.\n * - **Safari and iOS** hold sound back until a gesture. The room reports it (`playback-blocked`, with `sound` when the\n * sound is held back and not only the video) and the panel offers \"Tap to turn on sound\", whose tap calls\n * `enablePlayback`.\n * - **The end of the speech** is told (`speech-ended`) from what any viewer of the room sees: the other voice in it, the\n * server's speaker that the worker follows, leaving the room or unpublishing its voice. Its track is never subscribed.\n * - **Whether the worker's voice is heard** is told (`speaking`) each time it changes, from the room server's active\n * speakers, which it reports to a speaker's subscribers.\n *\n * Written for the website from the media library's public API: nothing here is copied from the product apps.\n */\nimport {\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n type Participant,\n type RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n} from \"livekit-client\";\n\nimport type { ReceiveOnlyJoinOptions, ReceiveOnlySession } from \"./types\";\n\n/** The reasons a room closes because the server closed it, rather than because the connection was lost. */\nconst CLOSED_BY_SERVER: ReadonlySet<DisconnectReason> = new Set([\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.ROOM_CLOSED,\n DisconnectReason.PARTICIPANT_REMOVED,\n]);\n\nfunction isMedia(publication: RemoteTrackPublication): boolean {\n return (\n publication.kind === Track.Kind.Video ||\n publication.kind === Track.Kind.Audio\n );\n}\n\n/**\n * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice\n * in `options.video` and `options.audio`. Resolves once connected; rejects when the room cannot be joined (the panel\n * then ends the session).\n */\nexport async function joinReceiveOnly(\n options: ReceiveOnlyJoinOptions,\n): Promise<ReceiveOnlySession> {\n const { url, token, workerIdentity, video, audio, onEvent } = options;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(\"live avatar: the room address is not a URL\");\n }\n if (parsed.protocol !== \"wss:\") {\n throw new Error(\"live avatar: the room address must be wss:\");\n }\n if (!token || !workerIdentity) {\n throw new Error(\"live avatar: the room token or speaker is missing\");\n }\n\n const room = new Room({\n // One small video, always shown while the session lasts: no quality switching, no layer pausing.\n adaptiveStream: false,\n dynacast: false,\n // Closing the tab or leaving the page leaves the room at once. Belt and braces with the panel's own page-hide\n // handler, which also asks the server to end the session: either may run first, and both are safe to repeat.\n disconnectOnPageLeave: true,\n });\n let leaving = false;\n\n const isSpeaker = (participant: RemoteParticipant) =>\n participant.identity === workerIdentity;\n\n /**\n * Everyone else in the room seen publishing a voice: the server's speaker, whose speech the worker voices. Noted\n * only, never subscribed to, so that the end of that speech can be told (`speech-ended`).\n */\n const voiced = new Set<string>();\n let speechEnded = false;\n\n /** Subscribe to `publication` when it is the speaker's video or audio; leave everything else alone. */\n const consider = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant)) {\n if (publication.kind === Track.Kind.Audio) {\n voiced.add(participant.identity);\n }\n return;\n }\n if (!isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n /** A voice seen published has gone (its track, or its participant): the speech has ended at its source. Once. */\n const voiceGone = (participant: RemoteParticipant) => {\n if (leaving || speechEnded || !voiced.has(participant.identity)) return;\n speechEnded = true;\n onEvent({ type: \"speech-ended\" });\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\n sound: !room.canPlaybackAudio,\n });\n };\n\n /** Whether the worker's voice is heard, as last told: `speaking` goes out only when it changes. */\n let workerSpeaking = false;\n const reportSpeakers = (speakers: Participant[]) => {\n if (leaving) return;\n const speaking = speakers.some(\n (speaker) => speaker.identity === workerIdentity,\n );\n if (speaking === workerSpeaking) return;\n workerSpeaking = speaking;\n onEvent({ type: \"speaking\", speaking });\n };\n\n room\n .on(RoomEvent.TrackPublished, consider)\n .on(\n RoomEvent.TrackSubscribed,\n (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant) || !isMedia(publication)) {\n // Never asked for: drop it rather than play it.\n publication.setSubscribed(false);\n return;\n }\n if (track.kind === Track.Kind.Video) {\n track.attach(video);\n onEvent({ type: \"speaker-track\", kind: \"video\" });\n } else {\n track.attach(audio);\n onEvent({ type: \"speaker-track\", kind: \"audio\" });\n }\n },\n )\n .on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n track.detach();\n })\n .on(\n RoomEvent.TrackUnpublished,\n (publication: RemoteTrackPublication, participant: RemoteParticipant) => {\n if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {\n voiceGone(participant);\n }\n },\n )\n .on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (leaving) return;\n if (isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\n } else {\n voiceGone(participant);\n }\n })\n .on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.ActiveSpeakersChanged, reportSpeakers)\n .on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n if (leaving) return;\n onEvent({\n type: \"disconnected\",\n byServer: reason !== undefined && CLOSED_BY_SERVER.has(reason),\n });\n });\n\n const detachAll = () => {\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n publication.track?.detach();\n }\n }\n video.srcObject = null;\n audio.srcObject = null;\n };\n\n try {\n await room.connect(url, token, { autoSubscribe: false });\n } catch (error) {\n leaving = true;\n room.removeAllListeners();\n detachAll();\n throw error;\n }\n\n // Whoever was in the room before this viewer joined: their tracks were published before the listener above.\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n consider(publication, participant);\n }\n }\n reportPlayback();\n\n return {\n enablePlayback: async () => {\n // Both started inside the visitor's gesture, before anything is awaited: Safari allows playback only there.\n // A refusal leaves the room as it was, and the report below keeps the button offered.\n await Promise.allSettled([room.startAudio(), room.startVideo()]);\n reportPlayback();\n },\n leave: async () => {\n if (leaving) return;\n leaving = true;\n detachAll();\n try {\n await room.disconnect();\n } finally {\n room.removeAllListeners();\n }\n },\n };\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-OO0hvQCK.mjs';
4
- import '../types-CXqaLlNB.mjs';
3
+ import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-BL8FTm0I.mjs';
4
+ import '../types-CTUJNvQu.mjs';
5
5
  import '../use-explore-chat-CbfohA9O.mjs';
6
6
  import '../explore-types-Ol1Mm3dW.mjs';
7
7
 
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-C-OwXNje.js';
4
- import '../types-CXqaLlNB.js';
3
+ import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-auLKxSzx.js';
4
+ import '../types-CTUJNvQu.js';
5
5
  import '../use-explore-chat-B3dOOvJN.js';
6
6
  import '../explore-types-Ol1Mm3dW.js';
7
7