@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,329 @@
|
|
|
1
|
+
import { ref, isRef, onUnmounted, getCurrentInstance, unref, reactive, computed } from 'vue'
|
|
2
|
+
import { path, live, actions, api as useApi } from '@live-change/vue3-ssr'
|
|
3
|
+
import { createPeerConnection } from "./PeerConnection.js"
|
|
4
|
+
|
|
5
|
+
const messagesBucketSize = 32
|
|
6
|
+
|
|
7
|
+
let lastInstanceId = 0
|
|
8
|
+
|
|
9
|
+
const createPeer = async ({ channelType, channel, instance, localMediaStreams, online, onUnmountedCb }) => {
|
|
10
|
+
if(!isRef(localMediaStreams)) {
|
|
11
|
+
localMediaStreams = ref(localMediaStreams ?? [])
|
|
12
|
+
}
|
|
13
|
+
if(!isRef(online)) {
|
|
14
|
+
online = ref(online ?? false)
|
|
15
|
+
}
|
|
16
|
+
if(!onUnmountedCb && typeof window != 'undefined') {
|
|
17
|
+
if(getCurrentInstance()) {
|
|
18
|
+
onUnmountedCb = onUnmounted
|
|
19
|
+
} else {
|
|
20
|
+
onUnmountedCb = () => {
|
|
21
|
+
console.error("peer outside component instance - leak possible")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if(!instance) instance = window.__WINDOW_ID__ + '.' + (++lastInstanceId)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
const api = useApi()
|
|
29
|
+
|
|
30
|
+
const peerId = [channelType, channel, 'session_Session', api.client.value.session, instance].join(':')
|
|
31
|
+
|
|
32
|
+
console.log("CREATE PEER!")
|
|
33
|
+
|
|
34
|
+
const [ peers, peerOnline, turnConfiguration ] = await Promise.all([
|
|
35
|
+
live(path().peerConnection.peers({ channelType, channel })),
|
|
36
|
+
live(path().online.session({ group: 'peer', peer: peerId })),
|
|
37
|
+
live(path().peerConnection.turnConfiguration({ peer: peerId }))
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
const localPeerState = ref(null)
|
|
41
|
+
|
|
42
|
+
const finished = ref(false)
|
|
43
|
+
const lastProcessedMessage = ref('')
|
|
44
|
+
const connections = ref([])
|
|
45
|
+
const waitingConnections = ref([]) // connections that are not initialized, but messages are received
|
|
46
|
+
const localTracks = ref([])
|
|
47
|
+
|
|
48
|
+
const otherPeers = computed(() => peers.value?.filter(peer => peer.id != peerId))
|
|
49
|
+
const isConnectionPossible = computed(() => online.value && (!!turnConfiguration.value))
|
|
50
|
+
|
|
51
|
+
const rtcConfiguration = computed(() => ({
|
|
52
|
+
iceServers: [ turnConfiguration.value ],
|
|
53
|
+
iceTransportPolicy: 'all', // 'all' or 'relay',
|
|
54
|
+
bundlePolicy: 'balanced'
|
|
55
|
+
}))
|
|
56
|
+
const clientIp = computed(() => turnConfiguration.value?.clientIp)
|
|
57
|
+
|
|
58
|
+
const anyLocalAudioEnabled = computed(() => localTracks.value
|
|
59
|
+
.some(trackInfo => trackInfo.track.kind == 'audio' && trackInfo.enabled))
|
|
60
|
+
const anyLocalVideoEnabled = computed(() => localTracks.value
|
|
61
|
+
.some(trackInfo => trackInfo.track.kind == 'video' && trackInfo.enabled))
|
|
62
|
+
const anyLocalAudioAvailable = computed(() => localTracks.value
|
|
63
|
+
.some(trackInfo => trackInfo.track.kind == 'audio'))
|
|
64
|
+
const anyLocalVideoAvailable = computed(() => localTracks.value
|
|
65
|
+
.some(trackInfo => trackInfo.track.kind == 'video'))
|
|
66
|
+
const computedLocalPeerState = computed(() => ({
|
|
67
|
+
audioState: anyLocalAudioAvailable.value ? (anyLocalAudioEnabled.value ? "enabled" : "muted") : "none",
|
|
68
|
+
videoState: anyLocalVideoAvailable.value ? (anyLocalVideoEnabled.value ? "enabled" : "muted") : "none"
|
|
69
|
+
}))
|
|
70
|
+
|
|
71
|
+
const summary = computed(() => ({
|
|
72
|
+
peerId, online: online.value, finished: finished.value,
|
|
73
|
+
computedLocalPeerState: computedLocalPeerState.value,
|
|
74
|
+
lastProcessedMessage: lastProcessedMessage.value,
|
|
75
|
+
peers: peers.value?.length,
|
|
76
|
+
otherPeers: otherPeers.value?.map(p => p.id),
|
|
77
|
+
connections: connections.value?.map(connection => connection.summary),
|
|
78
|
+
tracks: localTracks.value?.map(({ track, stream }) => {
|
|
79
|
+
const { id, kind, label, muted, enabled } = track
|
|
80
|
+
return { id, kind, label, muted, enabled, stream: stream.id }
|
|
81
|
+
}),
|
|
82
|
+
turnConfiguration: turnConfiguration.value && {
|
|
83
|
+
...turnConfiguration.value,
|
|
84
|
+
expire: new Date((+turnConfiguration.value.username.split(':')[0])*1000).toLocaleString()
|
|
85
|
+
},
|
|
86
|
+
isConnectionPossible: isConnectionPossible.value
|
|
87
|
+
}))
|
|
88
|
+
|
|
89
|
+
function setOnline(onlineValue) {
|
|
90
|
+
online.value = onlineValue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
peerId, online, isConnectionPossible,
|
|
95
|
+
connections, localTracks,
|
|
96
|
+
otherPeers,
|
|
97
|
+
summary,
|
|
98
|
+
setOnline
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
|
|
103
|
+
},
|
|
104
|
+
watch: {
|
|
105
|
+
otherPeers(peers) {
|
|
106
|
+
this.updateConnections()
|
|
107
|
+
},
|
|
108
|
+
isConnectionPossible(online) {
|
|
109
|
+
this.updateConnections()
|
|
110
|
+
},
|
|
111
|
+
computedLocalPeerState(newState) {
|
|
112
|
+
this.updatePeerState(newState)
|
|
113
|
+
},
|
|
114
|
+
localMediaStreams(newStreams, oldStreams) {
|
|
115
|
+
console.log("LOCAL MEDIA STREAMS CHANGE",
|
|
116
|
+
newStreams.map(stream => ({ id: stream.id, tracks: stream.getTracks().map(tr => tr.kind).join('/') })),
|
|
117
|
+
oldStreams.map(stream => ({ id: stream.id, tracks: stream.getTracks().map(tr => tr.kind).join('/') })))
|
|
118
|
+
|
|
119
|
+
let deletedTracks = []
|
|
120
|
+
let addedTracks = []
|
|
121
|
+
for(const oldStream of oldStreams) {
|
|
122
|
+
if(newStreams.indexOf(oldStream) != -1) continue; // existing stream
|
|
123
|
+
deletedTracks.push(...(oldStream.getTracks().map( track => ({ track, stream: oldStream }) )))
|
|
124
|
+
oldStream.removeEventListener('addtrack', this.mediaStreamAddTrackHandler)
|
|
125
|
+
oldStream.removeEventListener('removetrack', this.mediaStreamRemoveTrackHandler)
|
|
126
|
+
}
|
|
127
|
+
for(const newStream of newStreams) {
|
|
128
|
+
if(oldStreams.indexOf(newStream) != -1) continue; // existing stream
|
|
129
|
+
addedTracks.push(...(newStream.getTracks().map(track => {
|
|
130
|
+
const trackInfo = {
|
|
131
|
+
track, stream: newStream, muted: track.muted, enabled: track.enabled,
|
|
132
|
+
muteHandler: () => trackInfo.muted = track.muted,
|
|
133
|
+
unmuteHandler: () => trackInfo.muted = track.muted
|
|
134
|
+
}
|
|
135
|
+
return trackInfo
|
|
136
|
+
})))
|
|
137
|
+
newStream.addEventListener('addtrack', this.mediaStreamAddTrackHandler)
|
|
138
|
+
newStream.addEventListener('removetrack', this.mediaStreamRemoveTrackHandler)
|
|
139
|
+
}
|
|
140
|
+
for(const deletedTrack of deletedTracks) {
|
|
141
|
+
const trackIndex = this.localTracks.findIndex(track => track.track == deletedTrack.track)
|
|
142
|
+
if(trackIndex == -1) return console.error(`removal of non existing track ${deletedTrack.id}`)
|
|
143
|
+
const trackInfo = this.localTracks[trackIndex]
|
|
144
|
+
trackInfo.track.removeEventListener('mute', deletedTrack.muteHandler)
|
|
145
|
+
trackInfo.track.removeEventListener('unmute', deletedTrack.unmuteHandler)
|
|
146
|
+
this.localTracks.splice(trackIndex, 1)
|
|
147
|
+
}
|
|
148
|
+
for(const addedTrack of addedTracks) {
|
|
149
|
+
addedTrack.track.addEventListener('mute', addedTrack.muteHandler)
|
|
150
|
+
addedTrack.track.addEventListener('unmute', addedTrack.unmuteHandler)
|
|
151
|
+
this.localTracks.push(addedTrack)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
methods: {
|
|
156
|
+
setTrackEnabled(track, v) {
|
|
157
|
+
track.enabled = v
|
|
158
|
+
track.track.enabled = v
|
|
159
|
+
},
|
|
160
|
+
updatePeerState(newState) {
|
|
161
|
+
const updated = { ...this.localPeerState, ...newState }
|
|
162
|
+
if(JSON.stringify(updated) != JSON.stringify(this.localPeerState)) {
|
|
163
|
+
this.localPeerState = updated
|
|
164
|
+
const update = { ...updated, peer: peerId, _commandId: api.guid() }
|
|
165
|
+
this.sendPeerStateUpdate(update)
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
sendPeerStateUpdate(update) {
|
|
169
|
+
const requestTimeout = 10000
|
|
170
|
+
dao.requestWithSettings({ requestTimeout },
|
|
171
|
+
['peerConnection', 'setPeerState'], update)
|
|
172
|
+
.catch(error => {
|
|
173
|
+
console.log("SET PEER STATE ERROR", error)
|
|
174
|
+
if(error == 'timeout' && !this.finished
|
|
175
|
+
&& JSON.stringify({ ...this.localPeerState, ...update }) === JSON.stringify(this.localPeerState)
|
|
176
|
+
) {
|
|
177
|
+
console.log("RETRYING")
|
|
178
|
+
this.sendPeerStateUpdate()
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
},
|
|
182
|
+
observeMore() {
|
|
183
|
+
if(this.messagesObservable) {
|
|
184
|
+
this.messagesObservable.unobserve(this.messagesObserver)
|
|
185
|
+
this.messagesObservable = null
|
|
186
|
+
}
|
|
187
|
+
const path = ['peerConnection', 'messages', {
|
|
188
|
+
peer: peerId,
|
|
189
|
+
gt: this.lastProcessedMessage,
|
|
190
|
+
limit: messagesBucketSize
|
|
191
|
+
}]
|
|
192
|
+
this.messagesObservable = api.observable(path).observable
|
|
193
|
+
//console.log("MESSAGES OBSERVABLE", path, this.messagesObservable, this.messagesObservable.observable)
|
|
194
|
+
this.messagesObservable.observe(this.messagesObserver)
|
|
195
|
+
//this.messagesObservable.observe(this.messagesObserver)
|
|
196
|
+
},
|
|
197
|
+
handleMessagesSignal(signal, ...args) {
|
|
198
|
+
//console.log("HANDLE MESSAGE SIGNAL", signal, args)
|
|
199
|
+
if(signal == 'error') {
|
|
200
|
+
const error = args[0]
|
|
201
|
+
console.error("PEER MESSAGE ERROR", error.stack || error)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
if(signal == 'putByField') {
|
|
205
|
+
const [field, id, message] = args
|
|
206
|
+
this.handleMessage(message)
|
|
207
|
+
} else if(signal == 'set') {
|
|
208
|
+
const value = args[0]
|
|
209
|
+
for(const message of value) {
|
|
210
|
+
this.handleMessage(message)
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
console.error("SIGNAL NOT HANDLED", signal)
|
|
214
|
+
/!*for(const message of this.messagesObservable.list) {
|
|
215
|
+
this.handleMessage(message)
|
|
216
|
+
}*!/
|
|
217
|
+
}
|
|
218
|
+
//console.log("PEER MESSAGES OBSERVABLE", this.messagesObservable)
|
|
219
|
+
if(this.messagesObservable.list.length >= messagesBucketSize) {
|
|
220
|
+
this.observeMore()
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
handleMessage(message) {
|
|
224
|
+
if(message.id <= this.lastProcessedMessage) {
|
|
225
|
+
console.log("IGNORE OLD MESSAGE", message.id)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
this.lastProcessedMessage = message.id
|
|
229
|
+
//console.log("HANDLE PEER MESSAGE", message)
|
|
230
|
+
if(message.from) {
|
|
231
|
+
let connection = this.connections.find(c => c.to == message.from)
|
|
232
|
+
if(!connection) connection = this.waitingConnections.find(c => c.to == message.from)
|
|
233
|
+
if(!connection) {
|
|
234
|
+
connection = createPeerConnection(this, message.from)
|
|
235
|
+
this.waitingConnections.push(connection)
|
|
236
|
+
}
|
|
237
|
+
connection.handleMessage(message)
|
|
238
|
+
} else {
|
|
239
|
+
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
sendMessage(message) {
|
|
243
|
+
message.from = peerId
|
|
244
|
+
message.sent = message.sent || new Date().toISOString()
|
|
245
|
+
message._commandId = message._commandId || api.guid()
|
|
246
|
+
const requestTimeout = 10000
|
|
247
|
+
//console.log("SENDING PEER MESSAGE", message)
|
|
248
|
+
dao.requestWithSettings({ requestTimeout }, ['peerConnection', 'postMessage'], message)
|
|
249
|
+
.catch(error => {
|
|
250
|
+
console.log("PEER MESSAGE ERROR", error)
|
|
251
|
+
if(error == 'timeout' && !this.finished) {
|
|
252
|
+
console.log("RETRYING")
|
|
253
|
+
this.sendMessage(message)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
},
|
|
257
|
+
updateConnections() {
|
|
258
|
+
const peers = this.isConnectionPossible ? this.otherPeers : []
|
|
259
|
+
for(let connectionId = 0; connectionId < this.connections.length; connectionId++) {
|
|
260
|
+
const connection = this.connections[connectionId]
|
|
261
|
+
const connectionPeer = peers.find(peer => peer.id == connection.to)
|
|
262
|
+
if(!connectionPeer) {
|
|
263
|
+
connection.close()
|
|
264
|
+
connection.$destroy()
|
|
265
|
+
this.connections.splice(connectionId, 1)
|
|
266
|
+
connectionId --
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for(const peer of peers) {
|
|
270
|
+
let peerConnection = this.connections.find(connection => connection.to == peer.id)
|
|
271
|
+
if(peerConnection) continue;
|
|
272
|
+
const peerConnectionId = this.waitingConnections.findIndex(connection => connection.to == peer.id)
|
|
273
|
+
if(peerConnectionId != -1) { // use waiting connection with cached messages
|
|
274
|
+
peerConnection = this.waitingConnections[peerConnectionId]
|
|
275
|
+
this.waitingConnections.splice(peerConnectionId, 1)
|
|
276
|
+
} else { // create connection
|
|
277
|
+
peerConnection = createPeerConnection(this, peer.id)
|
|
278
|
+
}
|
|
279
|
+
this.connections.push(peerConnection)
|
|
280
|
+
peerConnection.connect()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
created() {
|
|
285
|
+
this.messagesObserver = (...args) => this.handleMessagesSignal(...args)
|
|
286
|
+
this.observeMore()
|
|
287
|
+
this.disconnectHandler = () => this.observeMore() // to avoid redownloading processed messages
|
|
288
|
+
api.on('disconnect', this.disconnectHandler)
|
|
289
|
+
|
|
290
|
+
this.mediaStreamAddTrackHandler = (event) => {
|
|
291
|
+
const track = event.track
|
|
292
|
+
const trackInfo = {
|
|
293
|
+
track, stream: newStream, muted: track.muted, enabled: track.enabled,
|
|
294
|
+
muteHandler: () => trackInfo.muted = track.muted,
|
|
295
|
+
unmuteHandler: () => trackInfo.muted = track.muted
|
|
296
|
+
}
|
|
297
|
+
console.log("MEDIA STREAM ADD TRACK!", trackInfo)
|
|
298
|
+
trackInfo.track.addEventListener('mute', trackInfo.muteHandler)
|
|
299
|
+
trackInfo.track.addEventListener('unmute', trackInfo.unmuteHandler)
|
|
300
|
+
this.localTracks.push(trackInfo)
|
|
301
|
+
}
|
|
302
|
+
this.mediaStreamRemoveTrackHandler = (event) => {
|
|
303
|
+
const trackIndex = this.localTracks.indexOf(event.track)
|
|
304
|
+
if(trackIndex == -1) return console.error(`removal of non existing track ${event.track.id}`)
|
|
305
|
+
const trackInfo = this.localTracks[trackIndex]
|
|
306
|
+
console.log("MEDIA STREAM REMOVE TRACK!", trackInfo)
|
|
307
|
+
trackInfo.track.removeEventListener('mute', trackInfo.muteHandler)
|
|
308
|
+
trackInfo.track.removeEventListener('unmute', trackInfo.unmuteHandler)
|
|
309
|
+
this.localTracks.splice(trackIndex, 1)
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
beforeDestroy() {
|
|
313
|
+
this.finished = true
|
|
314
|
+
if(this.messagesObservable) {
|
|
315
|
+
this.messagesObservable.unobserve(this.messagesObserver)
|
|
316
|
+
this.messagesObservable = null
|
|
317
|
+
}
|
|
318
|
+
for(const connection of this.waitingConnections) {
|
|
319
|
+
connection.$destroy()
|
|
320
|
+
}
|
|
321
|
+
for(const connection of this.connections) {
|
|
322
|
+
connection.$destroy()
|
|
323
|
+
}
|
|
324
|
+
api.removeListener('disconnect', this.disconnectHandler)
|
|
325
|
+
}
|
|
326
|
+
})*/
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export { createPeer }
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
//import Vue from "vue"
|
|
2
|
+
|
|
3
|
+
const createPeerConnection = (peer, to) => {
|
|
4
|
+
return new Vue({
|
|
5
|
+
data: {
|
|
6
|
+
waitingMessages: [],
|
|
7
|
+
state: "created",
|
|
8
|
+
rtc: null,
|
|
9
|
+
rtcSignalingState: "",
|
|
10
|
+
iceGatheringState: "",
|
|
11
|
+
iceConnectionState: "",
|
|
12
|
+
rtpSenders: [],
|
|
13
|
+
offerOptions: null,
|
|
14
|
+
answerOptions: null,
|
|
15
|
+
remoteTracks: [],
|
|
16
|
+
restartOnDisconnect: false // because could not set rtc configuration(firefox)
|
|
17
|
+
},
|
|
18
|
+
computed: {
|
|
19
|
+
to() {
|
|
20
|
+
return to
|
|
21
|
+
},
|
|
22
|
+
summary() {
|
|
23
|
+
return {
|
|
24
|
+
to: this.to,
|
|
25
|
+
state: this.state,
|
|
26
|
+
waitingMessages: this.waitingMessages.length,
|
|
27
|
+
rtpSenders: this.rtpSenders.map(({ sender, stream }) => {
|
|
28
|
+
const { id, kind, label } = sender.track
|
|
29
|
+
return { id, kind, label, stream: stream.id }
|
|
30
|
+
}),
|
|
31
|
+
rtcSignalingState: this.rtcSignalingState,
|
|
32
|
+
iceGatheringState: this.iceGatheringState,
|
|
33
|
+
iceConnectionState: this.iceConnectionState,
|
|
34
|
+
remoteTracks: this.remoteTracks.map(({ track, stream }) => {
|
|
35
|
+
const { id, kind, label, muted } = track
|
|
36
|
+
return { id, kind, label, muted, stream: stream.id }
|
|
37
|
+
}),
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
localTracks() {
|
|
41
|
+
return peer.localTracks
|
|
42
|
+
},
|
|
43
|
+
isEnabled() {
|
|
44
|
+
return this.state != 'closed' && this.state != 'created'
|
|
45
|
+
},
|
|
46
|
+
rtcConfiguration() {
|
|
47
|
+
return peer.rtcConfiguration
|
|
48
|
+
},
|
|
49
|
+
clientIp() {
|
|
50
|
+
return peer.clientIp
|
|
51
|
+
},
|
|
52
|
+
isPolite() {
|
|
53
|
+
return peer.peerId < this.to
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
watch: {
|
|
57
|
+
isEnabled() {
|
|
58
|
+
this.synchronizeLocalTracks()
|
|
59
|
+
},
|
|
60
|
+
localTracks() {
|
|
61
|
+
this.synchronizeLocalTracks()
|
|
62
|
+
},
|
|
63
|
+
rtcConfiguration(configuration) {
|
|
64
|
+
if(this.rtc) {
|
|
65
|
+
if(this.rtc.setConfiguration) {
|
|
66
|
+
this.rtc.setConfiguration(configuration)
|
|
67
|
+
} else {
|
|
68
|
+
this.restartOnDisconnect = true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
clientIp(newIp, oldIp) {
|
|
73
|
+
if(this.rtc) {
|
|
74
|
+
this.restartConnection()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
methods: {
|
|
79
|
+
async connect() {
|
|
80
|
+
console.log("PeerConnection connect")
|
|
81
|
+
if(this.rtc) throw new Error("can't connect twice!")
|
|
82
|
+
this.state = 'connecting'
|
|
83
|
+
this.rtc = new RTCPeerConnection(this.rtcConfiguration)
|
|
84
|
+
this.rtcSignalingState = this.rtc.signalingState
|
|
85
|
+
this.iceGatheringState = this.rtc.iceGatheringState
|
|
86
|
+
this.iceConnectionState = this.rtc.iceConnectionState
|
|
87
|
+
this.rtc.addEventListener('negotiationneeded', this.negotiationNeededHandler)
|
|
88
|
+
this.rtc.addEventListener('signalingstatechange', this.signalingStateChangeHandler)
|
|
89
|
+
this.rtc.addEventListener('icecandidate', this.iceCandidateHandler)
|
|
90
|
+
this.rtc.addEventListener('track', this.trackHandler)
|
|
91
|
+
this.rtc.addEventListener('icegatheringstatechange', this.iceGatheringStateChangeHandler)
|
|
92
|
+
this.rtc.addEventListener('iceconnectionstatechange', this.iceConnectionStateChangeHandler)
|
|
93
|
+
for(const message of this.waitingMessages) {
|
|
94
|
+
try {
|
|
95
|
+
await this.handleMessage(message)
|
|
96
|
+
} catch(error) {
|
|
97
|
+
console.error("MESSAGE", message, "HANDLING ERROR", error)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this.waitingMessages = []
|
|
101
|
+
},
|
|
102
|
+
close() {
|
|
103
|
+
console.log("PeerConnection close")
|
|
104
|
+
this.state = 'closed'
|
|
105
|
+
if(this.rtc) {
|
|
106
|
+
this.rtc.close()
|
|
107
|
+
this.rtc = null
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async handleMessage(message) {
|
|
111
|
+
//console.log("PC", to, "HANDLE MESSAGE", message)
|
|
112
|
+
if(this.state == 'created') {
|
|
113
|
+
console.log("ADD MESSAGE TO WAITING QUEUE")
|
|
114
|
+
this.waitingMessages.push(message)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
if(this.state == 'close') return;
|
|
118
|
+
//console.log("DO HANDLE MESSAGE")
|
|
119
|
+
switch(message.type) {
|
|
120
|
+
case "sdp": {
|
|
121
|
+
console.log("RECEIVED SDP", message.data.type, "IN STATE", this.rtc.signalingState)
|
|
122
|
+
if(message.data.type == 'offer') {
|
|
123
|
+
if(this.rtc.signalingState != "stable") {
|
|
124
|
+
console.log("SDP CONFLICT, RECEIVED OFFER IN UNSTABLE STATE")
|
|
125
|
+
if(this.isPolite) {
|
|
126
|
+
console.log("I AM POLITE SO I WILL ROLLBACK RTC STATE MACHINE")
|
|
127
|
+
await this.rtc.setLocalDescription({type: "rollback"}),
|
|
128
|
+
await this.rtc.setRemoteDescription(message.data)
|
|
129
|
+
console.log("ROLLBACK DONE")
|
|
130
|
+
const answer = await this.rtc.createAnswer(this.answerOptions || undefined)
|
|
131
|
+
console.log("GOT RTC ANSWER IN STATE", this.rtc.signalingState)
|
|
132
|
+
await this.rtc.setLocalDescription(answer)
|
|
133
|
+
console.log("LOCAL ANSWER DESCRIPTION SET! SENDING ANSWER!")
|
|
134
|
+
peer.sendMessage({ to, type: "sdp", data: answer })
|
|
135
|
+
} else {
|
|
136
|
+
console.log("I AM NOT POLITE SO I WILL IGNORE OFFER")
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
console.log("SDP STATE GOOD!")
|
|
140
|
+
await this.rtc.setRemoteDescription(message.data)
|
|
141
|
+
const answer = await this.rtc.createAnswer(this.answerOptions || undefined)
|
|
142
|
+
console.log("GOT RTC ANSWER IN STATE", this.rtc.signalingState)
|
|
143
|
+
await this.rtc.setLocalDescription(answer)
|
|
144
|
+
console.log("LOCAL ANSWER DESCRIPTION SET! SENDING ANSWER!")
|
|
145
|
+
peer.sendMessage({ to, type: "sdp", data: answer })
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
console.log("GOT ANSWER FROM REMOTE PEER")
|
|
149
|
+
await this.rtc.setRemoteDescription(message.data)
|
|
150
|
+
}
|
|
151
|
+
} break;
|
|
152
|
+
case "ice": {
|
|
153
|
+
console.log("RECEIVED ICE! IN STATE", this.rtc.signalingState)
|
|
154
|
+
let ice = message.data
|
|
155
|
+
//if(ice && ice.candidate === "") break;
|
|
156
|
+
if(ice && ice.candidate != "") {
|
|
157
|
+
console.log("ADDING ICE CANDIDATE", ice.candidate)
|
|
158
|
+
await this.rtc.addIceCandidate(new RTCIceCandidate(ice))
|
|
159
|
+
} else if(window.RTCPeerConnection.prototype.addIceCandidate.length === 0){
|
|
160
|
+
await this.rtc.addIceCandidate()
|
|
161
|
+
}
|
|
162
|
+
//console.log("REMOTE ICE CANDIDATE ADDED", ice && ice.candidate)
|
|
163
|
+
} break;
|
|
164
|
+
case "ping": {
|
|
165
|
+
peer.sendMessage({ to, type: "pong", data: message.data})
|
|
166
|
+
} break;
|
|
167
|
+
case "pong": break; // ignore pong
|
|
168
|
+
default:
|
|
169
|
+
console.error("Unknown peer message", message)
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
synchronizeLocalTracks() {
|
|
173
|
+
const tracks = this.isEnabled ? this.localTracks : []
|
|
174
|
+
let removedSenders = []
|
|
175
|
+
let somethingChanged = false
|
|
176
|
+
for(const senderInfo of this.rtpSenders) {
|
|
177
|
+
const trackInfo = tracks.find(trackInfo => trackInfo.track == senderInfo.sender.track)
|
|
178
|
+
if(!trackInfo) {
|
|
179
|
+
this.rtc.removeTrack(senderInfo.sender)
|
|
180
|
+
removedSenders.push(senderInfo)
|
|
181
|
+
somethingChanged = true
|
|
182
|
+
} else if(senderInfo.stream != trackInfo.stream) {
|
|
183
|
+
senderInfo.stream = trackInfo.stream
|
|
184
|
+
senderInfo.sender.setStreams(trackInfo.stream)
|
|
185
|
+
somethingChanged = true
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for(const removedSenderInfo of removedSenders) {
|
|
189
|
+
this.rtpSenders.splice(this.rtpSenders.indexOf(removedSenderInfo), 1)
|
|
190
|
+
}
|
|
191
|
+
for(const trackInfo of tracks) {
|
|
192
|
+
if(this.rtpSenders.find(senderInfo => senderInfo.sender.track == trackInfo.track)) continue; // existing track
|
|
193
|
+
const sender = this.rtc.addTrack(trackInfo.track, trackInfo.stream)
|
|
194
|
+
this.rtpSenders.push({ sender, stream: trackInfo.stream })
|
|
195
|
+
somethingChanged = true
|
|
196
|
+
}
|
|
197
|
+
if(somethingChanged) {
|
|
198
|
+
//this.updateOffer() // wait for onnegotiationneeded
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
async handleNegotiationNeeded(event) {
|
|
202
|
+
console.log("NEGOTIATION NEEDED! IN STATE", this.rtc.signalingState)
|
|
203
|
+
if(!this.isEnabled) return
|
|
204
|
+
if(this.state == 'negotiating') {
|
|
205
|
+
console.log("SKIP NESTED NEGOTIATIONS WITH", this.to)
|
|
206
|
+
//return
|
|
207
|
+
}
|
|
208
|
+
this.state = 'negotiating'
|
|
209
|
+
// if it's disabled there is no need for offer
|
|
210
|
+
console.log("UPDATING OFFER")
|
|
211
|
+
const offer = await this.rtc.createOffer(this.offerOptions || undefined)
|
|
212
|
+
if(this.rtc.signalingState != "stable") {
|
|
213
|
+
console.log("RTC GOT OUT OF STABLE WHILE CREATING OFFER. IGNORE GENERATED OFFER!")
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await this.rtc.setLocalDescription(offer)
|
|
217
|
+
peer.sendMessage({ to, type: "sdp", data: offer })
|
|
218
|
+
console.log("SDP OFFER SET! RTC IN STATE", this.rtc.signalingState)
|
|
219
|
+
|
|
220
|
+
},
|
|
221
|
+
async handleSignalingStateChange(event) {
|
|
222
|
+
if(this.state == 'closed') return;
|
|
223
|
+
console.log("RTC SIGNALING STATE CHANGE", this.rtc.signalingState)
|
|
224
|
+
this.rtcSignalingState = this.rtc.signalingState
|
|
225
|
+
},
|
|
226
|
+
async handleIceCandidate(event) {
|
|
227
|
+
if(this.state == 'closed') return
|
|
228
|
+
//console.log("GOT ICE CANDIDATE", event.candidate && event.candidate.candidate)
|
|
229
|
+
peer.sendMessage({ to, type: "ice", data: event.candidate })
|
|
230
|
+
},
|
|
231
|
+
handleTrack(event) {
|
|
232
|
+
if(this.state == 'closed') return
|
|
233
|
+
const track = event.track
|
|
234
|
+
let stream = event.streams && event.streams[0]
|
|
235
|
+
if(!stream) {
|
|
236
|
+
console.error(`Streamless track ${track.id} ${track.kind} from peer ${to} - something is wrong!`)
|
|
237
|
+
stream = new MediaStream([track])
|
|
238
|
+
}
|
|
239
|
+
const trackInfo = {
|
|
240
|
+
track: event.track,
|
|
241
|
+
stream,
|
|
242
|
+
muted: track.muted,
|
|
243
|
+
removeTrackHandler: () => {
|
|
244
|
+
const trackIndex = this.remoteTracks.findIndex(remoteTrack =>
|
|
245
|
+
remoteTrack.track == track && remoteTrack.stream == stream)
|
|
246
|
+
if(trackIndex != -1) {
|
|
247
|
+
const trackInfo = this.remoteTracks[trackIndex]
|
|
248
|
+
trackInfo.track.removeEventListener('mute', trackInfo.muteHandler)
|
|
249
|
+
trackInfo.track.removeEventListener('unmute', trackInfo.unmuteHandler)
|
|
250
|
+
this.remoteTracks.splice(trackIndex, 1)
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
muteHandler: () => trackInfo.muted = track.muted,
|
|
254
|
+
unmuteHandler: () => trackInfo.muted = track.muted
|
|
255
|
+
}
|
|
256
|
+
if(stream) {
|
|
257
|
+
stream.addEventListener('removetrack', trackInfo.removeTrackHandler)
|
|
258
|
+
}
|
|
259
|
+
const existingTrackInfo = this.remoteTracks.find(remoteTrack => remoteTrack.track == track)
|
|
260
|
+
if(existingTrackInfo) {
|
|
261
|
+
existingTrackInfo.stream = stream // Track stream changed
|
|
262
|
+
} else {
|
|
263
|
+
trackInfo.track.addEventListener('mute', trackInfo.muteHandler)
|
|
264
|
+
trackInfo.track.addEventListener('unmute', trackInfo.unmuteHandler)
|
|
265
|
+
this.remoteTracks.push(trackInfo)
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
handleIceGatheringStateChange(event) {
|
|
269
|
+
if(this.state == 'closed') return
|
|
270
|
+
console.log("ICE GATHERING STATE CHANGED", this.rtc.iceGatheringState)
|
|
271
|
+
this.iceGatheringState = this.rtc.iceGatheringState
|
|
272
|
+
},
|
|
273
|
+
handleIceConnectionStateChange(event) {
|
|
274
|
+
if(this.state == 'closed') return
|
|
275
|
+
this.iceConnectionState = this.rtc.iceConnectionState
|
|
276
|
+
console.log("ICE GATHERING STATE CHANGED", this.rtc.iceConnectionState)
|
|
277
|
+
if(this.iceConnectionState == 'connected') {
|
|
278
|
+
this.state = 'connected'
|
|
279
|
+
}
|
|
280
|
+
if(this.iceConnectionState == 'failed') {
|
|
281
|
+
this.state = 'failed'
|
|
282
|
+
this.restartConnection()
|
|
283
|
+
}
|
|
284
|
+
if(this.iceConnectionState == 'disconnected') {
|
|
285
|
+
this.state = 'disconnected'
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
async restartConnection() {
|
|
289
|
+
console.log("RESTARTING CONNECTION")
|
|
290
|
+
if(false && this.rtc.restartIce) {
|
|
291
|
+
console.log("RESTART ICE!")
|
|
292
|
+
this.rtc.restartIce()
|
|
293
|
+
} else {
|
|
294
|
+
console.log("RESTART OFFER!")
|
|
295
|
+
const offer = await this.rtc.createOffer({ ...this.offerOptions, iceRestart: true })
|
|
296
|
+
if(this.rtc.signalingState != "stable") {
|
|
297
|
+
console.log("RTC GOT OUT OF STABLE WHILE CREATING OFFER. IGNORE GENERATED OFFER!")
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
await this.rtc.setLocalDescription(offer)
|
|
301
|
+
peer.sendMessage({ to, type: "sdp", data: offer })
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
created() {
|
|
306
|
+
this.negotiationNeededHandler = (e) => this.handleNegotiationNeeded(e)
|
|
307
|
+
this.signalingStateChangeHandler = (e) => this.handleSignalingStateChange(e)
|
|
308
|
+
this.iceCandidateHandler = (e) => this.handleIceCandidate(e)
|
|
309
|
+
this.trackHandler = (e) => this.handleTrack(e)
|
|
310
|
+
this.iceGatheringStateChangeHandler = (e) => this.handleIceGatheringStateChange(e)
|
|
311
|
+
this.iceConnectionStateChangeHandler = (e) => this.handleIceConnectionStateChange(e)
|
|
312
|
+
},
|
|
313
|
+
beforeDestroy() {
|
|
314
|
+
if(this.state != 'closed') {
|
|
315
|
+
this.close()
|
|
316
|
+
}
|
|
317
|
+
if(this.rtc) {
|
|
318
|
+
this.rtc.removeEventListener('negotiationneeded', this.negotiationNeededHandler)
|
|
319
|
+
this.rtc.removeEventListener('signalingstatechange', this.signalingStateChangeHandler)
|
|
320
|
+
this.rtc.removeEventListener('icecandidate', this.iceCandidateHandler)
|
|
321
|
+
this.rtc.removeEventListener('track', this.trackHandler)
|
|
322
|
+
this.rtc.removeEventListener('icegatheringstatechanged', this.iceGatheringStateChangeHandler)
|
|
323
|
+
this.rtc.removeEventListener('iceconnectionstatechanged', this.iceConnectionStateChangeHandler)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export { createPeerConnection }
|