virtual-human-cf 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.fade-enter-active[data-v-
|
|
1
|
+
.fade-enter-active[data-v-2fb9caca],.fade-leave-active[data-v-2fb9caca]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-2fb9caca],.fade-leave-to[data-v-2fb9caca]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-2fb9caca]{position:relative;width:100%;max-width:400px;border-radius:1rem;overflow:hidden;box-shadow:0 10px 25px #0000001a;background:#fff;transition:background-color .3s ease}.virtual-human-container.is-dark[data-v-2fb9caca]{background:#1f2937;box-shadow:0 10px 25px #00000080}.video-wrapper[data-v-2fb9caca]{position:relative;width:100%;aspect-ratio:9 / 16;background:#000;overflow:visible}.persona-video[data-v-2fb9caca]{width:100%;height:100%;object-fit:cover;transform-origin:center;transition:transform .1s ease-out}.resize-handle[data-v-2fb9caca]{position:absolute;width:20px;height:20px;background:#00000080;border:2px solid rgba(255,255,255,.8);border-radius:4px;cursor:pointer;z-index:20;transition:background .2s,border-color .2s}.resize-handle[data-v-2fb9caca]:hover{background:#000000b3;border-color:#fff}.resize-handle.top-left[data-v-2fb9caca]{top:-10px;left:-10px;cursor:nwse-resize}.resize-handle.top-right[data-v-2fb9caca]{top:-10px;right:-10px;cursor:nesw-resize}.resize-handle.bottom-left[data-v-2fb9caca]{bottom:-10px;left:-10px;cursor:nesw-resize}.resize-handle.bottom-right[data-v-2fb9caca]{bottom:-10px;right:-10px;cursor:nwse-resize}.overlay[data-v-2fb9caca]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-2fb9caca]{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-2fb9caca]{background:#ef4444cc}.status-badge.playing[data-v-2fb9caca]{background:#22c55ecc}.dot[data-v-2fb9caca]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-2fb9caca{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-2fb9caca]{animation:pulse-2fb9caca 2s cubic-bezier(.4,0,.6,1) infinite}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { defineComponent as
|
|
2
|
-
const
|
|
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 = {
|
|
3
3
|
key: 0,
|
|
4
4
|
class: "status-badge paused"
|
|
5
|
-
},
|
|
5
|
+
}, Q = {
|
|
6
6
|
key: 1,
|
|
7
7
|
class: "status-badge playing"
|
|
8
|
-
},
|
|
8
|
+
}, Z = /* @__PURE__ */ T({
|
|
9
9
|
__name: "VirtualHumanPersona",
|
|
10
10
|
props: {
|
|
11
11
|
// 视频源URL
|
|
@@ -16,7 +16,7 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
|
|
|
16
16
|
// 是否可见
|
|
17
17
|
visible: {
|
|
18
18
|
type: Boolean,
|
|
19
|
-
default: !
|
|
19
|
+
default: !1
|
|
20
20
|
},
|
|
21
21
|
// 是否自动播放
|
|
22
22
|
isPlaying: {
|
|
@@ -46,90 +46,131 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
emits: ["update:isPlaying", "ended", "update:visible"],
|
|
49
|
-
setup(
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
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 = () => {
|
|
51
|
+
if (n.value && n.value.close(), !(!t.wsUrl || !t.screenClientId))
|
|
52
52
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
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`);
|
|
56
|
+
}, n.value.onmessage = (e) => {
|
|
57
57
|
try {
|
|
58
|
-
const a = JSON.parse(
|
|
59
|
-
|
|
58
|
+
const a = JSON.parse(e.data);
|
|
59
|
+
c(a);
|
|
60
60
|
} catch (a) {
|
|
61
|
-
console.error("[VirtualHumanPersona] Failed to parse message:",
|
|
61
|
+
console.error("[VirtualHumanPersona] Failed to parse message:", e.data, a);
|
|
62
62
|
}
|
|
63
|
-
},
|
|
64
|
-
console.error("[VirtualHumanPersona] WebSocket error:",
|
|
65
|
-
},
|
|
66
|
-
|
|
63
|
+
}, n.value.onerror = (e) => {
|
|
64
|
+
console.error("[VirtualHumanPersona] WebSocket error:", e);
|
|
65
|
+
}, n.value.onclose = () => {
|
|
66
|
+
m.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
|
|
67
67
|
};
|
|
68
|
-
} catch (
|
|
69
|
-
console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",
|
|
68
|
+
} catch (r) {
|
|
69
|
+
console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", r);
|
|
70
70
|
}
|
|
71
|
-
},
|
|
72
|
-
const { type:
|
|
73
|
-
|
|
71
|
+
}, c = (r) => {
|
|
72
|
+
const { type: e, action: a } = r;
|
|
73
|
+
e === "control" && (a === "play" ? (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)));
|
|
74
74
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}),
|
|
78
|
-
|
|
79
|
-
}),
|
|
80
|
-
|
|
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());
|
|
81
81
|
});
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
},
|
|
82
|
+
const w = () => {
|
|
83
|
+
t.isPlaying || s("update:isPlaying", !0);
|
|
84
|
+
}, g = () => {
|
|
85
|
+
t.isPlaying && s("update:isPlaying", !1);
|
|
86
|
+
}, A = () => {
|
|
87
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);
|
|
88
100
|
};
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
}),
|
|
92
|
-
|
|
93
|
-
}), (
|
|
94
|
-
default:
|
|
95
|
-
|
|
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(() => {
|
|
104
|
+
n.value && n.value.close();
|
|
105
|
+
}), (r, e) => (h(), D(F, { name: "fade" }, {
|
|
106
|
+
default: q(() => [
|
|
107
|
+
i.visible ? (h(), b("div", {
|
|
96
108
|
key: 0,
|
|
97
|
-
class:
|
|
109
|
+
class: N(["virtual-human-container", { "is-dark": i.isDark }])
|
|
98
110
|
}, [
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
H("div", {
|
|
112
|
+
class: "video-wrapper",
|
|
113
|
+
ref_key: "wrapperRef",
|
|
114
|
+
ref: k
|
|
115
|
+
}, [
|
|
116
|
+
H("video", {
|
|
101
117
|
ref_key: "videoRef",
|
|
102
|
-
ref:
|
|
103
|
-
src:
|
|
118
|
+
ref: l,
|
|
119
|
+
src: i.videoSrc,
|
|
104
120
|
class: "persona-video",
|
|
105
|
-
muted:
|
|
121
|
+
muted: i.muted,
|
|
106
122
|
playsinline: "",
|
|
107
123
|
loop: "",
|
|
108
|
-
onPlay:
|
|
109
|
-
onPause:
|
|
110
|
-
onEnded:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
onPlay: w,
|
|
125
|
+
onPause: g,
|
|
126
|
+
onEnded: A,
|
|
127
|
+
style: O({ transform: `scale(${V.value})` })
|
|
128
|
+
}, null, 44, j),
|
|
129
|
+
i.visible ? (h(), b("div", {
|
|
130
|
+
key: 0,
|
|
131
|
+
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", {
|
|
136
|
+
key: 1,
|
|
137
|
+
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", {
|
|
142
|
+
key: 2,
|
|
143
|
+
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", {
|
|
148
|
+
key: 3,
|
|
149
|
+
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)
|
|
119
160
|
])]))
|
|
120
161
|
])
|
|
121
|
-
])
|
|
122
|
-
], 2)) :
|
|
162
|
+
], 512)
|
|
163
|
+
], 2)) : I("", !0)
|
|
123
164
|
]),
|
|
124
165
|
_: 1
|
|
125
166
|
}));
|
|
126
167
|
}
|
|
127
|
-
}),
|
|
128
|
-
const
|
|
129
|
-
for (const [s,
|
|
130
|
-
|
|
131
|
-
return
|
|
132
|
-
},
|
|
168
|
+
}), _ = (i, P) => {
|
|
169
|
+
const t = i.__vccOpts || i;
|
|
170
|
+
for (const [s, l] of P)
|
|
171
|
+
t[s] = l;
|
|
172
|
+
return t;
|
|
173
|
+
}, ee = /* @__PURE__ */ _(Z, [["__scopeId", "data-v-2fb9caca"]]), te = /* @__PURE__ */ T({
|
|
133
174
|
__name: "VirtualHumanEventAdapter",
|
|
134
175
|
props: {
|
|
135
176
|
// 屏幕客户端ID
|
|
@@ -144,32 +185,32 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
|
|
|
144
185
|
}
|
|
145
186
|
},
|
|
146
187
|
emits: ["highlight", "showDialog", "end", "pause", "connected", "error"],
|
|
147
|
-
setup(
|
|
148
|
-
const
|
|
149
|
-
let n = null,
|
|
150
|
-
const
|
|
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 = () => {
|
|
151
192
|
n || (n = new (window.AudioContext || window.webkitAudioContext)({
|
|
152
193
|
sampleRate: 24e3
|
|
153
194
|
})), n.state === "suspended" && n.resume();
|
|
154
|
-
},
|
|
155
|
-
if (
|
|
195
|
+
}, E = (o) => {
|
|
196
|
+
if (V(), !!n)
|
|
156
197
|
try {
|
|
157
|
-
const
|
|
158
|
-
for (let d = 0; d <
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
for (let d = 0; d <
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
} catch (
|
|
168
|
-
console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",
|
|
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);
|
|
169
210
|
}
|
|
170
|
-
},
|
|
211
|
+
}, p = (o) => {
|
|
171
212
|
if (n)
|
|
172
|
-
switch (
|
|
213
|
+
switch (o) {
|
|
173
214
|
case "play":
|
|
174
215
|
n.state === "suspended" && n.resume();
|
|
175
216
|
break;
|
|
@@ -177,78 +218,78 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
|
|
|
177
218
|
n.state === "running" && n.suspend();
|
|
178
219
|
break;
|
|
179
220
|
case "stop":
|
|
180
|
-
n.close(), n = null,
|
|
221
|
+
n.close(), n = null, m = 0;
|
|
181
222
|
break;
|
|
182
223
|
default:
|
|
183
|
-
console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${
|
|
224
|
+
console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${o}`);
|
|
184
225
|
}
|
|
185
|
-
},
|
|
186
|
-
|
|
226
|
+
}, C = () => {
|
|
227
|
+
l.value && l.value.close();
|
|
187
228
|
try {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
},
|
|
229
|
+
const o = new URL(t.wsUrl);
|
|
230
|
+
o.searchParams.append("sessionId", t.screenClientId + "-event"), l.value = new WebSocket(o.toString()), l.value.onopen = () => {
|
|
231
|
+
k.value = !0, s("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`);
|
|
232
|
+
}, l.value.onmessage = (u) => {
|
|
192
233
|
try {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
} catch (
|
|
196
|
-
console.error("[VirtualHumanEventAdapter] Failed to parse message:",
|
|
234
|
+
const c = JSON.parse(u.data);
|
|
235
|
+
x(c);
|
|
236
|
+
} catch (c) {
|
|
237
|
+
console.error("[VirtualHumanEventAdapter] Failed to parse message:", u.data, c);
|
|
197
238
|
}
|
|
198
|
-
},
|
|
199
|
-
console.error("[VirtualHumanEventAdapter] WebSocket error:",
|
|
200
|
-
},
|
|
201
|
-
|
|
239
|
+
}, l.value.onerror = (u) => {
|
|
240
|
+
console.error("[VirtualHumanEventAdapter] WebSocket error:", u), s("error", u);
|
|
241
|
+
}, l.value.onclose = () => {
|
|
242
|
+
k.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
|
|
202
243
|
};
|
|
203
|
-
} catch (
|
|
204
|
-
console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",
|
|
244
|
+
} catch (o) {
|
|
245
|
+
console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", o), s("error", o);
|
|
205
246
|
}
|
|
206
|
-
},
|
|
207
|
-
const { type:
|
|
208
|
-
switch (
|
|
247
|
+
}, x = (o) => {
|
|
248
|
+
const { type: u, payload: c, action: w } = o;
|
|
249
|
+
switch (console.log("msgmsg", o), u) {
|
|
209
250
|
case "audio":
|
|
210
|
-
const
|
|
211
|
-
|
|
251
|
+
const g = (c == null ? void 0 : c.data) || o.data;
|
|
252
|
+
g && E(g);
|
|
212
253
|
break;
|
|
213
254
|
case "dialog_event":
|
|
214
|
-
|
|
255
|
+
o.event && s(o.event, o.params);
|
|
215
256
|
break;
|
|
216
257
|
case "control":
|
|
217
|
-
|
|
258
|
+
w && p(w);
|
|
218
259
|
break;
|
|
219
260
|
case "highlight":
|
|
220
|
-
s("highlight",
|
|
261
|
+
s("highlight", c);
|
|
221
262
|
break;
|
|
222
263
|
case "showDialog":
|
|
223
|
-
s("showDialog",
|
|
264
|
+
s("showDialog", c);
|
|
224
265
|
break;
|
|
225
266
|
case "end":
|
|
226
|
-
s("end",
|
|
267
|
+
s("end", c);
|
|
227
268
|
break;
|
|
228
269
|
case "pause":
|
|
229
|
-
s("pause",
|
|
270
|
+
s("pause", c);
|
|
230
271
|
break;
|
|
231
272
|
default:
|
|
232
|
-
console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${
|
|
273
|
+
console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${u}`);
|
|
233
274
|
}
|
|
234
275
|
};
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
}),
|
|
238
|
-
|
|
239
|
-
}),
|
|
240
|
-
|
|
241
|
-
}),
|
|
242
|
-
|
|
243
|
-
}), (
|
|
276
|
+
return z(() => t.screenClientId, () => {
|
|
277
|
+
t.screenClientId && t.wsUrl && C();
|
|
278
|
+
}), z(() => t.wsUrl, () => {
|
|
279
|
+
t.screenClientId && t.wsUrl && C();
|
|
280
|
+
}), W(() => {
|
|
281
|
+
t.screenClientId && t.wsUrl && C();
|
|
282
|
+
}), X(() => {
|
|
283
|
+
l.value && l.value.close(), n && n.close();
|
|
284
|
+
}), (o, u) => J(o.$slots, "default");
|
|
244
285
|
}
|
|
245
|
-
}),
|
|
246
|
-
|
|
247
|
-
},
|
|
248
|
-
install:
|
|
286
|
+
}), ne = (i) => {
|
|
287
|
+
i.component("VirtualHumanPersona", ee), i.component("VirtualHumanEventAdapter", te);
|
|
288
|
+
}, oe = {
|
|
289
|
+
install: ne
|
|
249
290
|
};
|
|
250
291
|
export {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
292
|
+
te as VirtualHumanEventAdapter,
|
|
293
|
+
ee as VirtualHumanPersona,
|
|
294
|
+
oe as default
|
|
254
295
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
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"?(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},[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,style:e.normalizeStyle({transform:`scale(${E.value})`})},null,44,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)])]))])],512)],2)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-2fb9caca"]]),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":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"}})});
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
class="virtual-human-container"
|
|
6
6
|
:class="{ 'is-dark': isDark }"
|
|
7
7
|
>
|
|
8
|
-
<div class="video-wrapper">
|
|
8
|
+
<div class="video-wrapper" ref="wrapperRef">
|
|
9
9
|
<video
|
|
10
10
|
ref="videoRef"
|
|
11
11
|
:src="videoSrc"
|
|
@@ -16,8 +16,35 @@
|
|
|
16
16
|
@play="handlePlay"
|
|
17
17
|
@pause="handlePause"
|
|
18
18
|
@ended="handleEnded"
|
|
19
|
+
:style="{ transform: `scale(${scale})` }"
|
|
19
20
|
></video>
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
<!-- 缩放控制点 - 四个角 -->
|
|
23
|
+
<div
|
|
24
|
+
v-if="visible"
|
|
25
|
+
class="resize-handle top-left"
|
|
26
|
+
@mousedown="startResize('top-left', $event)"
|
|
27
|
+
@touchstart="startResize('top-left', $event)"
|
|
28
|
+
></div>
|
|
29
|
+
<div
|
|
30
|
+
v-if="visible"
|
|
31
|
+
class="resize-handle top-right"
|
|
32
|
+
@mousedown="startResize('top-right', $event)"
|
|
33
|
+
@touchstart="startResize('top-right', $event)"
|
|
34
|
+
></div>
|
|
35
|
+
<div
|
|
36
|
+
v-if="visible"
|
|
37
|
+
class="resize-handle bottom-left"
|
|
38
|
+
@mousedown="startResize('bottom-left', $event)"
|
|
39
|
+
@touchstart="startResize('bottom-left', $event)"
|
|
40
|
+
></div>
|
|
41
|
+
<div
|
|
42
|
+
v-if="visible"
|
|
43
|
+
class="resize-handle bottom-right"
|
|
44
|
+
@mousedown="startResize('bottom-right', $event)"
|
|
45
|
+
@touchstart="startResize('bottom-right', $event)"
|
|
46
|
+
></div>
|
|
47
|
+
|
|
21
48
|
<!-- UI Overlay for controls or status -->
|
|
22
49
|
<div class="overlay">
|
|
23
50
|
<div v-if="!isPlaying" class="status-badge paused">
|
|
@@ -46,7 +73,7 @@ const props = defineProps({
|
|
|
46
73
|
// 是否可见
|
|
47
74
|
visible: {
|
|
48
75
|
type: Boolean,
|
|
49
|
-
default:
|
|
76
|
+
default: false,
|
|
50
77
|
},
|
|
51
78
|
// 是否自动播放
|
|
52
79
|
isPlaying: {
|
|
@@ -78,10 +105,19 @@ const props = defineProps({
|
|
|
78
105
|
const emit = defineEmits(['update:isPlaying', 'ended', 'update:visible']);
|
|
79
106
|
|
|
80
107
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
|
108
|
+
const wrapperRef = ref<HTMLDivElement | null>(null);
|
|
81
109
|
|
|
82
110
|
const ws = ref<WebSocket | null>(null);
|
|
83
111
|
const isConnected = ref(false);
|
|
84
112
|
|
|
113
|
+
// 缩放相关状态
|
|
114
|
+
const scale = ref(1);
|
|
115
|
+
const isResizing = ref(false);
|
|
116
|
+
const resizeHandle = ref<string | null>(null);
|
|
117
|
+
const startX = ref(0);
|
|
118
|
+
const startY = ref(0);
|
|
119
|
+
const startScale = ref(1);
|
|
120
|
+
|
|
85
121
|
const connectWebSocket = () => {
|
|
86
122
|
if (ws.value) {
|
|
87
123
|
ws.value.close();
|
|
@@ -129,7 +165,7 @@ const handleMessage = (msg: any) => {
|
|
|
129
165
|
emit('update:visible', true);
|
|
130
166
|
} else if (action === 'pause') {
|
|
131
167
|
emit('update:isPlaying', false);
|
|
132
|
-
|
|
168
|
+
// 暂停时不隐藏视频
|
|
133
169
|
} else if (action === 'stop') {
|
|
134
170
|
emit('update:isPlaying', false);
|
|
135
171
|
emit('update:visible', false);
|
|
@@ -179,6 +215,62 @@ const handleEnded = () => {
|
|
|
179
215
|
emit('ended');
|
|
180
216
|
};
|
|
181
217
|
|
|
218
|
+
// 缩放功能
|
|
219
|
+
const startResize = (handle: string, event: MouseEvent | TouchEvent) => {
|
|
220
|
+
isResizing.value = true;
|
|
221
|
+
resizeHandle.value = handle;
|
|
222
|
+
startScale.value = scale.value;
|
|
223
|
+
|
|
224
|
+
const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
|
|
225
|
+
const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
|
|
226
|
+
startX.value = clientX;
|
|
227
|
+
startY.value = clientY;
|
|
228
|
+
|
|
229
|
+
document.addEventListener('mousemove', handleResize);
|
|
230
|
+
document.addEventListener('mouseup', stopResize);
|
|
231
|
+
document.addEventListener('touchmove', handleResize);
|
|
232
|
+
document.addEventListener('touchend', stopResize);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleResize = (event: MouseEvent | TouchEvent) => {
|
|
236
|
+
if (!isResizing.value || !wrapperRef.value) return;
|
|
237
|
+
|
|
238
|
+
const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
|
|
239
|
+
const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
|
|
240
|
+
|
|
241
|
+
const deltaX = clientX - startX.value;
|
|
242
|
+
const deltaY = clientY - startY.value;
|
|
243
|
+
|
|
244
|
+
const rect = wrapperRef.value.getBoundingClientRect();
|
|
245
|
+
const minScale = 0.5;
|
|
246
|
+
const maxScale = 2;
|
|
247
|
+
|
|
248
|
+
// 根据拖动的角计算缩放比例
|
|
249
|
+
let newScale = startScale.value;
|
|
250
|
+
const baseSize = Math.min(rect.width, rect.height);
|
|
251
|
+
|
|
252
|
+
if (resizeHandle.value === 'bottom-right') {
|
|
253
|
+
newScale = startScale.value + (deltaX + deltaY) / baseSize;
|
|
254
|
+
} else if (resizeHandle.value === 'bottom-left') {
|
|
255
|
+
newScale = startScale.value + (-deltaX + deltaY) / baseSize;
|
|
256
|
+
} else if (resizeHandle.value === 'top-right') {
|
|
257
|
+
newScale = startScale.value + (deltaX - deltaY) / baseSize;
|
|
258
|
+
} else if (resizeHandle.value === 'top-left') {
|
|
259
|
+
newScale = startScale.value + (-deltaX - deltaY) / baseSize;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
scale.value = Math.max(minScale, Math.min(maxScale, newScale));
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const stopResize = () => {
|
|
266
|
+
isResizing.value = false;
|
|
267
|
+
resizeHandle.value = null;
|
|
268
|
+
document.removeEventListener('mousemove', handleResize);
|
|
269
|
+
document.removeEventListener('mouseup', stopResize);
|
|
270
|
+
document.removeEventListener('touchmove', handleResize);
|
|
271
|
+
document.removeEventListener('touchend', stopResize);
|
|
272
|
+
};
|
|
273
|
+
|
|
182
274
|
onMounted(() => {
|
|
183
275
|
if (props.isPlaying && videoRef.value) {
|
|
184
276
|
videoRef.value.play().catch(e => console.error('Video play failed:', e));
|
|
@@ -228,12 +320,56 @@ onUnmounted(() => {
|
|
|
228
320
|
width: 100%;
|
|
229
321
|
aspect-ratio: 9 / 16; /* Portrait ratio for digital human */
|
|
230
322
|
background: #000;
|
|
323
|
+
overflow: visible;
|
|
231
324
|
}
|
|
232
325
|
|
|
233
326
|
.persona-video {
|
|
234
327
|
width: 100%;
|
|
235
328
|
height: 100%;
|
|
236
329
|
object-fit: cover;
|
|
330
|
+
transform-origin: center;
|
|
331
|
+
transition: transform 0.1s ease-out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.resize-handle {
|
|
335
|
+
position: absolute;
|
|
336
|
+
width: 20px;
|
|
337
|
+
height: 20px;
|
|
338
|
+
background: rgba(0, 0, 0, 0.5);
|
|
339
|
+
border: 2px solid rgba(255, 255, 255, 0.8);
|
|
340
|
+
border-radius: 4px;
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
z-index: 20;
|
|
343
|
+
transition: background 0.2s, border-color 0.2s;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.resize-handle:hover {
|
|
347
|
+
background: rgba(0, 0, 0, 0.7);
|
|
348
|
+
border-color: rgba(255, 255, 255, 1);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.resize-handle.top-left {
|
|
352
|
+
top: -10px;
|
|
353
|
+
left: -10px;
|
|
354
|
+
cursor: nwse-resize;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.resize-handle.top-right {
|
|
358
|
+
top: -10px;
|
|
359
|
+
right: -10px;
|
|
360
|
+
cursor: nesw-resize;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.resize-handle.bottom-left {
|
|
364
|
+
bottom: -10px;
|
|
365
|
+
left: -10px;
|
|
366
|
+
cursor: nesw-resize;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.resize-handle.bottom-right {
|
|
370
|
+
bottom: -10px;
|
|
371
|
+
right: -10px;
|
|
372
|
+
cursor: nwse-resize;
|
|
237
373
|
}
|
|
238
374
|
|
|
239
375
|
.overlay {
|