对端通信

This commit is contained in:
2023-09-28 23:49:26 +08:00
parent a743a066be
commit 4f7741b341
5 changed files with 177 additions and 73 deletions

View File

@ -31,10 +31,10 @@ app.ws('/webrtc/:channel', (ws, req) => {
// 设备发送信令时转发给指定在线设备 // 设备发送信令时转发给指定在线设备
ws.on('message', message => { ws.on('message', message => {
console.log(ws.id, '设备发送信令:', ws.channel, wsInstance.getWss().clients.size) console.log(ws.id, '设备发送信令:', ws.channel, wsInstance.getWss().clients.size)
const { id } = JSON.parse(message) const data = JSON.parse(message)
wsInstance.getWss().clients.forEach(client => { wsInstance.getWss().clients.forEach(client => {
if (client !== ws && client.readyState === 1 && client.channel === ws.channel && client.id === id) { if (client !== ws && client.readyState === 1 && client.channel === ws.channel && client.id === data.id) {
client.send(message) client.send(JSON.stringify({ ...data, id: ws.id }))
} }
}) })
}) })
@ -43,7 +43,7 @@ app.ws('/webrtc/:channel', (ws, req) => {
wsInstance.getWss().clients.forEach(client => { wsInstance.getWss().clients.forEach(client => {
if (client !== ws && client.readyState === 1 && client.channel === ws.channel) { if (client !== ws && client.readyState === 1 && client.channel === ws.channel) {
client.send(JSON.stringify({ type: 'push', id: ws.id, channel: ws.channel })) client.send(JSON.stringify({ type: 'push', id: ws.id, channel: ws.channel }))
ws.send(JSON.stringify({ type: 'push', id: client.id, channel: client.channel })) ws.send(JSON.stringify({ type: 'list', id: client.id, channel: client.channel }))
} }
}) })
}) })

110
public/client.js Normal file
View File

@ -0,0 +1,110 @@
import { List, ListItem } from './weigets.js'
export default class ClientList {
constructor() {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
const host = window.location.host
this.websocket = new WebSocket(`${protocol}://${host}/webrtc/music`)
this.clientlist = []
this.ul = List({})
document.body.appendChild(this.ul)
//this.websocket.onopen = event => {
// console.log('clientlist websocket: onopen')
// console.log('当连接建立时,服务器将逐一发送所有客户端信息')
//}
this.websocket.onmessage = async event => {
const data = JSON.parse(event.data)
if (data.type === 'list') {
console.log('取得在线对端列表:', data)
const webrtc = new RTCPeerConnection()
webrtc.createDataChannel('music')
//webrtc.onicecandidate = event => {
// console.log('clientlist onicecandidate E', event)
// if (event.candidate) {
// this.websocket.send(JSON.stringify({ type: 'candidate', id: data.id, candidate: event.candidate }))
// }
//}
console.log('发送给对方 offer')
const offer = await webrtc.createOffer()
await webrtc.setLocalDescription(offer)
this.clientlist.push({ id: data.id, name: data.name, webrtc })
this.websocket.send(JSON.stringify({ type: 'offer', id: data.id, offer }))
return this.add(data)
}
if (data.type === 'push') {
console.log('新上线客户端:', data)
return this.add(data)
}
if (data.type === 'pull') {
console.log('移除客户端:', data)
return this.remove(data)
}
if (data.type === 'offer') {
console.log('收到对方 offer', data)
const webrtc = new RTCPeerConnection()
webrtc.createDataChannel('music')
//webrtc.onicecandidate = event => {
// console.log('clientlist onicecandidate X', event)
// if (event.candidate) {
// this.websocket.send(JSON.stringify({ type: 'candidate', id: data.id, candidate: event.candidate }))
// }
//}
this.clientlist.push({ id: data.id, name: data.name, webrtc })
console.log('发送给对方 answer')
await webrtc.setRemoteDescription(data.offer)
const answer = await webrtc.createAnswer()
await webrtc.setLocalDescription(answer)
this.websocket.send(JSON.stringify({ type: 'answer', id: data.id, answer }))
return
}
if (data.type === 'answer') {
console.log('收到对方 answer', data)
const pc = this.clientlist.find(client => client.id === data.id).webrtc
await pc.setRemoteDescription(data.answer)
return
}
if (data.type === 'candidate') {
const pc = this.clientlist.find(client => client.id === data.id).webrtc
await pc.addIceCandidate(data.candidate)
return console.log('收到 candidate 并将其添加到远程端', data.candidate)
}
console.log('收到未知数据:', data)
}
}
add(item) {
//this.clientlist.push(item)
this.ul.appendChild(ListItem({
id: item.id,
innerText: item.name ?? item.id,
onclick: event => {
},
chidren: []
}))
//// 与对方建立连接(即使不传输数据)
//const webrtc = new RTCPeerConnection()
//const channel = webrtc.createDataChannel('music')
//channel.onopen = event => {
// console.log('clientlist music: channel.onopen')
// channel.send('hello')
//}
//channel.onmessage = event => {
// console.log('clientlist music: channel.onmessage', event.data)
//}
//// 监听 ICE 候选事件
//webrtc.onicecandidate = event => {
// if (event.candidate) {
// console.log('clientlist onicecandidate', event.candidate)
// // 发送给对方(通过服务器)
// }
//}
}
remove(item) {
this.clientlist = this.clientlist.filter(client => client.id !== item.id)
this.ul.removeChild(document.getElementById(item.id))
}
update(item) { }
get(id) { }
getAll() { }
clear() { }
on(event, callback) { }
}

View File

@ -15,7 +15,6 @@ export default class IndexedDB {
} }
open() { open() {
console.log('open')
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = indexedDB.open(this.databaseName, this.databaseVersion) const request = indexedDB.open(this.databaseName, this.databaseVersion)
request.onerror = (event) => { request.onerror = (event) => {

View File

@ -9,24 +9,64 @@
<body> <body>
<div> <div>
<h1>webRTC</h1> <h1>webRTC</h1>
<p>message</p> <p>选择音乐使频道内所有设备同步播放</p>
</div> </div>
<script type="module"> <script type="module">
import IndexedDB from './database.js' import IndexedDB from './database.js'
import MusicList from './music.js' import MusicList from './music.js'
const musicObjectStore = new IndexedDB('musicDatabase', 1, 'musicObjectStore') import ClientList from './client.js'
await musicObjectStore.open()
// 初始化音乐列表
// 音乐列表
const musicList = new MusicList() const musicList = new MusicList()
console.log('musicList:', musicList)
//musicList.on('add', async item => { // 初始化客户端列表
// console.log('musicList add:', item) const clientList = new ClientList()
// const { name, size, type, lastModified, lastModifiedDate, arrayBuffer } = item
// const id = Date.now() //// 初始化音乐频道
// // 存储到 IndexDB //const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
// await musicObjectStore.add({ id, name, size, type, lastModified, lastModifiedDate, arrayBuffer }) //const host = window.location.host
//}) //const ws = new WebSocket(`${protocol}://${host}/webrtc/music`)
//ws.onopen = function () {
// console.log('music ws open')
//}
//ws.onmessage = function (event) {
// const data = JSON.parse(event.data)
// console.log('ws message:', data)
// if (data.type === 'push') {
// console.log('收到 type:push 将设备增加', data.id)
// clientList.add(data.id, data.channel)
// return
// }
// if (data.type === 'pull') {
// console.log('收到 type:pull 将设备删除', data.id)
// clientList.remove(data)
// return
// }
// if (data.type === 'error') {
// console.log('收到 type:error 没什么可操作的', data.id)
// return
// }
// if (data.offer) {
// console.log('收到 offer 并将其设置为远程描述')
// //clientList.setRemoteDescription(data)
// pc.setRemoteDescription(new RTCSessionDescription(data.offer))
// // 创建SDP answer并将其设置为本地描述, 发送给远程端
// pc.createAnswer().then(function (answer) {
// pc.setLocalDescription(answer);
// ws.send(JSON.stringify({ answer }))
// })
// return
// }
// if (data.answer) {
// console.log('收到 answer 并将其设置为远程描述')
// pc.setRemoteDescription(new RTCSessionDescription(data.answer))
// return
// }
// if (data.candidate) {
// console.log('收到 candidate 并将其添加到远程端')
// pc.addIceCandidate(new RTCIceCandidate(data.candidate))
// }
//}
// webRTC 传递音乐(分别传输文件和操作事件能更流畅) // webRTC 传递音乐(分别传输文件和操作事件能更流畅)
const music = async function () { const music = async function () {
@ -71,7 +111,6 @@
audioSource?.stop() audioSource?.stop()
audioSource = null audioSource = null
}) })
// 监听 ICE 候选事件 // 监听 ICE 候选事件
pc.onicecandidate = event => { pc.onicecandidate = event => {
if (event.candidate) { if (event.candidate) {
@ -130,7 +169,7 @@
console.log('收到 offer 并将其设置为远程描述', data.offer) console.log('收到 offer 并将其设置为远程描述', data.offer)
await pc.setRemoteDescription(new RTCSessionDescription(data.offer)) // 设置远程描述为 offer await pc.setRemoteDescription(new RTCSessionDescription(data.offer)) // 设置远程描述为 offer
await pc.setLocalDescription(await pc.createAnswer()) // 设置本地描述为 answer await pc.setLocalDescription(await pc.createAnswer()) // 设置本地描述为 answer
ws.send(JSON.stringify({ id, answer: pc.localDescription })) // 发送给远程终端 answer ws.send(JSON.stringify({ id, answer: pc.localDescription })) // 发送给远程终端 answer
return return
} }
if (data.answer) { if (data.answer) {
@ -144,55 +183,8 @@
return return
} }
} }
// 从数据库读取音乐文件
musicObjectStore.getAll().then(data => {
console.log('从数据库读取音乐文件', data)
const ol = document.createElement('ol')
data.forEach(item => {
const li = document.createElement('li')
li.innerText = `${item.name} - ${item.size} - ${item.type} - ${item.id}`
const start = document.createElement('button')
start.innerText = '播放'
start.onclick = async () => {
console.log('播放音乐', item)
// 传输音乐文件
const arrayBuffer = item.arrayBuffer
const audioContext = new AudioContext()
audioContext.decodeAudioData(arrayBuffer, async audioBuffer => {
// 将音乐流添加到 RTCPeerConnection
const mediaStreamDestination = audioContext.createMediaStreamDestination()
mediaStreamDestination.stream.getAudioTracks().forEach(function (track) {
pc.addTrack(track, mediaStreamDestination.stream)
})
// 播放音乐(远程)
const audioSource = audioContext.createBufferSource()
audioSource.buffer = audioBuffer
audioSource.connect(mediaStreamDestination)
audioSource.start()
// 播放音乐(本地)
const audioPlayer = new Audio()
audioPlayer.srcObject = mediaStreamDestination.stream
audioPlayer.play()
// 创建SDP offer并将其设置为本地描述, 发送给远程端
const id = clients[0].id
await pc.setLocalDescription(await pc.createOffer()) // 设置本地描述为 offer
ws.send(JSON.stringify({ id, offer: pc.localDescription })) // 发送给远程终端 offer
})
}
li.appendChild(start)
const remove = document.createElement('button')
remove.innerText = '移除'
remove.onclick = async () => {
await musicObjectStore.delete(item.id)
li.remove()
}
li.appendChild(remove)
ol.appendChild(li)
})
document.body.appendChild(ol)
})
} }
music() //music()
</script> </script>
<!--script type="module"> <!--script type="module">
// 创建 RTCPeerConnection // 创建 RTCPeerConnection

View File

@ -8,8 +8,7 @@ export default class MusicList {
this.store = new IndexedDB('musicDatabase', 1, 'musicObjectStore') this.store = new IndexedDB('musicDatabase', 1, 'musicObjectStore')
this.store.open().then(() => { this.store.open().then(() => {
this.store.getAll().then((data) => { this.store.getAll().then((data) => {
console.log(data) data.forEach(item => this.__add(item))
data.forEach(item => this.add(item))
}) })
}) })
document.body.appendChild(this.ul) document.body.appendChild(this.ul)
@ -29,7 +28,6 @@ export default class MusicList {
input.multiple = true input.multiple = true
input.accept = 'audio/*' input.accept = 'audio/*'
input.onchange = event => { input.onchange = event => {
console.log('event.target.files', event.target.files)
for (const file of event.target.files) { for (const file of event.target.files) {
const id = 'music' + Date.now() const id = 'music' + Date.now()
const { name, size, type } = file const { name, size, type } = file
@ -43,8 +41,8 @@ export default class MusicList {
} }
document.body.appendChild(input) document.body.appendChild(input)
} }
add(item) { // 仅添加UI
this.store.add(item) __add(item) {
const li = ListItem({ const li = ListItem({
id: item.id, id: item.id,
innerText: `${item.name} - ${item.size} - ${item.type} - ${item.id}`, innerText: `${item.name} - ${item.size} - ${item.type} - ${item.id}`,
@ -78,6 +76,11 @@ export default class MusicList {
}) })
this.ul.appendChild(li) this.ul.appendChild(li)
} }
// 添加数据并添加UI
add(item) {
this.store.add(item)
this.__add(item)
}
delete(item) { delete(item) {
this.store.delete(item.id) this.store.delete(item.id)
this.ul.removeChild(this.ul.querySelector(`#${item.id}`)) this.ul.removeChild(this.ul.querySelector(`#${item.id}`))