2024-05-18
WebRTCでデータチャネルで双方向の通信を手を動かしてP2Pでのデータ通信を体感する
Calls > Create: Tern Service Token
webrtc.rsのExample を参考に、RTCIceServerの箇所をCloudflareのものに変更する(トークンを払い出された際に表示されているもの)
ice_servers: vec![RTCIceServer {
urls: vec!["stun:stun.l.google.com:19302".to_owned()],
..Default::default()
}],
ICE (Interactive Connectivity Establishment)とはNAT超えのためにSTUN/TURNのプロトコル選択のための仕組み
参考:https://jsfiddle.net/swgxrp94/20/
"use client"
import { useEffect } from "react";
import { useState } from 'react';
export default function Home() {
const [data, setData] = useState('no data');
const [localsdp, setLocalDescription] = useState('');
const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
const [peer, setPeerConnection] = useState<RTCPeerConnection | null>(null);
const REPLACE_WITH_USERNAME = "";
const REPLACE_WITH_CREDENTIAL = "";
const config = {
iceServers: [
{
urls: "stun:stun.cloudflare.com:3478",
},
{
urls: "turn:turn.cloudflare.com:3478",
username: REPLACE_WITH_USERNAME,
credential: REPLACE_WITH_CREDENTIAL,
},
{
urls: "turns:turn.cloudflare.com:443?transport=tcp",
username: REPLACE_WITH_USERNAME,
credential: REPLACE_WITH_CREDENTIAL,
},
{
urls: "turn:turn.cloudflare.com:3478?transport=tcp",
username: REPLACE_WITH_USERNAME,
credential: REPLACE_WITH_CREDENTIAL,
},
],
};
useEffect(() => {
let peer = new RTCPeerConnection(config);
peer.onsignalingstatechange = (e) => {
console.log(peer.signalingState);
setLocalDescription(btoa(JSON.stringify(peer.localDescription)));
};
peer.oniceconnectionstatechange = (e) => {
console.log(e);
setLocalDescription(btoa(JSON.stringify(peer.localDescription)));
};
peer.ondatachannel = (e) => {
let dc = e.channel
dc.onclose = () => console.log('dc has closed');
dc.onopen = () => console.log('dc has opened');
dc.onmessage = e => {
setData(e.data);
console.log(`Message from DataChannel '${dc.label}' payload '${e.data}'`);
}
setDataChannel(dc);
};
console.log(peer);
setPeerConnection(peer);
}, []);
useEffect(() => {
console.log("peer change");
console.log(peer?.localDescription);
console.log(btoa(JSON.stringify(peer?.localDescription)));
setLocalDescription(btoa(JSON.stringify(peer?.localDescription)));
}, [peer]);
function handleSdp(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const formJson = Object.fromEntries(formData.entries());
if (formJson['sdp'] === '') {
return
}
const remoteSdp = formJson['sdp'] as string;
console.log('remotesdp: ' + remoteSdp);
peer?.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(remoteSdp))))
peer?.createAnswer().then(d => {
peer.setLocalDescription(d);
setLocalDescription(btoa(JSON.stringify(peer.localDescription)));
});
};
function handleSendData(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const formJson = Object.fromEntries(formData.entries());
if (formJson['send'] === '') {
return
}
const senddata = formJson['send'] as string;
dataChannel?.send(senddata);
}
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<p>hello world</p>
<div>
<form method="post" onSubmit={handleSdp}>
SDP :<textarea name="sdp" />
<button type="submit">Submit form</button>
</form>
</div>
<div>{localsdp}</div>
<button onClick={() => global.navigator.clipboard.writeText(localsdp)}>Copy SDP to Clipboard</button>
<div>{data}</div>
<form method="post" onSubmit={handleSendData}>
SendData :<input name="send" defaultValue="" />
<button type="submit">Send data</button>
</form>
</main >
);
}
let peer = new RTCPeerConnection(config);
peer.ondatachannel = (e) => {
let dc = e.channel
dc.onclose = () => console.log('dc has closed');
dc.onopen = () => console.log('dc has opened');
dc.onmessage = e => {
setData(e.data);
console.log(`Message from DataChannel '${dc.label}' payload '${e.data}'`);
}
setDataChannel(dc);
};
function handleSendData(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const formJson = Object.fromEntries(formData.entries());
if (formJson['send'] === '') {
return
}
const senddata = formJson['send'] as string;
dataChannel?.send(senddata);
}
function handleSdp(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const formJson = Object.fromEntries(formData.entries());
if (formJson['sdp'] === '') {
return
}
const remoteSdp = formJson['sdp'] as string;
console.log('remotesdp: ' + remoteSdp);
peer?.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(remoteSdp))))
peer?.createAnswer().then(d => {
peer.setLocalDescription(d);
setLocalDescription(btoa(JSON.stringify(peer.localDescription)));
});
};
P2Pで通信するために互いにSDPを交換する必要がある。今回はコピペで交換する。
実際の製品ではネットワーク経由で交換してもっと凝った方法で経路とプロトコルを決定する。
左がブラウザ、右がRaspi