virtual-human-cf 1.0.7 → 1.0.10
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 +17 -3
- package/dist/components/VirtualHumanPersona.vue.d.ts +19 -0
- package/dist/style.css +1 -1
- package/dist/virtual-human-cf.es.js +138 -185
- package/dist/virtual-human-cf.umd.js +1 -1
- package/package.json +1 -1
- package/src/components/VirtualHumanPersona.vue +10 -150
package/README.md
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
- 暗黑模式支持
|
|
12
12
|
- 响应式设计
|
|
13
13
|
|
|
14
|
+
## 更新日志
|
|
15
|
+
|
|
16
|
+
### 1.0.10
|
|
17
|
+
- 新增VirtualHumanPersona组件styles属性,用于控制视频容器 宽高 绝对定位位置等
|
|
18
|
+
|
|
14
19
|
## 安装
|
|
15
20
|
|
|
16
21
|
```bash
|
|
@@ -53,6 +58,7 @@ import { VirtualHumanPersona, VirtualHumanEventAdapter } from 'virtual-human-cf'
|
|
|
53
58
|
| isDark | Boolean | false | 否 | 是否暗黑模式 |
|
|
54
59
|
| screenClientId | String | - | 否 | 屏幕客户端 ID |
|
|
55
60
|
| wsUrl | String | - | 否 | WebSocket 连接地址 |
|
|
61
|
+
| styles | Object | - | 否 | 组件样式,包含 width、height、left、bottom 等属性 |
|
|
56
62
|
|
|
57
63
|
#### Events
|
|
58
64
|
|
|
@@ -73,6 +79,7 @@ import { VirtualHumanPersona, VirtualHumanEventAdapter } from 'virtual-human-cf'
|
|
|
73
79
|
:is-dark="false"
|
|
74
80
|
:screen-client-id="clientId"
|
|
75
81
|
:ws-url="websocketUrl"
|
|
82
|
+
:styles="styles"
|
|
76
83
|
@update:isPlaying="onPlayingChange"
|
|
77
84
|
@ended="onEnded"
|
|
78
85
|
/>
|
|
@@ -86,7 +93,12 @@ const videoUrl = ref('https://example.com/video.mp4');
|
|
|
86
93
|
const playing = ref(false);
|
|
87
94
|
const clientId = ref('screen-123');
|
|
88
95
|
const websocketUrl = ref('ws://localhost:8080/ws');
|
|
89
|
-
|
|
96
|
+
const styles = ref({
|
|
97
|
+
width:'400px',
|
|
98
|
+
height:'500px',
|
|
99
|
+
left: '16px',
|
|
100
|
+
bottom:'16px'
|
|
101
|
+
})
|
|
90
102
|
const onPlayingChange = (isPlaying) => {
|
|
91
103
|
console.log('播放状态改变:', isPlaying);
|
|
92
104
|
};
|
|
@@ -119,8 +131,9 @@ const onEnded = () => {
|
|
|
119
131
|
| error | 错误事件 | 错误信息 |
|
|
120
132
|
| playComplete | 播放完成,数字人对话结束,关闭数字人 | - |
|
|
121
133
|
|
|
122
|
-
```
|
|
123
|
-
eventNotifaction
|
|
134
|
+
```js
|
|
135
|
+
// eventNotifaction 载荷数据
|
|
136
|
+
{
|
|
124
137
|
|
|
125
138
|
"type": "send_event",
|
|
126
139
|
// 自定义事件名称
|
|
@@ -192,6 +205,7 @@ const onError = (error) => {
|
|
|
192
205
|
:is-dark="isDarkMode"
|
|
193
206
|
:screen-client-id="activeScreenClientId"
|
|
194
207
|
:ws-url="websocketUrl"
|
|
208
|
+
:styles="styles"
|
|
195
209
|
@update:isPlaying="onPersonaPlayingUpdate"
|
|
196
210
|
@update:visible="onPersonaVisibleUpdate"
|
|
197
211
|
@ended="onVideoEnded"
|
|
@@ -27,6 +27,15 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
|
|
|
27
27
|
type: StringConstructor;
|
|
28
28
|
required: false;
|
|
29
29
|
};
|
|
30
|
+
styles: {
|
|
31
|
+
type: ObjectConstructor;
|
|
32
|
+
default: () => {
|
|
33
|
+
width: string;
|
|
34
|
+
height: string;
|
|
35
|
+
left: string;
|
|
36
|
+
bottom: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
30
39
|
}>, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
31
40
|
ended: (...args: any[]) => void;
|
|
32
41
|
"update:isPlaying": (...args: any[]) => void;
|
|
@@ -60,6 +69,15 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
|
|
|
60
69
|
type: StringConstructor;
|
|
61
70
|
required: false;
|
|
62
71
|
};
|
|
72
|
+
styles: {
|
|
73
|
+
type: ObjectConstructor;
|
|
74
|
+
default: () => {
|
|
75
|
+
width: string;
|
|
76
|
+
height: string;
|
|
77
|
+
left: string;
|
|
78
|
+
bottom: string;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
63
81
|
}>> & Readonly<{
|
|
64
82
|
onEnded?: ((...args: any[]) => any) | undefined;
|
|
65
83
|
"onUpdate:isPlaying"?: ((...args: any[]) => any) | undefined;
|
|
@@ -69,5 +87,6 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
|
|
|
69
87
|
isPlaying: boolean;
|
|
70
88
|
muted: boolean;
|
|
71
89
|
isDark: boolean;
|
|
90
|
+
styles: Record<string, any>;
|
|
72
91
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
|
|
73
92
|
export default _default;
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.fade-enter-active[data-v-
|
|
1
|
+
.fade-enter-active[data-v-892abb8d],.fade-leave-active[data-v-892abb8d]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-892abb8d],.fade-leave-to[data-v-892abb8d]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-892abb8d]{position:fixed;z-index:2147483647;overflow:visible}.video-wrapper[data-v-892abb8d]{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-892abb8d]:hover{border-color:#409eff}.virtual-human-container.is-dark .video-wrapper[data-v-892abb8d]{background:#1f2937;box-shadow:0 10px 25px #00000080}.persona-video[data-v-892abb8d]{width:100%;height:100%;object-fit:cover;transform-origin:center;transition:transform .1s ease-out}.resize-handle[data-v-892abb8d]{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-892abb8d]{opacity:1;pointer-events:auto}.resize-handle[data-v-892abb8d]:hover{background:#000000b3;border-color:#fff}.resize-handle.top-left[data-v-892abb8d]{top:10px;left:10px;cursor:nwse-resize}.resize-handle.top-right[data-v-892abb8d]{top:10px;right:10px;cursor:nesw-resize}.resize-handle.bottom-left[data-v-892abb8d]{bottom:10px;left:10px;cursor:nesw-resize}.resize-handle.bottom-right[data-v-892abb8d]{bottom:10px;right:10px;cursor:nwse-resize}.overlay[data-v-892abb8d]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-892abb8d]{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-892abb8d]{background:#ef4444cc}.status-badge.playing[data-v-892abb8d]{background:#22c55ecc}.dot[data-v-892abb8d]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-892abb8d{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-892abb8d]{animation:pulse-892abb8d 2s cubic-bezier(.4,0,.6,1) infinite}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { defineComponent as
|
|
2
|
-
const
|
|
3
|
-
key: 0,
|
|
4
|
-
class: "status-badge paused"
|
|
5
|
-
}, oe = {
|
|
6
|
-
key: 1,
|
|
7
|
-
class: "status-badge playing"
|
|
8
|
-
}, re = /* @__PURE__ */ j({
|
|
1
|
+
import { defineComponent as H, ref as w, watch as C, onMounted as E, onUnmounted as x, openBlock as A, createBlock as B, Transition as W, withCtx as $, createElementBlock as R, normalizeStyle as F, normalizeClass as N, createElementVNode as U, createCommentVNode as T, renderSlot as q } from "vue";
|
|
2
|
+
const M = ["src", "muted"], z = /* @__PURE__ */ H({
|
|
9
3
|
__name: "VirtualHumanPersona",
|
|
10
4
|
props: {
|
|
11
5
|
// 视频源URL
|
|
@@ -43,141 +37,100 @@ const ae = ["src", "muted"], se = { class: "overlay" }, le = {
|
|
|
43
37
|
wsUrl: {
|
|
44
38
|
type: String,
|
|
45
39
|
required: !1
|
|
40
|
+
},
|
|
41
|
+
styles: {
|
|
42
|
+
type: Object,
|
|
43
|
+
default: () => ({
|
|
44
|
+
width: "400px",
|
|
45
|
+
height: "500px",
|
|
46
|
+
left: "16px",
|
|
47
|
+
bottom: "16px"
|
|
48
|
+
})
|
|
46
49
|
}
|
|
47
50
|
},
|
|
48
51
|
emits: ["update:isPlaying", "ended", "update:visible"],
|
|
49
|
-
setup(
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
+
setup(r, { emit: g }) {
|
|
53
|
+
const e = r, a = g, n = w(null), h = w(null), t = w(null), d = w(!1), f = () => {
|
|
54
|
+
if (t.value && t.value.close(), !(!e.wsUrl || !e.screenClientId))
|
|
52
55
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
56
|
+
const l = new URL(e.wsUrl);
|
|
57
|
+
l.searchParams.append("sessionId", e.screenClientId + "-persona"), t.value = new WebSocket(l.toString()), t.value.onopen = () => {
|
|
58
|
+
d.value = !0, console.log(`[VirtualHumanPersona] Connected to ${e.wsUrl} for session ${e.screenClientId}-persona`);
|
|
59
|
+
}, t.value.onmessage = (u) => {
|
|
57
60
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
} catch (
|
|
61
|
-
console.error("[VirtualHumanPersona] Failed to parse message:",
|
|
61
|
+
const i = JSON.parse(u.data);
|
|
62
|
+
m(i);
|
|
63
|
+
} catch (i) {
|
|
64
|
+
console.error("[VirtualHumanPersona] Failed to parse message:", u.data, i);
|
|
62
65
|
}
|
|
63
|
-
},
|
|
64
|
-
console.error("[VirtualHumanPersona] WebSocket error:",
|
|
65
|
-
},
|
|
66
|
-
|
|
66
|
+
}, t.value.onerror = (u) => {
|
|
67
|
+
console.error("[VirtualHumanPersona] WebSocket error:", u);
|
|
68
|
+
}, t.value.onclose = () => {
|
|
69
|
+
d.value = !1, console.log("[VirtualHumanPersona] WebSocket disconnected");
|
|
67
70
|
};
|
|
68
|
-
} catch (
|
|
69
|
-
console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",
|
|
71
|
+
} catch (l) {
|
|
72
|
+
console.error("[VirtualHumanPersona] Failed to initialize WebSocket:", l);
|
|
70
73
|
}
|
|
71
|
-
},
|
|
72
|
-
const { type:
|
|
73
|
-
|
|
74
|
+
}, m = (l) => {
|
|
75
|
+
const { type: u, action: i } = l;
|
|
76
|
+
u === "control" && (i === "play" || i === "resume" ? (a("update:isPlaying", !0), a("update:visible", !0)) : i === "pause" ? a("update:isPlaying", !1) : i === "stop" && (a("update:isPlaying", !1), a("update:visible", !1), n.value && (n.value.currentTime = 0)));
|
|
74
77
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}),
|
|
78
|
-
|
|
79
|
-
}),
|
|
80
|
-
|
|
78
|
+
C(() => e.screenClientId, () => {
|
|
79
|
+
e.screenClientId && e.wsUrl && f();
|
|
80
|
+
}), C(() => e.wsUrl, () => {
|
|
81
|
+
e.screenClientId && e.wsUrl && f();
|
|
82
|
+
}), C(() => e.isPlaying, (l) => {
|
|
83
|
+
n.value && (l ? n.value.play().catch((u) => console.error("Video play failed:", u)) : n.value.pause());
|
|
81
84
|
});
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
|
|
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);
|
|
85
|
+
const v = () => {
|
|
86
|
+
e.isPlaying || a("update:isPlaying", !0);
|
|
87
|
+
}, S = () => {
|
|
88
|
+
e.isPlaying && a("update:isPlaying", !1);
|
|
89
|
+
}, k = () => {
|
|
90
|
+
a("update:isPlaying", !1), a("ended");
|
|
100
91
|
};
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
}),
|
|
104
|
-
|
|
105
|
-
}), (
|
|
106
|
-
default:
|
|
107
|
-
|
|
92
|
+
return E(() => {
|
|
93
|
+
e.isPlaying && n.value && n.value.play().catch((l) => console.error("Video play failed:", l)), e.screenClientId && e.wsUrl && f();
|
|
94
|
+
}), x(() => {
|
|
95
|
+
t.value && t.value.close();
|
|
96
|
+
}), (l, u) => (A(), B(W, { name: "fade" }, {
|
|
97
|
+
default: $(() => [
|
|
98
|
+
r.visible ? (A(), R("div", {
|
|
108
99
|
key: 0,
|
|
109
|
-
class:
|
|
110
|
-
style:
|
|
111
|
-
width: y.value + "px",
|
|
112
|
-
height: g.value + "px",
|
|
113
|
-
left: w.value + "px",
|
|
114
|
-
bottom: z.value + "px"
|
|
115
|
-
})
|
|
100
|
+
class: N(["virtual-human-container", { "is-dark": r.isDark }]),
|
|
101
|
+
style: F(r.styles)
|
|
116
102
|
}, [
|
|
117
|
-
|
|
103
|
+
U("div", {
|
|
118
104
|
class: "video-wrapper",
|
|
119
105
|
ref_key: "wrapperRef",
|
|
120
|
-
ref:
|
|
106
|
+
ref: h
|
|
121
107
|
}, [
|
|
122
|
-
|
|
108
|
+
U("video", {
|
|
123
109
|
ref_key: "videoRef",
|
|
124
|
-
ref:
|
|
125
|
-
src:
|
|
110
|
+
ref: n,
|
|
111
|
+
src: r.videoSrc,
|
|
126
112
|
class: "persona-video",
|
|
127
|
-
muted:
|
|
113
|
+
muted: r.muted,
|
|
128
114
|
playsinline: "",
|
|
129
115
|
loop: "",
|
|
130
116
|
autoPlay: "",
|
|
131
117
|
disablePictureInPicture: "false",
|
|
132
|
-
onPlay:
|
|
133
|
-
onPause:
|
|
134
|
-
onEnded:
|
|
135
|
-
}, null, 40,
|
|
136
|
-
c.visible ? (S(), E("div", {
|
|
137
|
-
key: 0,
|
|
138
|
-
class: "resize-handle top-left",
|
|
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", {
|
|
143
|
-
key: 1,
|
|
144
|
-
class: "resize-handle top-right",
|
|
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", {
|
|
149
|
-
key: 2,
|
|
150
|
-
class: "resize-handle bottom-left",
|
|
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", {
|
|
155
|
-
key: 3,
|
|
156
|
-
class: "resize-handle bottom-right",
|
|
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)
|
|
167
|
-
])]))
|
|
168
|
-
])
|
|
118
|
+
onPlay: v,
|
|
119
|
+
onPause: S,
|
|
120
|
+
onEnded: k
|
|
121
|
+
}, null, 40, M)
|
|
169
122
|
], 512)
|
|
170
123
|
], 6)) : T("", !0)
|
|
171
124
|
]),
|
|
172
125
|
_: 1
|
|
173
126
|
}));
|
|
174
127
|
}
|
|
175
|
-
}),
|
|
176
|
-
const
|
|
177
|
-
for (const [
|
|
178
|
-
|
|
179
|
-
return
|
|
180
|
-
},
|
|
128
|
+
}), D = (r, g) => {
|
|
129
|
+
const e = r.__vccOpts || r;
|
|
130
|
+
for (const [a, n] of g)
|
|
131
|
+
e[a] = n;
|
|
132
|
+
return e;
|
|
133
|
+
}, O = /* @__PURE__ */ D(z, [["__scopeId", "data-v-892abb8d"]]), J = /* @__PURE__ */ H({
|
|
181
134
|
__name: "VirtualHumanEventAdapter",
|
|
182
135
|
props: {
|
|
183
136
|
// 屏幕客户端ID
|
|
@@ -192,114 +145,114 @@ const ae = ["src", "muted"], se = { class: "overlay" }, le = {
|
|
|
192
145
|
}
|
|
193
146
|
},
|
|
194
147
|
emits: ["eventNotifaction", "end", "pause", "connected", "error", "playComplete"],
|
|
195
|
-
setup(
|
|
196
|
-
const
|
|
197
|
-
let
|
|
198
|
-
const
|
|
199
|
-
|
|
148
|
+
setup(r, { emit: g }) {
|
|
149
|
+
const e = r, a = g, n = w(null), h = w(!1);
|
|
150
|
+
let t = null, d = 0, f = !1, m = 0, v = !1;
|
|
151
|
+
const S = () => {
|
|
152
|
+
t || (t = new (window.AudioContext || window.webkitAudioContext)({
|
|
200
153
|
sampleRate: 24e3
|
|
201
|
-
})),
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
},
|
|
205
|
-
if (
|
|
154
|
+
})), t.state === "suspended" && !v && t.resume();
|
|
155
|
+
}, k = () => {
|
|
156
|
+
f && m === 0 && (a("playComplete", e.screenClientId), f = !1);
|
|
157
|
+
}, l = (s) => {
|
|
158
|
+
if (S(), !!t)
|
|
206
159
|
try {
|
|
207
|
-
const
|
|
208
|
-
for (let
|
|
209
|
-
b[
|
|
210
|
-
const
|
|
211
|
-
for (let
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
160
|
+
const o = window.atob(s), c = o.length, b = new Uint8Array(c);
|
|
161
|
+
for (let p = 0; p < c; p++)
|
|
162
|
+
b[p] = o.charCodeAt(p);
|
|
163
|
+
const y = new Int16Array(b.buffer), V = new Float32Array(y.length);
|
|
164
|
+
for (let p = 0; p < y.length; p++)
|
|
165
|
+
V[p] = y[p] / 32768;
|
|
166
|
+
const I = t.createBuffer(1, V.length, 24e3);
|
|
167
|
+
I.getChannelData(0).set(V);
|
|
168
|
+
const P = t.createBufferSource();
|
|
169
|
+
P.buffer = I, P.connect(t.destination), d < t.currentTime && (d = t.currentTime), P.start(d), d += I.duration, m++, P.onended = () => {
|
|
170
|
+
m--, k();
|
|
218
171
|
};
|
|
219
|
-
} catch (
|
|
220
|
-
console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",
|
|
172
|
+
} catch (o) {
|
|
173
|
+
console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:", o);
|
|
221
174
|
}
|
|
222
|
-
},
|
|
175
|
+
}, u = (s) => {
|
|
223
176
|
switch (s) {
|
|
224
177
|
case "play":
|
|
225
|
-
|
|
178
|
+
f = !1, m = 0, d = 0, v = !1, t && t.state === "suspended" && t.resume();
|
|
226
179
|
break;
|
|
227
180
|
case "resume":
|
|
228
|
-
|
|
181
|
+
v = !1, t && t.state === "suspended" && t.resume();
|
|
229
182
|
break;
|
|
230
183
|
case "pause":
|
|
231
|
-
|
|
184
|
+
v = !0, t && t.state === "running" && t.suspend();
|
|
232
185
|
break;
|
|
233
186
|
case "stop":
|
|
234
|
-
|
|
187
|
+
v = !1, t && (t.close(), t = null), d = 0, f = !1, m = 0;
|
|
235
188
|
break;
|
|
236
189
|
case "tts_complete":
|
|
237
|
-
|
|
190
|
+
f = !0, k();
|
|
238
191
|
break;
|
|
239
192
|
default:
|
|
240
193
|
console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${s}`);
|
|
241
194
|
}
|
|
242
|
-
},
|
|
243
|
-
|
|
195
|
+
}, i = () => {
|
|
196
|
+
n.value && n.value.close();
|
|
244
197
|
try {
|
|
245
|
-
const s = new URL(
|
|
246
|
-
s.searchParams.append("sessionId",
|
|
247
|
-
|
|
248
|
-
},
|
|
198
|
+
const s = new URL(e.wsUrl);
|
|
199
|
+
s.searchParams.append("sessionId", e.screenClientId + "-event"), n.value = new WebSocket(s.toString()), n.value.onopen = () => {
|
|
200
|
+
h.value = !0, a("connected"), console.log(`[VirtualHumanEventAdapter] Connected to ${e.wsUrl} for session ${e.screenClientId}-event`);
|
|
201
|
+
}, n.value.onmessage = (o) => {
|
|
249
202
|
try {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
} catch (
|
|
253
|
-
console.error("[VirtualHumanEventAdapter] Failed to parse message:",
|
|
203
|
+
const c = JSON.parse(o.data);
|
|
204
|
+
_(c);
|
|
205
|
+
} catch (c) {
|
|
206
|
+
console.error("[VirtualHumanEventAdapter] Failed to parse message:", o.data, c);
|
|
254
207
|
}
|
|
255
|
-
},
|
|
256
|
-
console.error("[VirtualHumanEventAdapter] WebSocket error:",
|
|
257
|
-
},
|
|
258
|
-
|
|
208
|
+
}, n.value.onerror = (o) => {
|
|
209
|
+
console.error("[VirtualHumanEventAdapter] WebSocket error:", o), a("error", o);
|
|
210
|
+
}, n.value.onclose = () => {
|
|
211
|
+
h.value = !1, console.log("[VirtualHumanEventAdapter] WebSocket disconnected");
|
|
259
212
|
};
|
|
260
213
|
} catch (s) {
|
|
261
|
-
console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", s),
|
|
214
|
+
console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:", s), a("error", s);
|
|
262
215
|
}
|
|
263
|
-
},
|
|
264
|
-
const { type:
|
|
265
|
-
switch (console.log("msgmsg-002", s),
|
|
216
|
+
}, _ = (s) => {
|
|
217
|
+
const { type: o, payload: c, action: b } = s;
|
|
218
|
+
switch (console.log("msgmsg-002", s), o) {
|
|
266
219
|
case "audio":
|
|
267
|
-
const
|
|
268
|
-
|
|
220
|
+
const y = (c == null ? void 0 : c.data) || s.data;
|
|
221
|
+
y && l(y);
|
|
269
222
|
break;
|
|
270
223
|
case "send_event":
|
|
271
|
-
s.event &&
|
|
224
|
+
s.event && a("eventNotifaction", s);
|
|
272
225
|
break;
|
|
273
226
|
case "control":
|
|
274
|
-
b &&
|
|
227
|
+
b && u(b);
|
|
275
228
|
break;
|
|
276
229
|
case "end":
|
|
277
|
-
|
|
230
|
+
a("end", c);
|
|
278
231
|
break;
|
|
279
232
|
case "pause":
|
|
280
|
-
|
|
233
|
+
a("pause", c);
|
|
281
234
|
break;
|
|
282
235
|
default:
|
|
283
|
-
console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${
|
|
236
|
+
console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${o}`);
|
|
284
237
|
}
|
|
285
238
|
};
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
}),
|
|
289
|
-
|
|
290
|
-
}),
|
|
291
|
-
|
|
292
|
-
}),
|
|
293
|
-
|
|
294
|
-
}), (s,
|
|
239
|
+
return C(() => e.screenClientId, () => {
|
|
240
|
+
e.screenClientId && e.wsUrl && i();
|
|
241
|
+
}), C(() => e.wsUrl, () => {
|
|
242
|
+
e.screenClientId && e.wsUrl && i();
|
|
243
|
+
}), E(() => {
|
|
244
|
+
e.screenClientId && e.wsUrl && i();
|
|
245
|
+
}), x(() => {
|
|
246
|
+
n.value && n.value.close(), t && t.close();
|
|
247
|
+
}), (s, o) => q(s.$slots, "default");
|
|
295
248
|
}
|
|
296
|
-
}),
|
|
297
|
-
|
|
298
|
-
},
|
|
299
|
-
install:
|
|
249
|
+
}), L = (r) => {
|
|
250
|
+
r.component("VirtualHumanPersona", O), r.component("VirtualHumanEventAdapter", J);
|
|
251
|
+
}, G = {
|
|
252
|
+
install: L
|
|
300
253
|
};
|
|
301
254
|
export {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
255
|
+
J as VirtualHumanEventAdapter,
|
|
256
|
+
O as VirtualHumanPersona,
|
|
257
|
+
G as default
|
|
305
258
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(f,n){typeof exports=="object"&&typeof module<"u"?n(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],n):(f=typeof globalThis<"u"?globalThis:f||self,n(f.VirtualHumanCf={},f.Vue))})(this,function(f,n){"use strict";const H=["src","muted"],U=((o,b)=>{const e=o.__vccOpts||o;for(const[s,a]of b)e[s]=a;return e})(n.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},styles:{type:Object,default:()=>({width:"400px",height:"500px",left:"16px",bottom:"16px"})}},emits:["update:isPlaying","ended","update:visible"],setup(o,{emit:b}){const e=o,s=b,a=n.ref(null),C=n.ref(null),t=n.ref(null),p=n.ref(!1),m=()=>{if(t.value&&t.value.close(),!(!e.wsUrl||!e.screenClientId))try{const i=new URL(e.wsUrl);i.searchParams.append("sessionId",e.screenClientId+"-persona"),t.value=new WebSocket(i.toString()),t.value.onopen=()=>{p.value=!0,console.log(`[VirtualHumanPersona] Connected to ${e.wsUrl} for session ${e.screenClientId}-persona`)},t.value.onmessage=u=>{try{const c=JSON.parse(u.data);w(c)}catch(c){console.error("[VirtualHumanPersona] Failed to parse message:",u.data,c)}},t.value.onerror=u=>{console.error("[VirtualHumanPersona] WebSocket error:",u)},t.value.onclose=()=>{p.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(i){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",i)}},w=i=>{const{type:u,action:c}=i;u==="control"&&(c==="play"||c==="resume"?(s("update:isPlaying",!0),s("update:visible",!0)):c==="pause"?s("update:isPlaying",!1):c==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),a.value&&(a.value.currentTime=0)))};n.watch(()=>e.screenClientId,()=>{e.screenClientId&&e.wsUrl&&m()}),n.watch(()=>e.wsUrl,()=>{e.screenClientId&&e.wsUrl&&m()}),n.watch(()=>e.isPlaying,i=>{a.value&&(i?a.value.play().catch(u=>console.error("Video play failed:",u)):a.value.pause())});const g=()=>{e.isPlaying||s("update:isPlaying",!0)},S=()=>{e.isPlaying&&s("update:isPlaying",!1)},k=()=>{s("update:isPlaying",!1),s("ended")};return n.onMounted(()=>{e.isPlaying&&a.value&&a.value.play().catch(i=>console.error("Video play failed:",i)),e.screenClientId&&e.wsUrl&&m()}),n.onUnmounted(()=>{t.value&&t.value.close()}),(i,u)=>(n.openBlock(),n.createBlock(n.Transition,{name:"fade"},{default:n.withCtx(()=>[o.visible?(n.openBlock(),n.createElementBlock("div",{key:0,class:n.normalizeClass(["virtual-human-container",{"is-dark":o.isDark}]),style:n.normalizeStyle(o.styles)},[n.createElementVNode("div",{class:"video-wrapper",ref_key:"wrapperRef",ref:C},[n.createElementVNode("video",{ref_key:"videoRef",ref:a,src:o.videoSrc,class:"persona-video",muted:o.muted,playsinline:"",loop:"",autoPlay:"",disablePictureInPicture:"false",onPlay:g,onPause:S,onEnded:k},null,40,H)],512)],6)):n.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-892abb8d"]]),A=n.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["eventNotifaction","end","pause","connected","error","playComplete"],setup(o,{emit:b}){const e=o,s=b,a=n.ref(null),C=n.ref(!1);let t=null,p=0,m=!1,w=0,g=!1;const S=()=>{t||(t=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),t.state==="suspended"&&!g&&t.resume()},k=()=>{m&&w===0&&(s("playComplete",e.screenClientId),m=!1)},i=r=>{if(S(),!!t)try{const l=window.atob(r),d=l.length,v=new Uint8Array(d);for(let y=0;y<d;y++)v[y]=l.charCodeAt(y);const h=new Int16Array(v.buffer),V=new Float32Array(h.length);for(let y=0;y<h.length;y++)V[y]=h[y]/32768;const I=t.createBuffer(1,V.length,24e3);I.getChannelData(0).set(V);const P=t.createBufferSource();P.buffer=I,P.connect(t.destination),p<t.currentTime&&(p=t.currentTime),P.start(p),p+=I.duration,w++,P.onended=()=>{w--,k()}}catch(l){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",l)}},u=r=>{switch(r){case"play":m=!1,w=0,p=0,g=!1,t&&t.state==="suspended"&&t.resume();break;case"resume":g=!1,t&&t.state==="suspended"&&t.resume();break;case"pause":g=!0,t&&t.state==="running"&&t.suspend();break;case"stop":g=!1,t&&(t.close(),t=null),p=0,m=!1,w=0;break;case"tts_complete":m=!0,k();break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${r}`)}},c=()=>{a.value&&a.value.close();try{const r=new URL(e.wsUrl);r.searchParams.append("sessionId",e.screenClientId+"-event"),a.value=new WebSocket(r.toString()),a.value.onopen=()=>{C.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${e.wsUrl} for session ${e.screenClientId}-event`)},a.value.onmessage=l=>{try{const d=JSON.parse(l.data);E(d)}catch(d){console.error("[VirtualHumanEventAdapter] Failed to parse message:",l.data,d)}},a.value.onerror=l=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",l),s("error",l)},a.value.onclose=()=>{C.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(r){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",r),s("error",r)}},E=r=>{const{type:l,payload:d,action:v}=r;switch(console.log("msgmsg-002",r),l){case"audio":const h=(d==null?void 0:d.data)||r.data;h&&i(h);break;case"send_event":r.event&&s("eventNotifaction",r);break;case"control":v&&u(v);break;case"end":s("end",d);break;case"pause":s("pause",d);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${l}`)}};return n.watch(()=>e.screenClientId,()=>{e.screenClientId&&e.wsUrl&&c()}),n.watch(()=>e.wsUrl,()=>{e.screenClientId&&e.wsUrl&&c()}),n.onMounted(()=>{e.screenClientId&&e.wsUrl&&c()}),n.onUnmounted(()=>{a.value&&a.value.close(),t&&t.close()}),(r,l)=>n.renderSlot(r.$slots,"default")}}),_={install:o=>{o.component("VirtualHumanPersona",U),o.component("VirtualHumanEventAdapter",A)}};f.VirtualHumanEventAdapter=A,f.VirtualHumanPersona=U,f.default=_,Object.defineProperties(f,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
CHANGED
|
@@ -4,12 +4,7 @@
|
|
|
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
|
+
:style="styles"
|
|
13
8
|
>
|
|
14
9
|
<div class="video-wrapper" ref="wrapperRef">
|
|
15
10
|
<video
|
|
@@ -25,44 +20,6 @@
|
|
|
25
20
|
@pause="handlePause"
|
|
26
21
|
@ended="handleEnded"
|
|
27
22
|
></video>
|
|
28
|
-
|
|
29
|
-
<!-- 缩放控制点 - 四个角 -->
|
|
30
|
-
<div
|
|
31
|
-
v-if="visible"
|
|
32
|
-
class="resize-handle top-left"
|
|
33
|
-
@mousedown="startResize('top-left', $event)"
|
|
34
|
-
@touchstart="startResize('top-left', $event)"
|
|
35
|
-
></div>
|
|
36
|
-
<div
|
|
37
|
-
v-if="visible"
|
|
38
|
-
class="resize-handle top-right"
|
|
39
|
-
@mousedown="startResize('top-right', $event)"
|
|
40
|
-
@touchstart="startResize('top-right', $event)"
|
|
41
|
-
></div>
|
|
42
|
-
<div
|
|
43
|
-
v-if="visible"
|
|
44
|
-
class="resize-handle bottom-left"
|
|
45
|
-
@mousedown="startResize('bottom-left', $event)"
|
|
46
|
-
@touchstart="startResize('bottom-left', $event)"
|
|
47
|
-
></div>
|
|
48
|
-
<div
|
|
49
|
-
v-if="visible"
|
|
50
|
-
class="resize-handle bottom-right"
|
|
51
|
-
@mousedown="startResize('bottom-right', $event)"
|
|
52
|
-
@touchstart="startResize('bottom-right', $event)"
|
|
53
|
-
></div>
|
|
54
|
-
|
|
55
|
-
<!-- UI Overlay for controls or status -->
|
|
56
|
-
<div class="overlay">
|
|
57
|
-
<div v-if="!isPlaying" class="status-badge paused">
|
|
58
|
-
<span class="dot"></span>
|
|
59
|
-
暂停中
|
|
60
|
-
</div>
|
|
61
|
-
<div v-else class="status-badge playing">
|
|
62
|
-
<span class="dot animate-pulse"></span>
|
|
63
|
-
播放中
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
23
|
</div>
|
|
67
24
|
</div>
|
|
68
25
|
</Transition>
|
|
@@ -106,6 +63,15 @@ const props = defineProps({
|
|
|
106
63
|
wsUrl: {
|
|
107
64
|
type: String,
|
|
108
65
|
required: false,
|
|
66
|
+
},
|
|
67
|
+
styles: {
|
|
68
|
+
type: Object,
|
|
69
|
+
default: () => ({
|
|
70
|
+
width:'400px',
|
|
71
|
+
height:'500px',
|
|
72
|
+
left: '16px',
|
|
73
|
+
bottom:'16px'
|
|
74
|
+
})
|
|
109
75
|
}
|
|
110
76
|
});
|
|
111
77
|
|
|
@@ -117,20 +83,6 @@ const wrapperRef = ref<HTMLDivElement | null>(null);
|
|
|
117
83
|
const ws = ref<WebSocket | null>(null);
|
|
118
84
|
const isConnected = ref(false);
|
|
119
85
|
|
|
120
|
-
// 缩放相关状态
|
|
121
|
-
const containerWidth = ref(400);
|
|
122
|
-
const containerHeight = ref(500);
|
|
123
|
-
const containerLeft = ref(16);
|
|
124
|
-
const containerBottom = ref(16);
|
|
125
|
-
|
|
126
|
-
const isResizing = ref(false);
|
|
127
|
-
const resizeHandle = ref<string | null>(null);
|
|
128
|
-
const startX = ref(0);
|
|
129
|
-
const startY = ref(0);
|
|
130
|
-
const startW = ref(0);
|
|
131
|
-
const startH = ref(0);
|
|
132
|
-
const startL = ref(0);
|
|
133
|
-
const startB = ref(0);
|
|
134
86
|
|
|
135
87
|
const connectWebSocket = () => {
|
|
136
88
|
if (ws.value) {
|
|
@@ -229,99 +181,7 @@ const handleEnded = () => {
|
|
|
229
181
|
emit('ended');
|
|
230
182
|
};
|
|
231
183
|
|
|
232
|
-
// 缩放功能
|
|
233
|
-
const startResize = (handle: string, event: MouseEvent | TouchEvent) => {
|
|
234
|
-
event.preventDefault();
|
|
235
|
-
isResizing.value = true;
|
|
236
|
-
resizeHandle.value = handle;
|
|
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
|
-
startX.value = clientX;
|
|
241
|
-
startY.value = clientY;
|
|
242
|
-
|
|
243
|
-
startW.value = containerWidth.value;
|
|
244
|
-
startH.value = containerHeight.value;
|
|
245
|
-
startL.value = containerLeft.value;
|
|
246
|
-
startB.value = containerBottom.value;
|
|
247
|
-
|
|
248
|
-
document.addEventListener('mousemove', handleResize);
|
|
249
|
-
document.addEventListener('mouseup', stopResize);
|
|
250
|
-
document.addEventListener('touchmove', handleResize, { passive: false });
|
|
251
|
-
document.addEventListener('touchend', stopResize);
|
|
252
|
-
};
|
|
253
184
|
|
|
254
|
-
const handleResize = (event: MouseEvent | TouchEvent) => {
|
|
255
|
-
if (!isResizing.value) return;
|
|
256
|
-
event.preventDefault();
|
|
257
|
-
|
|
258
|
-
const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX;
|
|
259
|
-
const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY;
|
|
260
|
-
|
|
261
|
-
const deltaX = clientX - startX.value;
|
|
262
|
-
const deltaY = clientY - startY.value;
|
|
263
|
-
|
|
264
|
-
const minWidth = 200;
|
|
265
|
-
const minHeight = 250;
|
|
266
|
-
const maxWidth = 800;
|
|
267
|
-
const maxHeight = 1000;
|
|
268
|
-
|
|
269
|
-
let newW = startW.value;
|
|
270
|
-
let newH = startH.value;
|
|
271
|
-
let newL = startL.value;
|
|
272
|
-
let newB = startB.value;
|
|
273
|
-
|
|
274
|
-
if (resizeHandle.value === 'bottom-right') {
|
|
275
|
-
newW = startW.value + deltaX;
|
|
276
|
-
newB = startB.value - deltaY;
|
|
277
|
-
newH = startH.value + deltaY;
|
|
278
|
-
} else if (resizeHandle.value === 'bottom-left') {
|
|
279
|
-
newL = startL.value + deltaX;
|
|
280
|
-
newW = startW.value - deltaX;
|
|
281
|
-
newB = startB.value - deltaY;
|
|
282
|
-
newH = startH.value + deltaY;
|
|
283
|
-
} else if (resizeHandle.value === 'top-right') {
|
|
284
|
-
newW = startW.value + deltaX;
|
|
285
|
-
newH = startH.value - deltaY;
|
|
286
|
-
} else if (resizeHandle.value === 'top-left') {
|
|
287
|
-
newL = startL.value + deltaX;
|
|
288
|
-
newW = startW.value - deltaX;
|
|
289
|
-
newH = startH.value - deltaY;
|
|
290
|
-
}
|
|
291
|
-
|
|
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;
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const stopResize = () => {
|
|
318
|
-
isResizing.value = false;
|
|
319
|
-
resizeHandle.value = null;
|
|
320
|
-
document.removeEventListener('mousemove', handleResize);
|
|
321
|
-
document.removeEventListener('mouseup', stopResize);
|
|
322
|
-
document.removeEventListener('touchmove', handleResize);
|
|
323
|
-
document.removeEventListener('touchend', stopResize);
|
|
324
|
-
};
|
|
325
185
|
|
|
326
186
|
onMounted(() => {
|
|
327
187
|
if (props.isPlaying && videoRef.value) {
|