wip: 0.2.2
1. rtc init
This commit is contained in:
@@ -15,6 +15,10 @@ type RoomState = {
|
||||
conn: WebSocket | null
|
||||
client: Client | null
|
||||
clients: Client[]
|
||||
pc: RTCPeerConnection | null
|
||||
ch: RTCDataChannel | null
|
||||
candidate: RTCIceCandidate | null
|
||||
offer: RTCSessionDescription | null
|
||||
retryCount: number
|
||||
reconnectTimer: number | null
|
||||
}
|
||||
@@ -23,6 +27,7 @@ type RoomActions = {
|
||||
register: () => Promise<void>
|
||||
enter: () => Promise<void>
|
||||
list: () => Promise<void>
|
||||
send: (file: File) => Promise<void>
|
||||
cleanup: () => void
|
||||
}
|
||||
|
||||
@@ -32,6 +37,17 @@ interface Message {
|
||||
body: any;
|
||||
}
|
||||
|
||||
function setupDataChannel(ch: RTCDataChannel) {
|
||||
ch.onopen = () => console.log('通道已打开!');
|
||||
ch.onmessage = (e) => handleFileChunk(e.data);
|
||||
ch.onclose = () => console.log('通道关闭');
|
||||
}
|
||||
|
||||
// 接收文件块
|
||||
function handleFileChunk(chunk: any) {
|
||||
console.log("[D] rtc file chunk =", chunk)
|
||||
}
|
||||
|
||||
const MAX_RETRY_DELAY = 30000 // 最大重试间隔30秒
|
||||
const NORMAL_CLOSE_CODE = 1000 // 正常关闭的状态码
|
||||
|
||||
@@ -39,14 +55,67 @@ export const useRoom = create<RoomState & RoomActions>()((set, get) => ({
|
||||
conn: null,
|
||||
client: null,
|
||||
clients: [],
|
||||
pc: null,
|
||||
ch: null,
|
||||
candidate: null,
|
||||
offer: null,
|
||||
retryCount: 0,
|
||||
reconnectTimer: null,
|
||||
register: async () => {
|
||||
const api = `/api/ulocal/register`
|
||||
const res = await fetch(api, {method: 'POST'})
|
||||
const jes = await res.json() as Resp<Client>
|
||||
return set(state => {
|
||||
return {...state, client: jes.data}
|
||||
let candidate: RTCIceCandidate;
|
||||
let offer: RTCSessionDescription | null;
|
||||
const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
|
||||
// 处理接收方DataChannel
|
||||
rtc.ondatachannel = (e) => {
|
||||
setupDataChannel(e.channel);
|
||||
};
|
||||
|
||||
const waitCandidate = new Promise<void>(resolve => {
|
||||
rtc.onicecandidate = (e) => {
|
||||
if (e.candidate) {
|
||||
console.log('[D] candidate =', {candidate: e.candidate})
|
||||
candidate = e.candidate
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
|
||||
// rtc.onicecandidate = (e) => {
|
||||
// if (e.candidate) {
|
||||
// console.log('[D] candidate =', {candidate: e.candidate})
|
||||
// candidate = e.candidate
|
||||
// }
|
||||
// }
|
||||
|
||||
const waitOffer = new Promise<void>(resolve => {
|
||||
rtc.onnegotiationneeded = async () => {
|
||||
await rtc.setLocalDescription(await rtc.createOffer());
|
||||
console.log("[D] offer =", {offer: rtc.localDescription})
|
||||
offer = rtc.localDescription
|
||||
resolve();
|
||||
};
|
||||
})
|
||||
|
||||
// rtc.onnegotiationneeded = async () => {
|
||||
// await rtc.setLocalDescription(await rtc.createOffer());
|
||||
// console.log("[D] offer =", {offer: rtc.localDescription})
|
||||
// offer = rtc.localDescription
|
||||
// };
|
||||
|
||||
const ch = rtc.createDataChannel("fileTransfer", {ordered: true})
|
||||
|
||||
setupDataChannel(ch)
|
||||
|
||||
|
||||
Promise.all([waitCandidate, waitOffer]).then(() => {
|
||||
const api = `/api/ulocal/register`
|
||||
fetch(api, {
|
||||
method: 'POST',
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({candidate: candidate, offer: offer})
|
||||
}).then(res => {return res.json() as unknown as Resp<Client>}).then(jes => {
|
||||
set({client: jes.data, candidate: candidate, offer: offer})
|
||||
})
|
||||
})
|
||||
},
|
||||
enter: async () => {
|
||||
@@ -57,11 +126,11 @@ export const useRoom = create<RoomState & RoomActions>()((set, get) => ({
|
||||
if (conn) conn.close()
|
||||
|
||||
const api = `${window.location.protocol === 'https' ? 'wss' : 'ws'}://${window.location.host}/api/ulocal/ws?id=${get().client?.id}`
|
||||
console.log('[D] websocket api =',api)
|
||||
console.log('[D] websocket api =', api)
|
||||
const newConn = new WebSocket(api)
|
||||
|
||||
newConn.onopen = () => {
|
||||
set({conn: newConn, retryCount: 0}) // 重置重试计数器
|
||||
|
||||
}
|
||||
|
||||
newConn.onerror = (error) => {
|
||||
@@ -75,7 +144,7 @@ export const useRoom = create<RoomState & RoomActions>()((set, get) => ({
|
||||
switch (msg.type) {
|
||||
case "enter":
|
||||
nc = msg.body as Client
|
||||
if(nc.id && nc.name && nc.id !== get().client?.id) {
|
||||
if (nc.id && nc.name && nc.id !== get().client?.id) {
|
||||
console.log('[D] enter new client =', nc)
|
||||
set(state => {
|
||||
return {...state, clients: [...get().clients, nc]}
|
||||
@@ -84,7 +153,7 @@ export const useRoom = create<RoomState & RoomActions>()((set, get) => ({
|
||||
break
|
||||
case "leave":
|
||||
nc = msg.body as Client
|
||||
if(nc.id) {
|
||||
if (nc.id) {
|
||||
let idx = 0;
|
||||
let items = get().clients;
|
||||
for (const item of items) {
|
||||
@@ -93,7 +162,7 @@ export const useRoom = create<RoomState & RoomActions>()((set, get) => ({
|
||||
set(state => {
|
||||
return {...state, clients: items}
|
||||
})
|
||||
break;
|
||||
break;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
@@ -127,9 +196,22 @@ export const useRoom = create<RoomState & RoomActions>()((set, get) => ({
|
||||
const api = "/api/ulocal/clients?room="
|
||||
const res = await fetch(api + get().client?.room)
|
||||
const jes = await res.json() as Resp<Client[]>
|
||||
set(state => {
|
||||
return {...state, clients: jes.data}
|
||||
})
|
||||
set({clients: jes.data})
|
||||
},
|
||||
send: async (file: File) => {
|
||||
const reader = new FileReader();
|
||||
const channel = get().ch!;
|
||||
reader.onload = (e) => {
|
||||
const chunkSize = 16384; // 16KB每块
|
||||
const buffer = e.target!.result! as ArrayBuffer;
|
||||
let offset = 0;
|
||||
while (offset < buffer.byteLength) {
|
||||
const chunk = buffer.slice(offset, offset + chunkSize);
|
||||
channel.send(chunk);
|
||||
offset += chunkSize;
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
},
|
||||
cleanup: () => {
|
||||
const {conn, reconnectTimer} = get()
|
||||
|
||||
Reference in New Issue
Block a user