From 2aadae6b85a211efb82df2cc98066e80f4d35e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A7=89?= Date: Fri, 13 Oct 2023 05:45:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8vite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 134 ++----------------- index.html | 12 ++ package.json | 10 +- public/vite.svg | 1 + {public => src}/client.js | 0 {public => src}/database.js | 0 src/main.js | 253 ++++++++++++++++++++++++++++++++++++ {public => src}/music.js | 0 {public => src}/weigets.js | 0 style.css | 97 ++++++++++++++ vite.config.js | 24 ++++ 11 files changed, 409 insertions(+), 122 deletions(-) create mode 100644 index.html create mode 100644 public/vite.svg rename {public => src}/client.js (100%) rename {public => src}/database.js (100%) create mode 100644 src/main.js rename {public => src}/music.js (100%) rename {public => src}/weigets.js (100%) create mode 100644 style.css create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore index 13fa826..7334bee 100644 --- a/.gitignore +++ b/.gitignore @@ -4,129 +4,23 @@ logs npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* lerna-debug.log* -.pnpm-debug.log* package-lock.json -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt +node_modules dist +dist-ssr +*.local -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/index.html b/index.html new file mode 100644 index 0000000..0dad305 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + + webRTC + + + + + diff --git a/package.json b/package.json index 8226b73..87c8bf1 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,19 @@ "type": "module", "main": "index.js", "scripts": { - "dev": "nodemon index.js", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "devx": "nodemon index.js", "start": "node index.js" }, "author": "", "license": "GPL-3.0-or-later", "devDependencies": { - "nodemon": "^3.0.1" + "nodemon": "^3.0.1", + "vite": "^4.4.11", + "vite-plugin-windicss": "^1.9.1", + "windicss": "^3.5.6" }, "dependencies": { "express": "^4.18.2", diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/client.js b/src/client.js similarity index 100% rename from public/client.js rename to src/client.js diff --git a/public/database.js b/src/database.js similarity index 100% rename from public/database.js rename to src/database.js diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..e9a579a --- /dev/null +++ b/src/main.js @@ -0,0 +1,253 @@ +import 'virtual:windi.css' +import 'virtual:windi-devtools' + +import IndexedDB from './database.js' +import MusicList from './music.js' +import ClientList from './client.js' + +// 缓冲分片发送 +const CHUNK_SIZE = 1024 * 64 // 默认每个块的大小为128KB +const THRESHOLD = 1024 * 1024 // 默认缓冲区的阈值为1MB +const DELAY = 50 // 默认延迟500ms + +// 将两个ArrayBuffer合并成一个 +function appendBuffer(buffer1, buffer2) { + const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength) + tmp.set(new Uint8Array(buffer1), 0) + tmp.set(new Uint8Array(buffer2), buffer1.byteLength) + return tmp.buffer +} + +// 读取本地图像 +const imageStore = new IndexedDB('musicDatabase', 1, 'imageObjectStore') +await imageStore.open() + +// 读取本地音乐列表并标识为缓存状态(本地缓存) +const musicStore = new IndexedDB('musicDatabase', 1, 'musicObjectStore') +await musicStore.open() +const list = (await musicStore.getAll()).map(item => { + return { save: true, ...item } +}) + +// 读取本地用户名(本地缓存) +const name = localStorage.getItem('username') ?? '游客' +const avatar = localStorage.getItem('avatar') ?? '/favicon.ico' + + +// 初始化客户端列表 +const clientList = new ClientList({ + name, + onexit: async client => { + console.log(client.name, '离开频道', client) + // 从列表中移除未缓存的此用户的音乐, 但可能多人都有此音乐且未缓存 + // 因此每条音乐都要检查是否有其他用户也有此音乐, 如果有则不移除 + const 此用户音乐 = client.musicList?.map(item => item.id) || [] + const 无数据音乐 = musicList.list.filter(item => !item.arrayBuffer).filter(item => { + return 此用户音乐.includes(item.id) + }) + 无数据音乐.forEach(item => { + const client = clientList.clientlist.find(client => { + return client.musicList.find(x => x.id === item.id) + }) + if (!client) musicList.remove(item) + }) + + } +}) + + +// 初始化音乐列表(加入本地缓存) +const musicList = new MusicList({ + list, + onplay: item => { + console.log('播放音乐', item.name) + }, + onstop: item => { + console.log('停止音乐', item?.name) + }, + onlike: (item, list) => { + console.log('喜欢音乐', item.name) + if (item.arrayBuffer) { + musicStore.add(item) + clientList.send('base', JSON.stringify({ + type: 'set_music_list', + list: list.map(({ id, name, size, type }) => ({ id, name, size, type })) + })) + } + }, + onunlike: (item, list) => { + console.log('取消喜欢', item.name) + if (item.arrayBuffer) { + musicStore.delete(item.id) + clientList.send('base', JSON.stringify({ + type: 'set_music_list', + list: list.map(({ id, name, size, type }) => ({ id, name, size, type })) + })) + } + }, + onban: item => { + console.info('禁止音乐', item.name) + }, + onunban: item => { + console.info('解禁音乐', item.name) + }, + onremove: item => { + console.info('移除音乐', item.name) + musicStore.delete(item.id) + }, + onadd: (item, list) => { + console.info('添加音乐', item.name) + }, + onupdate: item => { + console.info('更新音乐', item.name) + musicStore.put(item) + }, + onerror: error => { + console.error('音乐列表错误', error) + }, + onload: async item => { + console.info('加载音乐', item) + return await new Promise((resolve) => { + var buffer = new ArrayBuffer(0) // 接收音乐数据 + var count = 0 // 接收分片计数 + const chunkNumber = Math.ceil(item.size / 1024 / 64) // 64KB每片 + clientList.setChannel(`music-data-${item.id}`, { + onmessage: async (event, client) => { + console.log('收到音乐数据 chunk', `${count + 1}/${chunkNumber}`, buffer.byteLength) + buffer = appendBuffer(buffer, event.data) // 合并分片准备存储 + item.arrayBufferChunks?.push(event.data) // 保存分片给边下边播 + count++ + if (buffer.byteLength >= item.size) { + console.log('音乐数据接收完毕') + item.arrayBuffer = buffer + event.target.close() // 关闭信道 + resolve(item) + } + } + }) + const client = clientList.clientlist.find(client => { + return client.musicList.find(x => x.id === item.id) + }) + console.info('向', client.name, '请求音乐数据') + + const c = Math.ceil(item.size / CHUNK_SIZE) + console.log('需要接收', c, '个分片') + + clientList.sendto(client.id, 'base', JSON.stringify({ + type: 'get_music_data', id: item.id, channel: `music-data-${item.id}` + })) + }) + } +}) + +const ImageList = [] + +// 只有一个基本信道, 用于交换和调度信息 +clientList.setChannel('base', { + onopen: async event => { + console.debug('打开信道', event.target.label, '广播请求音乐列表和身份信息') + clientList.send('base', JSON.stringify({ type: 'get_music_list' })) // 要求对方发送音乐列表 + clientList.send('base', JSON.stringify({ type: 'get_user_profile' })) // 要求对方发送身份信息 + }, + onmessage: async (event, client) => { + const data = JSON.parse(event.data) + if (data.type === 'get_user_profile') { + console.log(client.name, '请求身份信息:', data) + // 包过大会导致发送失败, 因此需要分开发送 + clientList.sendto(client.id, 'base', JSON.stringify({ + type: 'set_user_profile', + name: name, + avatar: avatar, + })) + return + } + if (data.type === 'set_user_profile') { + console.log(client.name, '发来身份信息:', data) + console.log('将身份信息保存到本机记录:', client) + client.name = data.name + client.avatar = data.avatar + // 还需要更新组件的用户信息 + console.log('更新组件的用户信息:', data, client) + clientList.setAvatar({ id: client.id, ...data }) + return + } + if (data.type === 'get_image_list') { + // webrtc://用户@域名:端口/信道标识/资源ID + } + if (data.type === 'get_music_list') { + const ms = musicList.list.filter(item => item.arrayBuffer) + console.log(client.name, '请求音乐列表:', ms) + clientList.sendto(client.id, 'base', JSON.stringify({ + type: 'set_music_list', + list: ms.map(({ id, name, size, type }) => ({ id, name, size, type })) + })) + return + } + if (data.type === 'set_music_list') { + console.log(client.name, '发来音乐列表:', `x${JSON.parse(event.data).list.length}`) + client.musicList = data.list + client.musicList.forEach(music => musicList.add(music)) + return + } + if (data.type === 'get_music_data') { + // 建立一个信道, 用于传输音乐数据(接收方已经准备好摘要信息) + console.log(client.name, '建立一个信道, 用于传输音乐数据', musicList.list) + musicList.list.filter(item => item.id === data.id).forEach(item => { + const ch = client.webrtc.createDataChannel(data.channel, { reliable: true }) + ch.onopen = async event => { + console.log(client.name, `打开 ${data.channel} 信道传输音乐数据`, item.name) + // 将音乐数据分成多个小块,并逐个发送 + async function sendChunk(dataChannel, data, index = 0, buffer = new ArrayBuffer(0)) { + while (index < data.byteLength) { + if (dataChannel.bufferedAmount <= THRESHOLD) { + const chunk = data.slice(index, index + CHUNK_SIZE) + dataChannel.send(chunk) + index += CHUNK_SIZE + buffer = appendBuffer(buffer, chunk) + } + await new Promise((resolve) => setTimeout(resolve, DELAY)) + } + return buffer + } + await sendChunk(ch, item.arrayBuffer) + console.log(client.name, `获取 ${data.channel} 信道数据结束`, item.name) + ch.close() // 关闭信道 + } + }) + return + } + console.log('未知类型:', data.type) + }, + onclose: event => { + console.log('关闭信道', event.target.label) + }, + onerror: event => { + console.error('信道错误', event.target.label, event.error) + } +}) + +// 延迟1500ms +//await new Promise((resolve) => setTimeout(resolve, 100)) + +// 设置自己的主机名 +const nameInput = document.createElement('input') +nameInput.type = 'text' +nameInput.placeholder = '请设置你的昵称' +nameInput.value = name +nameInput.onchange = event => { + localStorage.setItem('username', event.target.value) + window.location.reload() // 简单刷新页面 +} +document.body.appendChild(nameInput) + +// 设置标签为自己的头像 +if (localStorage.getItem('avatar')) { + const favicon = document.createElement('link') + favicon.rel = 'icon' + favicon.href = localStorage.getItem('avatar') + document.head.appendChild(favicon) +} +// 设置标题为自己的昵称 +if (localStorage.getItem('username')) { + document.title = localStorage.getItem('username') +} \ No newline at end of file diff --git a/public/music.js b/src/music.js similarity index 100% rename from public/music.js rename to src/music.js diff --git a/public/weigets.js b/src/weigets.js similarity index 100% rename from public/weigets.js rename to src/weigets.js diff --git a/style.css b/style.css new file mode 100644 index 0000000..abf9d15 --- /dev/null +++ b/style.css @@ -0,0 +1,97 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..cc550ee --- /dev/null +++ b/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite' +import WindiCSS from 'vite-plugin-windicss' + +export default defineConfig({ + server: { + port: 4096, + proxy: { + '/webrtc': { + target: 'wss://webrtc.satori.love', + changeOrigin: true, + ws: true, + }, + '/entanglement': { + target: 'wss://webrtc.satori.love', + changeOrigin: true, + ws: true, + }, + '/webhook': 'https://webrtc.satori.love', + } + }, + plugins: [ + WindiCSS(), + ] +})