@live-change/peer-connection-frontend 0.0.3

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,378 @@
1
+ <template>
2
+ <div v-if="isMounted" class="w-full sm:w-9 md:w-8 lg:w-6 surface-card p-4 shadow-2 border-round">
3
+ <div class="text-center mb-5">
4
+ <div class="text-900 text-3xl font-medium mb-3">
5
+ Peer Connection Debugger
6
+ </div>
7
+ </div>
8
+
9
+ <div v-if="peer">
10
+ <h2>Peer connection</h2>
11
+ <pre>{{ JSON.stringify(peer.summary, null, " ") }}</pre>
12
+ <div class="buttons">
13
+ <button type="button" role="button" class="button" @click="() => peer.setOnline(true)">Set Online</button>
14
+ <button type="button" role="button" class="button" @click="() => peer.setOnline(false)">Set Offline</button>
15
+ <button type="button" role="button" class="button" @click="sendTestMessage">Test Message</button>
16
+ </div>
17
+ </div>
18
+ <div v-for="remoteStream in remoteStreams">
19
+ <h2>Remote stream {{ remoteStream.stream.id }} from {{ remoteStream.from }}</h2>
20
+ <video autoplay playsinline :src-object.prop.camel="remoteStream.stream">
21
+ </video>
22
+ </div>
23
+
24
+ <!--<div>
25
+ <h2>Devices</h2>
26
+ <pre>{{ JSON.stringify(devices, null, " ") }}</pre>
27
+ </div>-->
28
+
29
+ <div>
30
+ <h2>User media</h2>
31
+
32
+ <Dropdown v-if="videoInputDevices && videoInputDevices.length>0"
33
+ v-model="selectedVideoInput"
34
+ :options="videoInputDevices"
35
+ :optionLabel="option => option ? (option.label || 'unknown') : 'Browser default'"
36
+ :placeholder="'Select video device...'">
37
+ </Dropdown>
38
+ <Dropdown v-if="audioInputDevices && audioInputDevices.length>0"
39
+ v-model="selectedAudioInput"
40
+ :options="audioInputDevices"
41
+ :optionLabel="option => option ? (option.label || 'unknown') : 'Browser default'"
42
+ :placeholder="'Select audio device...'">
43
+ </Dropdown>
44
+
45
+ <div class="buttons" v-if="!userMedia">
46
+ <button class="button" @click="getUserMedia">getUserMedia</button>
47
+ </div>
48
+ <div class="buttons" v-if="userMedia">
49
+ <button class="button" @click="dropUserMedia">drop UserMedia</button>
50
+ <button v-if="userMediaMuted" type="button" class="button" @click="() => userMediaMuted = false">
51
+ Unmute user media
52
+ </button>
53
+ <button v-if="!userMediaMuted" type="button" class="button" @click="() => userMediaMuted = true">
54
+ Mute user media
55
+ </button>
56
+ </div>
57
+ <video v-if="userMedia" autoplay playsinline :muted="userMediaMuted"
58
+ :src-object.prop.camel="userMedia">
59
+ </video>
60
+ </div>
61
+
62
+
63
+
64
+ <div>
65
+ <h2>Display media</h2>
66
+
67
+ <div class="buttons" v-if="!displayMedia">
68
+ <button class="button" @click="getDisplayMedia">getDisplayMedia</button>
69
+ </div>
70
+ <div class="buttons" v-if="displayMedia">
71
+ <button class="button" @click="dropDisplayMedia">drop DisplayMedia</button>
72
+ </div>
73
+ <video v-if="displayMedia" autoplay playsinline muted
74
+ :src-object.prop.camel="displayMedia">
75
+ </video>
76
+ </div>
77
+
78
+ <div v-for="(track, index) in (peer ? peer.localTracks : [])">
79
+ Track #{{ index }} {{ track.track.kind }} ({{ track.track.label }}) enabled: {{ track.enabled }}
80
+ id: {{ track.track.id }}
81
+ <div class="buttons">
82
+ <button type="button" class="button" v-if="!track.enabled"
83
+ @click="() => peer.setTrackEnabled(track, true)">
84
+ Enable Track
85
+ </button>
86
+ <button type="button" class="button" v-if="track.enabled"
87
+ @click="() => peer.setTrackEnabled(track, false)">
88
+ Disable Track
89
+ </button>
90
+ </div>
91
+ </div>
92
+
93
+ <Dialog header="Permissions" v-model:visible="permissionsDialog" modal>
94
+
95
+ </Dialog>
96
+
97
+ <Dialog header="Connect camera" v-model:visible="connectDeviceDialog" modal>
98
+ <template #header>
99
+ <h3>Connect camera and microphone</h3>
100
+ </template>
101
+
102
+ <template #footer>
103
+ <Button @click="connectDeviceCallbacks.connected()"
104
+ label="Ok, connected" icon="pi pi-check" class="p-button-success" autofocus />
105
+ <Button @click="connectDeviceCallbacks.camera()"
106
+ label="Use only camera" icon="pi pi-video" class="p-button-warning" />
107
+ <Button @click="connectDeviceCallbacks.microphone()"
108
+ label="Use only microphone" icon="pi pi-volume-up" class="p-button-warning" />
109
+ <Button @click="connectDeviceCallbacks.cancel()"
110
+ label="Cancel" icon="pi pi-times" class="p-button-danger" />
111
+ </template>
112
+ </Dialog>
113
+ </div>
114
+ </template>
115
+
116
+ <script setup>
117
+ import Button from "primevue/button"
118
+ import Dropdown from "primevue/dropdown"
119
+ import Dialog from "primevue/dialog"
120
+
121
+ import { ref, computed, watch, onMounted } from 'vue'
122
+ import { path, live, actions, api as useApi } from '@live-change/vue3-ssr'
123
+ const api = useApi()
124
+
125
+ import { createPeer } from "./Peer.js"
126
+ import { getUserMedia as getUserMediaNative, getDisplayMedia as getDisplayMediaNative, isUserMediaPermitted }
127
+ from "./userMedia.js"
128
+
129
+ const { channelType, channel } = defineProps({
130
+ channelType: {
131
+ type: String,
132
+ required: true
133
+ },
134
+ channel: {
135
+ type: String,
136
+ required: true
137
+ }
138
+ })
139
+
140
+ const isMounted = ref(false)
141
+ onMounted( () => isMounted.value = true )
142
+
143
+ const devices = ref([])
144
+ const videoInputDevices = computed(() => devices.value.filter(d => d.kind == 'videoinput'))
145
+ const audioInputDevices = computed(() => devices.value.filter(d => d.kind == 'audioinput'))
146
+
147
+ const selectedVideoInput = ref(null)
148
+ const selectedAudioInput = ref(null)
149
+ const userMediaConstraints = computed(() => ({
150
+ video: selectedVideoInput.value?.deviceId ? { deviceId: selectedVideoInput.value.deviceId } : true,
151
+ audio: selectedAudioInput.value?.deviceId ? { deviceId: selectedAudioInput.value.deviceId } : true,
152
+ }))
153
+
154
+ const userMedia = ref()
155
+ const displayMedia = ref()
156
+ const localMediaStreams = computed(() =>
157
+ (userMedia.value ? [userMedia.value] : []).concat(displayMedia.value ? [displayMedia.value] : [])
158
+ )
159
+
160
+ watch(() => userMediaConstraints.value, async value => {
161
+ if(userMedia.value) {
162
+ await dropUserMedia()
163
+ await getUserMedia()
164
+ }
165
+ })
166
+
167
+ watch(() => userMedia.value, (mediaStream, oldMediaStream) => {
168
+ console.log("USER MEDIA STREAM CHANGE:", mediaStream, oldMediaStream)
169
+ readDevices()
170
+ if(oldMediaStream) {
171
+ console.log("OLD MEDIA STREAM", oldMediaStream)
172
+ oldMediaStream.getTracks().forEach(track => { if (track.readyState == 'live') track.stop() })
173
+ }
174
+ })
175
+
176
+ const displayMediaEndedHandler = () => displayMedia.value = null
177
+ watch(() => displayMedia.value, (mediaStream, oldMediaStream) => {
178
+ console.log("DISPLAY MEDIA STREAM CHANGE:", mediaStream, oldMediaStream)
179
+ if(oldMediaStream) {
180
+ const track = oldMediaStream.getVideoTracks()[0]
181
+ if(track) track.removeEventListener('ended', displayMediaEndedHandler)
182
+
183
+ console.log("OLD MEDIA STREAM", oldMediaStream)
184
+ oldMediaStream.getTracks().forEach(track => { if (track.readyState == 'live') track.stop() })
185
+ }
186
+ if(mediaStream) {
187
+ const track = mediaStream.getVideoTracks()[0]
188
+ if(track) track.addEventListener('ended', displayMediaEndedHandler)
189
+ }
190
+ })
191
+
192
+ const peer = ref()
193
+ const remoteStreams = computed(() => {
194
+ if(!peer.value) return []
195
+ let remoteStreams = []
196
+ for(const connection of peer.value.connections) {
197
+ for(const remoteTrack of connection.remoteTracks) {
198
+ if(remoteStreams.find(remoteStream => remoteStream.stream == remoteTrack.stream)) continue
199
+ remoteStreams.push({
200
+ from: connection.to,
201
+ stream: remoteTrack.stream
202
+ })
203
+ }
204
+ }
205
+ return remoteStreams
206
+ })
207
+
208
+ const userMediaMuted = ref(true)
209
+
210
+ const deviceChangeHandler = () => readDevices()
211
+ onMounted(async () => {
212
+ console.log("MOUNTED!")
213
+ await initPeer()
214
+ console.log(" PEER INITIALIZED!", peer.value)
215
+ readDevices()
216
+
217
+ if(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
218
+ navigator.mediaDevices.addEventListener('devicechange', deviceChangeHandler)
219
+ }
220
+ })
221
+
222
+
223
+ async function readDevices() {
224
+ if(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
225
+ const nativeDevices = await navigator.mediaDevices.enumerateDevices()
226
+ devices.value = nativeDevices.map(({ deviceId, groupId, kind, label }) => ({ deviceId, groupId, kind, label }))
227
+ }
228
+ }
229
+
230
+ let createPeerPromise = null
231
+ async function initPeer() {
232
+ if(createPeerPromise) return createPeerPromise
233
+ createPeerPromise = createPeer({
234
+ channelType, channel,
235
+ localMediaStreams
236
+ })
237
+ peer.value = await createPeerPromise
238
+ createPeerPromise = null
239
+ }
240
+
241
+ async function getUserMedia() { // media stream retrival logic
242
+ let constraints = { ...userMediaConstraints.value } // make a copy
243
+ while(true) {
244
+ try {
245
+ console.log("TRY GET USER MEDIA", constraints)
246
+ const mediaStream = await getUserMediaNative(constraints)
247
+ const videoTracks = mediaStream.getVideoTracks()
248
+ const audioTracks = mediaStream.getAudioTracks()
249
+ console.log('Got stream with constraints:', constraints)
250
+ if(constraints.video) console.log(`Using video device: ${videoTracks[0] && videoTracks[0].label}`)
251
+ if(constraints.audio) console.log(`Using audio device: ${audioTracks[0] && audioTracks[0].label}`)
252
+ this.userMedia = mediaStream
253
+ return;
254
+ } catch(error) {
255
+ console.log("GET USER MEDIA ERROR", error)
256
+ const permitted = await isUserMediaPermitted(constraints)
257
+ if(permitted || error.code == error.NOT_FOUND_ERR) {
258
+ constraints = await askToConnectCamera({ ...userMediaConstraints.value })
259
+ if(!constraints) return
260
+ } else { // if not permitted display dialog
261
+ const permitted = await showPermissionsModal()
262
+ console.log("CAMERA PERMITTED", permitted)
263
+ if(!permitted) constraints.video = false
264
+ if(!(constraints.video || constraints.audio)) {
265
+ constraints = await askToConnectCamera({ ...userMediaConstraints.value })
266
+ if(!constraints) return
267
+ }
268
+ continue // retry get user media with new constraints
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ async function dropUserMedia() {
275
+ this.userMedia = null
276
+ }
277
+
278
+
279
+ import { usePermission } from "@vueuse/core"
280
+ const microphonePermission = usePermission('microphone')
281
+ const cameraPermission = usePermission('camera')
282
+
283
+ const permissionsDialog = ref(false)
284
+ const permissionsCallbacks = ref(null)
285
+
286
+ async function showPermissionsModal() {
287
+ return new Promise((resolve, reject) => {
288
+ permissionsCallbacks.value = {
289
+ disabled: () => {
290
+ resolve(false)
291
+ },
292
+ ok: () => {
293
+ resolve(true)
294
+ },
295
+ cancel: () => {
296
+ reject('canceled by user')
297
+ }
298
+ }
299
+ permissionsDialog.value = true
300
+ })
301
+ }
302
+
303
+ const connectDeviceDialog = ref(false)
304
+ const connectDeviceCallbacks = ref(null)
305
+
306
+ async function askToConnectCamera(constraints) {
307
+ return new Promise((resolve, reject) => {
308
+ connectDeviceCallbacks.value = {
309
+ connected: () => resolve({ ...constraints }),
310
+ camera: () => resolve({ ...constraints, audio: false }),
311
+ microphone: () => resolve({ ...constraints, video: false }),
312
+ cancel: () => resolve(null)
313
+ }
314
+ connectDeviceDialog.value = true
315
+ })
316
+ }
317
+
318
+ async function getDisplayMedia() { // media stream retrival logic
319
+ let initialConstraints = { video: true } // make a copy
320
+ let constraints = { ...initialConstraints }
321
+ while(true) {
322
+ try {
323
+ console.log("TRY GET DISPLAY MEDIA", constraints)
324
+ const mediaStream = await getDisplayMediaNative(constraints)
325
+ const videoTracks = mediaStream.getVideoTracks()
326
+ const audioTracks = mediaStream.getAudioTracks()
327
+ console.log('Got stream with constraints:', constraints)
328
+ if(constraints.video) console.log(`Using video device: ${videoTracks[0] && videoTracks[0].label}`)
329
+ if(constraints.audio) console.log(`Using audio device: ${audioTracks[0] && audioTracks[0].label}`)
330
+ displayMedia.value = mediaStream
331
+ return;
332
+ } catch(error) {
333
+ console.log("GET DISPLAY MEDIA ERROR", error)
334
+ return;
335
+ }
336
+ }
337
+ }
338
+
339
+ async function dropDisplayMedia() {
340
+ this.displayMedia = null
341
+ }
342
+
343
+
344
+ function sendTestMessage() {
345
+ for(const connection of this.peer.value.connections) {
346
+ peer.value.sendMessage({
347
+ to: connection.to,
348
+ type: "ping",
349
+ data: new Date().toISOString()
350
+ })
351
+ }
352
+ }
353
+
354
+
355
+ </script>
356
+
357
+ <style scoped lang="scss">
358
+ .peer-connection-debugger {
359
+
360
+ position: absolute;
361
+ left: 0;
362
+ bottom: 0;
363
+ width: 100%;
364
+ height: 100%;
365
+ background: white;
366
+
367
+ .peer-connection-debugger-content {
368
+ position: absolute;
369
+ left: 0;
370
+ top: 50px;
371
+ bottom: 0;
372
+ width: 100%;
373
+ height: auto;
374
+ overflow: auto;
375
+ background: white;
376
+ }
377
+ }
378
+ </style>