|
| 1 | +# Simulcast |
| 2 | + |
| 3 | +Simulcast is a technique where a client sends multiple encodings of the same video to the server, which is then responsible for dynamically choosing the appropraite encoding for every peer (other client). |
| 4 | +Encodings differ between each other in resolution and/or frame rate. |
| 5 | +The selection of the encoding is based on: |
| 6 | +* Receiver available bandwidth. |
| 7 | +* Receiver preferences (e.g. explicit request to receive video at HD resolution instead of FHD). |
| 8 | +* UI layout (e.g. videos displayed in smaller tiles will be sent at a lower resolution). |
| 9 | + |
| 10 | +Simulcast is not utilized in direct client-client connections (no intermediate server) because in such cases, |
| 11 | +the sender can adjust its resolution or frame rate based on a feedback from the receiver. |
| 12 | + |
| 13 | +Elixir WebRTC comes with: |
| 14 | +* Support for inbound simulcast - it allows to receive multiple incoming resolutions |
| 15 | +* RTP munger and keyframe detectors, which can be used for implementing encoding switching on the server side |
| 16 | + |
| 17 | +Currently there is no support for: |
| 18 | +* Outbound simulcast |
| 19 | +* Bandwidth estimation |
| 20 | +* Automatic encoding switching |
| 21 | + |
| 22 | +```mermaid |
| 23 | +flowchart LR |
| 24 | + WB1((Web Browser1)) -->|low| Server |
| 25 | + WB1((Web Browser1)) -->|medium| Server |
| 26 | + WB1((Web Browser1)) -->|high| Server |
| 27 | + Server -->|low| WB2((WebBrowser 2)) |
| 28 | + Server -->|high| WB3((WebBrowser 3)) |
| 29 | +``` |
| 30 | + |
| 31 | +## Turning simulcast on |
| 32 | + |
| 33 | +### Elixir WebRTC |
| 34 | + |
| 35 | +Elixir WebRTC automatically accepts incoming simulcast tracks so there are no extra steps required. |
| 36 | + |
| 37 | +### JavaScript |
| 38 | + |
| 39 | +Simulcast can be enabled when adding a new track. For example: |
| 40 | + |
| 41 | +```js |
| 42 | +const pc = new RTCPeerConnection(); |
| 43 | + |
| 44 | +const localStream = await navigator.mediaDevices.getUserMedia({ |
| 45 | + video: { |
| 46 | + width: { ideal: 1280 }, |
| 47 | + height: { ideal: 720 }, |
| 48 | + }, |
| 49 | +}); |
| 50 | + |
| 51 | +pc.addTransceiver(localStream.getVideoTracks()[0], { |
| 52 | + streams: [localStream], |
| 53 | + sendEncodings: [ |
| 54 | + { rid: 'h', maxBitrate: 1500 * 1024 }, |
| 55 | + { rid: 'm', scaleResolutionDownBy: 2, maxBitrate: 600 * 1024 }, |
| 56 | + { rid: 'l', scaleResolutionDownBy: 4, maxBitrate: 300 * 1024 }, |
| 57 | + ], |
| 58 | +}); |
| 59 | +``` |
| 60 | + |
| 61 | +> #### Minimal starting resolution {: .warning} |
| 62 | +> To run 3 simulcast encodings, the minimal starting resolution |
| 63 | +> must be 960x540. See more [here](https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/video/config/simulcast.cc;l=79?q=simulcast.cc). |
| 64 | +
|
| 65 | + |
| 66 | +## Receiving simulcast packets |
| 67 | + |
| 68 | +When simulcast is enabled, packets are labeled with an `rid`, which denotes simulcast |
| 69 | +encoding that a packet belongs to: |
| 70 | + |
| 71 | +```elixir |
| 72 | +{:ex_webrtc, input_pc_pid, {:rtp, input_track_id, rid, packet}} |
| 73 | +``` |
| 74 | + |
| 75 | +## Switching between simulcast encodings |
| 76 | + |
| 77 | +Switching between simulcast encodings requires some modifications to RTP packets. |
| 78 | +Every encoding starts with a random RTP sequence number and a random RTP timestamp. |
| 79 | +Because client that receives our stream is never aware of simulcast (they always receive |
| 80 | +a single encoding), we have to rewrite those sequence numbers and timestamps to be continuous and increasing. |
| 81 | +This process is known as munging. |
| 82 | + |
| 83 | +1. Create munger with codec sample rate |
| 84 | + |
| 85 | +```elixir |
| 86 | +alias ExWebRTC.PeerConnection |
| 87 | +alias ExWebRTC.RTP.{H264, Munger} |
| 88 | + |
| 89 | +m = Munger.new(90_000) |
| 90 | +``` |
| 91 | + |
| 92 | +2. When a packet from an encoding that we want to forward arrives, rewrite its sequnce number and timestamp: |
| 93 | + |
| 94 | +```elixir |
| 95 | +receive do |
| 96 | + {:ex_webrtc, input_pc, {:rtp, _input_track_id, "m", packet}} -> |
| 97 | + {packet, munger} = Munger.munge(munger, packet) |
| 98 | + :ok = PeerConnection.send_rtp(output_pc, output_track_id, packet) |
| 99 | + {:ex_webrtc, input_pc, {:rtp, _input_track_id, _rid, packet}} -> |
| 100 | + # ignore other packets |
| 101 | +end |
| 102 | +``` |
| 103 | + |
| 104 | +3. To switch to another encoding, request a keyframe for this encoding. |
| 105 | +Once the keyframe arrives, update the munger and start forwarding new packets. |
| 106 | +For example, transitioning from encoding `m` to `h`: |
| 107 | + |
| 108 | + |
| 109 | +```elixir |
| 110 | +# assume we have the following state |
| 111 | +state = %{ |
| 112 | + current_encoding: "m", |
| 113 | + munger: munger, |
| 114 | + input_pc: input_pc, |
| 115 | + input_track_id: input_track_id, |
| 116 | + output_pc: output_pc, |
| 117 | + output_track_id: output_track_id |
| 118 | +} |
| 119 | + |
| 120 | +:ok = PeerConnection.send_pli(state.input_pc, state.input_track_id, "h") |
| 121 | + |
| 122 | +# ... |
| 123 | + |
| 124 | +receive do |
| 125 | + {:ex_webrtc, input_pc, {:rtp, _input_track_id, rid, packet}} -> |
| 126 | + cond do |
| 127 | + rid == state.current_encoding -> |
| 128 | + {munger, packet} = Munger.munge(munger, packet) |
| 129 | + :ok = PeerConnection.send_rtp(state.output_pc, state.output_track_id, packet) |
| 130 | + %{state | munger: munger} |
| 131 | + rid == "h" and H264.keyframe?(packet) -> |
| 132 | + munger = Munger.update(munger) |
| 133 | + {munger, packet} = Munger.munge(munger, packet) |
| 134 | + :ok = PeerConnection.send_rtp(state.output_pc, state.output_track_id, packet) |
| 135 | + %{state | munger: munger, current_encoding: "h"} |
| 136 | + true -> |
| 137 | + state |
| 138 | + end |
| 139 | +end |
| 140 | +``` |
| 141 | + |
| 142 | +See our [Broadcaster](https://github.com/elixir-webrtc/apps/blob/master/broadcaster/lib/broadcaster/forwarder.ex) app source code for more. |
0 commit comments