virtual-human-cf 1.0.4 → 1.0.6

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
@@ -113,11 +113,20 @@ const onEnded = () => {
113
113
  | 事件名 | 说明 | 回调参数 |
114
114
  |--------|------|----------|
115
115
  | connected | WebSocket 连接成功 | - |
116
- | highlight | 高亮大屏某区域 | 载荷数据 |
117
- | showDialog | 显示弹窗 | 载荷数据 |
116
+ | eventNotifaction | 自定义事件接收器 | 载荷数据: {
117
+
118
+ "type": "send_event",
119
+ // 自定义事件名称
120
+ "event": "heightlight",
121
+ // 自定义事件参数
122
+ "params": {
123
+ "domid": "wrap-1"
124
+ }
125
+ } |
118
126
  | end | 数字人对话结束 | 载荷数据 |
119
127
  | pause | 暂停播放 | 载荷数据 |
120
128
  | error | 错误事件 | 错误信息 |
129
+ | playComplete | 播放完成,数字人对话结束,关闭数字人 | - |
121
130
 
122
131
  #### 示例
123
132
 
@@ -127,8 +136,8 @@ const onEnded = () => {
127
136
  :screen-client-id="clientId"
128
137
  :ws-url="websocketUrl"
129
138
  @connected="onConnected"
130
- @highlight="onHighlight"
131
- @showDialog="onShowDialog"
139
+ @eventNotifaction="onEventNotifaction"
140
+ @playComplete="onPlayComplete"
132
141
  @end="onEnd"
133
142
  @error="onError"
134
143
  >
@@ -146,12 +155,12 @@ const onConnected = () => {
146
155
  console.log('WebSocket 连接成功');
147
156
  };
148
157
 
149
- const onHighlight = (payload) => {
150
- console.log('高亮事件:', payload);
158
+ const onEventNotifaction = (payload) => {
159
+ console.log('自定义事件接收:', payload);
151
160
  };
152
161
 
153
- const onShowDialog = (payload) => {
154
- console.log('显示弹窗:', payload);
162
+ const onPlayComplete = () => {
163
+ console.log('播放完成');
155
164
  };
156
165
 
157
166
  const onEnd = (payload) => {
@@ -173,23 +182,25 @@ const onError = (error) => {
173
182
  <div class="container">
174
183
  <VirtualHumanPersona
175
184
  :video-src="videoUrl"
185
+ :visible="personaVisible"
176
186
  :is-playing="isPlaying"
177
187
  :muted="true"
178
188
  :is-dark="isDarkMode"
179
- :screen-client-id="clientId"
189
+ :screen-client-id="activeScreenClientId"
180
190
  :ws-url="websocketUrl"
181
- @update:isPlaying="(val) => isPlaying = val"
191
+ @update:isPlaying="onPersonaPlayingUpdate"
192
+ @update:visible="onPersonaVisibleUpdate"
182
193
  @ended="onVideoEnded"
183
194
  />
184
195
 
185
196
  <VirtualHumanEventAdapter
186
- :screen-client-id="clientId"
197
+ :screen-client-id="activeScreenClientId"
187
198
  :ws-url="websocketUrl"
188
199
  @connected="onConnected"
189
- @highlight="onHighlight"
190
- @showDialog="onShowDialog"
200
+ @eventNotifaction="onEventNotifaction"
191
201
  @end="onEnd"
192
202
  @error="onError"
203
+ @playComplete="onPlayComplete"
193
204
  />
194
205
  </div>
195
206
  </template>
@@ -201,7 +212,8 @@ import { VirtualHumanPersona, VirtualHumanEventAdapter } from 'virtual-human-cf'
201
212
  const videoUrl = ref('/path/to/digital-human-video.mp4');
202
213
  const isPlaying = ref(false);
203
214
  const isDarkMode = ref(false);
204
- const clientId = ref('screen-123');
215
+ const personaVisible = ref(true);
216
+ const activeScreenClientId = ref('screen-123');
205
217
  const websocketUrl = ref('ws://localhost:8080/ws');
206
218
 
207
219
  const onConnected = () => {
@@ -212,19 +224,22 @@ const onVideoEnded = () => {
212
224
  console.log('视频播放结束');
213
225
  };
214
226
 
215
- const onHighlight = (payload) => {
216
- console.log('收到高亮指令:', payload);
217
- };
218
-
219
- const onShowDialog = (payload) => {
220
- console.log('显示对话框:', payload);
221
- };
222
227
 
223
228
  const onEnd = (payload) => {
224
229
  console.log('数字人交互结束:', payload);
225
230
  isPlaying.value = false;
226
231
  };
227
232
 
233
+ const onEventNotifaction = (payload) => {
234
+ console.log('自定义事件接收:', payload);
235
+ };
236
+
237
+ const onPlayComplete = () => {
238
+ console.log('播放完成');
239
+ // 关闭数字人
240
+ personaVisible.value = false;
241
+ }
242
+
228
243
  const onError = (error) => {
229
244
  console.error('发生错误:', error);
230
245
  };
@@ -13,10 +13,10 @@ declare const __VLS_component: import('vue').DefineComponent<import('vue').Extra
13
13
  }>, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
14
14
  error: (...args: any[]) => void;
15
15
  pause: (...args: any[]) => void;
16
- highlight: (...args: any[]) => void;
17
- showDialog: (...args: any[]) => void;
16
+ eventNotifaction: (...args: any[]) => void;
18
17
  end: (...args: any[]) => void;
19
18
  connected: (...args: any[]) => void;
19
+ playComplete: (...args: any[]) => void;
20
20
  }, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
21
21
  screenClientId: {
22
22
  type: StringConstructor;
@@ -29,10 +29,10 @@ declare const __VLS_component: import('vue').DefineComponent<import('vue').Extra
29
29
  }>> & Readonly<{
30
30
  onError?: ((...args: any[]) => any) | undefined;
31
31
  onPause?: ((...args: any[]) => any) | undefined;
32
- onHighlight?: ((...args: any[]) => any) | undefined;
33
- onShowDialog?: ((...args: any[]) => any) | undefined;
32
+ onEventNotifaction?: ((...args: any[]) => any) | undefined;
34
33
  onEnd?: ((...args: any[]) => any) | undefined;
35
34
  onConnected?: ((...args: any[]) => any) | undefined;
35
+ onPlayComplete?: ((...args: any[]) => any) | undefined;
36
36
  }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
37
37
  declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, ReturnType<typeof __VLS_template>>;
38
38
  export default _default;
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .fade-enter-active[data-v-19d39d2e],.fade-leave-active[data-v-19d39d2e]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-19d39d2e],.fade-leave-to[data-v-19d39d2e]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-19d39d2e]{position:fixed;left:16px;bottom:16px;width:400px;height:500px;z-index:2147483647;overflow:visible}.video-wrapper[data-v-19d39d2e]{position:relative;width:400px;height:500px;border-radius:1rem;overflow:hidden;box-shadow:0 10px 25px #0000001a;background:#fff;transition:background-color .3s ease,box-shadow .3s ease}.virtual-human-container.is-dark .video-wrapper[data-v-19d39d2e]{background:#1f2937;box-shadow:0 10px 25px #00000080}.persona-video[data-v-19d39d2e]{width:100%;height:100%;object-fit:cover;transform-origin:center;transition:transform .1s ease-out}.resize-handle[data-v-19d39d2e]{position:absolute;width:20px;height:20px;background:#00000080;border:2px solid rgba(255,255,255,.8);border-radius:4px;cursor:pointer;z-index:20;opacity:0;transition:opacity .3s ease,background .2s,border-color .2s}.video-wrapper:hover .resize-handle[data-v-19d39d2e]{opacity:1}.resize-handle[data-v-19d39d2e]:hover{background:#000000b3;border-color:#fff}.resize-handle.top-left[data-v-19d39d2e]{top:10px;left:10px;cursor:nwse-resize}.resize-handle.top-right[data-v-19d39d2e]{top:10px;right:10px;cursor:nesw-resize}.resize-handle.bottom-left[data-v-19d39d2e]{bottom:10px;left:10px;cursor:nesw-resize}.resize-handle.bottom-right[data-v-19d39d2e]{bottom:10px;right:10px;cursor:nwse-resize}.overlay[data-v-19d39d2e]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-19d39d2e]{display:flex;align-items:center;gap:.5rem;padding:.25rem .75rem;border-radius:9999px;font-size:.75rem;font-weight:500;color:#fff;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.status-badge.paused[data-v-19d39d2e]{background:#ef4444cc}.status-badge.playing[data-v-19d39d2e]{background:#22c55ecc}.dot[data-v-19d39d2e]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-19d39d2e{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-19d39d2e]{animation:pulse-19d39d2e 2s cubic-bezier(.4,0,.6,1) infinite}
1
+ .fade-enter-active[data-v-6c9e435e],.fade-leave-active[data-v-6c9e435e]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-6c9e435e],.fade-leave-to[data-v-6c9e435e]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-6c9e435e]{position:fixed;z-index:2147483647;overflow:visible}.video-wrapper[data-v-6c9e435e]{position:relative;width:100%;height:100%;border-radius:1rem;overflow:hidden;box-shadow:0 10px 25px #0000001a;background:#fff;border:2px solid transparent;transition:background-color .3s ease,box-shadow .3s ease,border-color .3s ease}.video-wrapper[data-v-6c9e435e]:hover{border-color:#409eff}.virtual-human-container.is-dark .video-wrapper[data-v-6c9e435e]{background:#1f2937;box-shadow:0 10px 25px #00000080}.persona-video[data-v-6c9e435e]{width:100%;height:100%;object-fit:cover;transform-origin:center;transition:transform .1s ease-out}.resize-handle[data-v-6c9e435e]{position:absolute;width:20px;height:20px;background:#00000080;border:2px solid rgba(255,255,255,.8);border-radius:4px;cursor:pointer;z-index:20;opacity:0;pointer-events:none;transition:opacity .3s ease,background .2s,border-color .2s}.video-wrapper:hover .resize-handle[data-v-6c9e435e]{opacity:1;pointer-events:auto}.resize-handle[data-v-6c9e435e]:hover{background:#000000b3;border-color:#fff}.resize-handle.top-left[data-v-6c9e435e]{top:10px;left:10px;cursor:nwse-resize}.resize-handle.top-right[data-v-6c9e435e]{top:10px;right:10px;cursor:nesw-resize}.resize-handle.bottom-left[data-v-6c9e435e]{bottom:10px;left:10px;cursor:nesw-resize}.resize-handle.bottom-right[data-v-6c9e435e]{bottom:10px;right:10px;cursor:nwse-resize}.overlay[data-v-6c9e435e]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-6c9e435e]{display:flex;align-items:center;gap:.5rem;padding:.25rem .75rem;border-radius:9999px;font-size:.75rem;font-weight:500;color:#fff;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.status-badge.paused[data-v-6c9e435e]{background:#ef4444cc}.status-badge.playing[data-v-6c9e435e]{background:#22c55ecc}.dot[data-v-6c9e435e]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-6c9e435e{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-6c9e435e]{animation:pulse-6c9e435e 2s cubic-bezier(.4,0,.6,1) infinite}
@@ -1,11 +1,11 @@
1
- import { defineComponent as T, ref as v, watch as z, onMounted as W, onUnmounted as X, openBlock as h, createBlock as D, Transition as F, withCtx as q, createElementBlock as b, normalizeClass as N, createElementVNode as H, normalizeStyle as O, createCommentVNode as I, createTextVNode as L, renderSlot as J } from "vue";
2
- const j = ["src", "muted"], G = { class: "overlay" }, K = {
1
+ import { defineComponent as j, ref as v, watch as X, onMounted as G, onUnmounted as K, openBlock as S, createBlock as Q, Transition as Z, withCtx as _, createElementBlock as E, normalizeStyle as ee, normalizeClass as te, createElementVNode as R, createCommentVNode as T, createTextVNode as J, renderSlot as ne } from "vue";
2
+ const ae = ["src", "muted"], se = { class: "overlay" }, le = {
3
3
  key: 0,
4
4
  class: "status-badge paused"
5
- }, Q = {
5
+ }, oe = {
6
6
  key: 1,
7
7
  class: "status-badge playing"
8
- }, Z = /* @__PURE__ */ T({
8
+ }, re = /* @__PURE__ */ j({
9
9
  __name: "VirtualHumanPersona",
10
10
  props: {
11
11
  // 视频源URL
@@ -46,131 +46,138 @@ const j = ["src", "muted"], G = { class: "overlay" }, K = {
46
46
  }
47
47
  },
48
48
  emits: ["update:isPlaying", "ended", "update:visible"],
49
- setup(i, { emit: P }) {
50
- const t = i, s = P, l = v(null), k = v(null), n = v(null), m = v(!1), V = v(1), E = v(!1), p = v(null), C = v(0), x = v(0), o = v(1), u = () => {
49
+ setup(c, { emit: W }) {
50
+ const t = c, i = W, r = v(null), Y = v(null), n = v(null), p = v(!1), y = v(400), g = v(500), w = v(16), z = v(16), I = v(!1), h = v(null), M = v(0), A = v(0), k = v(0), s = v(0), l = v(0), u = v(0), b = () => {
51
51
  if (n.value && n.value.close(), !(!t.wsUrl || !t.screenClientId))
52
52
  try {
53
- const r = new URL(t.wsUrl);
54
- r.searchParams.append("sessionId", t.screenClientId + "-persona"), n.value = new WebSocket(r.toString()), n.value.onopen = () => {
55
- m.value = !0, console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`);
53
+ const o = new URL(t.wsUrl);
54
+ o.searchParams.append("sessionId", t.screenClientId + "-persona"), n.value = new WebSocket(o.toString()), n.value.onopen = () => {
55
+ p.value = !0, console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`);
56
56
  }, n.value.onmessage = (e) => {
57
57
  try {
58
58
  const a = JSON.parse(e.data);
59
- c(a);
59
+ C(a);
60
60
  } catch (a) {
61
61
  console.error("[VirtualHumanPersona] Failed to parse message:", e.data, a);
62
62
  }
63
63
  }, n.value.onerror = (e) => {
64
64
  console.error("[VirtualHumanPersona] WebSocket error:", e);
65
65
  }, n.value.onclose = () => {
66
- m.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
66
+ p.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
67
67
  };
68
- } catch (r) {
69
- console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", r);
68
+ } catch (o) {
69
+ console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", o);
70
70
  }
71
- }, c = (r) => {
72
- const { type: e, action: a } = r;
73
- e === "control" && (a === "play" || a === "resume" ? (s("update:isPlaying", !0), s("update:visible", !0)) : a === "pause" ? s("update:isPlaying", !1) : a === "stop" && (s("update:isPlaying", !1), s("update:visible", !1), l.value && (l.value.currentTime = 0)));
71
+ }, C = (o) => {
72
+ const { type: e, action: a } = o;
73
+ e === "control" && (a === "play" || a === "resume" ? (i("update:isPlaying", !0), i("update:visible", !0)) : a === "pause" ? i("update:isPlaying", !1) : a === "stop" && (i("update:isPlaying", !1), i("update:visible", !1), r.value && (r.value.currentTime = 0)));
74
74
  };
75
- z(() => t.screenClientId, () => {
76
- t.screenClientId && t.wsUrl && u();
77
- }), z(() => t.wsUrl, () => {
78
- t.screenClientId && t.wsUrl && u();
79
- }), z(() => t.isPlaying, (r) => {
80
- l.value && (r ? l.value.play().catch((e) => console.error("Video play failed:", e)) : l.value.pause());
75
+ X(() => t.screenClientId, () => {
76
+ t.screenClientId && t.wsUrl && b();
77
+ }), X(() => t.wsUrl, () => {
78
+ t.screenClientId && t.wsUrl && b();
79
+ }), X(() => t.isPlaying, (o) => {
80
+ r.value && (o ? r.value.play().catch((e) => console.error("Video play failed:", e)) : r.value.pause());
81
81
  });
82
- const w = () => {
83
- t.isPlaying || s("update:isPlaying", !0);
84
- }, g = () => {
85
- t.isPlaying && s("update:isPlaying", !1);
86
- }, A = () => {
87
- s("update:isPlaying", !1), s("ended");
88
- }, f = (r, e) => {
89
- E.value = !0, p.value = r, o.value = V.value;
90
- const a = "clientX" in e ? e.clientX : e.touches[0].clientX, S = "clientY" in e ? e.clientY : e.touches[0].clientY;
91
- C.value = a, x.value = S, document.addEventListener("mousemove", y), document.addEventListener("mouseup", d), document.addEventListener("touchmove", y), document.addEventListener("touchend", d);
92
- }, y = (r) => {
93
- if (!E.value || !k.value) return;
94
- const e = "clientX" in r ? r.clientX : r.touches[0].clientX, a = "clientY" in r ? r.clientY : r.touches[0].clientY, S = e - C.value, M = a - x.value, B = k.value.getBoundingClientRect(), Y = 0.5, $ = 2;
95
- let U = o.value;
96
- const R = Math.min(B.width, B.height);
97
- p.value === "bottom-right" ? U = o.value + (S + M) / R : p.value === "bottom-left" ? U = o.value + (-S + M) / R : p.value === "top-right" ? U = o.value + (S - M) / R : p.value === "top-left" && (U = o.value + (-S - M) / R), V.value = Math.max(Y, Math.min($, U));
98
- }, d = () => {
99
- E.value = !1, p.value = null, document.removeEventListener("mousemove", y), document.removeEventListener("mouseup", d), document.removeEventListener("touchmove", y), document.removeEventListener("touchend", d);
82
+ const B = () => {
83
+ t.isPlaying || i("update:isPlaying", !0);
84
+ }, L = () => {
85
+ t.isPlaying && i("update:isPlaying", !1);
86
+ }, U = () => {
87
+ i("update:isPlaying", !1), i("ended");
88
+ }, d = (o, e) => {
89
+ e.preventDefault(), I.value = !0, h.value = o;
90
+ const a = "clientX" in e ? e.clientX : e.touches[0].clientX, P = "clientY" in e ? e.clientY : e.touches[0].clientY;
91
+ M.value = a, A.value = P, k.value = y.value, s.value = g.value, l.value = w.value, u.value = z.value, document.addEventListener("mousemove", $), document.addEventListener("mouseup", N), document.addEventListener("touchmove", $, { passive: !1 }), document.addEventListener("touchend", N);
92
+ }, $ = (o) => {
93
+ if (!I.value) return;
94
+ o.preventDefault();
95
+ const e = "clientX" in o ? o.clientX : o.touches[0].clientX, a = "clientY" in o ? o.clientY : o.touches[0].clientY, P = e - M.value, x = a - A.value, D = 200, F = 250, q = 800, O = 1e3;
96
+ let f = k.value, m = s.value, H = l.value, V = u.value;
97
+ h.value === "bottom-right" ? (f = k.value + P, V = u.value - x, m = s.value + x) : h.value === "bottom-left" ? (H = l.value + P, f = k.value - P, V = u.value - x, m = s.value + x) : h.value === "top-right" ? (f = k.value + P, m = s.value - x) : h.value === "top-left" && (H = l.value + P, f = k.value - P, m = s.value - x), f < D && (H !== l.value && (H -= D - f), f = D), m < F && (V !== u.value && (V -= F - m), m = F), f > q && (H !== l.value && (H += f - q), f = q), m > O && (V !== u.value && (V += m - O), m = O), y.value = f, g.value = m, w.value = H, z.value = V;
98
+ }, N = () => {
99
+ I.value = !1, h.value = null, document.removeEventListener("mousemove", $), document.removeEventListener("mouseup", N), document.removeEventListener("touchmove", $), document.removeEventListener("touchend", N);
100
100
  };
101
- return W(() => {
102
- t.isPlaying && l.value && l.value.play().catch((r) => console.error("Video play failed:", r)), t.screenClientId && t.wsUrl && u();
103
- }), X(() => {
101
+ return G(() => {
102
+ t.isPlaying && r.value && r.value.play().catch((o) => console.error("Video play failed:", o)), t.screenClientId && t.wsUrl && b();
103
+ }), K(() => {
104
104
  n.value && n.value.close();
105
- }), (r, e) => (h(), D(F, { name: "fade" }, {
106
- default: q(() => [
107
- i.visible ? (h(), b("div", {
105
+ }), (o, e) => (S(), Q(Z, { name: "fade" }, {
106
+ default: _(() => [
107
+ c.visible ? (S(), E("div", {
108
108
  key: 0,
109
- class: N(["virtual-human-container", { "is-dark": i.isDark }])
109
+ class: te(["virtual-human-container", { "is-dark": c.isDark }]),
110
+ style: ee({
111
+ width: y.value + "px",
112
+ height: g.value + "px",
113
+ left: w.value + "px",
114
+ bottom: z.value + "px"
115
+ })
110
116
  }, [
111
- H("div", {
117
+ R("div", {
112
118
  class: "video-wrapper",
113
119
  ref_key: "wrapperRef",
114
- ref: k,
115
- style: O({ transform: `scale(${V.value})`, transformOrigin: "left bottom" })
120
+ ref: Y
116
121
  }, [
117
- H("video", {
122
+ R("video", {
118
123
  ref_key: "videoRef",
119
- ref: l,
120
- src: i.videoSrc,
124
+ ref: r,
125
+ src: c.videoSrc,
121
126
  class: "persona-video",
122
- muted: i.muted,
127
+ muted: c.muted,
123
128
  playsinline: "",
124
129
  loop: "",
125
- onPlay: w,
126
- onPause: g,
127
- onEnded: A
128
- }, null, 40, j),
129
- i.visible ? (h(), b("div", {
130
+ autoPlay: "",
131
+ disablePictureInPicture: "false",
132
+ onPlay: B,
133
+ onPause: L,
134
+ onEnded: U
135
+ }, null, 40, ae),
136
+ c.visible ? (S(), E("div", {
130
137
  key: 0,
131
138
  class: "resize-handle top-left",
132
- onMousedown: e[0] || (e[0] = (a) => f("top-left", a)),
133
- onTouchstart: e[1] || (e[1] = (a) => f("top-left", a))
134
- }, null, 32)) : I("", !0),
135
- i.visible ? (h(), b("div", {
139
+ onMousedown: e[0] || (e[0] = (a) => d("top-left", a)),
140
+ onTouchstart: e[1] || (e[1] = (a) => d("top-left", a))
141
+ }, null, 32)) : T("", !0),
142
+ c.visible ? (S(), E("div", {
136
143
  key: 1,
137
144
  class: "resize-handle top-right",
138
- onMousedown: e[2] || (e[2] = (a) => f("top-right", a)),
139
- onTouchstart: e[3] || (e[3] = (a) => f("top-right", a))
140
- }, null, 32)) : I("", !0),
141
- i.visible ? (h(), b("div", {
145
+ onMousedown: e[2] || (e[2] = (a) => d("top-right", a)),
146
+ onTouchstart: e[3] || (e[3] = (a) => d("top-right", a))
147
+ }, null, 32)) : T("", !0),
148
+ c.visible ? (S(), E("div", {
142
149
  key: 2,
143
150
  class: "resize-handle bottom-left",
144
- onMousedown: e[4] || (e[4] = (a) => f("bottom-left", a)),
145
- onTouchstart: e[5] || (e[5] = (a) => f("bottom-left", a))
146
- }, null, 32)) : I("", !0),
147
- i.visible ? (h(), b("div", {
151
+ onMousedown: e[4] || (e[4] = (a) => d("bottom-left", a)),
152
+ onTouchstart: e[5] || (e[5] = (a) => d("bottom-left", a))
153
+ }, null, 32)) : T("", !0),
154
+ c.visible ? (S(), E("div", {
148
155
  key: 3,
149
156
  class: "resize-handle bottom-right",
150
- onMousedown: e[6] || (e[6] = (a) => f("bottom-right", a)),
151
- onTouchstart: e[7] || (e[7] = (a) => f("bottom-right", a))
152
- }, null, 32)) : I("", !0),
153
- H("div", G, [
154
- i.isPlaying ? (h(), b("div", Q, [...e[9] || (e[9] = [
155
- H("span", { class: "dot animate-pulse" }, null, -1),
156
- L(" 播放中 ", -1)
157
- ])])) : (h(), b("div", K, [...e[8] || (e[8] = [
158
- H("span", { class: "dot" }, null, -1),
159
- L(" 暂停中 ", -1)
157
+ onMousedown: e[6] || (e[6] = (a) => d("bottom-right", a)),
158
+ onTouchstart: e[7] || (e[7] = (a) => d("bottom-right", a))
159
+ }, null, 32)) : T("", !0),
160
+ R("div", se, [
161
+ c.isPlaying ? (S(), E("div", oe, [...e[9] || (e[9] = [
162
+ R("span", { class: "dot animate-pulse" }, null, -1),
163
+ J(" 播放中 ", -1)
164
+ ])])) : (S(), E("div", le, [...e[8] || (e[8] = [
165
+ R("span", { class: "dot" }, null, -1),
166
+ J(" 暂停中 ", -1)
160
167
  ])]))
161
168
  ])
162
- ], 4)
163
- ], 2)) : I("", !0)
169
+ ], 512)
170
+ ], 6)) : T("", !0)
164
171
  ]),
165
172
  _: 1
166
173
  }));
167
174
  }
168
- }), _ = (i, P) => {
169
- const t = i.__vccOpts || i;
170
- for (const [s, l] of P)
171
- t[s] = l;
175
+ }), ie = (c, W) => {
176
+ const t = c.__vccOpts || c;
177
+ for (const [i, r] of W)
178
+ t[i] = r;
172
179
  return t;
173
- }, ee = /* @__PURE__ */ _(Z, [["__scopeId", "data-v-19d39d2e"]]), te = /* @__PURE__ */ T({
180
+ }, ue = /* @__PURE__ */ ie(re, [["__scopeId", "data-v-6c9e435e"]]), ce = /* @__PURE__ */ j({
174
181
  __name: "VirtualHumanEventAdapter",
175
182
  props: {
176
183
  // 屏幕客户端ID
@@ -184,113 +191,115 @@ const j = ["src", "muted"], G = { class: "overlay" }, K = {
184
191
  required: !0
185
192
  }
186
193
  },
187
- emits: ["highlight", "showDialog", "end", "pause", "connected", "error"],
188
- setup(i, { emit: P }) {
189
- const t = i, s = P, l = v(null), k = v(!1);
190
- let n = null, m = 0;
191
- const V = () => {
194
+ emits: ["eventNotifaction", "end", "pause", "connected", "error", "playComplete"],
195
+ setup(c, { emit: W }) {
196
+ const t = c, i = W, r = v(null), Y = v(!1);
197
+ let n = null, p = 0, y = !1, g = 0, w = !1;
198
+ const z = () => {
192
199
  n || (n = new (window.AudioContext || window.webkitAudioContext)({
193
200
  sampleRate: 24e3
194
- })), n.state === "suspended" && n.resume();
195
- }, E = (o) => {
196
- if (V(), !!n)
201
+ })), n.state === "suspended" && !w && n.resume();
202
+ }, I = () => {
203
+ y && g === 0 && (i("playComplete", t.screenClientId), y = !1);
204
+ }, h = (s) => {
205
+ if (z(), !!n)
197
206
  try {
198
- const u = window.atob(o), c = u.length, w = new Uint8Array(c);
199
- for (let d = 0; d < c; d++)
200
- w[d] = u.charCodeAt(d);
201
- const g = new Int16Array(w.buffer), A = new Float32Array(g.length);
202
- for (let d = 0; d < g.length; d++)
203
- A[d] = g[d] / 32768;
204
- const f = n.createBuffer(1, A.length, 24e3);
205
- f.getChannelData(0).set(A);
206
- const y = n.createBufferSource();
207
- y.buffer = f, y.connect(n.destination), m < n.currentTime && (m = n.currentTime), y.start(m), m += f.duration;
208
- } catch (u) {
209
- console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:", u);
210
- }
211
- }, p = (o) => {
212
- if (n)
213
- switch (o) {
214
- case "play":
215
- case "resume":
216
- n.state === "suspended" && n.resume();
217
- break;
218
- case "pause":
219
- n.state === "running" && n.suspend();
220
- break;
221
- case "stop":
222
- n.close(), n = null, m = 0;
223
- break;
224
- default:
225
- console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${o}`);
207
+ const l = window.atob(s), u = l.length, b = new Uint8Array(u);
208
+ for (let d = 0; d < u; d++)
209
+ b[d] = l.charCodeAt(d);
210
+ const C = new Int16Array(b.buffer), B = new Float32Array(C.length);
211
+ for (let d = 0; d < C.length; d++)
212
+ B[d] = C[d] / 32768;
213
+ const L = n.createBuffer(1, B.length, 24e3);
214
+ L.getChannelData(0).set(B);
215
+ const U = n.createBufferSource();
216
+ U.buffer = L, U.connect(n.destination), p < n.currentTime && (p = n.currentTime), U.start(p), p += L.duration, g++, U.onended = () => {
217
+ g--, I();
218
+ };
219
+ } catch (l) {
220
+ console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:", l);
226
221
  }
227
- }, C = () => {
228
- l.value && l.value.close();
222
+ }, M = (s) => {
223
+ switch (s) {
224
+ case "play":
225
+ y = !1, g = 0, p = 0, w = !1, n && n.state === "suspended" && n.resume();
226
+ break;
227
+ case "resume":
228
+ w = !1, n && n.state === "suspended" && n.resume();
229
+ break;
230
+ case "pause":
231
+ w = !0, n && n.state === "running" && n.suspend();
232
+ break;
233
+ case "stop":
234
+ w = !1, n && (n.close(), n = null), p = 0, y = !1, g = 0;
235
+ break;
236
+ case "tts_complete":
237
+ y = !0, I();
238
+ break;
239
+ default:
240
+ console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${s}`);
241
+ }
242
+ }, A = () => {
243
+ r.value && r.value.close();
229
244
  try {
230
- const o = new URL(t.wsUrl);
231
- o.searchParams.append("sessionId", t.screenClientId + "-event"), l.value = new WebSocket(o.toString()), l.value.onopen = () => {
232
- k.value = !0, s("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`);
233
- }, l.value.onmessage = (u) => {
245
+ const s = new URL(t.wsUrl);
246
+ s.searchParams.append("sessionId", t.screenClientId + "-event"), r.value = new WebSocket(s.toString()), r.value.onopen = () => {
247
+ Y.value = !0, i("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`);
248
+ }, r.value.onmessage = (l) => {
234
249
  try {
235
- const c = JSON.parse(u.data);
236
- x(c);
237
- } catch (c) {
238
- console.error("[VirtualHumanEventAdapter] Failed to parse message:", u.data, c);
250
+ const u = JSON.parse(l.data);
251
+ k(u);
252
+ } catch (u) {
253
+ console.error("[VirtualHumanEventAdapter] Failed to parse message:", l.data, u);
239
254
  }
240
- }, l.value.onerror = (u) => {
241
- console.error("[VirtualHumanEventAdapter] WebSocket error:", u), s("error", u);
242
- }, l.value.onclose = () => {
243
- k.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
255
+ }, r.value.onerror = (l) => {
256
+ console.error("[VirtualHumanEventAdapter] WebSocket error:", l), i("error", l);
257
+ }, r.value.onclose = () => {
258
+ Y.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
244
259
  };
245
- } catch (o) {
246
- console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", o), s("error", o);
260
+ } catch (s) {
261
+ console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", s), i("error", s);
247
262
  }
248
- }, x = (o) => {
249
- const { type: u, payload: c, action: w } = o;
250
- switch (console.log("msgmsg", o), u) {
263
+ }, k = (s) => {
264
+ const { type: l, payload: u, action: b } = s;
265
+ switch (console.log("msgmsg-002", s), l) {
251
266
  case "audio":
252
- const g = (c == null ? void 0 : c.data) || o.data;
253
- g && E(g);
267
+ const C = (u == null ? void 0 : u.data) || s.data;
268
+ C && h(C);
254
269
  break;
255
- case "dialog_event":
256
- o.event && s(o.event, o.params);
270
+ case "send_event":
271
+ s.event && i("eventNotifaction", s);
257
272
  break;
258
273
  case "control":
259
- w && p(w);
260
- break;
261
- case "highlight":
262
- s("highlight", c);
263
- break;
264
- case "showDialog":
265
- s("showDialog", c);
274
+ b && M(b);
266
275
  break;
267
276
  case "end":
268
- s("end", c);
277
+ i("end", u);
269
278
  break;
270
279
  case "pause":
271
- s("pause", c);
280
+ i("pause", u);
272
281
  break;
273
282
  default:
274
- console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${u}`);
283
+ console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${l}`);
275
284
  }
276
285
  };
277
- return z(() => t.screenClientId, () => {
278
- t.screenClientId && t.wsUrl && C();
279
- }), z(() => t.wsUrl, () => {
280
- t.screenClientId && t.wsUrl && C();
281
- }), W(() => {
282
- t.screenClientId && t.wsUrl && C();
283
- }), X(() => {
284
- l.value && l.value.close(), n && n.close();
285
- }), (o, u) => J(o.$slots, "default");
286
+ return X(() => t.screenClientId, () => {
287
+ t.screenClientId && t.wsUrl && A();
288
+ }), X(() => t.wsUrl, () => {
289
+ t.screenClientId && t.wsUrl && A();
290
+ }), G(() => {
291
+ t.screenClientId && t.wsUrl && A();
292
+ }), K(() => {
293
+ r.value && r.value.close(), n && n.close();
294
+ }), (s, l) => ne(s.$slots, "default");
286
295
  }
287
- }), ne = (i) => {
288
- i.component("VirtualHumanPersona", ee), i.component("VirtualHumanEventAdapter", te);
289
- }, oe = {
290
- install: ne
296
+ }), de = (c) => {
297
+ c.component("VirtualHumanPersona", ue), c.component("VirtualHumanEventAdapter", ce);
298
+ }, fe = {
299
+ install: de
291
300
  };
292
301
  export {
293
- te as VirtualHumanEventAdapter,
294
- ee as VirtualHumanPersona,
295
- oe as default
302
+ ce as VirtualHumanEventAdapter,
303
+ ue as VirtualHumanPersona,
304
+ fe as default
296
305
  };
@@ -1 +1 @@
1
- (function(p,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(p=typeof globalThis<"u"?globalThis:p||self,e(p.VirtualHumanCf={},p.Vue))})(this,function(p,e){"use strict";const z=["src","muted"],N={class:"overlay"},T={key:0,class:"status-badge paused"},R={key:1,class:"status-badge playing"},I=((c,V)=>{const n=c.__vccOpts||c;for(const[s,r]of V)n[s]=r;return n})(e.defineComponent({__name:"VirtualHumanPersona",props:{videoSrc:{type:String,required:!0},visible:{type:Boolean,default:!1},isPlaying:{type:Boolean,default:!1},muted:{type:Boolean,default:!0},isDark:{type:Boolean,default:!1},screenClientId:{type:String,required:!1},wsUrl:{type:String,required:!1}},emits:["update:isPlaying","ended","update:visible"],setup(c,{emit:V}){const n=c,s=V,r=e.ref(null),v=e.ref(null),o=e.ref(null),g=e.ref(!1),E=e.ref(1),S=e.ref(!1),h=e.ref(null),b=e.ref(0),H=e.ref(0),l=e.ref(1),d=()=>{if(o.value&&o.value.close(),!(!n.wsUrl||!n.screenClientId))try{const i=new URL(n.wsUrl);i.searchParams.append("sessionId",n.screenClientId+"-persona"),o.value=new WebSocket(i.toString()),o.value.onopen=()=>{g.value=!0,console.log(`[VirtualHumanPersona] Connected to ${n.wsUrl} for session ${n.screenClientId}-persona`)},o.value.onmessage=t=>{try{const a=JSON.parse(t.data);u(a)}catch(a){console.error("[VirtualHumanPersona] Failed to parse message:",t.data,a)}},o.value.onerror=t=>{console.error("[VirtualHumanPersona] WebSocket error:",t)},o.value.onclose=()=>{g.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(i){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",i)}},u=i=>{const{type:t,action:a}=i;t==="control"&&(a==="play"||a==="resume"?(s("update:isPlaying",!0),s("update:visible",!0)):a==="pause"?s("update:isPlaying",!1):a==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),r.value&&(r.value.currentTime=0)))};e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&d()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&d()}),e.watch(()=>n.isPlaying,i=>{r.value&&(i?r.value.play().catch(t=>console.error("Video play failed:",t)):r.value.pause())});const k=()=>{n.isPlaying||s("update:isPlaying",!0)},y=()=>{n.isPlaying&&s("update:isPlaying",!1)},P=()=>{s("update:isPlaying",!1),s("ended")},m=(i,t)=>{S.value=!0,h.value=i,l.value=E.value;const a="clientX"in t?t.clientX:t.touches[0].clientX,C="clientY"in t?t.clientY:t.touches[0].clientY;b.value=a,H.value=C,document.addEventListener("mousemove",w),document.addEventListener("mouseup",f),document.addEventListener("touchmove",w),document.addEventListener("touchend",f)},w=i=>{if(!S.value||!v.value)return;const t="clientX"in i?i.clientX:i.touches[0].clientX,a="clientY"in i?i.clientY:i.touches[0].clientY,C=t-b.value,U=a-H.value,x=v.value.getBoundingClientRect(),W=.5,X=2;let B=l.value;const A=Math.min(x.width,x.height);h.value==="bottom-right"?B=l.value+(C+U)/A:h.value==="bottom-left"?B=l.value+(-C+U)/A:h.value==="top-right"?B=l.value+(C-U)/A:h.value==="top-left"&&(B=l.value+(-C-U)/A),E.value=Math.max(W,Math.min(X,B))},f=()=>{S.value=!1,h.value=null,document.removeEventListener("mousemove",w),document.removeEventListener("mouseup",f),document.removeEventListener("touchmove",w),document.removeEventListener("touchend",f)};return e.onMounted(()=>{n.isPlaying&&r.value&&r.value.play().catch(i=>console.error("Video play failed:",i)),n.screenClientId&&n.wsUrl&&d()}),e.onUnmounted(()=>{o.value&&o.value.close()}),(i,t)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[c.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":c.isDark}])},[e.createElementVNode("div",{class:"video-wrapper",ref_key:"wrapperRef",ref:v,style:e.normalizeStyle({transform:`scale(${E.value})`,transformOrigin:"left bottom"})},[e.createElementVNode("video",{ref_key:"videoRef",ref:r,src:c.videoSrc,class:"persona-video",muted:c.muted,playsinline:"",loop:"",onPlay:k,onPause:y,onEnded:P},null,40,z),c.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:"resize-handle top-left",onMousedown:t[0]||(t[0]=a=>m("top-left",a)),onTouchstart:t[1]||(t[1]=a=>m("top-left",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:1,class:"resize-handle top-right",onMousedown:t[2]||(t[2]=a=>m("top-right",a)),onTouchstart:t[3]||(t[3]=a=>m("top-right",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:2,class:"resize-handle bottom-left",onMousedown:t[4]||(t[4]=a=>m("bottom-left",a)),onTouchstart:t[5]||(t[5]=a=>m("bottom-left",a))},null,32)):e.createCommentVNode("",!0),c.visible?(e.openBlock(),e.createElementBlock("div",{key:3,class:"resize-handle bottom-right",onMousedown:t[6]||(t[6]=a=>m("bottom-right",a)),onTouchstart:t[7]||(t[7]=a=>m("bottom-right",a))},null,32)):e.createCommentVNode("",!0),e.createElementVNode("div",N,[c.isPlaying?(e.openBlock(),e.createElementBlock("div",R,[...t[9]||(t[9]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",T,[...t[8]||(t[8]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])],4)],2)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-19d39d2e"]]),M=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["highlight","showDialog","end","pause","connected","error"],setup(c,{emit:V}){const n=c,s=V,r=e.ref(null),v=e.ref(!1);let o=null,g=0;const E=()=>{o||(o=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),o.state==="suspended"&&o.resume()},S=l=>{if(E(),!!o)try{const d=window.atob(l),u=d.length,k=new Uint8Array(u);for(let f=0;f<u;f++)k[f]=d.charCodeAt(f);const y=new Int16Array(k.buffer),P=new Float32Array(y.length);for(let f=0;f<y.length;f++)P[f]=y[f]/32768;const m=o.createBuffer(1,P.length,24e3);m.getChannelData(0).set(P);const w=o.createBufferSource();w.buffer=m,w.connect(o.destination),g<o.currentTime&&(g=o.currentTime),w.start(g),g+=m.duration}catch(d){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",d)}},h=l=>{if(o)switch(l){case"play":case"resume":o.state==="suspended"&&o.resume();break;case"pause":o.state==="running"&&o.suspend();break;case"stop":o.close(),o=null,g=0;break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${l}`)}},b=()=>{r.value&&r.value.close();try{const l=new URL(n.wsUrl);l.searchParams.append("sessionId",n.screenClientId+"-event"),r.value=new WebSocket(l.toString()),r.value.onopen=()=>{v.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${n.wsUrl} for session ${n.screenClientId}-event`)},r.value.onmessage=d=>{try{const u=JSON.parse(d.data);H(u)}catch(u){console.error("[VirtualHumanEventAdapter] Failed to parse message:",d.data,u)}},r.value.onerror=d=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",d),s("error",d)},r.value.onclose=()=>{v.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(l){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",l),s("error",l)}},H=l=>{const{type:d,payload:u,action:k}=l;switch(console.log("msgmsg",l),d){case"audio":const y=(u==null?void 0:u.data)||l.data;y&&S(y);break;case"dialog_event":l.event&&s(l.event,l.params);break;case"control":k&&h(k);break;case"highlight":s("highlight",u);break;case"showDialog":s("showDialog",u);break;case"end":s("end",u);break;case"pause":s("pause",u);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${d}`)}};return e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&b()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&b()}),e.onMounted(()=>{n.screenClientId&&n.wsUrl&&b()}),e.onUnmounted(()=>{r.value&&r.value.close(),o&&o.close()}),(l,d)=>e.renderSlot(l.$slots,"default")}}),L={install:c=>{c.component("VirtualHumanPersona",I),c.component("VirtualHumanEventAdapter",M)}};p.VirtualHumanEventAdapter=M,p.VirtualHumanPersona=I,p.default=L,Object.defineProperties(p,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ (function(v,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(v=typeof globalThis<"u"?globalThis:v||self,e(v.VirtualHumanCf={},v.Vue))})(this,function(v,e){"use strict";const F=["src","muted"],O={class:"overlay"},j={key:0,class:"status-badge paused"},J={key:1,class:"status-badge playing"},q=((d,A)=>{const n=d.__vccOpts||d;for(const[c,i]of A)n[c]=i;return n})(e.defineComponent({__name:"VirtualHumanPersona",props:{videoSrc:{type:String,required:!0},visible:{type:Boolean,default:!1},isPlaying:{type:Boolean,default:!1},muted:{type:Boolean,default:!0},isDark:{type:Boolean,default:!1},screenClientId:{type:String,required:!1},wsUrl:{type:String,required:!1}},emits:["update:isPlaying","ended","update:visible"],setup(d,{emit:A}){const n=d,c=A,i=e.ref(null),W=e.ref(null),a=e.ref(null),y=e.ref(!1),g=e.ref(400),w=e.ref(500),h=e.ref(16),x=e.ref(16),H=e.ref(!1),b=e.ref(null),z=e.ref(0),B=e.ref(0),C=e.ref(0),l=e.ref(0),s=e.ref(0),u=e.ref(0),k=()=>{if(a.value&&a.value.close(),!(!n.wsUrl||!n.screenClientId))try{const r=new URL(n.wsUrl);r.searchParams.append("sessionId",n.screenClientId+"-persona"),a.value=new WebSocket(r.toString()),a.value.onopen=()=>{y.value=!0,console.log(`[VirtualHumanPersona] Connected to ${n.wsUrl} for session ${n.screenClientId}-persona`)},a.value.onmessage=t=>{try{const o=JSON.parse(t.data);V(o)}catch(o){console.error("[VirtualHumanPersona] Failed to parse message:",t.data,o)}},a.value.onerror=t=>{console.error("[VirtualHumanPersona] WebSocket error:",t)},a.value.onclose=()=>{y.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(r){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",r)}},V=r=>{const{type:t,action:o}=r;t==="control"&&(o==="play"||o==="resume"?(c("update:isPlaying",!0),c("update:visible",!0)):o==="pause"?c("update:isPlaying",!1):o==="stop"&&(c("update:isPlaying",!1),c("update:visible",!1),i.value&&(i.value.currentTime=0)))};e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&k()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&k()}),e.watch(()=>n.isPlaying,r=>{i.value&&(r?i.value.play().catch(t=>console.error("Video play failed:",t)):i.value.pause())});const N=()=>{n.isPlaying||c("update:isPlaying",!0)},T=()=>{n.isPlaying&&c("update:isPlaying",!1)},I=()=>{c("update:isPlaying",!1),c("ended")},f=(r,t)=>{t.preventDefault(),H.value=!0,b.value=r;const o="clientX"in t?t.clientX:t.touches[0].clientX,P="clientY"in t?t.clientY:t.touches[0].clientY;z.value=o,B.value=P,C.value=g.value,l.value=w.value,s.value=h.value,u.value=x.value,document.addEventListener("mousemove",L),document.addEventListener("mouseup",M),document.addEventListener("touchmove",L,{passive:!1}),document.addEventListener("touchend",M)},L=r=>{if(!H.value)return;r.preventDefault();const t="clientX"in r?r.clientX:r.touches[0].clientX,o="clientY"in r?r.clientY:r.touches[0].clientY,P=t-z.value,U=o-B.value,R=200,X=250,Y=800,$=1e3;let m=C.value,p=l.value,E=s.value,S=u.value;b.value==="bottom-right"?(m=C.value+P,S=u.value-U,p=l.value+U):b.value==="bottom-left"?(E=s.value+P,m=C.value-P,S=u.value-U,p=l.value+U):b.value==="top-right"?(m=C.value+P,p=l.value-U):b.value==="top-left"&&(E=s.value+P,m=C.value-P,p=l.value-U),m<R&&(E!==s.value&&(E-=R-m),m=R),p<X&&(S!==u.value&&(S-=X-p),p=X),m>Y&&(E!==s.value&&(E+=m-Y),m=Y),p>$&&(S!==u.value&&(S+=p-$),p=$),g.value=m,w.value=p,h.value=E,x.value=S},M=()=>{H.value=!1,b.value=null,document.removeEventListener("mousemove",L),document.removeEventListener("mouseup",M),document.removeEventListener("touchmove",L),document.removeEventListener("touchend",M)};return e.onMounted(()=>{n.isPlaying&&i.value&&i.value.play().catch(r=>console.error("Video play failed:",r)),n.screenClientId&&n.wsUrl&&k()}),e.onUnmounted(()=>{a.value&&a.value.close()}),(r,t)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[d.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":d.isDark}]),style:e.normalizeStyle({width:g.value+"px",height:w.value+"px",left:h.value+"px",bottom:x.value+"px"})},[e.createElementVNode("div",{class:"video-wrapper",ref_key:"wrapperRef",ref:W},[e.createElementVNode("video",{ref_key:"videoRef",ref:i,src:d.videoSrc,class:"persona-video",muted:d.muted,playsinline:"",loop:"",autoPlay:"",disablePictureInPicture:"false",onPlay:N,onPause:T,onEnded:I},null,40,F),d.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:"resize-handle top-left",onMousedown:t[0]||(t[0]=o=>f("top-left",o)),onTouchstart:t[1]||(t[1]=o=>f("top-left",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:1,class:"resize-handle top-right",onMousedown:t[2]||(t[2]=o=>f("top-right",o)),onTouchstart:t[3]||(t[3]=o=>f("top-right",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:2,class:"resize-handle bottom-left",onMousedown:t[4]||(t[4]=o=>f("bottom-left",o)),onTouchstart:t[5]||(t[5]=o=>f("bottom-left",o))},null,32)):e.createCommentVNode("",!0),d.visible?(e.openBlock(),e.createElementBlock("div",{key:3,class:"resize-handle bottom-right",onMousedown:t[6]||(t[6]=o=>f("bottom-right",o)),onTouchstart:t[7]||(t[7]=o=>f("bottom-right",o))},null,32)):e.createCommentVNode("",!0),e.createElementVNode("div",O,[d.isPlaying?(e.openBlock(),e.createElementBlock("div",J,[...t[9]||(t[9]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",j,[...t[8]||(t[8]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])],512)],6)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-6c9e435e"]]),D=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["eventNotifaction","end","pause","connected","error","playComplete"],setup(d,{emit:A}){const n=d,c=A,i=e.ref(null),W=e.ref(!1);let a=null,y=0,g=!1,w=0,h=!1;const x=()=>{a||(a=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),a.state==="suspended"&&!h&&a.resume()},H=()=>{g&&w===0&&(c("playComplete",n.screenClientId),g=!1)},b=l=>{if(x(),!!a)try{const s=window.atob(l),u=s.length,k=new Uint8Array(u);for(let f=0;f<u;f++)k[f]=s.charCodeAt(f);const V=new Int16Array(k.buffer),N=new Float32Array(V.length);for(let f=0;f<V.length;f++)N[f]=V[f]/32768;const T=a.createBuffer(1,N.length,24e3);T.getChannelData(0).set(N);const I=a.createBufferSource();I.buffer=T,I.connect(a.destination),y<a.currentTime&&(y=a.currentTime),I.start(y),y+=T.duration,w++,I.onended=()=>{w--,H()}}catch(s){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",s)}},z=l=>{switch(l){case"play":g=!1,w=0,y=0,h=!1,a&&a.state==="suspended"&&a.resume();break;case"resume":h=!1,a&&a.state==="suspended"&&a.resume();break;case"pause":h=!0,a&&a.state==="running"&&a.suspend();break;case"stop":h=!1,a&&(a.close(),a=null),y=0,g=!1,w=0;break;case"tts_complete":g=!0,H();break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${l}`)}},B=()=>{i.value&&i.value.close();try{const l=new URL(n.wsUrl);l.searchParams.append("sessionId",n.screenClientId+"-event"),i.value=new WebSocket(l.toString()),i.value.onopen=()=>{W.value=!0,c("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${n.wsUrl} for session ${n.screenClientId}-event`)},i.value.onmessage=s=>{try{const u=JSON.parse(s.data);C(u)}catch(u){console.error("[VirtualHumanEventAdapter] Failed to parse message:",s.data,u)}},i.value.onerror=s=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",s),c("error",s)},i.value.onclose=()=>{W.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(l){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",l),c("error",l)}},C=l=>{const{type:s,payload:u,action:k}=l;switch(console.log("msgmsg-002",l),s){case"audio":const V=(u==null?void 0:u.data)||l.data;V&&b(V);break;case"send_event":l.event&&c("eventNotifaction",l);break;case"control":k&&z(k);break;case"end":c("end",u);break;case"pause":c("pause",u);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${s}`)}};return e.watch(()=>n.screenClientId,()=>{n.screenClientId&&n.wsUrl&&B()}),e.watch(()=>n.wsUrl,()=>{n.screenClientId&&n.wsUrl&&B()}),e.onMounted(()=>{n.screenClientId&&n.wsUrl&&B()}),e.onUnmounted(()=>{i.value&&i.value.close(),a&&a.close()}),(l,s)=>e.renderSlot(l.$slots,"default")}}),G={install:d=>{d.component("VirtualHumanPersona",q),d.component("VirtualHumanEventAdapter",D)}};v.VirtualHumanEventAdapter=D,v.VirtualHumanPersona=q,v.default=G,Object.defineProperties(v,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-human-cf",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Vue3 Digital Human Component Package by cf ",
5
5
  "main": "dist/virtual-human-cf.umd.js",
6
6
  "module": "dist/virtual-human-cf.es.js",
@@ -18,7 +18,7 @@ const props = defineProps({
18
18
  }
19
19
  });
20
20
 
21
- const emit = defineEmits(['highlight', 'showDialog', 'end', 'pause', 'connected', 'error']);
21
+ const emit = defineEmits(['eventNotifaction', 'end', 'pause', 'connected', 'error', 'playComplete']);
22
22
 
23
23
  const ws = ref<WebSocket | null>(null);
24
24
  const isConnected = ref(false);
@@ -26,6 +26,9 @@ const isConnected = ref(false);
26
26
  // Audio playback variables
27
27
  let audioContext: AudioContext | null = null;
28
28
  let nextStartTime = 0;
29
+ let isTtsComplete = false;
30
+ let activeSources = 0;
31
+ let isIntentionallyPaused = false;
29
32
 
30
33
  const initAudioContext = () => {
31
34
  if (!audioContext) {
@@ -33,11 +36,18 @@ const initAudioContext = () => {
33
36
  sampleRate: 24000
34
37
  });
35
38
  }
36
- if (audioContext.state === 'suspended') {
39
+ if (audioContext.state === 'suspended' && !isIntentionallyPaused) {
37
40
  audioContext.resume();
38
41
  }
39
42
  };
40
43
 
44
+ const checkPlayComplete = () => {
45
+ if (isTtsComplete && activeSources === 0) {
46
+ emit('playComplete', props.screenClientId);
47
+ isTtsComplete = false;
48
+ }
49
+ };
50
+
41
51
  const handleAudioMessage = (base64Data: string) => {
42
52
  initAudioContext();
43
53
  if (!audioContext) return;
@@ -70,29 +80,53 @@ const handleAudioMessage = (base64Data: string) => {
70
80
  }
71
81
  source.start(nextStartTime);
72
82
  nextStartTime += audioBuffer.duration;
83
+
84
+ activeSources++;
85
+ source.onended = () => {
86
+ activeSources--;
87
+ checkPlayComplete();
88
+ };
73
89
  } catch (error) {
74
90
  console.error('[VirtualHumanEventAdapter] Failed to decode and play audio:', error);
75
91
  }
76
92
  };
77
93
 
78
94
  const handleControlMessage = (action: string) => {
79
- if (!audioContext) return;
80
95
  switch (action) {
81
96
  case 'play':
97
+ isTtsComplete = false;
98
+ activeSources = 0;
99
+ nextStartTime = 0;
100
+ isIntentionallyPaused = false;
101
+ if (audioContext && audioContext.state === 'suspended') {
102
+ audioContext.resume();
103
+ }
104
+ break;
82
105
  case 'resume':
83
- if (audioContext.state === 'suspended') {
106
+ isIntentionallyPaused = false;
107
+ if (audioContext && audioContext.state === 'suspended') {
84
108
  audioContext.resume();
85
109
  }
86
110
  break;
87
111
  case 'pause':
88
- if (audioContext.state === 'running') {
112
+ isIntentionallyPaused = true;
113
+ if (audioContext && audioContext.state === 'running') {
89
114
  audioContext.suspend();
90
115
  }
91
116
  break;
92
117
  case 'stop':
93
- audioContext.close();
94
- audioContext = null;
118
+ isIntentionallyPaused = false;
119
+ if (audioContext) {
120
+ audioContext.close();
121
+ audioContext = null;
122
+ }
95
123
  nextStartTime = 0;
124
+ isTtsComplete = false;
125
+ activeSources = 0;
126
+ break;
127
+ case 'tts_complete':
128
+ isTtsComplete = true;
129
+ checkPlayComplete();
96
130
  break;
97
131
  default:
98
132
  console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${action}`);
@@ -141,32 +175,27 @@ const connectWebSocket = () => {
141
175
 
142
176
  const handleMessage = (msg: any) => {
143
177
  const { type, payload, action } = msg;
144
- console.log("msgmsg",msg)
178
+ console.log("msgmsg-002",msg)
145
179
  switch (type) {
180
+ // 接收音频
146
181
  case 'audio':
147
182
  const base64Data = payload?.data || msg.data;
148
183
  if (base64Data) {
149
184
  handleAudioMessage(base64Data);
150
185
  }
151
186
  break;
152
- case 'dialog_event':
187
+ // 接收事件通知
188
+ case 'send_event':
153
189
  if (msg.event) {
154
- emit(msg.event as any, msg.params);
190
+ emit('eventNotifaction', msg);
155
191
  }
156
192
  break;
193
+ // 控制指令接口
157
194
  case 'control':
158
195
  if (action) {
159
196
  handleControlMessage(action);
160
197
  }
161
198
  break;
162
- case 'highlight':
163
- // 触发高亮大屏某区域事件
164
- emit('highlight', payload);
165
- break;
166
- case 'showDialog':
167
- // 触发弹窗显示事件
168
- emit('showDialog', payload);
169
- break;
170
199
  case 'end':
171
200
  // 触发数字人对话结束事件
172
201
  emit('end', payload);
@@ -4,12 +4,14 @@
4
4
  v-if="visible"
5
5
  class="virtual-human-container"
6
6
  :class="{ 'is-dark': isDark }"
7
+ :style="{
8
+ width: containerWidth + 'px',
9
+ height: containerHeight + 'px',
10
+ left: containerLeft + 'px',
11
+ bottom: containerBottom + 'px'
12
+ }"
7
13
  >
8
- <div
9
- class="video-wrapper"
10
- ref="wrapperRef"
11
- :style="{ transform: `scale(${scale})`, transformOrigin: 'left bottom' }"
12
- >
14
+ <div class="video-wrapper" ref="wrapperRef">
13
15
  <video
14
16
  ref="videoRef"
15
17
  :src="videoSrc"
@@ -17,6 +19,8 @@
17
19
  :muted="muted"
18
20
  playsinline
19
21
  loop
22
+ autoPlay
23
+ disablePictureInPicture="false"
20
24
  @play="handlePlay"
21
25
  @pause="handlePause"
22
26
  @ended="handleEnded"
@@ -114,12 +118,19 @@ const ws = ref<WebSocket | null>(null);
114
118
  const isConnected = ref(false);
115
119
 
116
120
  // 缩放相关状态
117
- const scale = ref(1);
121
+ const containerWidth = ref(400);
122
+ const containerHeight = ref(500);
123
+ const containerLeft = ref(16);
124
+ const containerBottom = ref(16);
125
+
118
126
  const isResizing = ref(false);
119
127
  const resizeHandle = ref<string | null>(null);
120
128
  const startX = ref(0);
121
129
  const startY = ref(0);
122
- const startScale = ref(1);
130
+ const startW = ref(0);
131
+ const startH = ref(0);
132
+ const startL = ref(0);
133
+ const startB = ref(0);
123
134
 
124
135
  const connectWebSocket = () => {
125
136
  if (ws.value) {
@@ -220,23 +231,29 @@ const handleEnded = () => {
220
231
 
221
232
  // 缩放功能
222
233
  const startResize = (handle: string, event: MouseEvent | TouchEvent) => {
234
+ event.preventDefault();
223
235
  isResizing.value = true;
224
236
  resizeHandle.value = handle;
225
- startScale.value = scale.value;
226
237
 
227
238
  const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
228
239
  const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
229
240
  startX.value = clientX;
230
241
  startY.value = clientY;
231
242
 
243
+ startW.value = containerWidth.value;
244
+ startH.value = containerHeight.value;
245
+ startL.value = containerLeft.value;
246
+ startB.value = containerBottom.value;
247
+
232
248
  document.addEventListener('mousemove', handleResize);
233
249
  document.addEventListener('mouseup', stopResize);
234
- document.addEventListener('touchmove', handleResize);
250
+ document.addEventListener('touchmove', handleResize, { passive: false });
235
251
  document.addEventListener('touchend', stopResize);
236
252
  };
237
253
 
238
254
  const handleResize = (event: MouseEvent | TouchEvent) => {
239
- if (!isResizing.value || !wrapperRef.value) return;
255
+ if (!isResizing.value) return;
256
+ event.preventDefault();
240
257
 
241
258
  const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
242
259
  const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
@@ -244,25 +261,57 @@ const handleResize = (event: MouseEvent | TouchEvent) => {
244
261
  const deltaX = clientX - startX.value;
245
262
  const deltaY = clientY - startY.value;
246
263
 
247
- const rect = wrapperRef.value.getBoundingClientRect();
248
- const minScale = 0.5;
249
- const maxScale = 2;
264
+ const minWidth = 200;
265
+ const minHeight = 250;
266
+ const maxWidth = 800;
267
+ const maxHeight = 1000;
250
268
 
251
- // 根据拖动的角计算缩放比例
252
- let newScale = startScale.value;
253
- const baseSize = Math.min(rect.width, rect.height);
269
+ let newW = startW.value;
270
+ let newH = startH.value;
271
+ let newL = startL.value;
272
+ let newB = startB.value;
254
273
 
255
274
  if (resizeHandle.value === 'bottom-right') {
256
- newScale = startScale.value + (deltaX + deltaY) / baseSize;
275
+ newW = startW.value + deltaX;
276
+ newB = startB.value - deltaY;
277
+ newH = startH.value + deltaY;
257
278
  } else if (resizeHandle.value === 'bottom-left') {
258
- newScale = startScale.value + (-deltaX + deltaY) / baseSize;
279
+ newL = startL.value + deltaX;
280
+ newW = startW.value - deltaX;
281
+ newB = startB.value - deltaY;
282
+ newH = startH.value + deltaY;
259
283
  } else if (resizeHandle.value === 'top-right') {
260
- newScale = startScale.value + (deltaX - deltaY) / baseSize;
284
+ newW = startW.value + deltaX;
285
+ newH = startH.value - deltaY;
261
286
  } else if (resizeHandle.value === 'top-left') {
262
- newScale = startScale.value + (-deltaX - deltaY) / baseSize;
287
+ newL = startL.value + deltaX;
288
+ newW = startW.value - deltaX;
289
+ newH = startH.value - deltaY;
263
290
  }
264
291
 
265
- scale.value = Math.max(minScale, Math.min(maxScale, newScale));
292
+ // 约束最小宽高
293
+ if (newW < minWidth) {
294
+ if (newL !== startL.value) newL -= (minWidth - newW);
295
+ newW = minWidth;
296
+ }
297
+ if (newH < minHeight) {
298
+ if (newB !== startB.value) newB -= (minHeight - newH);
299
+ newH = minHeight;
300
+ }
301
+ // 约束最大宽高
302
+ if (newW > maxWidth) {
303
+ if (newL !== startL.value) newL += (newW - maxWidth);
304
+ newW = maxWidth;
305
+ }
306
+ if (newH > maxHeight) {
307
+ if (newB !== startB.value) newB += (newH - maxHeight);
308
+ newH = maxHeight;
309
+ }
310
+
311
+ containerWidth.value = newW;
312
+ containerHeight.value = newH;
313
+ containerLeft.value = newL;
314
+ containerBottom.value = newB;
266
315
  };
267
316
 
268
317
  const stopResize = () => {
@@ -304,23 +353,25 @@ onUnmounted(() => {
304
353
 
305
354
  .virtual-human-container {
306
355
  position: fixed;
307
- left: 16px;
308
- bottom: 16px;
309
- width: 400px;
310
- height: 500px;
356
+ /* left, bottom, width, height are set dynamically */
311
357
  z-index: 2147483647;
312
358
  overflow: visible;
313
359
  }
314
360
 
315
361
  .video-wrapper {
316
362
  position: relative;
317
- width: 400px;
318
- height: 500px;
363
+ width: 100%;
364
+ height: 100%;
319
365
  border-radius: 1rem;
320
366
  overflow: hidden;
321
367
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
322
368
  background: #ffffff;
323
- transition: background-color 0.3s ease, box-shadow 0.3s ease;
369
+ border: 2px solid transparent;
370
+ transition: background-color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
371
+ }
372
+
373
+ .video-wrapper:hover {
374
+ border-color: #409EFF;
324
375
  }
325
376
 
326
377
  .virtual-human-container.is-dark .video-wrapper {
@@ -346,11 +397,13 @@ onUnmounted(() => {
346
397
  cursor: pointer;
347
398
  z-index: 20;
348
399
  opacity: 0;
400
+ pointer-events: none;
349
401
  transition: opacity 0.3s ease, background 0.2s, border-color 0.2s;
350
402
  }
351
403
 
352
404
  .video-wrapper:hover .resize-handle {
353
405
  opacity: 1;
406
+ pointer-events: auto;
354
407
  }
355
408
 
356
409
  .resize-handle:hover {