@sctlib/rtc
Prototype web-components to establish webrtc peer connections, without the need of a predefined signaling server.
There are different signaling methods, some like matrix will call a matrix server.
Usage in code
To connect with peers using WebRTC, visit the website. From the interface, select a "signaling method" and follow the instructions, to connect with an other peer (a friend, or yourself in the same browser tab, an other tab, an other brwoser, an other device, from an other network etc. )
This project is available as a web page, and a javascript/html/css/web-component npm package.
npm i @sctlib/rtc
- check this project
index.html
file to see how to init the components, it is this demo page.
CDN and DOM usage
The web-components definitions can be imported from a CDN, and inserted in the HTML DOM with:
<script type="module" src="https://cdn.jsdelivr.net/npm/@sctlib/rtc"></script>
<rtc-user></rtc-user>
About the project and manual signaling
To establish a webrtc connection between two peers (A
, instanciating
the connection, and B
with whom we are trying to connect), data has
two be exchanged between them, this is the signaling part of the
connection.
-
peer A
creates a RTCPeerConnection, and anoffer
-
peer A
signals the offer topeer B
-
peer B
receives/read theoffer
, creates its own RTCPeerconnection, and creates andanswer
-
peer B
signals the answer topeer A
-
peer A
receive/reads theanswer
, adds it to its RTCPeerconnection - a webrtc connection between
peer A
andpeer B
is opened
To signal the offer and answer between peer A and B, we could use a server (which would transmit the data for us, the "signaling" part of a WebRTC connection, before the RTCConnection is established), but we want to be able to to that without relying on a specific server somewhere (which might be offline, unavailable, or we just want to be independant, decentralized, autonomous, p2p...).
For this reason we want to "manually signal" the data (see peer data section) ourselves in different ways. These signaling forms can be composed to establish a connection, so each independant part could be transmitted with different method (as long as peer A and B get what they need, in the correct order).
copy/paste & third party signaling server
-
peer A
copy/paste the offer data, singaling it topeer B
over a third party signaling server -
peer B
copy/paste the offer from peer A, into the app to generate the peer anwser -
peer B
copy/paste the answer and signals it topeer A
over a third party signaling server -
peer A
copy/paste the answer into the app, to finish the connection
Third party manual signaling servers could be:
- matrix, signal, whatsapp, telegram, instagram, steam, in app chat etc.
- bookmark synchronisation service, to share the data as URL between devices
- (push) notifications, such as ntfy; (problem is, there are rate limit for non paying user, so rolled out currently).
qr-code(s)
For devices which support displaying QR codes, and/or reading them, it is possible to generate a QR-code representation of all peer data that needs to be signaled.
A device can create peer offerbundle
, and display it as a
qr-code. It can also display it "unbundled", with multiple qr-codes
representing the same data.
A device can also read the qr-code of peer data (bundled or not), in
one qr-code, or multiple qr-codes displayed sequencecially. This
feature is possible thanks to the barcode-detector-polyfill
and
zbar-wasm
qr-code scanning
modules
(not bundled into the app, but served from a CDN because of their
licences).
window.postMessage()
- when receiving an
offer
, we can signal the peer contained in the?data
query parameter, to our local peer, so it creates an answer (to be signaled back to the instanciating peer) - when receiving an
answer
, we can also signal the incoming peer data contained in the?data
query param, to our local peer (so it can finish opening the connection)
Peer data (being signaled)
The peer(ing) data, is the data sent between peers to instanciate a webrtc peer connection. It is composed of:
- a webrtc peer
offer
oranswer
, (a javascript object) description of the peer with RTC capabilities - a list of ICECandidates, the possibe routes to our peer on the network
For signaling purpose, this data is transformed as a string of text, so it can:
- fit into the URL/URI scheme (here as a
?data=
query parameter), and not be too long (gitlab pages only support ±1048 chars urls; we moved the app to cloudflare pages). - fit into QR-codes and their maximum size
- be accessible, received and read by various (people and) devices, with multiple form factors and capabilities
researching how to best encode, compress, transmit, sign, the data in these "manual ways" so it is relyiable.
- right it uses
lz-strings
to compress and encode the data for the URL. - used to be using base64 (
atob
andbtoa
, withJSON.parse()
andJSON.stringify()
) - could research
protobuff
,brocli
, or UDP/TCP (or other data transmition protocol), to experiment with multi QR-code send/receive (device facing device, both doing both actions).
The data can also be signaled:
- "bundled", an object with
offer
/answer
+candidates
, (answerbundle
andofferbundle
) - "unbundled", several objects,
offer
/answer
,candidates
,type
Development
To use the code locally, or in other applications.
Local server
Run dev server with:
npm install
npm run dev
Web components
There is currently only one "public" web component though it is composed of a few others, that are undocumented, but should all work independantly.
A few HTML attributes allow to customize the experience of the rtc user.
Some are set in the DOM by the user, some other are set by the app, as reflection of its current state, and can be used to style the interface (but not for changing the state).
rtc-user
When inserted in the DOM, <rtc-user></rtc-user>
will create an interface to allow two peers to connect, with various signaling methods.
Attributes
-
user-name
=String
, only a display for the local user peer's -
signaling-methods
=JSON.stringify(Array[signaling-methods])
, a list of allowed signaling methods IDs. -
matrix-peers
=JSON.stringify(Array[matrix_user_id])
, is a list of Matrix.org user IDs that are allowed for incoming RTC offers (and preffiled for outgoing RTC offer/answers) -
search-params
=JSON.stringify(Array[<rtc-user ATTRIBUTE/>])
than can be preffiled from the current web page's URL search params (hash params too?)
Examples:
<rtc-user user-name="peer-a" signaling-methods='["copypaste"]'></rtc-user>
<rtc-user user-name="peer-b" search-params='["matrix-peers"]'></rtc-user>
Events
Currently only two events are sent outside.
const $user = document.querySelector('rtc-user')
$user.addEventListener('dataChannel', (event)=> {
const {detail} = event
console.log('Peer connected, received data channel', event)
console.log('Peer connection data channel', detail)
})
$user.addEventListener('channelMessage', (event)=> {
const {detail} = event
console.log(Peer received message from data channel, event)
console.log(Peer message , detail)
})
Methods
When a data channel is open, there are several methods that can be used on the rtc-user
:
-
send(data)
accepts data to be sen through the data channel. As a convenience, if ittypeof data === "object"
,JSON.stringify(data)
will be applied, so the data can be send as a Javascript string.
Slots
The rtc-user
has two slots, that can be used to render HTML elements as it's children; logs
and send
:
<rtc-user>
<output slot="logs"></output>
<form slot="send">
<fieldset>
<legend>Send data (custom send)</legend>
<input name="text" />
<button type="submit">Send message</button>
</fieldset>
</form>
</rtc-user>
The slots, do not handle automatically events (as we might expect; for this we'd have to clearely define the events/methods api for inserted elements into the slots; so the rtc-user can communicate with them directly).
To handle the slots, such as what's done with their default slot value (a textarea to send data, and a list of the messages as logs):
// the HTML elements we need for displaying rtc events
const $user = document.querySelector("rtc-user");
const $userLog = $user.querySelector('[slot="logs"]');
const $userSend = $user.querySelector('[slot="send"]');
// output the opened data channel (in/out) to the logs
$user.addEventListener("dataChannel", (event) => {
if ($userLog) {
const $log = document.createElement("p");
$log.innerText = event.detail.type;
$userLog.append($log);
}
});
// listen to rtc data channel messages in; append all as logs
$user.addEventListener("channelMessage", (event) => {
console.log("outside user receive channel message", event);
if ($userLog) {
const $log = document.createElement("p");
$log.innerText = event.detail;
$userLog.append($log);
}
});
// listen to the form "submit" event, send content through rtc data channel out
if ($userSend) {
$userSend.addEventListener("submit", (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const formDataObject = Object.fromEntries(formData);
$user.send({
detail: formDataObject,
});
});
}
Connection scenarios
This project is an experiment in establishing a web RTCPeerConnection
, between
two peers in their browsers, without the need of a predefined server to
establing signaling
-
Peer
sends a connection offer toPeer B
-
Peer B
receives and uses the offer sent byPeer A
, to create an answer, and sends it toPeer A
-
Peer A
receives the answer, and associate it to its peer. - The RTCPeerConnection succeeds (or fails), the two peers are now connected for real time communication, without intermediary server.
To get started, send an offer to a friend, and wait for them to send you an answer back.
As a first test, try the "copy/paste" signaling method. It is also possible to try signaling with QR-code(s). Open the developer console for debugging information.
We're using a bunch of rtc components to connect peers with webrtc, without the need of servers.
There are different scenarios we're trying to cover.
- 1 peer alone -> no data channel
- 2 peers, on the same browser, in 1 page
- 2 peers, on the same browser, in 1 page, in 2 iframes
- 2 peers, on the same browser, in 2 pages (tabs which are publich; not private browsing)
- 2 peers, on the same browser, 1 public page, and 1 in private browsing page
- 2 peers, on different browsers (private or public pages), on the same computer
- 2 peers, on different browsers (private or public pages), on different computers, on the same network
- 2 peers, on different browsers (private or public tabs), on different computers, on different networks (behind NAT/ROUTER)
- 2 peers, different computer/browser/network, through private VPN
- X peers, <- start again from 1.
It would be nice to have most of them working without the need of any server (signaling/TURN; STUN, since free are okay)?
Status:
- done: 1, 2, 3, 4
- doing: 5, 6, 7, (8 & 9)
- after: 10