virtual-human-cf 1.0.0 → 1.0.1

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 ADDED
@@ -0,0 +1,255 @@
1
+ # Virtual Human Component Framework (virtual-human-cf)
2
+
3
+ 一个基于 Vue 3 的虚拟数字人组件库,用于展示和控制数字人视频,并处理相关音频和交互事件。
4
+
5
+ ## 特性
6
+
7
+ - 支持数字人视频播放控制
8
+ - WebSocket 实时通信
9
+ - 音频播放处理
10
+ - 事件监听和响应
11
+ - 暗黑模式支持
12
+ - 响应式设计
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ npm install virtual-human-cf
18
+ ```
19
+
20
+ ## 使用方法
21
+
22
+ ### 全局注册
23
+
24
+ ```javascript
25
+ import { createApp } from 'vue';
26
+ import App from './App.vue';
27
+ import VirtualHumanCF from 'virtual-human-cf';
28
+
29
+ const app = createApp(App);
30
+ app.use(VirtualHumanCF);
31
+ ```
32
+
33
+ ### 按需导入
34
+
35
+ ```javascript
36
+ import { VirtualHumanPersona, VirtualHumanEventAdapter } from 'virtual-human-cf';
37
+ ```
38
+
39
+ ## 组件说明
40
+
41
+ ### VirtualHumanPersona
42
+
43
+ 数字人视频展示组件,负责视频播放和状态控制。
44
+
45
+ #### Props
46
+
47
+ | 属性 | 类型 | 默认值 | 必填 | 说明 |
48
+ |------|------|--------|------|------|
49
+ | videoSrc | String | - | 是 | 视频源 URL |
50
+ | visible | Boolean | true | 否 | 是否可见 |
51
+ | isPlaying | Boolean | false | 否 | 是否正在播放 |
52
+ | muted | Boolean | true | 否 | 是否静音 |
53
+ | isDark | Boolean | false | 否 | 是否暗黑模式 |
54
+ | screenClientId | String | - | 否 | 屏幕客户端 ID |
55
+ | wsUrl | String | - | 否 | WebSocket 连接地址 |
56
+
57
+ #### Events
58
+
59
+ | 事件名 | 说明 | 回调参数 |
60
+ |--------|------|----------|
61
+ | update:isPlaying | 播放状态变更 | 播放状态(Boolean) |
62
+ | update:visible | 可见性变更 | 可见状态(Boolean) |
63
+ | ended | 播放结束 | - |
64
+
65
+ #### 示例
66
+
67
+ ```vue
68
+ <template>
69
+ <VirtualHumanPersona
70
+ :video-src="videoUrl"
71
+ :is-playing="playing"
72
+ :muted="true"
73
+ :is-dark="false"
74
+ :screen-client-id="clientId"
75
+ :ws-url="websocketUrl"
76
+ @update:isPlaying="onPlayingChange"
77
+ @ended="onEnded"
78
+ />
79
+ </template>
80
+
81
+ <script setup>
82
+ import { ref } from 'vue';
83
+ import { VirtualHumanPersona } from 'virtual-human-cf';
84
+
85
+ const videoUrl = ref('https://example.com/video.mp4');
86
+ const playing = ref(false);
87
+ const clientId = ref('screen-123');
88
+ const websocketUrl = ref('ws://localhost:8080/ws');
89
+
90
+ const onPlayingChange = (isPlaying) => {
91
+ console.log('播放状态改变:', isPlaying);
92
+ };
93
+
94
+ const onEnded = () => {
95
+ console.log('播放结束');
96
+ };
97
+ </script>
98
+ ```
99
+
100
+ ### VirtualHumanEventAdapter
101
+
102
+ 数字人事件适配器组件,负责处理 WebSocket 消息、音频播放和各种交互事件。
103
+
104
+ #### Props
105
+
106
+ | 属性 | 类型 | 默认值 | 必填 | 说明 |
107
+ |------|------|--------|------|------|
108
+ | screenClientId | String | - | 是 | 屏幕客户端 ID |
109
+ | wsUrl | String | - | 是 | WebSocket 连接地址 |
110
+
111
+ #### Events
112
+
113
+ | 事件名 | 说明 | 回调参数 |
114
+ |--------|------|----------|
115
+ | connected | WebSocket 连接成功 | - |
116
+ | highlight | 高亮大屏某区域 | 载荷数据 |
117
+ | showDialog | 显示弹窗 | 载荷数据 |
118
+ | end | 数字人对话结束 | 载荷数据 |
119
+ | pause | 暂停播放 | 载荷数据 |
120
+ | error | 错误事件 | 错误信息 |
121
+
122
+ #### 示例
123
+
124
+ ```vue
125
+ <template>
126
+ <VirtualHumanEventAdapter
127
+ :screen-client-id="clientId"
128
+ :ws-url="websocketUrl"
129
+ @connected="onConnected"
130
+ @highlight="onHighlight"
131
+ @showDialog="onShowDialog"
132
+ @end="onEnd"
133
+ @error="onError"
134
+ >
135
+ <div>其他内容</div>
136
+ </VirtualHumanEventAdapter>
137
+ </template>
138
+
139
+ <script setup>
140
+ import { VirtualHumanEventAdapter } from 'virtual-human-cf';
141
+
142
+ const clientId = 'screen-123';
143
+ const websocketUrl = 'ws://localhost:8080/ws';
144
+
145
+ const onConnected = () => {
146
+ console.log('WebSocket 连接成功');
147
+ };
148
+
149
+ const onHighlight = (payload) => {
150
+ console.log('高亮事件:', payload);
151
+ };
152
+
153
+ const onShowDialog = (payload) => {
154
+ console.log('显示弹窗:', payload);
155
+ };
156
+
157
+ const onEnd = (payload) => {
158
+ console.log('对话结束:', payload);
159
+ };
160
+
161
+ const onError = (error) => {
162
+ console.error('错误:', error);
163
+ };
164
+ </script>
165
+ ```
166
+
167
+ ## 完整示例
168
+
169
+ 以下是一个结合两个组件使用的完整示例:
170
+
171
+ ```vue
172
+ <template>
173
+ <div class="container">
174
+ <VirtualHumanPersona
175
+ :video-src="videoUrl"
176
+ :is-playing="isPlaying"
177
+ :muted="true"
178
+ :is-dark="isDarkMode"
179
+ :screen-client-id="clientId"
180
+ :ws-url="websocketUrl"
181
+ @update:isPlaying="(val) => isPlaying = val"
182
+ @ended="onVideoEnded"
183
+ />
184
+
185
+ <VirtualHumanEventAdapter
186
+ :screen-client-id="clientId"
187
+ :ws-url="websocketUrl"
188
+ @connected="onConnected"
189
+ @highlight="onHighlight"
190
+ @showDialog="onShowDialog"
191
+ @end="onEnd"
192
+ @error="onError"
193
+ />
194
+ </div>
195
+ </template>
196
+
197
+ <script setup>
198
+ import { ref } from 'vue';
199
+ import { VirtualHumanPersona, VirtualHumanEventAdapter } from 'virtual-human-cf';
200
+
201
+ const videoUrl = ref('/path/to/digital-human-video.mp4');
202
+ const isPlaying = ref(false);
203
+ const isDarkMode = ref(false);
204
+ const clientId = ref('screen-123');
205
+ const websocketUrl = ref('ws://localhost:8080/ws');
206
+
207
+ const onConnected = () => {
208
+ console.log('连接到 WebSocket 服务器');
209
+ };
210
+
211
+ const onVideoEnded = () => {
212
+ console.log('视频播放结束');
213
+ };
214
+
215
+ const onHighlight = (payload) => {
216
+ console.log('收到高亮指令:', payload);
217
+ };
218
+
219
+ const onShowDialog = (payload) => {
220
+ console.log('显示对话框:', payload);
221
+ };
222
+
223
+ const onEnd = (payload) => {
224
+ console.log('数字人交互结束:', payload);
225
+ isPlaying.value = false;
226
+ };
227
+
228
+ const onError = (error) => {
229
+ console.error('发生错误:', error);
230
+ };
231
+ </script>
232
+ ```
233
+
234
+ ## 开发
235
+
236
+ ```bash
237
+ # 安装依赖
238
+ npm install
239
+
240
+ # 构建项目
241
+ npm run build
242
+ ```
243
+
244
+ ## 发布
245
+
246
+ 在发布之前,请确保按照 npm 包发布安全与流程规范操作:
247
+
248
+ 1. 确保项目已通过 `npm run build` 成功构建
249
+ 2. 检查 package.json 中版本号是否更新
250
+ 3. 登录 npm 账户并使用令牌认证
251
+ 4. 确保 registry 指向官方源
252
+
253
+ ## 许可证
254
+
255
+ MIT
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .fade-enter-active[data-v-288a71c0],.fade-leave-active[data-v-288a71c0]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-288a71c0],.fade-leave-to[data-v-288a71c0]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-288a71c0]{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-288a71c0]{background:#1f2937;box-shadow:0 10px 25px #00000080}.video-wrapper[data-v-288a71c0]{position:relative;width:100%;aspect-ratio:9 / 16;background:#000}.persona-video[data-v-288a71c0]{width:100%;height:100%;object-fit:cover}.overlay[data-v-288a71c0]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-288a71c0]{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-288a71c0]{background:#ef4444cc}.status-badge.playing[data-v-288a71c0]{background:#22c55ecc}.dot[data-v-288a71c0]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-288a71c0{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-288a71c0]{animation:pulse-288a71c0 2s cubic-bezier(.4,0,.6,1) infinite}
1
+ .fade-enter-active[data-v-8d9d61ad],.fade-leave-active[data-v-8d9d61ad]{transition:opacity .5s ease,transform .5s ease}.fade-enter-from[data-v-8d9d61ad],.fade-leave-to[data-v-8d9d61ad]{opacity:0;transform:translateY(10px)}.virtual-human-container[data-v-8d9d61ad]{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-8d9d61ad]{background:#1f2937;box-shadow:0 10px 25px #00000080}.video-wrapper[data-v-8d9d61ad]{position:relative;width:100%;aspect-ratio:9 / 16;background:#000}.persona-video[data-v-8d9d61ad]{width:100%;height:100%;object-fit:cover}.overlay[data-v-8d9d61ad]{position:absolute;top:1rem;right:1rem;z-index:10}.status-badge[data-v-8d9d61ad]{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-8d9d61ad]{background:#ef4444cc}.status-badge.playing[data-v-8d9d61ad]{background:#22c55ecc}.dot[data-v-8d9d61ad]{width:6px;height:6px;border-radius:50%;background-color:#fff}@keyframes pulse-8d9d61ad{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse[data-v-8d9d61ad]{animation:pulse-8d9d61ad 2s cubic-bezier(.4,0,.6,1) infinite}
@@ -8,31 +8,38 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
8
8
  }, R = /* @__PURE__ */ H({
9
9
  __name: "VirtualHumanPersona",
10
10
  props: {
11
+ // 视频源URL
11
12
  videoSrc: {
12
13
  type: String,
13
14
  required: !0
14
15
  },
16
+ // 是否可见
15
17
  visible: {
16
18
  type: Boolean,
17
19
  default: !0
18
20
  },
21
+ // 是否自动播放
19
22
  isPlaying: {
20
23
  type: Boolean,
21
24
  default: !1
22
25
  },
26
+ // 是否静音
23
27
  muted: {
24
28
  type: Boolean,
25
29
  default: !0
26
30
  // Auto-play policies usually require muted
27
31
  },
32
+ // 是否暗黑模式
28
33
  isDark: {
29
34
  type: Boolean,
30
35
  default: !1
31
36
  },
37
+ // 屏幕客户端ID
32
38
  screenClientId: {
33
39
  type: String,
34
40
  required: !1
35
41
  },
42
+ // WebSocket URL
36
43
  wsUrl: {
37
44
  type: String,
38
45
  required: !1
@@ -122,13 +129,15 @@ const F = { class: "video-wrapper" }, T = ["src", "muted"], q = { class: "overla
122
129
  for (const [s, o] of p)
123
130
  e[s] = o;
124
131
  return e;
125
- }, O = /* @__PURE__ */ z(R, [["__scopeId", "data-v-288a71c0"]]), J = /* @__PURE__ */ H({
132
+ }, O = /* @__PURE__ */ z(R, [["__scopeId", "data-v-8d9d61ad"]]), J = /* @__PURE__ */ H({
126
133
  __name: "VirtualHumanEventAdapter",
127
134
  props: {
135
+ // 屏幕客户端ID
128
136
  screenClientId: {
129
137
  type: String,
130
138
  required: !0
131
139
  },
140
+ // WebSocket URL
132
141
  wsUrl: {
133
142
  type: String,
134
143
  required: !0
@@ -1 +1 @@
1
- (function(u,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(u=typeof globalThis<"u"?globalThis:u||self,e(u.VirtualHumanCf={},u.Vue))})(this,function(u,e){"use strict";const U={class:"video-wrapper"},_=["src","muted"],A={class:"overlay"},E={key:0,class:"status-badge paused"},H={key:1,class:"status-badge playing"},P=((l,y)=>{const t=l.__vccOpts||l;for(const[s,r]of y)t[s]=r;return t})(e.defineComponent({__name:"VirtualHumanPersona",props:{videoSrc:{type:String,required:!0},visible:{type:Boolean,default:!0},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(l,{emit:y}){const t=l,s=y,r=e.ref(null),d=e.ref(null),a=e.ref(!1),f=()=>{if(d.value&&d.value.close(),!(!t.wsUrl||!t.screenClientId))try{const c=new URL(t.wsUrl);c.searchParams.append("sessionId",t.screenClientId+"-persona"),d.value=new WebSocket(c.toString()),d.value.onopen=()=>{a.value=!0,console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`)},d.value.onmessage=n=>{try{const o=JSON.parse(n.data);w(o)}catch(o){console.error("[VirtualHumanPersona] Failed to parse message:",n.data,o)}},d.value.onerror=n=>{console.error("[VirtualHumanPersona] WebSocket error:",n)},d.value.onclose=()=>{a.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(c){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",c)}},w=c=>{const{type:n,action:o}=c;n==="control"&&(o==="play"?(s("update:isPlaying",!0),s("update:visible",!0)):o==="pause"?(s("update:isPlaying",!1),s("update:visible",!1)):o==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),r.value&&(r.value.currentTime=0)))};e.watch(()=>t.screenClientId,()=>{t.screenClientId&&t.wsUrl&&f()}),e.watch(()=>t.wsUrl,()=>{t.screenClientId&&t.wsUrl&&f()}),e.watch(()=>t.isPlaying,c=>{r.value&&(c?r.value.play().catch(n=>console.error("Video play failed:",n)):r.value.pause())});const k=()=>{t.isPlaying||s("update:isPlaying",!0)},b=()=>{t.isPlaying&&s("update:isPlaying",!1)},g=()=>{s("update:isPlaying",!1),s("ended")};return e.onMounted(()=>{t.isPlaying&&r.value&&r.value.play().catch(c=>console.error("Video play failed:",c)),t.screenClientId&&t.wsUrl&&f()}),e.onUnmounted(()=>{d.value&&d.value.close()}),(c,n)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[l.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":l.isDark}])},[e.createElementVNode("div",U,[e.createElementVNode("video",{ref_key:"videoRef",ref:r,src:l.videoSrc,class:"persona-video",muted:l.muted,playsinline:"",loop:"",onPlay:k,onPause:b,onEnded:g},null,40,_),e.createElementVNode("div",A,[l.isPlaying?(e.openBlock(),e.createElementBlock("div",H,[...n[1]||(n[1]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",E,[...n[0]||(n[0]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])])],2)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-288a71c0"]]),S=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["highlight","showDialog","end","pause","connected","error"],setup(l,{emit:y}){const t=l,s=y,r=e.ref(null),d=e.ref(!1);let a=null,f=0;const w=()=>{a||(a=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),a.state==="suspended"&&a.resume()},k=n=>{if(w(),!!a)try{const o=window.atob(n),i=o.length,h=new Uint8Array(i);for(let p=0;p<i;p++)h[p]=o.charCodeAt(p);const m=new Int16Array(h.buffer),v=new Float32Array(m.length);for(let p=0;p<m.length;p++)v[p]=m[p]/32768;const C=a.createBuffer(1,v.length,24e3);C.getChannelData(0).set(v);const V=a.createBufferSource();V.buffer=C,V.connect(a.destination),f<a.currentTime&&(f=a.currentTime),V.start(f),f+=C.duration}catch(o){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",o)}},b=n=>{if(a)switch(n){case"play":a.state==="suspended"&&a.resume();break;case"pause":a.state==="running"&&a.suspend();break;case"stop":a.close(),a=null,f=0;break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${n}`)}},g=()=>{r.value&&r.value.close();try{const n=new URL(t.wsUrl);n.searchParams.append("sessionId",t.screenClientId+"-event"),r.value=new WebSocket(n.toString()),r.value.onopen=()=>{d.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`)},r.value.onmessage=o=>{try{const i=JSON.parse(o.data);c(i)}catch(i){console.error("[VirtualHumanEventAdapter] Failed to parse message:",o.data,i)}},r.value.onerror=o=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",o),s("error",o)},r.value.onclose=()=>{d.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(n){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",n),s("error",n)}},c=n=>{const{type:o,payload:i,action:h}=n;switch(o){case"audio":const m=(i==null?void 0:i.data)||n.data;m&&k(m);break;case"dialog_event":n.event&&s(n.event,n.params);break;case"control":h&&b(h);break;case"highlight":s("highlight",i);break;case"showDialog":s("showDialog",i);break;case"end":s("end",i);break;case"pause":s("pause",i);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${o}`)}};return e.watch(()=>t.screenClientId,()=>{t.screenClientId&&t.wsUrl&&g()}),e.watch(()=>t.wsUrl,()=>{t.screenClientId&&t.wsUrl&&g()}),e.onMounted(()=>{t.screenClientId&&t.wsUrl&&g()}),e.onUnmounted(()=>{r.value&&r.value.close(),a&&a.close()}),(n,o)=>e.renderSlot(n.$slots,"default")}}),I={install:l=>{l.component("VirtualHumanPersona",P),l.component("VirtualHumanEventAdapter",S)}};u.VirtualHumanEventAdapter=S,u.VirtualHumanPersona=P,u.default=I,Object.defineProperties(u,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ (function(u,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(u=typeof globalThis<"u"?globalThis:u||self,e(u.VirtualHumanCf={},u.Vue))})(this,function(u,e){"use strict";const U={class:"video-wrapper"},_=["src","muted"],A={class:"overlay"},E={key:0,class:"status-badge paused"},H={key:1,class:"status-badge playing"},P=((l,y)=>{const t=l.__vccOpts||l;for(const[s,r]of y)t[s]=r;return t})(e.defineComponent({__name:"VirtualHumanPersona",props:{videoSrc:{type:String,required:!0},visible:{type:Boolean,default:!0},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(l,{emit:y}){const t=l,s=y,r=e.ref(null),d=e.ref(null),a=e.ref(!1),f=()=>{if(d.value&&d.value.close(),!(!t.wsUrl||!t.screenClientId))try{const c=new URL(t.wsUrl);c.searchParams.append("sessionId",t.screenClientId+"-persona"),d.value=new WebSocket(c.toString()),d.value.onopen=()=>{a.value=!0,console.log(`[VirtualHumanPersona] Connected to ${t.wsUrl} for session ${t.screenClientId}-persona`)},d.value.onmessage=n=>{try{const o=JSON.parse(n.data);w(o)}catch(o){console.error("[VirtualHumanPersona] Failed to parse message:",n.data,o)}},d.value.onerror=n=>{console.error("[VirtualHumanPersona] WebSocket error:",n)},d.value.onclose=()=>{a.value=!1,console.log("[VirtualHumanPersona] WebSocket disconnected")}}catch(c){console.error("[VirtualHumanPersona] Failed to initialize WebSocket:",c)}},w=c=>{const{type:n,action:o}=c;n==="control"&&(o==="play"?(s("update:isPlaying",!0),s("update:visible",!0)):o==="pause"?(s("update:isPlaying",!1),s("update:visible",!1)):o==="stop"&&(s("update:isPlaying",!1),s("update:visible",!1),r.value&&(r.value.currentTime=0)))};e.watch(()=>t.screenClientId,()=>{t.screenClientId&&t.wsUrl&&f()}),e.watch(()=>t.wsUrl,()=>{t.screenClientId&&t.wsUrl&&f()}),e.watch(()=>t.isPlaying,c=>{r.value&&(c?r.value.play().catch(n=>console.error("Video play failed:",n)):r.value.pause())});const k=()=>{t.isPlaying||s("update:isPlaying",!0)},b=()=>{t.isPlaying&&s("update:isPlaying",!1)},g=()=>{s("update:isPlaying",!1),s("ended")};return e.onMounted(()=>{t.isPlaying&&r.value&&r.value.play().catch(c=>console.error("Video play failed:",c)),t.screenClientId&&t.wsUrl&&f()}),e.onUnmounted(()=>{d.value&&d.value.close()}),(c,n)=>(e.openBlock(),e.createBlock(e.Transition,{name:"fade"},{default:e.withCtx(()=>[l.visible?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(["virtual-human-container",{"is-dark":l.isDark}])},[e.createElementVNode("div",U,[e.createElementVNode("video",{ref_key:"videoRef",ref:r,src:l.videoSrc,class:"persona-video",muted:l.muted,playsinline:"",loop:"",onPlay:k,onPause:b,onEnded:g},null,40,_),e.createElementVNode("div",A,[l.isPlaying?(e.openBlock(),e.createElementBlock("div",H,[...n[1]||(n[1]=[e.createElementVNode("span",{class:"dot animate-pulse"},null,-1),e.createTextVNode(" 播放中 ",-1)])])):(e.openBlock(),e.createElementBlock("div",E,[...n[0]||(n[0]=[e.createElementVNode("span",{class:"dot"},null,-1),e.createTextVNode(" 暂停中 ",-1)])]))])])],2)):e.createCommentVNode("",!0)]),_:1}))}}),[["__scopeId","data-v-8d9d61ad"]]),S=e.defineComponent({__name:"VirtualHumanEventAdapter",props:{screenClientId:{type:String,required:!0},wsUrl:{type:String,required:!0}},emits:["highlight","showDialog","end","pause","connected","error"],setup(l,{emit:y}){const t=l,s=y,r=e.ref(null),d=e.ref(!1);let a=null,f=0;const w=()=>{a||(a=new(window.AudioContext||window.webkitAudioContext)({sampleRate:24e3})),a.state==="suspended"&&a.resume()},k=n=>{if(w(),!!a)try{const o=window.atob(n),i=o.length,h=new Uint8Array(i);for(let p=0;p<i;p++)h[p]=o.charCodeAt(p);const m=new Int16Array(h.buffer),v=new Float32Array(m.length);for(let p=0;p<m.length;p++)v[p]=m[p]/32768;const C=a.createBuffer(1,v.length,24e3);C.getChannelData(0).set(v);const V=a.createBufferSource();V.buffer=C,V.connect(a.destination),f<a.currentTime&&(f=a.currentTime),V.start(f),f+=C.duration}catch(o){console.error("[VirtualHumanEventAdapter] Failed to decode and play audio:",o)}},b=n=>{if(a)switch(n){case"play":a.state==="suspended"&&a.resume();break;case"pause":a.state==="running"&&a.suspend();break;case"stop":a.close(),a=null,f=0;break;default:console.warn(`[VirtualHumanEventAdapter] Unknown control action: ${n}`)}},g=()=>{r.value&&r.value.close();try{const n=new URL(t.wsUrl);n.searchParams.append("sessionId",t.screenClientId+"-event"),r.value=new WebSocket(n.toString()),r.value.onopen=()=>{d.value=!0,s("connected"),console.log(`[VirtualHumanEventAdapter] Connected to ${t.wsUrl} for session ${t.screenClientId}-event`)},r.value.onmessage=o=>{try{const i=JSON.parse(o.data);c(i)}catch(i){console.error("[VirtualHumanEventAdapter] Failed to parse message:",o.data,i)}},r.value.onerror=o=>{console.error("[VirtualHumanEventAdapter] WebSocket error:",o),s("error",o)},r.value.onclose=()=>{d.value=!1,console.log("[VirtualHumanEventAdapter] WebSocket disconnected")}}catch(n){console.error("[VirtualHumanEventAdapter] Failed to initialize WebSocket:",n),s("error",n)}},c=n=>{const{type:o,payload:i,action:h}=n;switch(o){case"audio":const m=(i==null?void 0:i.data)||n.data;m&&k(m);break;case"dialog_event":n.event&&s(n.event,n.params);break;case"control":h&&b(h);break;case"highlight":s("highlight",i);break;case"showDialog":s("showDialog",i);break;case"end":s("end",i);break;case"pause":s("pause",i);break;default:console.warn(`[VirtualHumanEventAdapter] Unknown message type: ${o}`)}};return e.watch(()=>t.screenClientId,()=>{t.screenClientId&&t.wsUrl&&g()}),e.watch(()=>t.wsUrl,()=>{t.screenClientId&&t.wsUrl&&g()}),e.onMounted(()=>{t.screenClientId&&t.wsUrl&&g()}),e.onUnmounted(()=>{r.value&&r.value.close(),a&&a.close()}),(n,o)=>e.renderSlot(n.$slots,"default")}}),I={install:l=>{l.component("VirtualHumanPersona",P),l.component("VirtualHumanEventAdapter",S)}};u.VirtualHumanEventAdapter=S,u.VirtualHumanPersona=P,u.default=I,Object.defineProperties(u,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-human-cf",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Vue3 Digital Human Component Package by cf ",
5
5
  "main": "dist/virtual-human-cf.umd.js",
6
6
  "module": "dist/virtual-human-cf.es.js",
@@ -6,10 +6,12 @@
6
6
  import { ref, watch, onMounted, onUnmounted } from 'vue';
7
7
 
8
8
  const props = defineProps({
9
+ // 屏幕客户端ID
9
10
  screenClientId: {
10
11
  type: String,
11
12
  required: true,
12
13
  },
14
+ // WebSocket URL
13
15
  wsUrl: {
14
16
  type: String,
15
17
  required: true,
@@ -38,30 +38,37 @@
38
38
  import { ref, watch, onMounted, onUnmounted } from 'vue';
39
39
 
40
40
  const props = defineProps({
41
+ // 视频源URL
41
42
  videoSrc: {
42
43
  type: String,
43
44
  required: true,
44
45
  },
46
+ // 是否可见
45
47
  visible: {
46
48
  type: Boolean,
47
49
  default: true,
48
50
  },
51
+ // 是否自动播放
49
52
  isPlaying: {
50
53
  type: Boolean,
51
54
  default: false,
52
55
  },
56
+ // 是否静音
53
57
  muted: {
54
58
  type: Boolean,
55
59
  default: true, // Auto-play policies usually require muted
56
60
  },
61
+ // 是否暗黑模式
57
62
  isDark: {
58
63
  type: Boolean,
59
64
  default: false,
60
65
  },
66
+ // 屏幕客户端ID
61
67
  screenClientId: {
62
68
  type: String,
63
69
  required: false,
64
70
  },
71
+ // WebSocket URL
65
72
  wsUrl: {
66
73
  type: String,
67
74
  required: false,