@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.
- package/e2e/codecept.conf.js +60 -0
- package/e2e/steps.d.ts +12 -0
- package/e2e/steps_file.js +11 -0
- package/front/index.html +11 -0
- package/front/public/favicon.ico +0 -0
- package/front/public/images/empty-photo.svg +38 -0
- package/front/public/images/empty-user-photo.svg +33 -0
- package/front/public/images/logo.svg +34 -0
- package/front/public/images/logo128.png +0 -0
- package/front/src/App.vue +84 -0
- package/front/src/NavBar.vue +105 -0
- package/front/src/Page.vue +47 -0
- package/front/src/components/Debugger.vue +378 -0
- package/front/src/components/Peer.js +329 -0
- package/front/src/components/PeerConnection.js +329 -0
- package/front/src/components/userMedia.js +51 -0
- package/front/src/entry-client.js +24 -0
- package/front/src/entry-server.js +59 -0
- package/front/src/main.js +60 -0
- package/front/src/router.js +63 -0
- package/front/vite.config.js +118 -0
- package/package.json +73 -0
- package/server/init.js +7 -0
- package/server/services.config.js +25 -0
|
@@ -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>
|