@burdenoff/website-sdk 2026.1104.1 → 2026.1104.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 +24 -0
- package/dist/avatar-live/index.d.mts +2 -2
- package/dist/avatar-live/index.d.ts +2 -2
- package/dist/avatar-live/index.js +29 -4
- package/dist/avatar-live/index.js.map +1 -1
- package/dist/avatar-live/index.mjs +29 -4
- package/dist/avatar-live/index.mjs.map +1 -1
- package/dist/avatar-live-view/index.d.mts +2 -2
- package/dist/avatar-live-view/index.d.ts +2 -2
- package/dist/avatar-live-view/index.js +73 -9
- package/dist/avatar-live-view/index.js.map +1 -1
- package/dist/avatar-live-view/index.mjs +68 -4
- package/dist/avatar-live-view/index.mjs.map +1 -1
- package/dist/{chunk-K3QZGJEH.mjs → chunk-7MSB3EUI.mjs} +4 -3
- package/dist/chunk-7MSB3EUI.mjs.map +1 -0
- package/dist/{chunk-7O2I32ZN.js → chunk-WSEQ2I6I.js} +4 -3
- package/dist/chunk-WSEQ2I6I.js.map +1 -0
- package/dist/{explore-avatar-live-config-N4WhJzgS.d.mts → explore-avatar-live-config-B6_p0q1c.d.mts} +14 -1
- package/dist/{explore-avatar-live-config-CAfN5eAX.d.ts → explore-avatar-live-config-DRpS-mqQ.d.ts} +14 -1
- package/dist/index.d.mts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CndbmBK-.d.mts → types-CXqaLlNB.d.mts} +14 -1
- package/dist/{types-CndbmBK-.d.ts → types-CXqaLlNB.d.ts} +14 -1
- package/package.json +1 -1
- package/dist/chunk-7O2I32ZN.js.map +0 -1
- package/dist/chunk-K3QZGJEH.mjs.map +0 -1
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-
|
|
2
|
-
export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-
|
|
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-
|
|
2
|
-
export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-
|
|
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
|
|
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(
|
|
68
|
-
|
|
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":";;;;;AA6BA,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;AAG3B,EAAA,MAAM,QAAA,GAAW,CACf,WAAA,EACA,WAAA,KACG;AACH,IAAA,IAAI,OAAA,IAAW,CAAC,SAAA,CAAU,WAAW,KAAK,CAAC,OAAA,CAAQ,WAAW,CAAA,EAAG;AACjE,IAAA,IAAI,CAAC,WAAA,CAAY,YAAA,EAAc,WAAA,CAAY,cAAc,IAAI,CAAA;AAAA,EAC/D,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;AAAA,KAC1C,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,IAAA,CACG,EAAA,CAAGC,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,CAAGA,uBAAA,CAAU,uBAAA,EAAyB,CAAC,WAAA,KAAmC;AACzE,IAAA,IAAI,CAAC,OAAA,IAAW,SAAA,CAAU,WAAW,CAAA,EAAG;AACtC,MAAA,OAAA,CAAQ,EAAE,IAAA,EAAM,cAAA,EAAgB,CAAA;AAAA,IAClC;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`) 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":"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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"]}
|
|
@@ -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
|
|
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(
|
|
66
|
-
|
|
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":";;;AA6BA,IAAM,gBAAA,uBAAsD,GAAA,CAAI;AAAA,EAC9D,gBAAA,CAAiB,YAAA;AAAA,EACjB,gBAAA,CAAiB,WAAA;AAAA,EACjB,gBAAA,CAAiB;AACnB,CAAC,CAAA;AAED,SAAS,QAAQ,WAAA,EAA8C;AAC7D,EAAA,OACE,WAAA,CAAY,SAAS,KAAA,CAAM,IAAA,CAAK,SAChC,WAAA,CAAY,IAAA,KAAS,MAAM,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,IAAI,IAAA,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;AAG3B,EAAA,MAAM,QAAA,GAAW,CACf,WAAA,EACA,WAAA,KACG;AACH,IAAA,IAAI,OAAA,IAAW,CAAC,SAAA,CAAU,WAAW,KAAK,CAAC,OAAA,CAAQ,WAAW,CAAA,EAAG;AACjE,IAAA,IAAI,CAAC,WAAA,CAAY,YAAA,EAAc,WAAA,CAAY,cAAc,IAAI,CAAA;AAAA,EAC/D,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;AAAA,KAC1C,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,IAAA,CACG,EAAA,CAAG,SAAA,CAAU,cAAA,EAAgB,QAAQ,CAAA,CACrC,EAAA;AAAA,IACC,SAAA,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,KAAS,KAAA,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,CAAG,SAAA,CAAU,iBAAA,EAAmB,CAAC,KAAA,KAAuB;AACvD,IAAA,KAAA,CAAM,MAAA,EAAO;AAAA,EACf,CAAC,CAAA,CACA,EAAA,CAAG,SAAA,CAAU,uBAAA,EAAyB,CAAC,WAAA,KAAmC;AACzE,IAAA,IAAI,CAAC,OAAA,IAAW,SAAA,CAAU,WAAW,CAAA,EAAG;AACtC,MAAA,OAAA,CAAQ,EAAE,IAAA,EAAM,cAAA,EAAgB,CAAA;AAAA,IAClC;AAAA,EACF,CAAC,CAAA,CACA,EAAA,CAAG,SAAA,CAAU,0BAAA,EAA4B,cAAc,CAAA,CACvD,EAAA,CAAG,SAAA,CAAU,0BAAA,EAA4B,cAAc,CAAA,CACvD,EAAA,CAAG,SAAA,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.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-
|
|
4
|
-
import '../types-
|
|
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-
|
|
4
|
-
import '../types-
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkWSEQ2I6I_js = require('../chunk-WSEQ2I6I.js');
|
|
4
4
|
var chunkO4GS3XPZ_js = require('../chunk-O4GS3XPZ.js');
|
|
5
5
|
var chunkC6OJIHNO_js = require('../chunk-C6OJIHNO.js');
|
|
6
6
|
require('../chunk-ZSTFX7FV.js');
|
|
@@ -68,7 +68,7 @@ function readAvailability(status, body) {
|
|
|
68
68
|
const { consentVersion, maxSessionSeconds } = body;
|
|
69
69
|
const botCheck = readBotCheck(body.botCheck);
|
|
70
70
|
const questions = readQuestions(body.questions);
|
|
71
|
-
if (available !== true || typeof consentVersion !== "string" || !
|
|
71
|
+
if (available !== true || typeof consentVersion !== "string" || !chunkWSEQ2I6I_js.isAvatarVersion(consentVersion) || typeof maxSessionSeconds !== "number" || !Number.isInteger(maxSessionSeconds) || maxSessionSeconds < 1 || botCheck === null || questions === null) {
|
|
72
72
|
return INERT;
|
|
73
73
|
}
|
|
74
74
|
return {
|
|
@@ -160,7 +160,7 @@ function deadline(timeoutMs, outer) {
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
async function requestAvailability(target) {
|
|
163
|
-
if (!
|
|
163
|
+
if (!chunkWSEQ2I6I_js.isAvatarSite(target.site) || !LOCALE.test(target.locale)) return INERT;
|
|
164
164
|
const query = new URLSearchParams({
|
|
165
165
|
site: target.site,
|
|
166
166
|
locale: target.locale
|
|
@@ -737,7 +737,7 @@ var CONSENT = /* @__PURE__ */ new Map([
|
|
|
737
737
|
]
|
|
738
738
|
]);
|
|
739
739
|
function avatarConsentCopy(version, locale) {
|
|
740
|
-
if (!
|
|
740
|
+
if (!chunkWSEQ2I6I_js.isAvatarLiveLocaleReviewed(locale)) return null;
|
|
741
741
|
return CONSENT.get(version)?.[locale] ?? null;
|
|
742
742
|
}
|
|
743
743
|
|
|
@@ -859,6 +859,8 @@ var PRIVACY_HREF = "/privacy#live-avatar";
|
|
|
859
859
|
var SPEAKER_WAIT_MS = 3e4;
|
|
860
860
|
var END_GRACE_MS = 1e4;
|
|
861
861
|
var CLOCK_TOLERANCE_MS = 5e3;
|
|
862
|
+
var UNHEARD_GRACE_MS = 5e3;
|
|
863
|
+
var PLAYOUT_TAIL_MS = 5e3;
|
|
862
864
|
var SERVICE_MAX_AGE_MS = 15 * 6e4;
|
|
863
865
|
var ANSWER_OFFER_MS = SERVICE_MAX_AGE_MS - 6e4;
|
|
864
866
|
var MAX_TIMER_MS = 2147483647;
|
|
@@ -868,6 +870,12 @@ var USED_ON_KEY = "boff.explore.avatarUsedOn";
|
|
|
868
870
|
var PRIMARY = '[data-avatar-primary], [data-boff-explore="panel-continue-chat"]';
|
|
869
871
|
var IDLE = { name: "idle" };
|
|
870
872
|
var INERT2 = { name: "inert" };
|
|
873
|
+
function stopAttending(handle) {
|
|
874
|
+
if (handle.unheard !== null) clearTimeout(handle.unheard);
|
|
875
|
+
if (handle.tail !== null) clearTimeout(handle.tail);
|
|
876
|
+
handle.unheard = null;
|
|
877
|
+
handle.tail = null;
|
|
878
|
+
}
|
|
871
879
|
var REFUSAL_TEXT = {
|
|
872
880
|
busy: "busy",
|
|
873
881
|
limits: "limits",
|
|
@@ -1151,6 +1159,7 @@ function AvatarLiveView({
|
|
|
1151
1159
|
const handle = sessionRef.current;
|
|
1152
1160
|
if (!handle || handle.done) return;
|
|
1153
1161
|
handle.done = true;
|
|
1162
|
+
stopAttending(handle);
|
|
1154
1163
|
sessionRef.current = null;
|
|
1155
1164
|
if (reason !== null) {
|
|
1156
1165
|
sendEnd({
|
|
@@ -1180,6 +1189,33 @@ function AvatarLiveView({
|
|
|
1180
1189
|
}
|
|
1181
1190
|
go({ name: "live", session: current.session });
|
|
1182
1191
|
}, [go]);
|
|
1192
|
+
const attend = react.useCallback(() => {
|
|
1193
|
+
const handle = sessionRef.current;
|
|
1194
|
+
if (!handle || handle.done || latest.current.live.endUnattended !== true) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const current = phaseRef.current.name;
|
|
1198
|
+
if (current !== "pending" && current !== "live") return;
|
|
1199
|
+
const away = !inSight();
|
|
1200
|
+
if (away && handle.playedOut && current === "live") {
|
|
1201
|
+
finish("finished", "user");
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (!(away && handle.soundHeld && handle.voiceArrived)) {
|
|
1205
|
+
if (handle.unheard !== null) {
|
|
1206
|
+
clearTimeout(handle.unheard);
|
|
1207
|
+
handle.unheard = null;
|
|
1208
|
+
}
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (handle.unheard !== null) return;
|
|
1212
|
+
handle.unheard = setTimeout(() => {
|
|
1213
|
+
handle.unheard = null;
|
|
1214
|
+
if (sessionRef.current === handle && !handle.done && latest.current.live.endUnattended === true && handle.soundHeld && handle.voiceArrived && !inSight()) {
|
|
1215
|
+
finish("interrupted", "user");
|
|
1216
|
+
}
|
|
1217
|
+
}, UNHEARD_GRACE_MS);
|
|
1218
|
+
}, [finish, inSight]);
|
|
1183
1219
|
const cancelStart = react.useCallback((reason) => {
|
|
1184
1220
|
const run = startRef.current;
|
|
1185
1221
|
if (!run) return;
|
|
@@ -1207,7 +1243,7 @@ function AvatarLiveView({
|
|
|
1207
1243
|
go(INERT2);
|
|
1208
1244
|
return;
|
|
1209
1245
|
}
|
|
1210
|
-
if (result.offer.botCheck === "turnstile" &&
|
|
1246
|
+
if (result.offer.botCheck === "turnstile" && chunkWSEQ2I6I_js.turnstileSiteKeyOf(live2) === null) {
|
|
1211
1247
|
setMode("suggested");
|
|
1212
1248
|
go(INERT2);
|
|
1213
1249
|
return;
|
|
@@ -1256,6 +1292,11 @@ function AvatarLiveView({
|
|
|
1256
1292
|
joined: null,
|
|
1257
1293
|
endsAt,
|
|
1258
1294
|
speakerArrived: false,
|
|
1295
|
+
voiceArrived: false,
|
|
1296
|
+
soundHeld: false,
|
|
1297
|
+
playedOut: false,
|
|
1298
|
+
unheard: null,
|
|
1299
|
+
tail: null,
|
|
1259
1300
|
done: false
|
|
1260
1301
|
};
|
|
1261
1302
|
setBlocked(false);
|
|
@@ -1335,7 +1376,7 @@ function AvatarLiveView({
|
|
|
1335
1376
|
return;
|
|
1336
1377
|
}
|
|
1337
1378
|
const refusedFrom = exchangeAtStart.offerUntil - ANSWER_OFFER_MS + SERVICE_MAX_AGE_MS;
|
|
1338
|
-
const siteKey = offer.botCheck === "turnstile" ?
|
|
1379
|
+
const siteKey = offer.botCheck === "turnstile" ? chunkWSEQ2I6I_js.turnstileSiteKeyOf(live2) : null;
|
|
1339
1380
|
if (offer.botCheck === "turnstile" && (siteKey === null || !canBindBotCheck())) {
|
|
1340
1381
|
go(INERT2);
|
|
1341
1382
|
return;
|
|
@@ -1581,6 +1622,9 @@ function AvatarLiveView({
|
|
|
1581
1622
|
check();
|
|
1582
1623
|
}
|
|
1583
1624
|
}, [showing, behindChat, check, leave, offeredNow]);
|
|
1625
|
+
react.useEffect(() => {
|
|
1626
|
+
attend();
|
|
1627
|
+
}, [showing, attend]);
|
|
1584
1628
|
react.useEffect(() => {
|
|
1585
1629
|
if (askedLocale.current === locale) return;
|
|
1586
1630
|
askedLocale.current = locale;
|
|
@@ -1667,12 +1711,29 @@ function AvatarLiveView({
|
|
|
1667
1711
|
const wasLive = phaseRef.current.name === "live";
|
|
1668
1712
|
switch (event.type) {
|
|
1669
1713
|
case "speaker-track":
|
|
1670
|
-
if (event.kind !== "video")
|
|
1714
|
+
if (event.kind !== "video") {
|
|
1715
|
+
handle.voiceArrived = true;
|
|
1716
|
+
attend();
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1671
1719
|
handle.speakerArrived = true;
|
|
1672
1720
|
if (!inSight()) goLive();
|
|
1673
1721
|
return;
|
|
1674
1722
|
case "playback-blocked":
|
|
1675
1723
|
setBlocked(event.blocked);
|
|
1724
|
+
handle.soundHeld = event.sound ?? event.blocked;
|
|
1725
|
+
attend();
|
|
1726
|
+
return;
|
|
1727
|
+
case "speech-ended":
|
|
1728
|
+
if (latest.current.live.endUnattended !== true || handle.tail !== null || handle.playedOut) {
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
handle.tail = setTimeout(() => {
|
|
1732
|
+
handle.tail = null;
|
|
1733
|
+
if (sessionRef.current !== handle || handle.done) return;
|
|
1734
|
+
handle.playedOut = true;
|
|
1735
|
+
attend();
|
|
1736
|
+
}, PLAYOUT_TAIL_MS);
|
|
1676
1737
|
return;
|
|
1677
1738
|
case "speaker-left":
|
|
1678
1739
|
finish(
|
|
@@ -1686,7 +1747,7 @@ function AvatarLiveView({
|
|
|
1686
1747
|
return;
|
|
1687
1748
|
}
|
|
1688
1749
|
},
|
|
1689
|
-
[finish, goLive, inSight]
|
|
1750
|
+
[attend, finish, goLive, inSight]
|
|
1690
1751
|
);
|
|
1691
1752
|
react.useEffect(() => {
|
|
1692
1753
|
if (phase.name !== "pending") return;
|
|
@@ -1773,10 +1834,12 @@ function AvatarLiveView({
|
|
|
1773
1834
|
const onVisibility = () => {
|
|
1774
1835
|
if (document.visibilityState === "hidden") {
|
|
1775
1836
|
leave("hidden");
|
|
1837
|
+
attend();
|
|
1776
1838
|
return;
|
|
1777
1839
|
}
|
|
1778
1840
|
const current = phaseRef.current.name;
|
|
1779
1841
|
if (current === "pending" || current === "live") setNow(Date.now());
|
|
1842
|
+
attend();
|
|
1780
1843
|
};
|
|
1781
1844
|
window.addEventListener("pagehide", onPageHide);
|
|
1782
1845
|
document.addEventListener("visibilitychange", onVisibility);
|
|
@@ -1784,7 +1847,7 @@ function AvatarLiveView({
|
|
|
1784
1847
|
window.removeEventListener("pagehide", onPageHide);
|
|
1785
1848
|
document.removeEventListener("visibilitychange", onVisibility);
|
|
1786
1849
|
};
|
|
1787
|
-
}, [leave]);
|
|
1850
|
+
}, [attend, leave]);
|
|
1788
1851
|
react.useEffect(
|
|
1789
1852
|
() => () => {
|
|
1790
1853
|
const controller = checkRef.current;
|
|
@@ -1803,6 +1866,7 @@ function AvatarLiveView({
|
|
|
1803
1866
|
sessionRef.current = null;
|
|
1804
1867
|
if (handle && !handle.done) {
|
|
1805
1868
|
handle.done = true;
|
|
1869
|
+
stopAttending(handle);
|
|
1806
1870
|
sendEnd({
|
|
1807
1871
|
base: handle.base,
|
|
1808
1872
|
headers: handle.headers,
|