huliu 1 year ago
commit
bef333e8ea

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
1
+# Logs
2
+logs
3
+*.log
4
+npm-debug.log*
5
+yarn-debug.log*
6
+yarn-error.log*
7
+pnpm-debug.log*
8
+lerna-debug.log*
9
+
10
+node_modules
11
+dist
12
+dist-ssr
13
+*.local
14
+
15
+# Editor directories and files
16
+.vscode/*
17
+!.vscode/extensions.json
18
+.idea
19
+.DS_Store
20
+*.suo
21
+*.ntvs*
22
+*.njsproj
23
+*.sln
24
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
1
+{
2
+  "recommendations": ["Vue.volar"]
3
+}

+ 7 - 0
README.md

@@ -0,0 +1,7 @@
1
+# Vue 3 + Vite
2
+
3
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
4
+
5
+## Recommended IDE Setup
6
+
7
+- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)

+ 21 - 0
index.html

@@ -0,0 +1,21 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+
4
+<head>
5
+  <meta charset="UTF-8" />
6
+  <link rel="icon" href="/favicon.ico" />
7
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+  <title>元宇宙 App</title>
9
+  <style>
10
+    * {
11
+      margin: 0;
12
+    }
13
+  </style>
14
+</head>
15
+
16
+<body>
17
+  <div id="app"></div>
18
+  <script type="module" src="/src/main.js"></script>
19
+</body>
20
+
21
+</html>

File diff suppressed because it is too large
+ 1088 - 0
package-lock.json


+ 22 - 0
package.json

@@ -0,0 +1,22 @@
1
+{
2
+  "name": "ue5_web1",
3
+  "private": true,
4
+  "version": "0.0.0",
5
+  "scripts": {
6
+    "dev": "vite",
7
+    "build": "vite build",
8
+    "preview": "vite preview"
9
+  },
10
+  "dependencies": {
11
+    "echarts": "^5.4.3",
12
+    "element-plus": "^2.3.9",
13
+    "vue": "^3.2.25"
14
+  },
15
+  "devDependencies": {
16
+    "@vitejs/plugin-vue": "^2.3.3",
17
+    "sass": "^1.64.2",
18
+    "unplugin-auto-import": "^0.16.6",
19
+    "unplugin-vue-components": "^0.25.1",
20
+    "vite": "^2.9.9"
21
+  }
22
+}

BIN
public/favicon.ico


+ 33 - 0
src/App.vue

@@ -0,0 +1,33 @@
1
+
2
+<template >
3
+    <UeVideo />
4
+</template>
5
+<script setup>
6
+import UeVideo from './components/UeVideo.vue'
7
+</script>
8
+<style scoped>
9
+/* html,
10
+body,
11
+#app,
12
+#videoPlayOverlay {
13
+    width: 100%;
14
+    height: 100%;
15
+    margin: 0;
16
+    padding: 0;
17
+    overflow: hidden;
18
+    border: 3px solid darkcyan;
19
+}
20
+
21
+#streamingVideo {
22
+    width: 100%;
23
+    height: 100%;
24
+    margin: 0;
25
+    padding: 0;
26
+    overflow: hidden;
27
+    position: absolute;
28
+    z-index: 100;
29
+    
30
+} */
31
+</style>
32
+
33
+ 

BIN
src/assets/img/1.png


BIN
src/assets/img/2.png


BIN
src/assets/img/3.png


BIN
src/assets/img/4.png


BIN
src/assets/img/d1.png


BIN
src/assets/img/d2.png


BIN
src/assets/img/d3.png


BIN
src/assets/img/head.png


BIN
src/assets/img/r1.png


BIN
src/assets/img/r2.png


BIN
src/assets/img/r3.png


BIN
src/assets/img/r4.png


BIN
src/assets/img/r5.png


BIN
src/assets/img/r6.png


BIN
src/assets/img/rb1.png


BIN
src/assets/img/rb2.png


BIN
src/assets/img/头部.png


BIN
src/assets/logo.png


+ 0 - 0
src/assets/ue.css


+ 62 - 0
src/components/UeVideo.vue

@@ -0,0 +1,62 @@
1
+
2
+<template>
3
+  <div ref="video" id="player"></div>
4
+  <home></home>
5
+  <div @click="toUE" style="position: absolute;top: 5%; left: 50%;background-color: darkcyan;z-index: 200">向UE发信息</div>
6
+</template>
7
+
8
+<script>
9
+import { onMounted, ref } from "vue";
10
+import {
11
+  initLoad,
12
+  callUIInteraction,
13
+  addResponseEventListener,
14
+} from "../webrtcVideo.js";
15
+import { ElButton, ElInput } from "element-plus";
16
+import Home from './home.vue'
17
+export default {
18
+  components: { ElButton, ElInput, Home },
19
+  setup(props, context) {
20
+    let video = ref(null);
21
+    let videoInstance = ref(null);
22
+
23
+    onMounted(() => {
24
+      console.log("video.value", video.value);
25
+
26
+      videoInstance = initLoad({
27
+        context,
28
+        serverUrl: "http://127.0.0.1:80",
29
+        autoConnection: false,
30
+        showPlayOverlay: true,
31
+        qualityControl: true,
32
+        inputOptions: {
33
+          controlScheme: 1, // 鼠标:0是锁定,1是滑过
34
+          suppressBrowserKeys: false,
35
+        },
36
+      });
37
+
38
+      // addResponseEventListener("hello", async (data) => {
39
+      //   alert(data);
40
+      // });
41
+    });
42
+    function hadleResponseFunction() {
43
+      console.log("监听一下数据---asdfadsf",)
44
+    }
45
+
46
+    return {
47
+      video,
48
+      toUE() {
49
+        console.log("点击了ToUE");
50
+        addResponseEventListener("hello", hadleResponseFunction)
51
+      },
52
+    };
53
+  },
54
+};
55
+</script>
56
+<style scoped>
57
+#player {
58
+  width: 100%;
59
+  height: 100%;
60
+  position: absolute;
61
+}
62
+</style>

+ 761 - 0
src/components/home.vue

@@ -0,0 +1,761 @@
1
+<template>
2
+    <div class="container">
3
+        <div class="header">
4
+            <img src="../assets/img/head.png" alt="" />
5
+        </div>
6
+        <el-container>
7
+            <el-aside width="412px" class="left">
8
+                <div class="title">
9
+                    <div class="text">教室分类统计</div>
10
+                </div>
11
+                <div class="content">
12
+                    <div id="myChart" :style="{ width: '200px', height: '200px' }"></div>
13
+                    <div class="list">
14
+                        <div class="item"> <span></span> <span>基础型</span> <span>40%</span></div>
15
+                        <div class="item"> <span></span><span>扩展型</span><span>30%</span></div>
16
+                        <div class="item"> <span></span><span>其他</span><span>35%</span></div>
17
+
18
+                    </div>
19
+                </div>
20
+                <div class="title">
21
+                    <div class="text">本周课程统计</div>
22
+                </div>
23
+                <div class="content">
24
+                    <div class="count" v-for="(item, index) in classCount" :key="index">
25
+                        <el-progress type="circle" :percentage="25" :color="item.color">
26
+                            <img :src="'src/assets/img/' + item.imgnumber + '.png'" alt="">
27
+                        </el-progress>
28
+                        <div class="number"><span> {{ item.number }}</span>(节)</div>
29
+                        <div class="text">{{ item.text }}</div>
30
+                    </div>
31
+                </div>
32
+                <div class="title">
33
+                    <div class="text">智慧教室使用明细</div>
34
+                </div>
35
+                <div class="content">
36
+                    <div class="table">
37
+                        <ul class="th">
38
+                            <li>教室</li>
39
+                            <li>使用状态</li>
40
+                            <li class="long">实到/应到人数</li>
41
+                            <li>到课率</li>
42
+                            <li>操作</li>
43
+                        </ul>
44
+
45
+                        <el-scrollbar height="400px">
46
+                            <ul v-for="item in classRoom">
47
+                                <li>
48
+                                    {{ item.class }}
49
+                                </li>
50
+                                <li>{{ item.status }}</li>
51
+                                <li>{{ item.people }}</li>
52
+                                <li>{{ item.percentage }}</li>
53
+                                <li>
54
+                                    <el-button size="small" class="look" @click="handleRoom">操作</el-button>
55
+                                </li>
56
+                            </ul>
57
+                        </el-scrollbar>
58
+
59
+                    </div>
60
+                </div>
61
+            </el-aside>
62
+            <el-aside width="412px" class="right">
63
+                <div class="title">
64
+                    <div class="text">物联设备统计</div>
65
+                </div>
66
+                <div class="content">
67
+                    <div class="img">
68
+                        <span>120</span>
69
+                        <img src="../assets/img/d1.png" alt="">
70
+                        <span>总数(个)</span>
71
+                    </div>
72
+                    <div class="list">
73
+                        <div class="item_r">
74
+                            <img src="../assets/img/d2.png" alt="">
75
+                            <div class="percent">
76
+                                在线&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 40
77
+                                <span>(个)</span>
78
+                            </div>
79
+
80
+                        </div>
81
+                        <div class="item_r">
82
+                            <img src="../assets/img/d3.png" alt="">
83
+                            <div class="percent">
84
+                                离线&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 71
85
+                                <span>(个)</span>
86
+                            </div>
87
+                        </div>
88
+                    </div>
89
+                </div>
90
+                <div class="title">
91
+                    <div class="text">物联设备类型统计</div>
92
+                </div>
93
+                <div class="contentwrap">
94
+
95
+                    <div class="count" v-for="(item, index) in interDevice" :key="index">
96
+                        <div class="number"><span> {{ item.number }}</span>(节)</div>
97
+                        <el-progress type="circle" :percentage="100" :color="item.color">
98
+                            <img :src="'src/assets/img/' + item.icon + '.png'" alt="">
99
+                        </el-progress>
100
+                        <div class="text">{{ item.text }}</div>
101
+                    </div>
102
+                </div>
103
+                <div class="title">
104
+                    <div class="text">智慧教室实时监控</div>
105
+                </div>
106
+                <div class="content">
107
+                    <div class="monitor">
108
+                        <p>多屏互动教室</p>
109
+                        <div class="interactclass">
110
+                            <div class="room"><span>互动教室1</span>
111
+                                <img src="../assets/img/rb1.png" alt="">
112
+                            </div>
113
+                            <div class="room"><span>互动教室2</span>
114
+                                <img src="../assets/img/rb2.png" alt="">
115
+                            </div>
116
+                        </div>
117
+                        <p>多屏智慧教室</p>
118
+                        <div class="interactclass">
119
+                            <div class="room"><span>智慧教室1</span>
120
+                                <img src="../assets/img/rb1.png" alt="">
121
+                            </div>
122
+                            <div class="room"><span>智慧教室2</span>
123
+                                <img src="../assets/img/rb2.png" alt="">
124
+                            </div>
125
+                        </div>
126
+                        <el-pagination layout="prev, pager, next" :total="50" />
127
+
128
+                    </div>
129
+                </div>
130
+            </el-aside>
131
+        </el-container>
132
+        <el-dialog v-model="centerDialogVisible" title="Warning" width="30%" align-center>
133
+            <span>Open the dialog from the center from the screen</span>
134
+            <template #footer>
135
+                <span class="dialog-footer">
136
+                    <el-button @click="centerDialogVisible = false">Cancel</el-button>
137
+                    <el-button type="primary" @click="centerDialogVisible = false">
138
+                        Confirm
139
+                    </el-button>
140
+                </span>
141
+            </template>
142
+        </el-dialog>
143
+    </div>
144
+</template>
145
+
146
+<script>
147
+import * as echarts from "echarts";
148
+import { toRefs, reactive, onMounted, ref } from 'vue';
149
+import { ElScrollbar, ElPagination, ElDialog } from "element-plus";
150
+export default ({
151
+    name: 'Histogram',
152
+    components: { ElScrollbar, ElPagination, ElDialog },
153
+    setup() {
154
+        const pieData = reactive([{
155
+            value: 1048,
156
+            name: '基础型'
157
+        },
158
+        {
159
+            value: 735,
160
+            name: '扩展型'
161
+        },
162
+        {
163
+            value: 580,
164
+            name: '其他'
165
+        }])
166
+        const classPie = reactive({
167
+            option: {
168
+                color: ['#4ED2E4', '#5E91F6', '#67F0A8'],
169
+                tooltip: {
170
+                    trigger: 'item'
171
+                },
172
+
173
+                grid: {
174
+                    x: '10',
175
+                    y: '234'
176
+                },
177
+                series: [{
178
+                    type: 'pie',
179
+                    radius: ['40%', '60%'],
180
+                    avoidLabelOverlap: false,
181
+                    label: {
182
+                        show: true,
183
+                        position: 'center',
184
+                        color: '#fff',
185
+                        formatter: () => { // 格式化要展示的文本
186
+                            return `总计\n\n1430间`
187
+                        },
188
+                        // formatter: '{total|' + 200 + '}' + '\n\r' + '{active|共发布活动}',
189
+                        // 等着获取了数据在给样式
190
+                        rich: {
191
+                            total: {
192
+                                fontSize: 35,
193
+                                fontFamily: "微软雅黑",
194
+                                color: '#454c5c'
195
+                            },
196
+                            active: {
197
+                                fontFamily: "微软雅黑",
198
+                                fontSize: 16,
199
+                                color: '#6c7a89',
200
+                                lineHeight: 30,
201
+                            },
202
+                        }
203
+
204
+
205
+                    },
206
+
207
+                    // emphasis: {
208
+                    //     label: {
209
+                    //         show: true,
210
+                    //         fontSize: 40,
211
+                    //         fontWeight: 'bold'
212
+                    //     }
213
+                    // },
214
+                    labelLine: {
215
+                        show: false
216
+                    },
217
+                    data: pieData
218
+
219
+                }]
220
+            },
221
+        })
222
+        const initeCharts = () => {
223
+            let myChart = echarts.init(document.getElementById('myChart'))
224
+            // 绘制图表
225
+            myChart.setOption(classPie.option)
226
+        }
227
+
228
+        // 本周课程统计数据
229
+        const classCount = ref([
230
+            {
231
+                number: 112,
232
+                text: '应上课程',
233
+                imgnumber: "1",
234
+                color: '#f56c6c',
235
+                percentage: 20
236
+            },
237
+            {
238
+                number: 473,
239
+                text: '实上课程',
240
+                imgnumber: "2",
241
+                color: '#e6a23c'
242
+            },
243
+            {
244
+                number: 1352,
245
+                text: '应到学生数',
246
+                imgnumber: "3",
247
+                color: '#5cb87a',
248
+            },
249
+            {
250
+                number: 2532,
251
+                text: '实到学生数',
252
+
253
+                imgnumber: "4",
254
+                color: '#1989fa',
255
+            }
256
+        ])
257
+        // 智慧教室使用数据
258
+        const classRoom = ref([
259
+
260
+            {
261
+                class: '法医实验室',
262
+                status: '在用',
263
+                people: '35/40',
264
+                percentage: '50%',
265
+            },
266
+            {
267
+                class: '法医实验室',
268
+                status: '在用',
269
+                people: '35/40',
270
+                percentage: '50%',
271
+            },
272
+            {
273
+                class: '法医实验室',
274
+                status: '在用',
275
+                people: '35/40',
276
+                percentage: '50%',
277
+            },
278
+            {
279
+                class: '法医实验室',
280
+                status: '在用',
281
+                people: '35/40',
282
+                percentage: '50%',
283
+            },
284
+            {
285
+                class: '法医实验室',
286
+                status: '在用',
287
+                people: '35/40',
288
+                percentage: '50%',
289
+            },
290
+            {
291
+                class: '法医实验室',
292
+                status: '在用',
293
+                people: '35/40',
294
+                percentage: '50%',
295
+            },
296
+            {
297
+                class: '法医实验室',
298
+                status: '在用',
299
+                people: '35/40',
300
+                percentage: '50%',
301
+            },
302
+            {
303
+                class: '法医实验室',
304
+                status: '在用',
305
+                people: '35/40',
306
+                percentage: '50%',
307
+            },
308
+            {
309
+                class: '法医实验室',
310
+                status: '在用',
311
+                people: '35/40',
312
+                percentage: '50%',
313
+            },
314
+            {
315
+                class: '法医实验室',
316
+                status: '在用',
317
+                people: '35/40',
318
+                percentage: '50%',
319
+            },
320
+            {
321
+                class: '法医实验室',
322
+                status: '在用',
323
+                people: '35/40',
324
+                percentage: '50%',
325
+            },
326
+            {
327
+                class: '法医实验室',
328
+                status: '在用',
329
+                people: '35/40',
330
+                percentage: '50%',
331
+            },
332
+
333
+        ])
334
+        // 物联设备类型统计
335
+        const interDevice = ref([
336
+            {
337
+                number: 98,
338
+                color: "#63ABFF",
339
+                icon: 'r1',
340
+                text: "显示系统"
341
+            },
342
+            {
343
+                number: 185,
344
+                color: "#63FFC7",
345
+                icon: 'r2',
346
+                text: "控制设备"
347
+            },
348
+            {
349
+                number: 58,
350
+                color: "#918EFF",
351
+                icon: 'r3',
352
+                text: "音频设备"
353
+            },
354
+            {
355
+                number: 68,
356
+                color: "#00C8E3",
357
+                icon: 'r4',
358
+                text: "录播设备"
359
+            },
360
+            {
361
+                number: 189,
362
+                color: "#FFBB54",
363
+                icon: 'r5',
364
+                text: "环境设备"
365
+            },
366
+            {
367
+                number: 98,
368
+                color: "#00CF78",
369
+                icon: 'r6',
370
+                text: "安全管理"
371
+            },
372
+
373
+        ])
374
+        // 位置弹框
375
+        let centerDialogVisible = ref(false)
376
+
377
+        const handleRoom = function () {
378
+            console.log("查看当前教室使用情况", 'adsfasdfasdfa')
379
+            centerDialogVisible.value = true
380
+        }
381
+        onMounted(() => {
382
+            initeCharts()
383
+        })
384
+        return {
385
+            ...toRefs(classPie),
386
+            classCount,
387
+            classRoom,
388
+            centerDialogVisible,
389
+            interDevice,
390
+            handleRoom
391
+        }
392
+    },
393
+})
394
+
395
+</script>
396
+
397
+<style  lang="scss">
398
+.container {
399
+    margin: 0;
400
+    padding: 0;
401
+    overflow: hidden;
402
+
403
+    .header {
404
+        position: absolute;
405
+        z-index: 555;
406
+        margin-bottom: 20px;
407
+        height: 80px;
408
+        width: 100%;
409
+        top: 22px;
410
+
411
+        img {
412
+            width: 100%;
413
+            height: 100%;
414
+        }
415
+    }
416
+}
417
+
418
+.el-container {
419
+    position: absolute;
420
+    top: 40px;
421
+    left: 0;
422
+    z-index: 300;
423
+    // border: 3px solid darkgreen;
424
+    width: calc(100% - 30px);
425
+    margin: 20px;
426
+    padding-left: 12px;
427
+    padding-top: 30px;
428
+    height: 888px;
429
+
430
+    overflow: hidden;
431
+    color: #FFF;
432
+    font-size: 14px;
433
+    display: flex;
434
+    justify-content: space-between;
435
+
436
+    .left,
437
+    .right {
438
+        flex-shrink: 0;
439
+        border-radius: 2px;
440
+        background: rgba(125, 125, 125, 0.17);
441
+        backdrop-filter: blur(30.5px);
442
+        overflow: hidden;
443
+    }
444
+
445
+    .title {
446
+        width: 380px;
447
+        height: 32px;
448
+        flex-shrink: 0;
449
+        background: linear-gradient(90deg, rgba(255, 255, 255, 0.17) 2.49%, rgba(255, 255, 255, 0.00) 98.69%);
450
+        ;
451
+        line-height: 32px;
452
+
453
+        .text {
454
+            color: #FFF;
455
+            font-family: PingFang SC;
456
+            font-size: 14px;
457
+            padding-left: 18px;
458
+            letter-spacing: 1px;
459
+        }
460
+    }
461
+
462
+    // 教室分类统计
463
+    .content {
464
+        display: flex;
465
+        justify-content: space-between;
466
+        width: 100%;
467
+        overflow: hidden;
468
+
469
+        .list {
470
+            display: flex;
471
+            flex-direction: column;
472
+            justify-content: center;
473
+            width: 200px;
474
+
475
+            .item {
476
+                width: 150px;
477
+                display: flex;
478
+                justify-content: space-between;
479
+                align-items: center;
480
+                margin: 10px;
481
+                padding-bottom: 6px;
482
+                border-bottom: 1px dotted rgba(255, 255, 255, 0.33);
483
+                font-size: 14px;
484
+
485
+                span {
486
+                    &:first-child {
487
+                        width: 8px;
488
+                        height: 8px;
489
+                        border-radius: 4px;
490
+                        background-color: firebrick;
491
+                    }
492
+
493
+                    &:nth-child(2n) {
494
+                        display: inline-block;
495
+                        text-align: left;
496
+                        width: 50px;
497
+                        // border: 2px solid darkcyan;
498
+                        margin-left: -40px;
499
+                    }
500
+
501
+                }
502
+            }
503
+        }
504
+    }
505
+
506
+    // 本周课程统计
507
+    .content {
508
+        justify-content: space-evenly;
509
+
510
+        .count {
511
+            display: flex;
512
+            flex-direction: column;
513
+            align-items: center;
514
+            width: 70px;
515
+            height: 107px;
516
+            text-align: center;
517
+            justify-content: space-around;
518
+            margin: 30px 0;
519
+            padding: 20px;
520
+
521
+            .el-progress-circle {
522
+                width: 56px !important;
523
+                height: 56px !important;
524
+
525
+                .el-progress-circle__track {
526
+                    stroke: rgba(255, 255, 255, 0.2);
527
+                }
528
+
529
+                .el-progress__text {
530
+                    margin-top: 5px;
531
+                }
532
+            }
533
+
534
+            .number {
535
+                margin-top: 10px;
536
+                margin-bottom: 5px;
537
+                font-size: 12px;
538
+
539
+                span {
540
+                    font-size: 14px;
541
+                    font-weight: bold;
542
+                }
543
+            }
544
+
545
+            .text {
546
+                font-size: 12px;
547
+                line-height: 20px;
548
+                width: 60px;
549
+            }
550
+        }
551
+    }
552
+
553
+    //智慧教室使用情况
554
+    .content {
555
+        // display: flex;
556
+        // padding-right: 10px;
557
+        // padding: 0;
558
+        // border: 2px solid firebrick;
559
+
560
+        .table {
561
+            width: 100%;
562
+            padding: 10px 10px;
563
+
564
+            .th {
565
+                font-weight: 700;
566
+                display: flex;
567
+                width: 100%;
568
+
569
+
570
+                li {
571
+                    .long {
572
+                        width: 120px;
573
+                        border: 1px solid purple;
574
+                    }
575
+
576
+                    // &:nth-child()
577
+                    // width: 130px;
578
+                    // 
579
+                    // text-align: center !important;
580
+                }
581
+            }
582
+
583
+
584
+            ul {
585
+                display: flex;
586
+                list-style: none;
587
+                height: 40px;
588
+                line-height: 40px;
589
+                font-size: 12px;
590
+                border-bottom: 1px dotted #fff;
591
+                padding: 0;
592
+
593
+                &:last-child {
594
+                    margin-bottom: 10px;
595
+                }
596
+
597
+                li {
598
+                    display: flex;
599
+                    width: 100%;
600
+                    align-items: center;
601
+                    justify-content: space-around
602
+                }
603
+            }
604
+
605
+            .el-button {
606
+                background-color: #5F7BDC;
607
+                color: #fff;
608
+                border: none;
609
+            }
610
+        }
611
+    }
612
+
613
+    // 物联设备统计
614
+    .content {
615
+        .img {
616
+            // border: 2px solid firebrick;
617
+            display: flex;
618
+            flex-direction: column;
619
+            align-items: center;
620
+            padding: 20px 10px;
621
+
622
+            span {
623
+                &:first-child {
624
+                    font-size: 18px;
625
+                    font-weight: bold;
626
+                }
627
+
628
+                &:last-child {
629
+                    margin-top: 10px;
630
+                    font-size: 16px;
631
+                }
632
+            }
633
+        }
634
+
635
+        .list {
636
+            .item_r {
637
+                display: flex;
638
+                align-items: center;
639
+                width: 100%;
640
+                margin-bottom: 10px;
641
+                // border: 1px solid forestgreen;
642
+
643
+                img {
644
+                    border-bottom: none;
645
+
646
+                }
647
+
648
+                .percent {
649
+                    width: 120px;
650
+                    border-bottom: 1px solid rgba(255, 255, 255, .73);
651
+                    margin: 10px;
652
+                    padding-bottom: 10px;
653
+                    font-size: 16px;
654
+
655
+                    span {
656
+                        font-size: 12px;
657
+                        color: #fff;
658
+                        opacity: .76;
659
+                    }
660
+                }
661
+            }
662
+        }
663
+    }
664
+
665
+    // 物联设备类型统计
666
+    .contentwrap {
667
+
668
+        display: flex;
669
+        flex-wrap: wrap;
670
+
671
+        .count {
672
+            display: flex;
673
+            flex-direction: column;
674
+            align-items: center;
675
+            padding: 10px;
676
+            text-align: center;
677
+            width: 110px;
678
+
679
+            .el-progress-circle {
680
+                width: 54.6px !important;
681
+                height: 54.6px !important;
682
+
683
+                .el-progress-circle__track {
684
+                    stroke: rgba(255, 255, 255, 0.2);
685
+                }
686
+
687
+                margin: 5px 0;
688
+            }
689
+
690
+            .number {
691
+                font-size: 12px;
692
+
693
+                span {
694
+                    font-size: 14px;
695
+                    font-weight: bold;
696
+                }
697
+            }
698
+
699
+            .text {
700
+                width: 60px;
701
+            }
702
+        }
703
+    }
704
+
705
+    // 智慧教室实时监控
706
+    .monitor {
707
+        display: flex;
708
+        flex-direction: column;
709
+        width: 100%;
710
+        height: 350px;
711
+        background: rgba(0, 0, 0, 0.2);
712
+        padding: 10px;
713
+        // border: 1px solid fuchsia;
714
+
715
+        p {
716
+            margin-left: 5px;
717
+            margin-top: 5px;
718
+        }
719
+
720
+        .interactclass {
721
+            display: flex;
722
+            justify-content: space-around;
723
+
724
+            .room {
725
+                position: relative;
726
+                width: 170px;
727
+                height: 94px;
728
+                // background: saddlebrown;
729
+                margin-top: 10px;
730
+
731
+                span {
732
+                    position: absolute;
733
+                    top: 0;
734
+                    left: 0;
735
+                }
736
+            }
737
+        }
738
+
739
+        .el-pagination {
740
+            justify-content: center;
741
+            --el-pagination-bg-color: 'tranparent';
742
+            --el-pagination-text-color: '#fff'
743
+
744
+
745
+        }
746
+
747
+        .el-pagination button:disabled {
748
+            background: none;
749
+            color: '#fff';
750
+        }
751
+
752
+        .el-pagination button:hover {
753
+            color: '#fff';
754
+        }
755
+
756
+        .el-pager li {
757
+            color: #fff;
758
+        }
759
+    }
760
+}
761
+</style>

+ 4 - 0
src/main.js

@@ -0,0 +1,4 @@
1
+import { createApp } from 'vue'
2
+import App from './App.vue'
3
+
4
+createApp(App).mount('#app')

+ 600 - 0
src/webRtcPlayer.js

@@ -0,0 +1,600 @@
1
+// Copyright Epic Games, Inc. All Rights Reserved.
2
+
3
+function webRtcPlayer(parOptions) {
4
+    parOptions = typeof parOptions !== 'undefined' ? parOptions : {};
5
+
6
+    var self = this;
7
+    const urlParams = new URLSearchParams(window.location.search);
8
+
9
+    //**********************
10
+    //Config setup
11
+    //**********************
12
+    this.cfg = typeof parOptions.peerConnectionOptions !== 'undefined' ? parOptions.peerConnectionOptions : {};
13
+    this.cfg.sdpSemantics = 'unified-plan';
14
+    // this.cfg.rtcAudioJitterBufferMaxPackets = 10;
15
+    // this.cfg.rtcAudioJitterBufferFastAccelerate = true;
16
+    // this.cfg.rtcAudioJitterBufferMinDelayMs = 0;
17
+
18
+    // If this is true in Chrome 89+ SDP is sent that is incompatible with UE Pixel Streaming 4.26 and below.
19
+    // However 4.27 Pixel Streaming does not need this set to false as it supports `offerExtmapAllowMixed`.
20
+    // tdlr; uncomment this line for older versions of Pixel Streaming that need Chrome 89+.
21
+    this.cfg.offerExtmapAllowMixed = false;
22
+
23
+    this.forceTURN = urlParams.has('ForceTURN');
24
+    if (this.forceTURN) {
25
+        console.log("Forcing TURN usage by setting ICE Transport Policy in peer connection config.");
26
+        this.cfg.iceTransportPolicy = "relay";
27
+    }
28
+
29
+    this.cfg.bundlePolicy = "balanced";
30
+    this.forceMaxBundle = urlParams.has('ForceMaxBundle');
31
+    if (this.forceMaxBundle) {
32
+        this.cfg.bundlePolicy = "max-bundle";
33
+    }
34
+
35
+    //**********************
36
+    //Variables
37
+    //**********************
38
+    this.pcClient = null;
39
+    this.dcClient = null;
40
+    this.tnClient = null;
41
+
42
+    this.sdpConstraints = {
43
+        offerToReceiveAudio: 1, //Note: if you don't need audio you can get improved latency by turning this off.
44
+        offerToReceiveVideo: 1,
45
+        voiceActivityDetection: false
46
+    };
47
+
48
+    // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
49
+    this.dataChannelOptions = { ordered: true };
50
+
51
+    // This is useful if the video/audio needs to autoplay (without user input) as browsers do not allow autoplay non-muted of sound sources without user interaction.
52
+    this.startVideoMuted = typeof parOptions.startVideoMuted !== 'undefined' ? parOptions.startVideoMuted : false;
53
+    this.autoPlayAudio = typeof parOptions.autoPlayAudio !== 'undefined' ? parOptions.autoPlayAudio : true;
54
+
55
+    // To enable mic in browser use SSL/localhost and have ?useMic in the query string.
56
+    this.useMic = urlParams.has('useMic');
57
+    if (!this.useMic) {
58
+        console.log("Microphone access is not enabled. Pass ?useMic in the url to enable it.");
59
+    }
60
+
61
+    // When ?useMic check for SSL or localhost
62
+    let isLocalhostConnection = location.hostname === "localhost" || location.hostname === "127.0.0.1";
63
+    let isHttpsConnection = location.protocol === 'https:';
64
+    if (this.useMic && !isLocalhostConnection && !isHttpsConnection) {
65
+        this.useMic = false;
66
+        console.error("Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.");
67
+        console.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
68
+    }
69
+
70
+    // Prefer SFU or P2P connection
71
+    this.preferSFU = urlParams.has('preferSFU');
72
+    console.log(this.preferSFU ?
73
+        "The browser will signal it would prefer an SFU connection. Remove ?preferSFU from the url to signal for P2P usage." :
74
+        "The browser will signal for a P2P connection. Pass ?preferSFU in the url to signal for SFU usage.");
75
+
76
+    // Latency tester
77
+    this.latencyTestTimings =
78
+    {
79
+        TestStartTimeMs: null,
80
+        UEReceiptTimeMs: null,
81
+        UEEncodeMs: null,
82
+        UECaptureToSendMs: null,
83
+        UETransmissionTimeMs: null,
84
+        BrowserReceiptTimeMs: null,
85
+        FrameDisplayDeltaTimeMs: null,
86
+        Reset: function () {
87
+            this.TestStartTimeMs = null;
88
+            this.UEReceiptTimeMs = null;
89
+            this.UEEncodeMs = null,
90
+                this.UECaptureToSendMs = null,
91
+                this.UETransmissionTimeMs = null;
92
+            this.BrowserReceiptTimeMs = null;
93
+            this.FrameDisplayDeltaTimeMs = null;
94
+        },
95
+        SetUETimings: function (UETimings) {
96
+            this.UEReceiptTimeMs = UETimings.ReceiptTimeMs;
97
+            this.UEEncodeMs = UETimings.EncodeMs,
98
+                this.UECaptureToSendMs = UETimings.CaptureToSendMs,
99
+                this.UETransmissionTimeMs = UETimings.TransmissionTimeMs;
100
+            this.BrowserReceiptTimeMs = Date.now();
101
+            this.OnAllLatencyTimingsReady(this);
102
+        },
103
+        SetFrameDisplayDeltaTime: function (DeltaTimeMs) {
104
+            if (this.FrameDisplayDeltaTimeMs == null) {
105
+                this.FrameDisplayDeltaTimeMs = Math.round(DeltaTimeMs);
106
+                this.OnAllLatencyTimingsReady(this);
107
+            }
108
+        },
109
+        OnAllLatencyTimingsReady: function (Timings) { }
110
+    }
111
+
112
+    //**********************
113
+    //Functions
114
+    //**********************
115
+
116
+    //Create Video element and expose that as a parameter
117
+    this.createWebRtcVideo = function () {
118
+        var video = document.createElement('video');
119
+
120
+        video.id = "streamingVideo";
121
+        video.playsInline = true;
122
+        video.disablepictureinpicture = true;
123
+        // video.muted = self.startVideoMuted;
124
+        // 音频
125
+        video.muted = true;
126
+        // 开启全屏
127
+        video.style.width = "100%"
128
+        video.style.height = "100%"
129
+        video.style.objectFit = "fill"
130
+        video.style.margin = 0;
131
+        video.style.padding = 0;
132
+        video.style.top = 0;
133
+        video.style.left = 0;
134
+        video.style.position = "relative";
135
+        video.style.zIndex = 100;
136
+        video.style.cursor = "pointer";
137
+        // video.style.overflow="hidden";
138
+
139
+
140
+        video.addEventListener('loadedmetadata', function (e) {
141
+            if (self.onVideoInitialised) {
142
+                self.onVideoInitialised();
143
+            }
144
+        }, true);
145
+
146
+        // Check if request video frame callback is supported
147
+        if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
148
+            // The API is supported! 
149
+
150
+            const onVideoFrameReady = (now, metadata) => {
151
+
152
+                if (metadata.receiveTime && metadata.expectedDisplayTime) {
153
+                    const receiveToCompositeMs = metadata.presentationTime - metadata.receiveTime;
154
+                    self.aggregatedStats.receiveToCompositeMs = receiveToCompositeMs;
155
+                }
156
+
157
+
158
+                // Re-register the callback to be notified about the next frame.
159
+                video.requestVideoFrameCallback(onVideoFrameReady);
160
+            };
161
+
162
+            // Initially register the callback to be notified about the first frame.
163
+            video.requestVideoFrameCallback(onVideoFrameReady);
164
+        }
165
+
166
+        return video;
167
+    }
168
+
169
+    this.video = this.createWebRtcVideo();
170
+    this.availableVideoStreams = new Map();
171
+
172
+    function onsignalingstatechange(state) {
173
+        console.info('Signaling state change. |', state.srcElement.signalingState, "|")
174
+    };
175
+
176
+    function oniceconnectionstatechange(state) {
177
+        console.info('Browser ICE connection |', state.srcElement.iceConnectionState, '|')
178
+    };
179
+
180
+    function onicegatheringstatechange(state) {
181
+        console.info('Browser ICE gathering |', state.srcElement.iceGatheringState, '|')
182
+    };
183
+
184
+    function handleOnTrack(e) {
185
+        if (e.track) {
186
+            console.log('Got track. | Kind=' + e.track.kind + ' | Id=' + e.track.id + ' | readyState=' + e.track.readyState + ' |');
187
+        }
188
+
189
+        if (e.track.kind == "audio") {
190
+            handleOnAudioTrack(e.streams[0]);
191
+            return;
192
+        }
193
+        else (e.track.kind == "video")
194
+        {
195
+            for (const s of e.streams) {
196
+                if (!self.availableVideoStreams.has(s.id)) {
197
+                    self.availableVideoStreams.set(s.id, s);
198
+                }
199
+            }
200
+
201
+            self.video.srcObject = e.streams[0];
202
+
203
+            // All tracks are added "muted" by WebRTC/browser and become unmuted when media is being sent
204
+            e.track.onunmute = () => {
205
+                self.video.srcObject = e.streams[0];
206
+                self.onNewVideoTrack(e.streams);
207
+            }
208
+        }
209
+    };
210
+
211
+    function handleOnAudioTrack(audioMediaStream) {
212
+        // do nothing the video has the same media stream as the audio track we have here (they are linked)
213
+        if (self.video.srcObject == audioMediaStream) {
214
+            return;
215
+        }
216
+        // video element has some other media stream that is not associated with this audio track
217
+        else if (self.video.srcObject && self.video.srcObject !== audioMediaStream) {
218
+            // create a new audio element
219
+            let audioElem = document.createElement("Audio");
220
+            audioElem.srcObject = audioMediaStream;
221
+
222
+            // there is no way to autoplay audio (even muted), so we defer audio until first click
223
+            if (!self.autoPlayAudio) {
224
+
225
+                let clickToPlayAudio = function () {
226
+                    audioElem.play();
227
+                    self.video.removeEventListener("click", clickToPlayAudio);
228
+                };
229
+
230
+                self.video.addEventListener("click", clickToPlayAudio);
231
+            }
232
+            // we assume the user has clicked somewhere on the page and autoplaying audio will work
233
+            else {
234
+                audioElem.play();
235
+            }
236
+            console.log('Created new audio element to play seperate audio stream.');
237
+        }
238
+
239
+    }
240
+
241
+    function onDataChannel(dataChannelEvent) {
242
+        // This is the primary data channel code path when we are "receiving"
243
+        console.log("Data channel created for us by browser as we are a receiving peer.");
244
+        self.dcClient = dataChannelEvent.channel;
245
+        setupDataChannelCallbacks(self.dcClient);
246
+    }
247
+
248
+    function createDataChannel(pc, label, options) {
249
+        // This is the primary data channel code path when we are "offering"
250
+        let datachannel = pc.createDataChannel(label, options);
251
+        console.log(`Created datachannel (${label})`);
252
+        setupDataChannelCallbacks(datachannel);
253
+        return datachannel;
254
+    }
255
+
256
+    function setupDataChannelCallbacks(datachannel) {
257
+        try {
258
+            // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
259
+            datachannel.binaryType = "arraybuffer";
260
+
261
+            datachannel.onopen = function (e) {
262
+                console.log("Data channel connected");
263
+                if (self.onDataChannelConnected) {
264
+                    self.onDataChannelConnected();
265
+                }
266
+            }
267
+
268
+            datachannel.onclose = function (e) {
269
+                console.log("Data channel connected", e);
270
+            }
271
+
272
+            datachannel.onmessage = function (e) {
273
+                if (self.onDataChannelMessage) {
274
+                    self.onDataChannelMessage(e.data);
275
+                }
276
+            }
277
+
278
+            datachannel.onerror = function (e) {
279
+                console.error("Data channel error", e);
280
+            }
281
+
282
+            return datachannel;
283
+        } catch (e) {
284
+            console.warn('No data channel', e);
285
+            return null;
286
+        }
287
+    }
288
+
289
+    function onicecandidate(e) {
290
+        let candidate = e.candidate;
291
+        if (candidate && candidate.candidate) {
292
+            console.log("%c[Browser ICE candidate]", "background: violet; color: black", "| Type=", candidate.type, "| Protocol=", candidate.protocol, "| Address=", candidate.address, "| Port=", candidate.port, "|");
293
+            self.onWebRtcCandidate(candidate);
294
+        }
295
+    };
296
+
297
+    function handleCreateOffer(pc) {
298
+        pc.createOffer(self.sdpConstraints).then(function (offer) {
299
+
300
+            // Munging is where we modifying the sdp string to set parameters that are not exposed to the browser's WebRTC API
301
+            mungeSDPOffer(offer);
302
+
303
+            // Set our munged SDP on the local peer connection so it is "set" and will be send across
304
+            pc.setLocalDescription(offer);
305
+            if (self.onWebRtcOffer) {
306
+                self.onWebRtcOffer(offer);
307
+            }
308
+        },
309
+            function () { console.warn("Couldn't create offer") });
310
+    }
311
+
312
+    function mungeSDPOffer(offer) {
313
+
314
+        // turn off video-timing sdp sent from browser
315
+        //offer.sdp = offer.sdp.replace("http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "");
316
+
317
+        // this indicate we support stereo (Chrome needs this)
318
+        offer.sdp = offer.sdp.replace('useinbandfec=1', 'useinbandfec=1;stereo=1;sprop-maxcapturerate=48000');
319
+
320
+    }
321
+
322
+    function setupPeerConnection(pc) {
323
+        //Setup peerConnection events
324
+        pc.onsignalingstatechange = onsignalingstatechange;
325
+        pc.oniceconnectionstatechange = oniceconnectionstatechange;
326
+        pc.onicegatheringstatechange = onicegatheringstatechange;
327
+
328
+        pc.ontrack = handleOnTrack;
329
+        pc.onicecandidate = onicecandidate;
330
+        pc.ondatachannel = onDataChannel;
331
+    };
332
+
333
+    function generateAggregatedStatsFunction() {
334
+        if (!self.aggregatedStats)
335
+            self.aggregatedStats = {};
336
+
337
+        return function (stats) {
338
+            //console.log('Printing Stats');
339
+
340
+            let newStat = {};
341
+
342
+            stats.forEach(stat => {
343
+                //                    console.log(JSON.stringify(stat, undefined, 4));
344
+                if (stat.type == 'inbound-rtp'
345
+                    && !stat.isRemote
346
+                    && (stat.mediaType == 'video' || stat.id.toLowerCase().includes('video'))) {
347
+
348
+                    newStat.timestamp = stat.timestamp;
349
+                    newStat.bytesReceived = stat.bytesReceived;
350
+                    newStat.framesDecoded = stat.framesDecoded;
351
+                    newStat.packetsLost = stat.packetsLost;
352
+                    newStat.bytesReceivedStart = self.aggregatedStats && self.aggregatedStats.bytesReceivedStart ? self.aggregatedStats.bytesReceivedStart : stat.bytesReceived;
353
+                    newStat.framesDecodedStart = self.aggregatedStats && self.aggregatedStats.framesDecodedStart ? self.aggregatedStats.framesDecodedStart : stat.framesDecoded;
354
+                    newStat.timestampStart = self.aggregatedStats && self.aggregatedStats.timestampStart ? self.aggregatedStats.timestampStart : stat.timestamp;
355
+
356
+                    if (self.aggregatedStats && self.aggregatedStats.timestamp) {
357
+                        if (self.aggregatedStats.bytesReceived) {
358
+                            // bitrate = bits received since last time / number of ms since last time
359
+                            //This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)
360
+                            newStat.bitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceived) / (newStat.timestamp - self.aggregatedStats.timestamp);
361
+                            newStat.bitrate = Math.floor(newStat.bitrate);
362
+                            newStat.lowBitrate = self.aggregatedStats.lowBitrate && self.aggregatedStats.lowBitrate < newStat.bitrate ? self.aggregatedStats.lowBitrate : newStat.bitrate
363
+                            newStat.highBitrate = self.aggregatedStats.highBitrate && self.aggregatedStats.highBitrate > newStat.bitrate ? self.aggregatedStats.highBitrate : newStat.bitrate
364
+                        }
365
+
366
+                        if (self.aggregatedStats.bytesReceivedStart) {
367
+                            newStat.avgBitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceivedStart) / (newStat.timestamp - self.aggregatedStats.timestampStart);
368
+                            newStat.avgBitrate = Math.floor(newStat.avgBitrate);
369
+                        }
370
+
371
+                        if (self.aggregatedStats.framesDecoded) {
372
+                            // framerate = frames decoded since last time / number of seconds since last time
373
+                            newStat.framerate = (newStat.framesDecoded - self.aggregatedStats.framesDecoded) / ((newStat.timestamp - self.aggregatedStats.timestamp) / 1000);
374
+                            newStat.framerate = Math.floor(newStat.framerate);
375
+                            newStat.lowFramerate = self.aggregatedStats.lowFramerate && self.aggregatedStats.lowFramerate < newStat.framerate ? self.aggregatedStats.lowFramerate : newStat.framerate
376
+                            newStat.highFramerate = self.aggregatedStats.highFramerate && self.aggregatedStats.highFramerate > newStat.framerate ? self.aggregatedStats.highFramerate : newStat.framerate
377
+                        }
378
+
379
+                        if (self.aggregatedStats.framesDecodedStart) {
380
+                            newStat.avgframerate = (newStat.framesDecoded - self.aggregatedStats.framesDecodedStart) / ((newStat.timestamp - self.aggregatedStats.timestampStart) / 1000);
381
+                            newStat.avgframerate = Math.floor(newStat.avgframerate);
382
+                        }
383
+                    }
384
+                }
385
+
386
+                //Read video track stats
387
+                if (stat.type == 'track' && (stat.trackIdentifier == 'video_label' || stat.kind == 'video')) {
388
+                    newStat.framesDropped = stat.framesDropped;
389
+                    newStat.framesReceived = stat.framesReceived;
390
+                    newStat.framesDroppedPercentage = stat.framesDropped / stat.framesReceived * 100;
391
+                    newStat.frameHeight = stat.frameHeight;
392
+                    newStat.frameWidth = stat.frameWidth;
393
+                    newStat.frameHeightStart = self.aggregatedStats && self.aggregatedStats.frameHeightStart ? self.aggregatedStats.frameHeightStart : stat.frameHeight;
394
+                    newStat.frameWidthStart = self.aggregatedStats && self.aggregatedStats.frameWidthStart ? self.aggregatedStats.frameWidthStart : stat.frameWidth;
395
+                }
396
+
397
+                if (stat.type == 'candidate-pair' && stat.hasOwnProperty('currentRoundTripTime') && stat.currentRoundTripTime != 0) {
398
+                    newStat.currentRoundTripTime = stat.currentRoundTripTime;
399
+                }
400
+            });
401
+
402
+
403
+            if (self.aggregatedStats.receiveToCompositeMs) {
404
+                newStat.receiveToCompositeMs = self.aggregatedStats.receiveToCompositeMs;
405
+                self.latencyTestTimings.SetFrameDisplayDeltaTime(self.aggregatedStats.receiveToCompositeMs);
406
+            }
407
+
408
+            self.aggregatedStats = newStat;
409
+
410
+            if (self.onAggregatedStats)
411
+                self.onAggregatedStats(newStat)
412
+        }
413
+    };
414
+
415
+    let setupTransceiversAsync = async function (pc) {
416
+
417
+        let hasTransceivers = pc.getTransceivers().length > 0;
418
+
419
+        // Setup a transceiver for getting UE video
420
+        pc.addTransceiver("video", { direction: "recvonly" });
421
+
422
+        // Setup a transceiver for sending mic audio to UE and receiving audio from UE
423
+        if (!self.useMic) {
424
+            pc.addTransceiver("audio", { direction: "recvonly" });
425
+        }
426
+        else {
427
+            let audioSendOptions = self.useMic ?
428
+                {
429
+                    autoGainControl: false,
430
+                    channelCount: 1,
431
+                    echoCancellation: false,
432
+                    latency: 0,
433
+                    noiseSuppression: false,
434
+                    sampleRate: 48000,
435
+                    volume: 1.0
436
+                } : false;
437
+
438
+            // Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
439
+            const stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: audioSendOptions });
440
+            if (stream) {
441
+                if (hasTransceivers) {
442
+                    for (let transceiver of pc.getTransceivers()) {
443
+                        if (transceiver && transceiver.receiver && transceiver.receiver.track && transceiver.receiver.track.kind === "audio") {
444
+                            for (const track of stream.getTracks()) {
445
+                                if (track.kind && track.kind == "audio") {
446
+                                    transceiver.sender.replaceTrack(track);
447
+                                    transceiver.direction = "sendrecv";
448
+                                }
449
+                            }
450
+                        }
451
+                    }
452
+                }
453
+                else {
454
+                    for (const track of stream.getTracks()) {
455
+                        if (track.kind && track.kind == "audio") {
456
+                            pc.addTransceiver(track, { direction: "sendrecv" });
457
+                        }
458
+                    }
459
+                }
460
+            }
461
+            else {
462
+                pc.addTransceiver("audio", { direction: "recvonly" });
463
+            }
464
+        }
465
+    };
466
+
467
+
468
+    //**********************
469
+    //Public functions
470
+    //**********************
471
+
472
+    this.setVideoEnabled = function (enabled) {
473
+        self.video.srcObject.getTracks().forEach(track => track.enabled = enabled);
474
+    }
475
+
476
+    this.startLatencyTest = function (onTestStarted) {
477
+        // Can't start latency test without a video element
478
+        if (!self.video) {
479
+            return;
480
+        }
481
+
482
+        self.latencyTestTimings.Reset();
483
+        self.latencyTestTimings.TestStartTimeMs = Date.now();
484
+        onTestStarted(self.latencyTestTimings.TestStartTimeMs);
485
+    }
486
+
487
+    //This is called when revceiving new ice candidates individually instead of part of the offer
488
+    this.handleCandidateFromServer = function (iceCandidate) {
489
+        let candidate = new RTCIceCandidate(iceCandidate);
490
+
491
+        console.log("%c[Unreal ICE candidate]", "background: pink; color: black", "| Type=", candidate.type, "| Protocol=", candidate.protocol, "| Address=", candidate.address, "| Port=", candidate.port, "|");
492
+
493
+        // if forcing TURN, reject any candidates not relay
494
+        if (self.forceTURN) {
495
+            // check if no relay address is found, if so, we are assuming it means no TURN server
496
+            if (candidate.candidate.indexOf("relay") < 0) {
497
+                console.warn("Dropping candidate because it was not TURN relay.", "| Type=", candidate.type, "| Protocol=", candidate.protocol, "| Address=", candidate.address, "| Port=", candidate.port, "|")
498
+                return;
499
+            }
500
+        }
501
+
502
+        self.pcClient.addIceCandidate(candidate).catch(function (e) {
503
+            console.error("Failed to add ICE candidate", e);
504
+        });
505
+    };
506
+
507
+    //Called externaly to create an offer for the server
508
+    this.createOffer = function () {
509
+        if (self.pcClient) {
510
+            console.log("Closing existing PeerConnection")
511
+            self.pcClient.close();
512
+            self.pcClient = null;
513
+        }
514
+        self.pcClient = new RTCPeerConnection(self.cfg);
515
+        setupPeerConnection(self.pcClient);
516
+
517
+        setupTransceiversAsync(self.pcClient).finally(function () {
518
+            self.dcClient = createDataChannel(self.pcClient, 'cirrus', self.dataChannelOptions);
519
+            handleCreateOffer(self.pcClient);
520
+        });
521
+
522
+    };
523
+
524
+    //Called externaly when an offer is received from the server
525
+    this.receiveOffer = function (offer) {
526
+        var offerDesc = new RTCSessionDescription(offer);
527
+
528
+        if (!self.pcClient) {
529
+            console.log("Creating a new PeerConnection in the browser.")
530
+            self.pcClient = new RTCPeerConnection(self.cfg);
531
+            setupPeerConnection(self.pcClient);
532
+
533
+            // Put things here that happen post transceiver setup
534
+            self.pcClient.setRemoteDescription(offerDesc)
535
+                .then(() => {
536
+                    setupTransceiversAsync(self.pcClient).finally(function () {
537
+                        self.pcClient.createAnswer()
538
+                            .then(answer => self.pcClient.setLocalDescription(answer))
539
+                            .then(() => {
540
+                                if (self.onWebRtcAnswer) {
541
+                                    self.onWebRtcAnswer(self.pcClient.currentLocalDescription);
542
+                                }
543
+                            })
544
+                            .then(() => {
545
+                                let receivers = self.pcClient.getReceivers();
546
+                                for (let receiver of receivers) {
547
+                                    receiver.playoutDelayHint = 0;
548
+                                }
549
+                            })
550
+                            .catch((error) => console.error("createAnswer() failed:", error));
551
+                    });
552
+                });
553
+        }
554
+    };
555
+
556
+    //Called externaly when an answer is received from the server
557
+    this.receiveAnswer = function (answer) {
558
+        var answerDesc = new RTCSessionDescription(answer);
559
+        self.pcClient.setRemoteDescription(answerDesc);
560
+
561
+        let receivers = self.pcClient.getReceivers();
562
+        for (let receiver of receivers) {
563
+            receiver.playoutDelayHint = 0;
564
+        }
565
+    };
566
+
567
+    this.close = function () {
568
+        if (self.pcClient) {
569
+            console.log("Closing existing peerClient")
570
+            self.pcClient.close();
571
+            self.pcClient = null;
572
+        }
573
+        if (self.aggregateStatsIntervalId)
574
+            clearInterval(self.aggregateStatsIntervalId);
575
+    }
576
+
577
+    //Sends data across the datachannel
578
+    this.send = function (data) {
579
+        if (self.dcClient && self.dcClient.readyState == 'open') {
580
+            //console.log('Sending data on dataconnection', self.dcClient)
581
+            self.dcClient.send(data);
582
+        }
583
+    };
584
+
585
+    this.getStats = function (onStats) {
586
+        if (self.pcClient && onStats) {
587
+            self.pcClient.getStats(null).then((stats) => {
588
+                onStats(stats);
589
+            });
590
+        }
591
+    }
592
+
593
+    this.aggregateStats = function (checkInterval) {
594
+        let calcAggregatedStats = generateAggregatedStatsFunction();
595
+        let printAggregatedStats = () => { self.getStats(calcAggregatedStats); }
596
+        self.aggregateStatsIntervalId = setInterval(printAggregatedStats, checkInterval);
597
+    }
598
+}
599
+
600
+export default webRtcPlayer

File diff suppressed because it is too large
+ 2245 - 0
src/webrtcVideo.js


+ 19 - 0
vite.config.js

@@ -0,0 +1,19 @@
1
+import { defineConfig } from 'vite'
2
+import vue from '@vitejs/plugin-vue'
3
+import AutoImport from 'unplugin-auto-import/vite'
4
+import Components from 'unplugin-vue-components/vite'
5
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
6
+
7
+// https://vitejs.dev/config/
8
+export default defineConfig({
9
+  plugins: [
10
+    vue(),
11
+    Components({
12
+      resolvers: [ElementPlusResolver()]
13
+    }),
14
+    AutoImport({
15
+      resolvers: [ElementPlusResolver()]
16
+    })
17
+  ]
18
+
19
+})

+ 253 - 0
yarn.lock

@@ -0,0 +1,253 @@
1
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2
+# yarn lockfile v1
3
+
4
+
5
+"@babel/parser@^7.16.4":
6
+  "integrity" "sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw=="
7
+  "resolved" "https://registry.npmmirror.com/@babel/parser/-/parser-7.18.5.tgz"
8
+  "version" "7.18.5"
9
+
10
+"@vitejs/plugin-vue@^2.3.3":
11
+  "integrity" "sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw=="
12
+  "resolved" "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-2.3.3.tgz"
13
+  "version" "2.3.3"
14
+
15
+"@vue/compiler-core@3.2.37":
16
+  "integrity" "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg=="
17
+  "resolved" "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz"
18
+  "version" "3.2.37"
19
+  dependencies:
20
+    "@babel/parser" "^7.16.4"
21
+    "@vue/shared" "3.2.37"
22
+    "estree-walker" "^2.0.2"
23
+    "source-map" "^0.6.1"
24
+
25
+"@vue/compiler-dom@3.2.37":
26
+  "integrity" "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ=="
27
+  "resolved" "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz"
28
+  "version" "3.2.37"
29
+  dependencies:
30
+    "@vue/compiler-core" "3.2.37"
31
+    "@vue/shared" "3.2.37"
32
+
33
+"@vue/compiler-sfc@3.2.37":
34
+  "integrity" "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg=="
35
+  "resolved" "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz"
36
+  "version" "3.2.37"
37
+  dependencies:
38
+    "@babel/parser" "^7.16.4"
39
+    "@vue/compiler-core" "3.2.37"
40
+    "@vue/compiler-dom" "3.2.37"
41
+    "@vue/compiler-ssr" "3.2.37"
42
+    "@vue/reactivity-transform" "3.2.37"
43
+    "@vue/shared" "3.2.37"
44
+    "estree-walker" "^2.0.2"
45
+    "magic-string" "^0.25.7"
46
+    "postcss" "^8.1.10"
47
+    "source-map" "^0.6.1"
48
+
49
+"@vue/compiler-ssr@3.2.37":
50
+  "integrity" "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw=="
51
+  "resolved" "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz"
52
+  "version" "3.2.37"
53
+  dependencies:
54
+    "@vue/compiler-dom" "3.2.37"
55
+    "@vue/shared" "3.2.37"
56
+
57
+"@vue/reactivity-transform@3.2.37":
58
+  "integrity" "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg=="
59
+  "resolved" "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz"
60
+  "version" "3.2.37"
61
+  dependencies:
62
+    "@babel/parser" "^7.16.4"
63
+    "@vue/compiler-core" "3.2.37"
64
+    "@vue/shared" "3.2.37"
65
+    "estree-walker" "^2.0.2"
66
+    "magic-string" "^0.25.7"
67
+
68
+"@vue/reactivity@3.2.37":
69
+  "integrity" "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A=="
70
+  "resolved" "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.37.tgz"
71
+  "version" "3.2.37"
72
+  dependencies:
73
+    "@vue/shared" "3.2.37"
74
+
75
+"@vue/runtime-core@3.2.37":
76
+  "integrity" "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ=="
77
+  "resolved" "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz"
78
+  "version" "3.2.37"
79
+  dependencies:
80
+    "@vue/reactivity" "3.2.37"
81
+    "@vue/shared" "3.2.37"
82
+
83
+"@vue/runtime-dom@3.2.37":
84
+  "integrity" "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw=="
85
+  "resolved" "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz"
86
+  "version" "3.2.37"
87
+  dependencies:
88
+    "@vue/runtime-core" "3.2.37"
89
+    "@vue/shared" "3.2.37"
90
+    "csstype" "^2.6.8"
91
+
92
+"@vue/server-renderer@3.2.37":
93
+  "integrity" "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA=="
94
+  "resolved" "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz"
95
+  "version" "3.2.37"
96
+  dependencies:
97
+    "@vue/compiler-ssr" "3.2.37"
98
+    "@vue/shared" "3.2.37"
99
+
100
+"@vue/shared@3.2.37":
101
+  "integrity" "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw=="
102
+  "resolved" "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz"
103
+  "version" "3.2.37"
104
+
105
+"csstype@^2.6.8":
106
+  "integrity" "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
107
+  "resolved" "https://registry.npmmirror.com/csstype/-/csstype-2.6.20.tgz"
108
+  "version" "2.6.20"
109
+
110
+"esbuild-windows-64@0.14.47":
111
+  "integrity" "sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ=="
112
+  "resolved" "https://registry.npmmirror.com/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz"
113
+  "version" "0.14.47"
114
+
115
+"esbuild@^0.14.27":
116
+  "integrity" "sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA=="
117
+  "resolved" "https://registry.npmmirror.com/esbuild/-/esbuild-0.14.47.tgz"
118
+  "version" "0.14.47"
119
+  optionalDependencies:
120
+    "esbuild-android-64" "0.14.47"
121
+    "esbuild-android-arm64" "0.14.47"
122
+    "esbuild-darwin-64" "0.14.47"
123
+    "esbuild-darwin-arm64" "0.14.47"
124
+    "esbuild-freebsd-64" "0.14.47"
125
+    "esbuild-freebsd-arm64" "0.14.47"
126
+    "esbuild-linux-32" "0.14.47"
127
+    "esbuild-linux-64" "0.14.47"
128
+    "esbuild-linux-arm" "0.14.47"
129
+    "esbuild-linux-arm64" "0.14.47"
130
+    "esbuild-linux-mips64le" "0.14.47"
131
+    "esbuild-linux-ppc64le" "0.14.47"
132
+    "esbuild-linux-riscv64" "0.14.47"
133
+    "esbuild-linux-s390x" "0.14.47"
134
+    "esbuild-netbsd-64" "0.14.47"
135
+    "esbuild-openbsd-64" "0.14.47"
136
+    "esbuild-sunos-64" "0.14.47"
137
+    "esbuild-windows-32" "0.14.47"
138
+    "esbuild-windows-64" "0.14.47"
139
+    "esbuild-windows-arm64" "0.14.47"
140
+
141
+"estree-walker@^2.0.2":
142
+  "integrity" "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
143
+  "resolved" "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz"
144
+  "version" "2.0.2"
145
+
146
+"function-bind@^1.1.1":
147
+  "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
148
+  "resolved" "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz"
149
+  "version" "1.1.1"
150
+
151
+"has@^1.0.3":
152
+  "integrity" "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw=="
153
+  "resolved" "https://registry.npmmirror.com/has/-/has-1.0.3.tgz"
154
+  "version" "1.0.3"
155
+  dependencies:
156
+    "function-bind" "^1.1.1"
157
+
158
+"is-core-module@^2.9.0":
159
+  "integrity" "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A=="
160
+  "resolved" "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.9.0.tgz"
161
+  "version" "2.9.0"
162
+  dependencies:
163
+    "has" "^1.0.3"
164
+
165
+"magic-string@^0.25.7":
166
+  "integrity" "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="
167
+  "resolved" "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz"
168
+  "version" "0.25.9"
169
+  dependencies:
170
+    "sourcemap-codec" "^1.4.8"
171
+
172
+"nanoid@^3.3.4":
173
+  "integrity" "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
174
+  "resolved" "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz"
175
+  "version" "3.3.4"
176
+
177
+"path-parse@^1.0.7":
178
+  "integrity" "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
179
+  "resolved" "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz"
180
+  "version" "1.0.7"
181
+
182
+"picocolors@^1.0.0":
183
+  "integrity" "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
184
+  "resolved" "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz"
185
+  "version" "1.0.0"
186
+
187
+"postcss@^8.1.10", "postcss@^8.4.13":
188
+  "integrity" "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig=="
189
+  "resolved" "https://registry.npmmirror.com/postcss/-/postcss-8.4.14.tgz"
190
+  "version" "8.4.14"
191
+  dependencies:
192
+    "nanoid" "^3.3.4"
193
+    "picocolors" "^1.0.0"
194
+    "source-map-js" "^1.0.2"
195
+
196
+"resolve@^1.22.0":
197
+  "integrity" "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw=="
198
+  "resolved" "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz"
199
+  "version" "1.22.1"
200
+  dependencies:
201
+    "is-core-module" "^2.9.0"
202
+    "path-parse" "^1.0.7"
203
+    "supports-preserve-symlinks-flag" "^1.0.0"
204
+
205
+"rollup@^2.59.0":
206
+  "integrity" "sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ=="
207
+  "resolved" "https://registry.npmmirror.com/rollup/-/rollup-2.75.7.tgz"
208
+  "version" "2.75.7"
209
+  optionalDependencies:
210
+    "fsevents" "~2.3.2"
211
+
212
+"source-map-js@^1.0.2":
213
+  "integrity" "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
214
+  "resolved" "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz"
215
+  "version" "1.0.2"
216
+
217
+"source-map@^0.6.1":
218
+  "integrity" "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
219
+  "resolved" "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz"
220
+  "version" "0.6.1"
221
+
222
+"sourcemap-codec@^1.4.8":
223
+  "integrity" "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
224
+  "resolved" "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
225
+  "version" "1.4.8"
226
+
227
+"supports-preserve-symlinks-flag@^1.0.0":
228
+  "integrity" "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
229
+  "resolved" "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
230
+  "version" "1.0.0"
231
+
232
+"vite@^2.5.10", "vite@^2.9.9":
233
+  "integrity" "sha512-suxC36dQo9Rq1qMB2qiRorNJtJAdxguu5TMvBHOc/F370KvqAe9t48vYp+/TbPKRNrMh/J55tOUmkuIqstZaew=="
234
+  "resolved" "https://registry.npmmirror.com/vite/-/vite-2.9.12.tgz"
235
+  "version" "2.9.12"
236
+  dependencies:
237
+    "esbuild" "^0.14.27"
238
+    "postcss" "^8.4.13"
239
+    "resolve" "^1.22.0"
240
+    "rollup" "^2.59.0"
241
+  optionalDependencies:
242
+    "fsevents" "~2.3.2"
243
+
244
+"vue@^3.2.25", "vue@3.2.37":
245
+  "integrity" "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ=="
246
+  "resolved" "https://registry.npmmirror.com/vue/-/vue-3.2.37.tgz"
247
+  "version" "3.2.37"
248
+  dependencies:
249
+    "@vue/compiler-dom" "3.2.37"
250
+    "@vue/compiler-sfc" "3.2.37"
251
+    "@vue/runtime-dom" "3.2.37"
252
+    "@vue/server-renderer" "3.2.37"
253
+    "@vue/shared" "3.2.37"