@burdenoff/website-sdk 2026.1104.1 → 2026.1104.3

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
@@ -301,6 +301,8 @@ publishes audio, video or data, and it sends the avatar nothing to say.
301
301
  manifestVersion: 'hb-en-1', // the version of `suggestedQuestions`
302
302
  // Optional: used only where the server checks for bots (see "The bot check is the server's setting").
303
303
  turnstileSiteKey: TURNSTILE_SITE_KEY,
304
+ // Optional, default false: end a session nobody attends to (see "A session nobody attends to").
305
+ endUnattended: true,
304
306
  },
305
307
  }}
306
308
  />
@@ -442,6 +444,28 @@ publishes audio, video or data, and it sends the avatar nothing to say.
442
444
  `text/plain;charset=UTF-8` (which the gateway accepts for this route), and on
443
445
  production nothing else is added, so no preflight has to complete while the
444
446
  page unloads. The server ends sessions on its own deadlines as well.
447
+ - **A session nobody attends to (opt-in: `live.endUnattended: true`, or
448
+ `websiteAvatarLive({ …, endUnattended: true })`).** Out of sight (the Chat
449
+ tab, a hidden page) the answer under way still plays to its end, and then
450
+ the session ends, with reason `user`, once:
451
+ - **its answer has played out**: at once. This is the lifecycle rule for
452
+ every later stage: an idle tail after an answer runs only while the Avatar
453
+ view is in sight, and going out of sight during one ends the session then.
454
+ The answer counts as played out 5 s after its speech ends at the source
455
+ (the room's other voice, the server's speaker, leaves or unpublishes): the
456
+ server's 3 s tail allowance for the worker's lag, plus 2 s. Today the
457
+ server ends a session about 3 s after its answer, so its own end comes
458
+ first and the browser sends nothing.
459
+ - **the browser holds its sound back** (autoplay refused, or the audio
460
+ context suspended) once the avatar's voice has arrived: after a 5 s grace,
461
+ since "Tap to turn on sound" is out of sight. The view then says "The live avatar stopped early." and "The
462
+ full answer is in Chat.". Back in sight within the grace, the button shows
463
+ and nothing ends; in sight, a held-back sound never ends a session.
464
+
465
+ Unset (the default) or `false`: a session out of sight plays on to the
466
+ server's planned end. A browser that silences a background page without
467
+ telling the room (iOS Safari may) cannot be seen; the server's planned end
468
+ bounds that session either way.
445
469
  - **Consent copy is versioned.** The panel shows a consent card only when it
446
470
  holds reviewed copy for exactly the version the server names; otherwise the
447
471
  view stays as it was. `web-consent-2026-11-02` is product-neutral: its card
@@ -470,6 +494,27 @@ publishes audio, video or data, and it sends the avatar nothing to say.
470
494
  chat started in the Avatar tab, provided it passes the speech checks, whether
471
495
  you chose a suggested question or typed your own", and the visitor is told
472
496
  not to put personal details in a typed question.
497
+ `web-consent-2026-11-12-b` (bot check `turnstile`, 1,010 bytes, SHA-256
498
+ `e8f4495d1e7f179b16de86c1eba0d2528623afb088f904f100e54523ad350c8c`) and
499
+ `web-consent-2026-11-13-b` (bot check `off`, 966 bytes, SHA-256
500
+ `4c88c229da1331ebe0f7e6e32f425612071efb2e4cf01ece813ed827ea8fc174`) are the
501
+ same two with only the timing sentence changed, for the server's Stage B
502
+ sessions of up to three minutes: "It reads for up to three minutes; if an
503
+ answer is longer, it reads the first part, says so, and the full answer stays
504
+ in Chat."
505
+ - **A part-read answer.** Where the server speaks only the first part of an
506
+ answer because the whole of it does not fit the session (the create
507
+ answer's `excerpt: true`, web API contract v1.4, only under the server's
508
+ `AVATAR_WEB_SIZE_BY_AUDIO`), the status line says "The avatar reads the
509
+ first part of this answer. The full answer is in Chat." (key `excerpt`) while
510
+ the avatar speaks, in place of "The avatar is speaking.". The layout is the
511
+ same. Without the field, or with any value but `true`, nothing changes.
512
+ - **Session length.** The countdown and the room's timers follow the server's
513
+ `endsAt` for each session, up to the availability answer's
514
+ `maxSessionSeconds` (three minutes under Stage B); an `endsAt` this
515
+ browser's clock cannot place within that is counted down from
516
+ `maxSessionSeconds` on the browser's own clock. Nothing in the view assumes a
517
+ minute.
473
518
  - **How a session ended.** When the worker leaves, the server closes the room,
474
519
  or the session's time runs out with the room still open, the view says only
475
520
  "The live avatar has ended." (key `finished`). The browser
@@ -1,5 +1,5 @@
1
- import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CndbmBK-.mjs';
2
- export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CndbmBK-.mjs';
1
+ import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CXqaLlNB.mjs';
2
+ export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CXqaLlNB.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-CndbmBK-.js';
2
- export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CndbmBK-.js';
1
+ import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CXqaLlNB.js';
2
+ export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CXqaLlNB.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
@@ -35,15 +35,30 @@ async function joinReceiveOnly(options) {
35
35
  });
36
36
  let leaving = false;
37
37
  const isSpeaker = (participant) => participant.identity === workerIdentity;
38
+ const voiced = /* @__PURE__ */ new Set();
39
+ let speechEnded = false;
38
40
  const consider = (publication, participant) => {
39
- if (leaving || !isSpeaker(participant) || !isMedia(publication)) return;
41
+ if (leaving) return;
42
+ if (!isSpeaker(participant)) {
43
+ if (publication.kind === livekitClient.Track.Kind.Audio) {
44
+ voiced.add(participant.identity);
45
+ }
46
+ return;
47
+ }
48
+ if (!isMedia(publication)) return;
40
49
  if (!publication.isSubscribed) publication.setSubscribed(true);
41
50
  };
51
+ const voiceGone = (participant) => {
52
+ if (leaving || speechEnded || !voiced.has(participant.identity)) return;
53
+ speechEnded = true;
54
+ onEvent({ type: "speech-ended" });
55
+ };
42
56
  const reportPlayback = () => {
43
57
  if (leaving) return;
44
58
  onEvent({
45
59
  type: "playback-blocked",
46
- blocked: !room.canPlaybackAudio || !room.canPlaybackVideo
60
+ blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,
61
+ sound: !room.canPlaybackAudio
47
62
  });
48
63
  };
49
64
  room.on(livekitClient.RoomEvent.TrackPublished, consider).on(
@@ -64,9 +79,19 @@ async function joinReceiveOnly(options) {
64
79
  }
65
80
  ).on(livekitClient.RoomEvent.TrackUnsubscribed, (track) => {
66
81
  track.detach();
67
- }).on(livekitClient.RoomEvent.ParticipantDisconnected, (participant) => {
68
- if (!leaving && isSpeaker(participant)) {
82
+ }).on(
83
+ livekitClient.RoomEvent.TrackUnpublished,
84
+ (publication, participant) => {
85
+ if (!isSpeaker(participant) && publication.kind === livekitClient.Track.Kind.Audio) {
86
+ voiceGone(participant);
87
+ }
88
+ }
89
+ ).on(livekitClient.RoomEvent.ParticipantDisconnected, (participant) => {
90
+ if (leaving) return;
91
+ if (isSpeaker(participant)) {
69
92
  onEvent({ type: "speaker-left" });
93
+ } else {
94
+ voiceGone(participant);
70
95
  }
71
96
  }).on(livekitClient.RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.Disconnected, (reason) => {
72
97
  if (leaving) return;
@@ -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`) and the panel offers\n * \"Tap to turn on sound\", whose tap calls `enablePlayback`.\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 /** 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 || !isSpeaker(participant) || !isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\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(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (!leaving && isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\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":";;;;;AAgCA,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;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,CAAA,CACA,EAAA,CAAGA,uBAAA,CAAU,0BAAA,EAA4B,cAAc,CAAA,CACvD,EAAA,CAAGA,uBAAA,CAAU,0BAAA,EAA4B,cAAc,CAAA,CACvD,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 *\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"]}
@@ -33,15 +33,30 @@ async function joinReceiveOnly(options) {
33
33
  });
34
34
  let leaving = false;
35
35
  const isSpeaker = (participant) => participant.identity === workerIdentity;
36
+ const voiced = /* @__PURE__ */ new Set();
37
+ let speechEnded = false;
36
38
  const consider = (publication, participant) => {
37
- if (leaving || !isSpeaker(participant) || !isMedia(publication)) return;
39
+ if (leaving) return;
40
+ if (!isSpeaker(participant)) {
41
+ if (publication.kind === Track.Kind.Audio) {
42
+ voiced.add(participant.identity);
43
+ }
44
+ return;
45
+ }
46
+ if (!isMedia(publication)) return;
38
47
  if (!publication.isSubscribed) publication.setSubscribed(true);
39
48
  };
49
+ const voiceGone = (participant) => {
50
+ if (leaving || speechEnded || !voiced.has(participant.identity)) return;
51
+ speechEnded = true;
52
+ onEvent({ type: "speech-ended" });
53
+ };
40
54
  const reportPlayback = () => {
41
55
  if (leaving) return;
42
56
  onEvent({
43
57
  type: "playback-blocked",
44
- blocked: !room.canPlaybackAudio || !room.canPlaybackVideo
58
+ blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,
59
+ sound: !room.canPlaybackAudio
45
60
  });
46
61
  };
47
62
  room.on(RoomEvent.TrackPublished, consider).on(
@@ -62,9 +77,19 @@ async function joinReceiveOnly(options) {
62
77
  }
63
78
  ).on(RoomEvent.TrackUnsubscribed, (track) => {
64
79
  track.detach();
65
- }).on(RoomEvent.ParticipantDisconnected, (participant) => {
66
- if (!leaving && isSpeaker(participant)) {
80
+ }).on(
81
+ RoomEvent.TrackUnpublished,
82
+ (publication, participant) => {
83
+ if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {
84
+ voiceGone(participant);
85
+ }
86
+ }
87
+ ).on(RoomEvent.ParticipantDisconnected, (participant) => {
88
+ if (leaving) return;
89
+ if (isSpeaker(participant)) {
67
90
  onEvent({ type: "speaker-left" });
91
+ } else {
92
+ voiceGone(participant);
68
93
  }
69
94
  }).on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(RoomEvent.Disconnected, (reason) => {
70
95
  if (leaving) return;
@@ -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`) and the panel offers\n * \"Tap to turn on sound\", whose tap calls `enablePlayback`.\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 /** 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 || !isSpeaker(participant) || !isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\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(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (!leaving && isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\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 *\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,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-N4WhJzgS.mjs';
4
- import '../types-CndbmBK-.mjs';
3
+ import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-B6_p0q1c.mjs';
4
+ import '../types-CXqaLlNB.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-CAfN5eAX.js';
4
- import '../types-CndbmBK-.js';
3
+ import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-DRpS-mqQ.js';
4
+ import '../types-CXqaLlNB.js';
5
5
  import '../use-explore-chat-B3dOOvJN.js';
6
6
  import '../explore-types-Ol1Mm3dW.js';
7
7