@live-change/peer-connection-frontend 0.8.33 → 0.8.35
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/build-stats/ssr-srcentryserverjs-outDir-distserver.html +4842 -0
- package/build-stats/ssrManifest-outDir-distclient.html +4842 -0
- package/front/components.d.ts +20 -0
- package/front/public/images/cameraAccess/en.png +0 -0
- package/front/src/App.vue +42 -77
- package/front/src/components/Debugger.vue +68 -177
- package/front/src/components/DevicesSelect.vue +393 -0
- package/front/src/components/Peer.js +167 -252
- package/front/src/components/PeerConnection.js +296 -312
- package/front/src/components/PermissionsDialog.vue +146 -0
- package/front/src/components/mediaStreamsTracks.js +60 -0
- package/front/src/components/userMedia.js +2 -2
- package/front/src/entry-client.js +4 -22
- package/front/src/entry-server.js +5 -4
- package/front/src/router.js +6 -1
- package/front/vite.config.js +8 -107
- package/package-deps.json +41 -0
- package/package.json +24 -23
- package/server/app.config.js +114 -0
- package/server/init.js +10 -2
- package/server/security.config.js +53 -0
- package/server/services.list.js +50 -0
- package/server/start.js +37 -0
- package/server/services.config.js +0 -25
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="videoInputRequest !== 'none'" @click="handleEmptyPreviewClick"
|
|
4
|
+
class="w-full bg-gray-900 relative" style="aspect-ratio: 16/9">
|
|
5
|
+
<div v-if="!model?.videoInput?.deviceId"
|
|
6
|
+
class="flex flex-column align-items-center justify-content-center h-full">
|
|
7
|
+
<i class="pi pi-eye-slash text-9xl text-gray-500" style="font-size: 2.5rem" />
|
|
8
|
+
<div class="text-xl">Video input not found!</div>
|
|
9
|
+
<div class>Please connect camera.</div>
|
|
10
|
+
<audio v-if="model?.audioInput?.deviceId"
|
|
11
|
+
autoplay playsinline :muted="userMediaMuted"
|
|
12
|
+
:src-object.prop.camel="userMedia">
|
|
13
|
+
</audio>
|
|
14
|
+
</div>
|
|
15
|
+
<div v-else class="bg-red-200 flex align-items-center justify-content-center">
|
|
16
|
+
<video v-if="userMedia" autoplay playsinline :muted="userMediaMuted"
|
|
17
|
+
:src-object.prop.camel="userMedia"
|
|
18
|
+
class="max-w-full max-h-full" style="object-fit: contain; transform: scaleX(-1)">
|
|
19
|
+
</video>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="absolute top-0 left-0 w-full h-full flex flex-column justify-content-end align-items-center">
|
|
22
|
+
<div class="flex flex-row justify-content-between align-items-center h-5rem w-6rem">
|
|
23
|
+
<Button v-if="!selectedConstraints?.audio?.deviceId"
|
|
24
|
+
@click="handleDisabledAudioClick" raised
|
|
25
|
+
icon="pi pi-microphone" severity="secondary" rounded v-ripple />
|
|
26
|
+
<Button v-else-if="audioInputMuted" @click="audioInputMuted = false" raised
|
|
27
|
+
icon="pi pi-microphone" severity="danger" rounded v-ripple />
|
|
28
|
+
<Button v-else @click="audioInputMuted = true" raised
|
|
29
|
+
icon="pi pi-microphone" severity="success" rounded v-ripple />
|
|
30
|
+
|
|
31
|
+
<Button v-if="!selectedConstraints?.video?.deviceId"
|
|
32
|
+
@click="handleDisabledVideoClick" raised
|
|
33
|
+
icon="pi pi-camera" severity="secondary" rounded v-ripple />
|
|
34
|
+
<Button v-else-if="videoInputMuted" @click="videoInputMuted = false" raised
|
|
35
|
+
icon="pi pi-camera" severity="danger" rounded v-ripple />
|
|
36
|
+
<Button v-else @click="videoInputMuted = true" raised
|
|
37
|
+
icon="pi pi-camera" severity="success" rounded v-ripple />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="flex flex-row gap-2 pt-2 justify-content-around">
|
|
42
|
+
<div v-if="audioInputRequest !== 'none' && audioInputs.length > 1"
|
|
43
|
+
class="flex flex-column align-items-stretch flex-grow-1">
|
|
44
|
+
<div class="text-sm mb-1 pl-1">Microphone</div>
|
|
45
|
+
<Dropdown v-model="model.audioInput" :options="audioInputs"
|
|
46
|
+
optionLabel="label"
|
|
47
|
+
placeholder="Select">
|
|
48
|
+
<template #value="slotProps">
|
|
49
|
+
<div class="flex flex-row align-items-center">
|
|
50
|
+
<i class="pi pi-microphone mr-2" />
|
|
51
|
+
|
|
52
|
+
<div class="absolute overflow-hidden text-overflow-ellipsis" style="left: 2em; right: 2em;">
|
|
53
|
+
{{ slotProps.value ? slotProps.value.label : slotProps.placeholder }}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
</Dropdown>
|
|
58
|
+
</div>
|
|
59
|
+
<div v-if="audioOutputRequest !== 'none' && audioOutputs.length > 1"
|
|
60
|
+
class="flex flex-column align-items-stretch flex-grow-1">
|
|
61
|
+
<div class="text-sm mb-1 pl-1">Audio output</div>
|
|
62
|
+
<Dropdown v-model="model.audioOutput" :options="audioOutputs" optionLabel="label"
|
|
63
|
+
placeholder="Select">
|
|
64
|
+
<template #value="slotProps">
|
|
65
|
+
<div class="flex flex-row align-items-center">
|
|
66
|
+
<i class="pi pi-volume-up mr-2" />
|
|
67
|
+
|
|
68
|
+
<div class="absolute overflow-hidden text-overflow-ellipsis" style="left: 2em; right: 2em;">
|
|
69
|
+
{{ slotProps.value ? slotProps.value.label : slotProps.placeholder }}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
</Dropdown>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div v-if="videoInputRequest !== 'none' && videoInputs.length > 1"
|
|
77
|
+
class="flex flex-column align-items-stretch flex-grow-1">
|
|
78
|
+
<div class="text-sm mb-1 pl-1">Camera</div>
|
|
79
|
+
<Dropdown v-model="model.videoInput" :options="videoInputs" optionLabel="label"
|
|
80
|
+
placeholder="Select">
|
|
81
|
+
<template #value="slotProps">
|
|
82
|
+
<div class="flex flex-row align-items-center">
|
|
83
|
+
<i class="pi pi-camera mr-2" />
|
|
84
|
+
|
|
85
|
+
<div class="absolute overflow-hidden text-overflow-ellipsis" style="left: 2em; right: 2em;">
|
|
86
|
+
{{ slotProps.value ? slotProps.value.label : slotProps.placeholder }}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
</Dropdown>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<PermissionsDialog
|
|
95
|
+
v-model="permissionsDialog" @ok="permissionsCallbacks.ok"
|
|
96
|
+
:required-permissions="[{ name: 'camera' }, { name: 'microphone' }]"
|
|
97
|
+
title="User media permissions" auto-close>
|
|
98
|
+
<template #introduction>
|
|
99
|
+
<div class="flex flex-column align-items-center">
|
|
100
|
+
<p>For the best experience, please allow access to your camera and microphone.</p>
|
|
101
|
+
<img src="/images/cameraAccess/en.png" style="height:50vh">
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
<template #buttons="{ permissions: { camera, microphone } }">
|
|
105
|
+
<Button v-if="camera === 'denied' && microphone === 'granted'
|
|
106
|
+
&& audioInputRequest !== 'none' && videoInputRequest !== 'required'"
|
|
107
|
+
@click="permissionsCallbacks.audioOnly()"
|
|
108
|
+
label="Audio Only" icon="pi pi-eye-slash" class="p-button-warning" autofocus />
|
|
109
|
+
<Button v-if="camera === 'granted' && microphone === 'denied'
|
|
110
|
+
&& videoInputRequest !== 'none' && audioInputRequest !== 'required'"
|
|
111
|
+
@click="permissionsCallbacks.videoOnly()"
|
|
112
|
+
label="Video Only" icon="pi pi-volume-off" class="p-button-warning" autofocus />
|
|
113
|
+
<Button @click="permissionsCallbacks.cancel()"
|
|
114
|
+
label="Cancel" icon="pi pi-times" class="p-button-warning" autofocus />
|
|
115
|
+
</template>
|
|
116
|
+
</PermissionsDialog>
|
|
117
|
+
<!--
|
|
118
|
+
|
|
119
|
+
<pre>PD: {{ permissionsDialog }}</pre>
|
|
120
|
+
|
|
121
|
+
<pre>DEV: {{ devices }}</pre>
|
|
122
|
+
|
|
123
|
+
<pre>UM: {{ userMedia }}</pre>-->
|
|
124
|
+
|
|
125
|
+
</div>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<script setup>
|
|
129
|
+
|
|
130
|
+
import { defineProps, defineModel, computed, ref, toRefs, onMounted, watch } from 'vue'
|
|
131
|
+
import { useInterval, useEventListener } from "@vueuse/core"
|
|
132
|
+
import { getUserMedia as getUserMediaNative, getDisplayMedia as getDisplayMediaNative, isUserMediaPermitted }
|
|
133
|
+
from "./userMedia.js"
|
|
134
|
+
import PermissionsDialog from './PermissionsDialog.vue'
|
|
135
|
+
|
|
136
|
+
const props = defineProps({
|
|
137
|
+
audioInputRequest: {
|
|
138
|
+
type: String,
|
|
139
|
+
default: 'wanted'
|
|
140
|
+
},
|
|
141
|
+
audioOutputRequest: {
|
|
142
|
+
type: String,
|
|
143
|
+
default: 'wanted' // can be wanted required or none
|
|
144
|
+
},
|
|
145
|
+
videoInputRequest: {
|
|
146
|
+
type: String,
|
|
147
|
+
default: 'wanted'
|
|
148
|
+
},
|
|
149
|
+
constraints: {
|
|
150
|
+
type: Object,
|
|
151
|
+
default: () => ({})
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
const { audioInputRequest, audioOutputRequest, videoInputRequest, constraints } = toRefs(props)
|
|
155
|
+
|
|
156
|
+
const model = defineModel({
|
|
157
|
+
required: true,
|
|
158
|
+
type: Object,
|
|
159
|
+
properties: {
|
|
160
|
+
audioInput: {
|
|
161
|
+
type: Object
|
|
162
|
+
},
|
|
163
|
+
audioOutput: {
|
|
164
|
+
type: Object
|
|
165
|
+
},
|
|
166
|
+
videoInput: {
|
|
167
|
+
type: Object
|
|
168
|
+
},
|
|
169
|
+
audioMuted: {
|
|
170
|
+
type: Boolean
|
|
171
|
+
},
|
|
172
|
+
videoMuted: {
|
|
173
|
+
type: Boolean
|
|
174
|
+
},
|
|
175
|
+
userMedia: {
|
|
176
|
+
type: Object
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const devices = ref([])
|
|
182
|
+
async function updateDevices() {
|
|
183
|
+
console.log("UPDATE DEVICES")
|
|
184
|
+
devices.value = await navigator.mediaDevices.enumerateDevices()
|
|
185
|
+
console.log("DEVICES", JSON.stringify(devices.value))
|
|
186
|
+
}
|
|
187
|
+
useEventListener(navigator.mediaDevices, 'devicechange', updateDevices)
|
|
188
|
+
onMounted(updateDevices)
|
|
189
|
+
|
|
190
|
+
const audioInputs = computed(() => devices.value.filter(device => device.kind === 'audioinput'))
|
|
191
|
+
const audioOutputs = computed(() => devices.value.filter(device => device.kind === 'audiooutput'))
|
|
192
|
+
const videoInputs = computed(() => devices.value.filter(device => device.kind === 'videoinput'))
|
|
193
|
+
|
|
194
|
+
const audioInputMuted = computed({
|
|
195
|
+
get: () => model.value?.audioMuted,
|
|
196
|
+
set: (value) => model.value = {
|
|
197
|
+
...model.value,
|
|
198
|
+
audioMuted: value
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const videoInputMuted = computed({
|
|
203
|
+
get: () => model.value?.videoMuted,
|
|
204
|
+
set: (value) => model.value = {
|
|
205
|
+
...model.value,
|
|
206
|
+
videoMuted: value
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
watch(audioInputs, (value) => {
|
|
211
|
+
model.value.audioInput = value[0]
|
|
212
|
+
}, { immediate: true })
|
|
213
|
+
watch(audioOutputs, (value) => {
|
|
214
|
+
model.value.audioOutput = value[0]
|
|
215
|
+
}, { immediate: true })
|
|
216
|
+
watch(videoInputs, (value) => {
|
|
217
|
+
model.value.videoInput = value[0]
|
|
218
|
+
}, { immediate: true })
|
|
219
|
+
|
|
220
|
+
/* onMounted(() => {
|
|
221
|
+
if(!model.value?.audioInput || !model.value?.audioOutput || !model.value?.videoInput) {
|
|
222
|
+
console.log("AUTO SELECT", audioInputs.value)
|
|
223
|
+
model.value = {
|
|
224
|
+
...model.value,
|
|
225
|
+
audioInput: model.audioInput || audioInputs.value[0],
|
|
226
|
+
audioOutput: model.audioOutput || audioOutputs.value[0],
|
|
227
|
+
videoInput: model.videoInput || videoInputs.value[0]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
})*/
|
|
231
|
+
|
|
232
|
+
const limitedMedia = ref(null)
|
|
233
|
+
|
|
234
|
+
const selectedConstraints = ref({ video: false, audio: false })
|
|
235
|
+
watch(() => ({
|
|
236
|
+
video: limitedMedia.value === 'audio' ? false :
|
|
237
|
+
{ deviceId: model.value.videoInput?.deviceId, ...constraints.value.video },
|
|
238
|
+
audio: limitedMedia.value === 'video' ? false :
|
|
239
|
+
{ deviceId: model.value.audioInput?.deviceId, ...constraints.value.audio }
|
|
240
|
+
}), ({ video, audio }) => {
|
|
241
|
+
console.log("SELECTED CONSTRAINTS", {
|
|
242
|
+
video: selectedConstraints.value.video?.deviceId,
|
|
243
|
+
audio: selectedConstraints.value.audio?.deviceId
|
|
244
|
+
})
|
|
245
|
+
if(selectedConstraints.value.video?.deviceId !== video?.deviceId
|
|
246
|
+
|| selectedConstraints.value.audio?.deviceId !== audio?.deviceId) {
|
|
247
|
+
console.log("SELECTED CONSTRAINTS CHANGE", { video: video.deviceId, audio: audio.deviceId })
|
|
248
|
+
selectedConstraints.value = { video, audio }
|
|
249
|
+
}
|
|
250
|
+
}, { immediate: true })
|
|
251
|
+
|
|
252
|
+
const userMedia = ref(null)
|
|
253
|
+
async function updateUserMedia() {
|
|
254
|
+
if(userMedia.value) {
|
|
255
|
+
console.log("CLOSE USER MEDIA")
|
|
256
|
+
userMedia.value.getTracks().forEach(track => track.stop())
|
|
257
|
+
userMedia.value = null
|
|
258
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
259
|
+
}
|
|
260
|
+
const constraints = selectedConstraints.value
|
|
261
|
+
const videoAllowed = videoInputRequest.value !== 'none' && constraints.video
|
|
262
|
+
const audioAllowed = audioInputRequest.value !== 'none' && constraints.audio
|
|
263
|
+
if(!videoAllowed && !audioAllowed) {
|
|
264
|
+
console.log("USER MEDIA NOT ALLOWED")
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
console.log("TRY GET USER MEDIA", JSON.stringify(constraints, null, 2))
|
|
268
|
+
try {
|
|
269
|
+
console.log("GET USER MEDIA")
|
|
270
|
+
const mediaStream = await getUserMediaNative(constraints)
|
|
271
|
+
console.log("Got User Media", mediaStream)
|
|
272
|
+
userMedia.value = mediaStream
|
|
273
|
+
} catch(e) {
|
|
274
|
+
console.error("Failed to get user media", e)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
watch(() => selectedConstraints.value, updateUserMedia, { immediate: true })
|
|
279
|
+
|
|
280
|
+
const userMediaMuted = true
|
|
281
|
+
|
|
282
|
+
watch(() => userMedia.value, stream => {
|
|
283
|
+
console.log("MEDIA STREAM CHANGE", stream)
|
|
284
|
+
model.value = {
|
|
285
|
+
...model.value,
|
|
286
|
+
media: stream
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
watch(() => [userMedia.value, audioInputMuted.value, videoInputMuted.value],
|
|
291
|
+
([stream, audioMuted, videoMuted]) => {
|
|
292
|
+
if(stream) {
|
|
293
|
+
console.log("STREAM", stream, audioMuted, videoMuted)
|
|
294
|
+
stream.getAudioTracks().forEach(track => track.enabled = !audioMuted)
|
|
295
|
+
stream.getVideoTracks().forEach(track => track.enabled = !videoMuted)
|
|
296
|
+
}}, { immediate: true })
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
const permissionsDialog = ref({ })
|
|
300
|
+
const permissionsCallbacks = ref({})
|
|
301
|
+
|
|
302
|
+
async function showPermissionsDialog() {
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
permissionsCallbacks.value = {
|
|
305
|
+
audioOnly: () => {
|
|
306
|
+
limitedMedia.value = 'audio'
|
|
307
|
+
permissionsDialog.value = {
|
|
308
|
+
...permissionsDialog.value,
|
|
309
|
+
visible: false
|
|
310
|
+
}
|
|
311
|
+
updateDevices()
|
|
312
|
+
resolve(true)
|
|
313
|
+
},
|
|
314
|
+
videoOnly: () => {
|
|
315
|
+
permissionsDialog.value = {
|
|
316
|
+
...permissionsDialog.value,
|
|
317
|
+
visible: false
|
|
318
|
+
}
|
|
319
|
+
limitedMedia.value = 'video'
|
|
320
|
+
updateDevices()
|
|
321
|
+
resolve(true)
|
|
322
|
+
},
|
|
323
|
+
ok: () => {
|
|
324
|
+
console.log("OK")
|
|
325
|
+
updateDevices()
|
|
326
|
+
resolve(true)
|
|
327
|
+
},
|
|
328
|
+
cancel: () => {
|
|
329
|
+
permissionsDialog.value = {}
|
|
330
|
+
reject('canceled by user')
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
permissionsDialog.value = {
|
|
334
|
+
...permissionsDialog.value,
|
|
335
|
+
visible: true
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function handleEmptyPreviewClick() {
|
|
341
|
+
if(userMedia.value) return
|
|
342
|
+
const { camera, microphone } = permissionsDialog.value.permissions
|
|
343
|
+
if(camera === 'denied' || microphone === 'denied') {
|
|
344
|
+
// open permissions dialog
|
|
345
|
+
showPermissionsDialog()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function handleDisabledAudioClick() {
|
|
350
|
+
limitedMedia.value = null
|
|
351
|
+
const { camera, microphone } = permissionsDialog.value.permissions
|
|
352
|
+
if(camera === 'denied' || microphone === 'denied') {
|
|
353
|
+
// open permissions dialog
|
|
354
|
+
showPermissionsDialog()
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function handleDisabledVideoClick() {
|
|
358
|
+
console.log("DISABLED VIDEO CLICK")
|
|
359
|
+
limitedMedia.value = null
|
|
360
|
+
const { camera, microphone } = permissionsDialog.value.permissions
|
|
361
|
+
if(camera === 'denied' || microphone === 'denied') {
|
|
362
|
+
// open permissions dialog
|
|
363
|
+
showPermissionsDialog()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const permissionsMap = ref({})
|
|
368
|
+
watch(() => permissionsDialog.value.permissions, value => {
|
|
369
|
+
console.log("PERMISSIONS DIALOG PERMISSIONS", value)
|
|
370
|
+
if(!permissionsMap.value.camera && !permissionsMap.value.microphone) { // first update
|
|
371
|
+
console.log('FIRST PERMISSIONS UPDATE', value)
|
|
372
|
+
if(value.camera === 'denied' || value.microphone === 'denied') {
|
|
373
|
+
showPermissionsDialog()
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if(permissionsMap.value.camera !== value.camera || permissionsMap.value.microphone !== value.microphone) {
|
|
377
|
+
permissionsMap.value = value
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
watch(() => permissionsMap.value, value => {
|
|
381
|
+
console.log("PERMISSIONS MAP CHANGED", value)
|
|
382
|
+
updateDevices()
|
|
383
|
+
//updateUserMedia()
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
window.um = userMedia
|
|
387
|
+
window.model = model
|
|
388
|
+
|
|
389
|
+
</script>
|
|
390
|
+
|
|
391
|
+
<style scoped>
|
|
392
|
+
|
|
393
|
+
</style>
|