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 +36 -21
- package/dist/components/VirtualHumanEventAdapter.vue.d.ts +4 -4
- package/dist/style.css +1 -1
- package/dist/virtual-human-cf.es.js +182 -173
- package/dist/virtual-human-cf.umd.js +1 -1
- package/package.json +1 -1
- package/src/components/VirtualHumanEventAdapter.vue +47 -18
- package/src/components/VirtualHumanPersona.vue +81 -28
package/README.md
CHANGED
|
@@ -113,11 +113,20 @@ const onEnded = () => {
|
|
|
113
113
|
| 事件名 | 说明 | 回调参数 |
|
|
114
114
|
|--------|------|----------|
|
|
115
115
|
| connected | WebSocket 连接成功 | - |
|
|
116
|
-
|
|
|
117
|
-
|
|
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
|
-
@
|
|
131
|
-
@
|
|
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
|
|
150
|
-
console.log('
|
|
158
|
+
const onEventNotifaction = (payload) => {
|
|
159
|
+
console.log('自定义事件接收:', payload);
|
|
151
160
|
};
|
|
152
161
|
|
|
153
|
-
const
|
|
154
|
-
console.log('
|
|
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="
|
|
189
|
+
:screen-client-id="activeScreenClientId"
|
|
180
190
|
:ws-url="websocketUrl"
|
|
181
|
-
@update:isPlaying="
|
|
191
|
+
@update:isPlaying="onPersonaPlayingUpdate"
|
|
192
|
+
@update:visible="onPersonaVisibleUpdate"
|
|
182
193
|
@ended="onVideoEnded"
|
|
183
194
|
/>
|
|
184
195
|
|
|
185
196
|
<VirtualHumanEventAdapter
|
|
186
|
-
:screen-client-id="
|
|
197
|
+
:screen-client-id="activeScreenClientId"
|
|
187
198
|
:ws-url="websocketUrl"
|
|
188
199
|
@connected="onConnected"
|
|
189
|
-
@
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
2
|
-
const
|
|
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
|
-
},
|
|
5
|
+
}, oe = {
|
|
6
6
|
key: 1,
|
|
7
7
|
class: "status-badge playing"
|
|
8
|
-
},
|
|
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(
|
|
50
|
-
const t =
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
p.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
|
|
67
67
|
};
|
|
68
|
-
} catch (
|
|
69
|
-
console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",
|
|
68
|
+
} catch (o) {
|
|
69
|
+
console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", o);
|
|
70
70
|
}
|
|
71
|
-
},
|
|
72
|
-
const { type: e, action: a } =
|
|
73
|
-
e === "control" && (a === "play" || a === "resume" ? (
|
|
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
|
-
|
|
76
|
-
t.screenClientId && t.wsUrl &&
|
|
77
|
-
}),
|
|
78
|
-
t.screenClientId && t.wsUrl &&
|
|
79
|
-
}),
|
|
80
|
-
|
|
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
|
|
83
|
-
t.isPlaying ||
|
|
84
|
-
},
|
|
85
|
-
t.isPlaying &&
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
const a = "clientX" in e ? e.clientX : e.touches[0].clientX,
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
if (!
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
},
|
|
99
|
-
|
|
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
|
|
102
|
-
t.isPlaying &&
|
|
103
|
-
}),
|
|
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
|
-
}), (
|
|
106
|
-
default:
|
|
107
|
-
|
|
105
|
+
}), (o, e) => (S(), Q(Z, { name: "fade" }, {
|
|
106
|
+
default: _(() => [
|
|
107
|
+
c.visible ? (S(), E("div", {
|
|
108
108
|
key: 0,
|
|
109
|
-
class:
|
|
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
|
-
|
|
117
|
+
R("div", {
|
|
112
118
|
class: "video-wrapper",
|
|
113
119
|
ref_key: "wrapperRef",
|
|
114
|
-
ref:
|
|
115
|
-
style: O({ transform: `scale(${V.value})`, transformOrigin: "left bottom" })
|
|
120
|
+
ref: Y
|
|
116
121
|
}, [
|
|
117
|
-
|
|
122
|
+
R("video", {
|
|
118
123
|
ref_key: "videoRef",
|
|
119
|
-
ref:
|
|
120
|
-
src:
|
|
124
|
+
ref: r,
|
|
125
|
+
src: c.videoSrc,
|
|
121
126
|
class: "persona-video",
|
|
122
|
-
muted:
|
|
127
|
+
muted: c.muted,
|
|
123
128
|
playsinline: "",
|
|
124
129
|
loop: "",
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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) =>
|
|
133
|
-
onTouchstart: e[1] || (e[1] = (a) =>
|
|
134
|
-
}, null, 32)) :
|
|
135
|
-
|
|
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) =>
|
|
139
|
-
onTouchstart: e[3] || (e[3] = (a) =>
|
|
140
|
-
}, null, 32)) :
|
|
141
|
-
|
|
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) =>
|
|
145
|
-
onTouchstart: e[5] || (e[5] = (a) =>
|
|
146
|
-
}, null, 32)) :
|
|
147
|
-
|
|
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) =>
|
|
151
|
-
onTouchstart: e[7] || (e[7] = (a) =>
|
|
152
|
-
}, null, 32)) :
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
])])) : (
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
],
|
|
163
|
-
],
|
|
169
|
+
], 512)
|
|
170
|
+
], 6)) : T("", !0)
|
|
164
171
|
]),
|
|
165
172
|
_: 1
|
|
166
173
|
}));
|
|
167
174
|
}
|
|
168
|
-
}),
|
|
169
|
-
const t =
|
|
170
|
-
for (const [
|
|
171
|
-
t[
|
|
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
|
-
},
|
|
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: ["
|
|
188
|
-
setup(
|
|
189
|
-
const t =
|
|
190
|
-
let n = null,
|
|
191
|
-
const
|
|
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
|
-
},
|
|
196
|
-
|
|
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
|
|
199
|
-
for (let d = 0; d <
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
for (let d = 0; d <
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
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
|
-
},
|
|
228
|
-
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
},
|
|
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
|
|
236
|
-
|
|
237
|
-
} catch (
|
|
238
|
-
console.error("[VirtualHumanEventAdapter] Failed to parse message:",
|
|
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
|
-
},
|
|
241
|
-
console.error("[VirtualHumanEventAdapter] WebSocket error:",
|
|
242
|
-
},
|
|
243
|
-
|
|
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 (
|
|
246
|
-
console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",
|
|
260
|
+
} catch (s) {
|
|
261
|
+
console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", s), i("error", s);
|
|
247
262
|
}
|
|
248
|
-
},
|
|
249
|
-
const { type:
|
|
250
|
-
switch (console.log("msgmsg",
|
|
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
|
|
253
|
-
|
|
267
|
+
const C = (u == null ? void 0 : u.data) || s.data;
|
|
268
|
+
C && h(C);
|
|
254
269
|
break;
|
|
255
|
-
case "
|
|
256
|
-
|
|
270
|
+
case "send_event":
|
|
271
|
+
s.event && i("eventNotifaction", s);
|
|
257
272
|
break;
|
|
258
273
|
case "control":
|
|
259
|
-
|
|
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
|
-
|
|
277
|
+
i("end", u);
|
|
269
278
|
break;
|
|
270
279
|
case "pause":
|
|
271
|
-
|
|
280
|
+
i("pause", u);
|
|
272
281
|
break;
|
|
273
282
|
default:
|
|
274
|
-
console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${
|
|
283
|
+
console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${l}`);
|
|
275
284
|
}
|
|
276
285
|
};
|
|
277
|
-
return
|
|
278
|
-
t.screenClientId && t.wsUrl &&
|
|
279
|
-
}),
|
|
280
|
-
t.screenClientId && t.wsUrl &&
|
|
281
|
-
}),
|
|
282
|
-
t.screenClientId && t.wsUrl &&
|
|
283
|
-
}),
|
|
284
|
-
|
|
285
|
-
}), (
|
|
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
|
-
}),
|
|
288
|
-
|
|
289
|
-
},
|
|
290
|
-
install:
|
|
296
|
+
}), de = (c) => {
|
|
297
|
+
c.component("VirtualHumanPersona", ue), c.component("VirtualHumanEventAdapter", ce);
|
|
298
|
+
}, fe = {
|
|
299
|
+
install: de
|
|
291
300
|
};
|
|
292
301
|
export {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
302
|
+
ce as VirtualHumanEventAdapter,
|
|
303
|
+
ue as VirtualHumanPersona,
|
|
304
|
+
fe as default
|
|
296
305
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
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
|
@@ -18,7 +18,7 @@ const props = defineProps({
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
const emit = defineEmits(['
|
|
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
|
-
|
|
106
|
+
isIntentionallyPaused = false;
|
|
107
|
+
if (audioContext && audioContext.state === 'suspended') {
|
|
84
108
|
audioContext.resume();
|
|
85
109
|
}
|
|
86
110
|
break;
|
|
87
111
|
case 'pause':
|
|
88
|
-
|
|
112
|
+
isIntentionallyPaused = true;
|
|
113
|
+
if (audioContext && audioContext.state === 'running') {
|
|
89
114
|
audioContext.suspend();
|
|
90
115
|
}
|
|
91
116
|
break;
|
|
92
117
|
case 'stop':
|
|
93
|
-
|
|
94
|
-
audioContext
|
|
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
|
-
|
|
187
|
+
// 接收事件通知
|
|
188
|
+
case 'send_event':
|
|
153
189
|
if (msg.event) {
|
|
154
|
-
emit(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
248
|
-
const
|
|
249
|
-
const
|
|
264
|
+
const minWidth = 200;
|
|
265
|
+
const minHeight = 250;
|
|
266
|
+
const maxWidth = 800;
|
|
267
|
+
const maxHeight = 1000;
|
|
250
268
|
|
|
251
|
-
|
|
252
|
-
let
|
|
253
|
-
|
|
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
|
-
|
|
275
|
+
newW = startW.value + deltaX;
|
|
276
|
+
newB = startB.value - deltaY;
|
|
277
|
+
newH = startH.value + deltaY;
|
|
257
278
|
} else if (resizeHandle.value === 'bottom-left') {
|
|
258
|
-
|
|
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
|
-
|
|
284
|
+
newW = startW.value + deltaX;
|
|
285
|
+
newH = startH.value - deltaY;
|
|
261
286
|
} else if (resizeHandle.value === 'top-left') {
|
|
262
|
-
|
|
287
|
+
newL = startL.value + deltaX;
|
|
288
|
+
newW = startW.value - deltaX;
|
|
289
|
+
newH = startH.value - deltaY;
|
|
263
290
|
}
|
|
264
291
|
|
|
265
|
-
|
|
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
|
|
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:
|
|
318
|
-
height:
|
|
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
|
-
|
|
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 {
|