205 lines
6.3 KiB
TypeScript
205 lines
6.3 KiB
TypeScript
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}> = {},
|
|
// @ts-ignore
|
|
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
|
|
});
|
|
}
|
|
}
|
|
this.peer.onnegotiationneeded = async () => {
|
|
const offer = await this.peer.createOffer();
|
|
await this.peer.setLocalDescription(offer);
|
|
|
|
// Send the offer to the remote peer
|
|
this.channel.send({
|
|
type: 'RtcOffer',
|
|
offer
|
|
});
|
|
console.log('negotiationneeded');
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
connectVideoStream(stream: MediaStream) {
|
|
this.peer.addTrack(stream.getVideoTracks()[0], stream);
|
|
}
|
|
|
|
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) => {
|
|
event.streams.forEach((stream) => {
|
|
stream.getTracks().forEach((track) => {
|
|
if (event.track.kind === 'audio') {
|
|
console.log('audio track', track);
|
|
const audioElement = document.createElement('audio');
|
|
audioElement.srcObject = stream; // Attach the received stream
|
|
audioElement.play();
|
|
} else if (event.track.kind === 'video') {
|
|
console.log('video track', track);
|
|
const videoEl = document.getElementById('mainVideo') as HTMLVideoElement;
|
|
if (videoEl) {
|
|
videoEl.srcObject = stream;
|
|
videoEl.play();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
};
|
|
this.peer.ondatachannel = (event) => {
|
|
if (event.channel.label === 'video') {
|
|
// @ts-ignore
|
|
this.videoChannel = event.channel;
|
|
} else 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));
|
|
}
|
|
}
|