Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions README.it.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,14 @@ I browser consentono la videocamera solo su pagine sicure (HTTPS), quindi la fin

- **I link internet** usano lo strumento gratuito di Cloudflare [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/):
- va installato una volta: `brew install cloudflared` su macOS, `winget install --id Cloudflare.cloudflared` su Windows;
- l'indirizzo cambia a ogni riavvio di streamFree;
- attraverso Cloudflare passano solo la pagina ospite e la segnalazione; audio e video viaggiano direttamente tra te e gli ospiti.
- **Ospiti che non riescono a connettersi** (reti aziendali o mobili restrittive): aggiungi un server TURN in **Impostazioni → Ospiti → Server ICE**.
- l'indirizzo cambia a ogni riavvio di streamFree (e se il link cade, streamFree ne crea uno nuovo e te lo segnala).
- **Come viaggiano audio e video.** Audio e video passano direttamente tra te e ogni ospite (WebRTC) quando le due reti lo permettono.
Reti aziendali restrittive, rete mobile e CGNAT degli operatori spesso non lo permettono. Dopo circa 10 secondi senza connessione diretta,
streamFree passa automaticamente quell'ospite al suo **relay**: lo stesso audio e video, compressi dal browser e trasportati
da streamFree stesso, sullo stesso link che l'ospite ha aperto. Non servono account, server o configurazioni; il ritardo è un po' maggiore.
Privacy: le connessioni dirette sono cifrate end-to-end tra te e l'ospite. Anche il media del relay viaggia cifrato,
ma con un link Internet passa dalla rete di Cloudflare, che decifra e ricifra il traffico ai suoi nodi.
- **Un percorso più veloce per quegli ospiti:** aggiungi un server TURN in **Impostazioni → Ospiti → Server ICE**. **Impostazioni → Ospiti → Connessione** può anche far passare tutti gli ospiti dal relay.
- **Un tuo proxy HTTPS:** avvia streamFree con `--public-url https://studio.example.com`.

## Riga di comando
Expand Down Expand Up @@ -162,17 +167,19 @@ streamFree è giovane. Cosa è stato verificato, e come:
| Registrazione, due destinazioni RTMP in contemporanea, sincronia audio/video | Test automatici nel browser (Chrome vero, videocamera simulata) a ogni push |
| Ospiti: tre insieme, ognuno sente tutti tranne se stesso, chat del backstage nei due sensi, rimozione di un ospite, uscita e rientro, limite di 16 ospiti, sostituzione del link di invito | Test automatici nel browser |
| Ospiti dalla rete locale (HTTPS) e da internet (tunnel Cloudflare reale) | Test automatici nel browser, eseguiti in locale (in CI manca cloudflared) |
| Ospiti la cui rete non permette connessioni dirette (simulata: ICE solo relay, senza TURN): passaggio automatico al relay, video e audio nei due sensi, mix-minus, registrazione con l'ospite sul palco, anche attraverso un tunnel Cloudflare reale | Test automatici nel browser |
| Un ospite la cui connessione cade torna sul palco da solo | Test automatici nel browser |
| QR code, pulsanti di condivisione, interfaccia in italiano, ogni stringa tradotta | Test automatici nel browser |
| Uscita RTMP e file MP4 | FFmpeg come verificatore indipendente nella suite di test |
| App macOS: finestra nativa, 30 fps con la finestra ridotta a icona, audio sincronizzato | Smoke test integrato (`streamfree --smoke-test 8`) |
| App macOS che ospita un ospite (host WebKit, ospite Chrome, programma ricevuto a 15 fps) | `npm run test:native` |
| App macOS che ospita un ospite (host WebKit, ospite Chrome, programma ricevuto a 15 fps), più un ospite tramite relay | `npm run test:native` |
| App macOS: videocamera, microfono e condivisione schermo | Test manuale |
| I server di YouTube, Twitch, Facebook (RTMPS) e Kick (RTMPS) accettano la connessione di streamFree, e una chiave sbagliata dà un errore chiaro | Pulsante **Prova** e `cargo run --example ingest_probe`, con una chiave fittizia |

Non ancora verificato:

- **Una diretta con una chiave vera.** I controlli di protocollo qui sopra passano, ma nessuna diretta reale è stata ancora fatta. Segnala cosa funziona per te.
- **Ospiti da telefono e da altri browser.** Nessun ospite è ancora entrato da un telefono in rete mobile, né da Safari o Firefox.
- **Ospiti da telefono e da altri browser.** Nessun ospite è ancora entrato da un telefono in rete mobile, né da Safari o Firefox. Il relay è testato con una rete restrittiva simulata, non ancora con una reale.
- **Windows.** App e installer si compilano e passano i test in CI, ma non sono ancora stati eseguiti su un PC Windows.
- **Condurre da Linux.** Chrome su Linux sembra non avere un encoder AAC, quindi su Linux streamFree potrebbe funzionare solo per gli ospiti; in quel caso mostra un errore chiaro.

Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,14 @@ Browsers only allow cameras on secure (HTTPS) pages, so the Invite window asks w

- **Internet links** use Cloudflare's free [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) tool:
- install it once: `brew install cloudflared` on macOS, `winget install --id Cloudflare.cloudflared` on Windows;
- the address changes every time streamFree restarts;
- only the guest page and its signaling go through Cloudflare; audio and video travel directly between you and your guests.
- **Guests who cannot connect** (strict company or mobile networks): add a TURN server in **Settings → Guests → ICE servers**.
- the address changes every time streamFree restarts (and if the link drops, streamFree makes a new one and tells you).
- **How media travels.** Audio and video go directly between you and each guest (WebRTC) whenever the two networks allow it.
Strict company networks, mobile data and carrier-grade NAT often don't. After about 10 seconds without a direct connection,
streamFree switches that guest automatically to its **relay**: the same audio and video, compressed by the browser and carried
through streamFree itself, over the same link the guest opened. No account, server or setup is needed; there is a little more delay.
Privacy: direct connections are encrypted end to end between you and the guest. Relayed media is encrypted on the way too,
but with an Internet link it passes through Cloudflare's network, which decrypts and re-encrypts traffic at its edge.
- **A faster path for those guests:** add a TURN server in **Settings → Guests → ICE servers**. **Settings → Guests → Connection** can also send every guest through the relay.
- **Your own HTTPS proxy:** start streamFree with `--public-url https://studio.example.com`.

## Command line
Expand Down Expand Up @@ -162,17 +167,19 @@ streamFree is young. What has been checked, and how:
| Recording, two simultaneous RTMP destinations, audio/video sync | Automated browser tests (real Chrome, fake camera) on every push |
| Guests: three at once, each hearing everyone but themselves, backstage chat both ways, removing a guest, leaving and rejoining, the 16-guest limit, replacing the invite link | Automated browser tests |
| Guests over the local network (HTTPS) and over the internet (a real Cloudflare tunnel) | Automated browser tests, run locally (CI has no cloudflared) |
| Guests whose network allows no direct connection (simulated: relay-only ICE, no TURN): automatic switch to the relay, video and audio both ways, mix-minus, recording with them on stage, also through a real Cloudflare tunnel | Automated browser tests |
| A guest whose connection drops comes back on stage by itself | Automated browser tests |
| QR code, share buttons, Italian interface, every string translated | Automated browser tests |
| RTMP output and MP4 files | FFmpeg as an independent oracle in the test suite |
| macOS app: native window, 30 fps with the window minimized, audio in sync | Built-in smoke test (`streamfree --smoke-test 8`) |
| macOS app hosting a guest (WebKit host, Chrome guest, program received at 15 fps) | `npm run test:native` |
| macOS app hosting a guest (WebKit host, Chrome guest, program received at 15 fps), plus a relayed guest | `npm run test:native` |
| macOS app: camera, microphone and screen sharing | Manual test |
| YouTube, Twitch, Facebook (RTMPS) and Kick (RTMPS) ingest servers accept streamFree's connection, and a wrong key gets a clear error | **Test** button and `cargo run --example ingest_probe`, with a dummy key |

Not verified yet:

- **Going live with a real stream key.** The protocol checks above pass, but no real broadcast has been made yet. Please report what works for you.
- **Phones and other browsers as guests.** No guest has yet joined from a phone on mobile data, or from Safari or Firefox.
- **Phones and other browsers as guests.** No guest has yet joined from a phone on mobile data, or from Safari or Firefox. The relay is tested with a simulated strict network, not yet with a real one.
- **Windows.** The app and installer build and pass the test suite in CI, but have not been run on a Windows PC yet.
- **Linux hosting.** Chrome on Linux appears to have no AAC encoder, so on Linux streamFree may only work for guests; it shows a clear error if so.

Expand Down
25 changes: 23 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,36 @@ The server answers with `started`, then a `status` message every second
answers with its camera and microphone on the same transceivers.
- The Rust server only relays signaling messages between the host and guests;
guests cannot address each other.
- **Relay fallback.** When a guest's peer connection is not `connected` about
10 s after the offer (or turns `failed`, or stays `disconnected`), the host
closes it and sends `relay-start`. Both pages then exchange media over two
more WebSockets through the server (`/ws/relay/guest`, authenticated with a
per-guest key sent only in that guest's `welcome`, and `/ws/relay/host`),
which work wherever the guest page loads, the Cloudflare tunnel included.
Media is encoded with WebCodecs (`web/js/relay.js`): H.264 (Annex B) or VP8
video, at most 640 px on the long side so portrait phones stay portrait, and
Opus or 16-bit PCM audio, each side choosing what the other can decode. Packets
carry sequence numbers; a gap makes the receiver ask for a keyframe. Audio
plays through a ~100 ms jitter buffer that skips ahead past 250 ms, so delay
does not grow over a long call (`relay-worklet.js`). The server forwards
packets without decoding them and drops them when a receiver's queue is full.
A relayed guest enters the mixer like any other guest, with its own
mix-minus bus, and the host encodes the return feed once for all relayed guests.
- Every signaling and relay socket is pinged every 20 s, because proxies such as
Cloudflare close silent WebSockets. A guest whose socket drops reconnects by
itself for about a minute (unless the host removed them, close code 4001) and,
identified by a per-tab client id, goes back on stage if it was on.
- Cameras require a secure context, so guests need HTTPS unless they are on
`localhost`. The Invite window sets the **sharing mode** (`src/sharing.rs`),
which can change while streamFree runs and is remembered in the settings:
- *This computer*: only `http://localhost`.
- *Local network*: an extra HTTPS listener on port + 1 with a self-signed
certificate made on first use (`src/net.rs`), stopped when the mode changes.
- *Internet*: a Cloudflare quick tunnel (`cloudflared`), started in the
background; the studio is notified when the public address is ready, if it
fails, or if the tunnel stops later. cloudflared is looked up in PATH and in
background. The address is announced only once cloudflared reports a
registered edge connection (before that, guests get an error page). A tunnel
that fails to start or stops later is restarted up to four times, and the
studio is told about the new address. cloudflared is looked up in PATH and in
the usual Homebrew and Windows install folders, because apps started from
Finder or the Start menu do not inherit the shell's PATH.
`--lan`, `--tunnel` and `--public-url` (own reverse proxy) override the mode
Expand Down
36 changes: 33 additions & 3 deletions src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,11 @@ impl Tunnel {
}
}

/// Starts `cloudflared tunnel --url http://127.0.0.1:<port>` and waits for the
/// public `https://*.trycloudflare.com` URL it prints. `on_exit` runs if the
/// tunnel process ends later on its own.
/// Starts `cloudflared tunnel --url http://127.0.0.1:<port>` and waits until
/// the public `https://*.trycloudflare.com` address it prints actually works:
/// cloudflared prints the address before the edge has registered the tunnel,
/// and a guest who opens the link in that window gets an error page.
/// `on_exit` runs if the tunnel process ends later on its own.
pub async fn start_tunnel(port: u16, on_exit: impl FnOnce() + Send + 'static) -> Result<Tunnel> {
let program = find_cloudflared().ok_or_else(|| anyhow!("cloudflared is not installed"))?;
let mut child = Command::new(program)
Expand Down Expand Up @@ -171,6 +173,20 @@ pub async fn start_tunnel(port: u16, on_exit: impl FnOnce() + Send + 'static) ->
Ok(Err(e)) => return Err(e.into()),
Err(_) => bail!("cloudflared did not print an address within 45 s"),
};
// Then wait for the first registered edge connection. Older cloudflared
// versions may word it differently, so a timeout is not an error.
let registered = tokio::time::timeout(Duration::from_secs(30), async {
while let Some(line) = lines.next_line().await? {
if is_registered(&line) {
return Ok::<_, std::io::Error>(true);
}
}
Ok(false)
})
.await;
if let Ok(Ok(false)) = registered {
bail!("cloudflared exited before the internet link was ready");
}
// Keep draining stderr so cloudflared never blocks on a full pipe; when it
// closes, the process has ended.
tokio::spawn(async move {
Expand All @@ -180,6 +196,10 @@ pub async fn start_tunnel(port: u16, on_exit: impl FnOnce() + Send + 'static) ->
Ok(Tunnel { url, child })
}

fn is_registered(line: &str) -> bool {
line.contains("Registered tunnel connection")
}

fn find_tunnel_url(line: &str) -> Option<String> {
let start = line.find("https://")?;
let url: String = line[start..]
Expand All @@ -206,6 +226,16 @@ mod tests {
);
}

#[test]
fn detects_registered_connection() {
assert!(is_registered(
"2026-10-14T10:00:02Z INF Registered tunnel connection connIndex=0 connection=abc event=0 ip=198.41.200.13 location=mxp01 protocol=quic"
));
assert!(!is_registered(
"INF Starting metrics server on 127.0.0.1:20241/metrics"
));
}

#[test]
fn cloudflared_override_is_used() {
let dir = tempfile::tempdir().unwrap();
Expand Down
Loading
Loading