sounds, better files
This commit is contained in:
172
src/model/Remote/Channels/WebRTCChannel.ts
Normal file
172
src/model/Remote/Channels/WebRTCChannel.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { VirtualFile } from "../../FileSystem/types";
|
||||
import { RemoteDeviceManagerCtx } from "../RemoteDeviceManager";
|
||||
import { JsonChannelCaster, JsonChannelSummon, Message, MessageEmitter } from "./type";
|
||||
|
||||
|
||||
export class WebRtcChannelCaster extends MessageEmitter implements JsonChannelCaster {
|
||||
|
||||
private peer = new RTCPeerConnection()
|
||||
private offerSent = false;
|
||||
private dataChannel = this.peer.createDataChannel('json');
|
||||
public readonly sink: MediaStreamAudioDestinationNode;
|
||||
|
||||
constructor(
|
||||
private channel: JsonChannelCaster,
|
||||
{onOpen}:Partial<{onOpen: Function}> = {},
|
||||
private ctx: RemoteDeviceManagerCtx
|
||||
) {
|
||||
super();
|
||||
this.sink = ctx.audioContext.createMediaStreamDestination();
|
||||
this.channel.on(this.onJsonMessage);
|
||||
this.dataChannel.addEventListener('close', () => {
|
||||
this.emit({ type: 'disconnected' });
|
||||
});
|
||||
this.dataChannel.addEventListener('open', () => {
|
||||
onOpen?.();
|
||||
this.emit({ type: 'connected' });
|
||||
});
|
||||
this.dataChannel.addEventListener('message', this.onMessage);
|
||||
|
||||
this.peer.addTrack(this.sink.stream.getAudioTracks()[0], this.sink.stream);
|
||||
this.peer.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.channel.send({
|
||||
type: 'RtcIce',
|
||||
candidate: event.candidate
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onMessage = (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data) as Message;
|
||||
this.emit(data);
|
||||
}
|
||||
|
||||
private onJsonMessage = (data: Message) => {
|
||||
if (data.type === 'summoned') {
|
||||
if (!this.offerSent) {
|
||||
this.peer.createOffer().then(offer => {
|
||||
this.peer.setLocalDescription(offer);
|
||||
this.channel.send({
|
||||
type: 'RtcOffer',
|
||||
offer
|
||||
});
|
||||
});
|
||||
this.offerSent = true;
|
||||
}
|
||||
} else if (data.type === 'RtcAnswer') {
|
||||
this.peer.setRemoteDescription(new RTCSessionDescription(data.answer));
|
||||
} else if (data.type === 'RtcIce') {
|
||||
this.peer.addIceCandidate(new RTCIceCandidate(data.candidate));
|
||||
}
|
||||
}
|
||||
|
||||
async sendBinary(file: VirtualFile, data: Blob) {
|
||||
const channelName = `file:${Math.random()}`;
|
||||
this.dataChannel.send(JSON.stringify({
|
||||
type: 'fileTransfer',
|
||||
channel: channelName,
|
||||
size: data.size,
|
||||
file,
|
||||
}));
|
||||
const channel = this.peer.createDataChannel(channelName);
|
||||
channel.binaryType = 'arraybuffer';
|
||||
const buffer = await data.arrayBuffer();
|
||||
const chunkSize = 16384;
|
||||
for (let i = 0; i < buffer.byteLength; i += chunkSize) {
|
||||
const chunk = buffer.slice(i, i + chunkSize);
|
||||
channel.send(chunk);
|
||||
}
|
||||
channel.close();
|
||||
await new Promise((resolve) => channel.addEventListener('close', resolve, { once: true }));
|
||||
}
|
||||
|
||||
send(data: Message) {
|
||||
this.dataChannel.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
super.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class WebRtcChannelSummon extends MessageEmitter implements JsonChannelSummon {
|
||||
private peer = new RTCPeerConnection()
|
||||
private dataChannel?: RTCDataChannel;
|
||||
private transferedFilesMeta = new Map<string, VirtualFile>();
|
||||
|
||||
constructor(private channel: JsonChannelCaster) {
|
||||
super();
|
||||
this.channel.on(this.onJsonMessage);
|
||||
this.peer.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.channel.send({
|
||||
type: 'RtcIce',
|
||||
candidate: event.candidate
|
||||
});
|
||||
}
|
||||
}
|
||||
this.peer.ontrack = (event) => {
|
||||
console.log('ontrack', event);
|
||||
const audioElement = document.createElement('audio');
|
||||
audioElement.srcObject = event.streams[0]; // Attach the received stream
|
||||
audioElement.play();
|
||||
}
|
||||
this.peer.ondatachannel = (event) => {
|
||||
if (event.channel.label === 'json') {
|
||||
this.dataChannel = event.channel;
|
||||
this.dataChannel.addEventListener('message', this.onRtcMessage);
|
||||
this.emit({ type: 'connected' });
|
||||
} else {
|
||||
const channel = event.channel;
|
||||
const chunks:ArrayBuffer[] = [];
|
||||
channel.addEventListener('message', (event: MessageEvent<ArrayBuffer>) => {
|
||||
console.log('>', channel.label, event.data);
|
||||
chunks.push(event.data);
|
||||
});
|
||||
channel.addEventListener('close', () => {
|
||||
console.log('>', channel.label, 'closed');
|
||||
const meta = this.transferedFilesMeta.get(channel.label)!;
|
||||
// TODO: handle data before meta?
|
||||
const blob = new Blob(chunks, { type: meta?.mimeType ?? 'application/octet-stream' });
|
||||
console.log(meta, blob);
|
||||
this.emit({
|
||||
type: 'fileTransfered',
|
||||
meta,
|
||||
blob,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onRtcMessage = (event: MessageEvent) => {
|
||||
const msg = JSON.parse(event.data) as Message;
|
||||
if (msg.type === 'fileTransfer') {
|
||||
this.transferedFilesMeta.set(msg.channel, msg.file);
|
||||
} else {
|
||||
this.emit(msg);
|
||||
}
|
||||
};
|
||||
|
||||
private onJsonMessage = (data: Message) => {
|
||||
if (data.type === 'RtcOffer') {
|
||||
this.peer.setRemoteDescription(new RTCSessionDescription(data.offer));
|
||||
this.peer.createAnswer().then(answer => {
|
||||
this.peer.setLocalDescription(answer);
|
||||
this.channel.send({
|
||||
type: 'RtcAnswer',
|
||||
answer
|
||||
});
|
||||
});
|
||||
} else if (data.type === 'RtcIce') {
|
||||
this.peer.addIceCandidate(new RTCIceCandidate(data.candidate));
|
||||
}
|
||||
}
|
||||
|
||||
send(data: Message) {
|
||||
this.dataChannel?.send(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user