@live-change/peer-connection-frontend 0.8.34 → 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.
@@ -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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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>