Compare commits
6 Commits
3dd7e0fa0f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b72be4e004 | |||
| c3c0a37a41 | |||
| 03e19fbf58 | |||
| 099e36e1b5 | |||
| db8b33299c | |||
| 231e7f4376 |
21
README.md
21
README.md
@@ -1,14 +1,23 @@
|
|||||||
# webRTC
|
# webRTC
|
||||||
webrtc 实现的 p2p 信道
|
webrtc 实现的 p2p 信道
|
||||||
|
|
||||||
rtc rtc rtc: 稳定, 多重连接
|
```bash
|
||||||
channel channel channel: 细流
|
# 使用 git 克隆到本地或者直接下载zip压缩包
|
||||||
part-server: 调谐, 从不同服务器请求资源分片
|
git clone git@git.satori.love:LaniakeaSupercluster/webrtc.git
|
||||||
webrtc://用户@域名:端口/信道标识/资源ID
|
cd webrtc
|
||||||
|
|
||||||
封包格式
|
# 安装依赖
|
||||||
资源ID 分片信息(位置) 分片数据
|
npm i
|
||||||
|
|
||||||
|
# 编译
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 运行服务
|
||||||
|
npm run start
|
||||||
|
|
||||||
|
# 或者使用 pm2 作为守护进程
|
||||||
|
pm2 start npm --name webrtc -- run start
|
||||||
|
```
|
||||||
|
|
||||||
插件市场
|
插件市场
|
||||||
1. 从浏览器创建插件(单文件)
|
1. 从浏览器创建插件(单文件)
|
||||||
|
|||||||
5
public/remove.js
Normal file
5
public/remove.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Chessboard } from './ChineseChess.js'
|
||||||
|
|
||||||
|
// 中国象棋
|
||||||
|
const chessboard = new Chessboard()
|
||||||
|
chessboard.绘制棋盘({比例: 48, 边距: 20})
|
||||||
13
public/send.svg
Normal file
13
public/send.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 495.003 495.003" xml:space="preserve">
|
||||||
|
<g id="XMLID_51_">
|
||||||
|
<path id="XMLID_53_" d="M164.711,456.687c0,2.966,1.647,5.686,4.266,7.072c2.617,1.385,5.799,1.207,8.245-0.468l55.09-37.616
|
||||||
|
l-67.6-32.22V456.687z"/>
|
||||||
|
<path id="XMLID_52_" d="M492.431,32.443c-1.513-1.395-3.466-2.125-5.44-2.125c-1.19,0-2.377,0.264-3.5,0.816L7.905,264.422
|
||||||
|
c-4.861,2.389-7.937,7.353-7.904,12.783c0.033,5.423,3.161,10.353,8.057,12.689l125.342,59.724l250.62-205.99L164.455,364.414
|
||||||
|
l156.145,74.4c1.918,0.919,4.012,1.376,6.084,1.376c1.768,0,3.519-0.322,5.186-0.977c3.637-1.438,6.527-4.318,7.97-7.956
|
||||||
|
L494.436,41.257C495.66,38.188,494.862,34.679,492.431,32.443z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 973 B |
@@ -162,17 +162,6 @@ export class Chessboard {
|
|||||||
context.lineTo(边距, 边距)
|
context.lineTo(边距, 边距)
|
||||||
context.stroke()
|
context.stroke()
|
||||||
|
|
||||||
//// 绘制棋子们
|
|
||||||
//context.font = '20px serif'
|
|
||||||
//context.fillStyle = 'red'
|
|
||||||
//context.fillText('車', 0+边距-(比例/5), 20+边距-(比例/5))
|
|
||||||
//context.stroke()
|
|
||||||
//// 绘制棋子们
|
|
||||||
//context.font = '20px serif'
|
|
||||||
//context.fillStyle = 'red'
|
|
||||||
//context.fillText('馬', 0+边距-(比例/5) + (比例*1), 20+边距-(比例/5))
|
|
||||||
//context.stroke()
|
|
||||||
|
|
||||||
// 单独保存绘制好的棋盘
|
// 单独保存绘制好的棋盘
|
||||||
const chessboard = canvas.toDataURL()
|
const chessboard = canvas.toDataURL()
|
||||||
|
|
||||||
|
|||||||
11
src/chat.js
11
src/chat.js
@@ -41,6 +41,7 @@ export default class Chat {
|
|||||||
boxShadow: '0 0 1rem #eee',
|
boxShadow: '0 0 1rem #eee',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
|
borderRadius: '2rem'
|
||||||
},
|
},
|
||||||
onkeydown: event => {
|
onkeydown: event => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -79,7 +80,6 @@ export default class Chat {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Button({
|
Button({
|
||||||
textContent: '发送(Enter)',
|
|
||||||
onclick: event => {
|
onclick: event => {
|
||||||
const text = event.target.previousSibling.value.trim()
|
const text = event.target.previousSibling.value.trim()
|
||||||
if (text) {
|
if (text) {
|
||||||
@@ -88,10 +88,11 @@ export default class Chat {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
padding: '.5rem 1rem',
|
width: '1.2rem',
|
||||||
boxSizing: 'border-box',
|
height: '1.2rem',
|
||||||
boxShadow: '0 0 1rem #eee',
|
border: 'none',
|
||||||
borderRadius: '1rem',
|
background: 'url("/send.svg") no-repeat center / cover',
|
||||||
|
margin: 'auto 0 auto -2.6rem'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
export default class IndexedDB {
|
|
||||||
constructor(databaseName, databaseVersion) {
|
|
||||||
this.databaseName = databaseName
|
|
||||||
this.databaseVersion = databaseVersion
|
|
||||||
this.db = null
|
|
||||||
}
|
|
||||||
|
|
||||||
open(name) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open(this.databaseName, this.databaseVersion)
|
|
||||||
request.onerror = (event) => {
|
|
||||||
reject(event.target.error)
|
|
||||||
}
|
|
||||||
request.onsuccess = (event) => {
|
|
||||||
this.db = event.target.result
|
|
||||||
resolve(this.db)
|
|
||||||
}
|
|
||||||
request.onupgradeneeded = (event) => {
|
|
||||||
const db = event.target.result
|
|
||||||
if (!db.objectStoreNames.contains(name)) {
|
|
||||||
db.createObjectStore(name, { keyPath: 'id' })
|
|
||||||
console.log('store created:', name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async store(name) {
|
|
||||||
if (!this.db) await this.open(name)
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction([name], 'readwrite')
|
|
||||||
const objectStore = transaction.objectStore(name)
|
|
||||||
resolve(objectStore)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getAll(name) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction([name], 'readonly')
|
|
||||||
const objectStore = transaction.objectStore(name)
|
|
||||||
const request = objectStore.getAll()
|
|
||||||
request.onerror = (event) => {
|
|
||||||
reject(event.target.error)
|
|
||||||
}
|
|
||||||
request.onsuccess = (event) => {
|
|
||||||
resolve(event.target.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
add(name, data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction([name], 'readwrite')
|
|
||||||
const objectStore = transaction.objectStore(name)
|
|
||||||
const request = objectStore.add(data)
|
|
||||||
request.onerror = (event) => {
|
|
||||||
reject(event.target.error)
|
|
||||||
}
|
|
||||||
request.onsuccess = (event) => {
|
|
||||||
resolve(event.target.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
put(name, data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction([name], 'readwrite')
|
|
||||||
const objectStore = transaction.objectStore(name)
|
|
||||||
const request = objectStore.put(data)
|
|
||||||
request.onerror = (event) => {
|
|
||||||
reject(event.target.error)
|
|
||||||
}
|
|
||||||
request.onsuccess = (event) => {
|
|
||||||
resolve(event.target.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(name, id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const transaction = this.db.transaction([name], 'readwrite')
|
|
||||||
const objectStore = transaction.objectStore(name)
|
|
||||||
const request = objectStore.delete(id)
|
|
||||||
request.onerror = (event) => {
|
|
||||||
reject(event.target.error)
|
|
||||||
}
|
|
||||||
request.onsuccess = (event) => {
|
|
||||||
resolve(event.target.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
84
src/main.js
84
src/main.js
@@ -1,6 +1,4 @@
|
|||||||
import 'virtual:windi.css'
|
import * as idb from 'idb-keyval'
|
||||||
|
|
||||||
import IndexedDB from './indexeddb.js'
|
|
||||||
import MusicList from './music.js'
|
import MusicList from './music.js'
|
||||||
import ClientList from './client.js'
|
import ClientList from './client.js'
|
||||||
import Chat from './chat.js'
|
import Chat from './chat.js'
|
||||||
@@ -11,12 +9,6 @@ window.Buffer = Buffer
|
|||||||
window.process = process
|
window.process = process
|
||||||
import { parseBlob } from 'music-metadata-browser'
|
import { parseBlob } from 'music-metadata-browser'
|
||||||
|
|
||||||
import { Chessboard } from './ChineseChess.js'
|
|
||||||
|
|
||||||
// 中国象棋
|
|
||||||
const chessboard = new Chessboard()
|
|
||||||
chessboard.绘制棋盘({比例: 48, 边距: 20})
|
|
||||||
|
|
||||||
// 缓冲分片发送
|
// 缓冲分片发送
|
||||||
const CHUNK_SIZE = 1024 * 64 // 默认每个块的大小为128KB
|
const CHUNK_SIZE = 1024 * 64 // 默认每个块的大小为128KB
|
||||||
const THRESHOLD = 1024 * 1024 // 默认缓冲区的阈值为1MB
|
const THRESHOLD = 1024 * 1024 // 默认缓冲区的阈值为1MB
|
||||||
@@ -31,11 +23,11 @@ function appendBuffer(buffer1, buffer2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 读取本地音乐列表并标识为缓存状态(本地缓存)
|
// 读取本地音乐列表并标识为缓存状态(本地缓存)
|
||||||
const database = new IndexedDB('musicDatabase', 1)
|
const musicStore = idb.createStore('database', 'music')
|
||||||
await database.store('musicObjectStore') // 音乐(为什么会用这么丑的格式呢)
|
|
||||||
|
|
||||||
// 读取本地音乐列表并标识为缓存状态(本地缓存)
|
// 读取本地音乐列表并标识为缓存状态(本地缓存)
|
||||||
const list = await Promise.all((await database.getAll('musicObjectStore')).map(async item => {
|
const list = await idb.values(musicStore)
|
||||||
|
for (const item of list) {
|
||||||
if (!item.picture && item.picture !== false) {
|
if (!item.picture && item.picture !== false) {
|
||||||
console.log('提取封面', item.name)
|
console.log('提取封面', item.name)
|
||||||
const blob = new Blob([item.arrayBuffer], { type: item.type })
|
const blob = new Blob([item.arrayBuffer], { type: item.type })
|
||||||
@@ -48,10 +40,10 @@ const list = await Promise.all((await database.getAll('musicObjectStore')).map(a
|
|||||||
} else {
|
} else {
|
||||||
item.picture = false
|
item.picture = false
|
||||||
}
|
}
|
||||||
database.put('musicObjectStore', item)
|
idb.set(item.id, item, musicStore)
|
||||||
|
}
|
||||||
|
item.save = true
|
||||||
}
|
}
|
||||||
return { save: true, ...item }
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 读取本地用户名(本地缓存)
|
// 读取本地用户名(本地缓存)
|
||||||
const name = localStorage.getItem('username') ?? '匿'
|
const name = localStorage.getItem('username') ?? '匿'
|
||||||
@@ -88,8 +80,7 @@ const musicList = new MusicList({
|
|||||||
onlike: (item, list) => {
|
onlike: (item, list) => {
|
||||||
console.log('喜欢音乐', item.name)
|
console.log('喜欢音乐', item.name)
|
||||||
if (item.arrayBuffer) {
|
if (item.arrayBuffer) {
|
||||||
//musicStore.add(item)
|
idb.set(item.id, item, musicStore)
|
||||||
database.add('musicObjectStore', item)
|
|
||||||
clientList.send('base', JSON.stringify({
|
clientList.send('base', JSON.stringify({
|
||||||
type: 'set_music_list',
|
type: 'set_music_list',
|
||||||
list: list.map(({ id, name, size, type }) => ({ id, name, size, type }))
|
list: list.map(({ id, name, size, type }) => ({ id, name, size, type }))
|
||||||
@@ -99,14 +90,30 @@ const musicList = new MusicList({
|
|||||||
onunlike: (item, list) => {
|
onunlike: (item, list) => {
|
||||||
console.log('取消喜欢', item.name)
|
console.log('取消喜欢', item.name)
|
||||||
if (item.arrayBuffer) {
|
if (item.arrayBuffer) {
|
||||||
database.delete('musicObjectStore', item.id)
|
idb.del(item.id, musicStore)
|
||||||
//musicStore.delete(item.id)
|
|
||||||
clientList.send('base', JSON.stringify({
|
clientList.send('base', JSON.stringify({
|
||||||
type: 'set_music_list',
|
type: 'set_music_list',
|
||||||
list: list.map(({ id, name, size, type }) => ({ id, name, size, type }))
|
list: list.map(({ id, name, size, type }) => ({ id, name, size, type }))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onsetlrc(item) {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = '.lrc'
|
||||||
|
input.onchange = function () {
|
||||||
|
const file = this.files[0]
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = function (e) {
|
||||||
|
const lrc = e.target.result
|
||||||
|
console.log(lrc)
|
||||||
|
console.log(item)
|
||||||
|
idb.set(item.id, { lrc, ...item }, musicStore)
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
},
|
||||||
onban: item => {
|
onban: item => {
|
||||||
//console.info('禁止音乐', item.name)
|
//console.info('禁止音乐', item.name)
|
||||||
},
|
},
|
||||||
@@ -115,8 +122,7 @@ const musicList = new MusicList({
|
|||||||
},
|
},
|
||||||
onremove: item => {
|
onremove: item => {
|
||||||
//console.info('移除音乐', item.name)
|
//console.info('移除音乐', item.name)
|
||||||
//musicStore.delete(item.id)
|
idb.del(item.id, musicStore)
|
||||||
database.delete('musicObjectStore', item.id)
|
|
||||||
},
|
},
|
||||||
onadd: (item, list) => {
|
onadd: (item, list) => {
|
||||||
//console.info('添加音乐', item.name)
|
//console.info('添加音乐', item.name)
|
||||||
@@ -173,29 +179,6 @@ const chat = new Chat({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
//// 与每个客户端保持心跳
|
|
||||||
//clientList.setChannel('ping', {
|
|
||||||
// onopen: async (event, client) => {
|
|
||||||
// console.log('打开信道', event.target.label)
|
|
||||||
// clientList.sendto(client.id, 'ping', JSON.stringify({ type: 'ping' }))
|
|
||||||
// },
|
|
||||||
// onmessage: async (event, client) => {
|
|
||||||
// const data = JSON.parse(event.data)
|
|
||||||
// if (data.type === 'ping') {
|
|
||||||
// console.log(client.name, '心跳:', data)
|
|
||||||
// clientList.sendto(client.id, 'ping', JSON.stringify({ type: 'pong' }))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if (data.type === 'pong') {
|
|
||||||
// console.log(client.name, '心跳:', data)
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
||||||
// clientList.sendto(client.id, 'ping', JSON.stringify({ type: 'ping' }))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// console.log('未知类型:', data.type)
|
|
||||||
// }
|
|
||||||
//})
|
|
||||||
|
|
||||||
// 与每个客户端都建立聊天信道
|
// 与每个客户端都建立聊天信道
|
||||||
clientList.setChannel('chat', {
|
clientList.setChannel('chat', {
|
||||||
onopen: async (event, client) => {
|
onopen: async (event, client) => {
|
||||||
@@ -394,3 +377,16 @@ if (localStorage.getItem('avatar')) {
|
|||||||
if (localStorage.getItem('username')) {
|
if (localStorage.getItem('username')) {
|
||||||
document.title = localStorage.getItem('username')
|
document.title = localStorage.getItem('username')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 链接 GIT 仓库
|
||||||
|
const git = document.createElement('a')
|
||||||
|
git.href = 'https://git.satori.love/laniakeaSupercluster/webrtc'
|
||||||
|
git.textContent = 'Git'
|
||||||
|
git.target = '_blank'
|
||||||
|
git.style.position = 'absolute'
|
||||||
|
git.style.bottom = 0
|
||||||
|
git.style.right = 0
|
||||||
|
git.style.padding = '.5rem'
|
||||||
|
git.style.textDecoration = 'none'
|
||||||
|
git.style.color = '#555'
|
||||||
|
document.body.appendChild(git)
|
||||||
|
|||||||
18
src/music.js
18
src/music.js
@@ -1,8 +1,8 @@
|
|||||||
import { Img, Span, Button, List, ListItem, UploadMusic, createElement } from './weigets.js'
|
import { Img, Span, Button, List, ListItem, UploadMusic, createElement } from './weigets.js'
|
||||||
|
|
||||||
export default class MusicList {
|
export default class MusicList {
|
||||||
constructor({ list = [], EventListeners = {}, onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }) {
|
constructor({ list = [], EventListeners = {}, onsetlrc, onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }) {
|
||||||
this.event = { onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }
|
this.event = { onsetlrc, onplay, onstop, onadd, onremove, onlike, onunlike, onban, onload }
|
||||||
|
|
||||||
// 添加音乐播放器
|
// 添加音乐播放器
|
||||||
this.audio = new Audio()
|
this.audio = new Audio()
|
||||||
@@ -16,12 +16,6 @@ export default class MusicList {
|
|||||||
this.audio.addEventListener('ended', () => {
|
this.audio.addEventListener('ended', () => {
|
||||||
this.next()
|
this.next()
|
||||||
})
|
})
|
||||||
//this.audio.addEventListener('timeupdate', () => {
|
|
||||||
// console.log(this.audio.currentTime)
|
|
||||||
//})
|
|
||||||
//this.audio.addEventListener('error', event => {
|
|
||||||
// console.error('音乐播放器错误:', event)
|
|
||||||
//})
|
|
||||||
this.ul = List({
|
this.ul = List({
|
||||||
classList: ['music-list'],
|
classList: ['music-list'],
|
||||||
style: {
|
style: {
|
||||||
@@ -324,10 +318,16 @@ export default class MusicList {
|
|||||||
this.like(item)
|
this.like(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
Button({
|
||||||
|
textContent:'lrc',
|
||||||
|
onclick: () => {
|
||||||
|
this.event.onsetlrc?.(item)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
this.event.onadd(item, this.list)
|
this.event.onadd?.(item, this.list)
|
||||||
}
|
}
|
||||||
async remove(item) {
|
async remove(item) {
|
||||||
this.ul.querySelector(`#${item.id}`)?.remove()
|
this.ul.querySelector(`#${item.id}`)?.remove()
|
||||||
|
|||||||
Reference in New Issue
Block a user