|
| 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 | +## Turning simulcast on |
| 23 | + |
| 24 | +### Elixir WebRTC |
| 25 | + |
| 26 | +Elixir WebRTC automatically accepts incoming simulcast tracks so there are no extra steps required. |
| 27 | + |
| 28 | +### JavaScript |
| 29 | + |
| 30 | +Simulcast can be enabled when adding a new track. For example: |
| 31 | + |
| 32 | +```js |
| 33 | +const pc = new RTCPeerConnection(); |
| 34 | + |
| 35 | +const localStream = await navigator.mediaDevices.getUserMedia({ |
| 36 | + video: { |
| 37 | + width: { ideal: 1280 }, |
| 38 | + height: { ideal: 720 }, |
| 39 | + }, |
| 40 | +}); |
| 41 | + |
| 42 | +pc.addTransceiver(localStream.getVideoTracks()[0], { |
| 43 | + streams: [localStream], |
| 44 | + sendEncodings: [ |
| 45 | + { rid: 'h', maxBitrate: 1500 * 1024 }, |
| 46 | + { rid: 'm', scaleResolutionDownBy: 2, maxBitrate: 600 * 1024 }, |
| 47 | + { rid: 'l', scaleResolutionDownBy: 4, maxBitrate: 300 * 1024 }, |
| 48 | + ], |
| 49 | +}); |
| 50 | +``` |
| 51 | + |
| 52 | +> #### Minimal starting resolution {: .warning} |
| 53 | +> To run 3 simulcast encodings, the minimal starting resolution |
| 54 | +> 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) |
| 55 | +
|
| 56 | + |
| 57 | +## Receiving simulcast packets |
| 58 | + |
| 59 | +When simulcast is enabled, packets are labeled with an `rid`, which denotes simulcast |
| 60 | +encoding that a packet belongs to: |
| 61 | + |
| 62 | +```elixir |
| 63 | +{:ex_webrtc, pc_pid, {:rtp, track_id, rid, packet}} |
| 64 | +``` |
| 65 | + |
| 66 | +## Switching between simulcast encodings |
| 67 | + |
| 68 | +Switching between simulcast encodings requires some modifications to RTP packets. |
| 69 | +Every encoding starts with a random RTP sequence number and a random RTP timestamp. |
| 70 | +Because client that receives our stream is never aware of simulcast (they always receive |
| 71 | +a single encoding), we have to rewrite those sequence numbers and timestamps to be continuous and increasing. |
| 72 | +This process is known as munging. |
| 73 | + |
| 74 | +1. Create munger with codec sample rate |
| 75 | + |
| 76 | +```elixir |
| 77 | +alias ExWebRTC.PeerConnection |
| 78 | +alias ExWebRTC.RTP.{H264, Munger} |
| 79 | + |
| 80 | +m = Munger.new(90_000) |
| 81 | +``` |
| 82 | + |
| 83 | +2. When a packet arrives, rewrite its sequnce number and timestamp: |
| 84 | + |
| 85 | +```elixir |
| 86 | +{packet, munger} = Munger.munge(munger, packet) |
| 87 | +``` |
| 88 | + |
| 89 | +3. To switch to another encoding, request a keyframe for this encoding. |
| 90 | +Once the keyframe arrives, update the munger and start forwarding new packets. |
| 91 | +For example, transitioning from encoding `m` to `h`: |
| 92 | + |
| 93 | + |
| 94 | +```elixir |
| 95 | +:ok = PeerConnection.send_pli(input_pc, track_id, "h") |
| 96 | + |
| 97 | +# ... |
| 98 | + |
| 99 | +def handle_info({:ex_webrtc, input_pc, {:rtp, _track_id, "h", packet}}, state) do |
| 100 | + if H264.keyframe?(packet) do |
| 101 | + munger = Munger.update(munger) |
| 102 | + {munger, packet} = Munger.munge(munger, packet) |
| 103 | + PeerConnection.send_rtp(state.output_pc, state.output_track_id, packet) |
| 104 | + state = %{state | munger: munger} |
| 105 | + {:noreply, state} |
| 106 | + else |
| 107 | + # Ignore packets from 'h' until we receive a keyframe. |
| 108 | + {:noreply, state} |
| 109 | + end |
| 110 | +end |
| 111 | +``` |
| 112 | + |
| 113 | +See our [Broadcaster](https://github.com/elixir-webrtc/apps/tree/master/broadcaster) app source code for more. |
0 commit comments