Skip to content

Commit a81d19e

Browse files
committed
Add support for simulcast
1 parent 31e4cc7 commit a81d19e

File tree

4 files changed

+241
-117
lines changed

4 files changed

+241
-117
lines changed

assets/player.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export function createPlayerHook(iceServers = []) {
22
return {
33
async mounted() {
4+
this.videoQuality = document.getElementById("lexp-video-quality");
5+
this.videoQuality.onchange = () => {
6+
this.pushEventTo(this.el, "layer", this.videoQuality.value);
7+
};
8+
49
this.pc = new RTCPeerConnection({ iceServers: iceServers });
510

611
this.pc.onicecandidate = (ev) => {

assets/publisher.js

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export function createPublisherHook(iceServers = []) {
3030
view.videoApplyButton = document.getElementById("lex-video-apply-button");
3131
view.button = document.getElementById("lex-button");
3232

33+
view.simulcast = document.getElementById("lex-simulcast");
34+
3335
view.audioDevices.onchange = function () {
3436
view.setupStream(view);
3537
};
@@ -93,6 +95,7 @@ export function createPublisherHook(iceServers = []) {
9395
view.audioApplyButton.disabled = true;
9496
view.videoApplyButton.disabled = true;
9597
view.bitrate.disabled = true;
98+
view.simulcast.disabled = true;
9699
},
97100

98101
enableControls(view) {
@@ -107,6 +110,7 @@ export function createPublisherHook(iceServers = []) {
107110
view.audioApplyButton.disabled = false;
108111
view.videoApplyButton.disabled = false;
109112
view.bitrate.disabled = false;
113+
view.simulcast.disabled = false;
110114
},
111115

112116
async findDevices(view) {
@@ -269,7 +273,7 @@ export function createPublisherHook(iceServers = []) {
269273
}
270274
}, 1000);
271275
} else if (view.pc.connectionState === "failed") {
272-
view.pushEvent("stop-streaming", {reason: "failed"})
276+
view.pushEvent("stop-streaming", { reason: "failed" });
273277
view.stopStreaming(view);
274278
}
275279
};
@@ -279,6 +283,41 @@ export function createPublisherHook(iceServers = []) {
279283
};
280284

281285
view.pc.addTrack(view.localStream.getAudioTracks()[0], view.localStream);
286+
287+
if (view.simulcast.checked === true) {
288+
// view.addNormalVideo(view);
289+
view.addSimulcastVideo(view);
290+
} else {
291+
view.addNormalVideo(view);
292+
}
293+
294+
const offer = await view.pc.createOffer();
295+
await view.pc.setLocalDescription(offer);
296+
297+
view.pushEventTo(view.el, "offer", offer);
298+
},
299+
300+
addSimulcastVideo(view) {
301+
const maxTotalBitrate = view.bitrate.value * 1024;
302+
// we do a very simple calculation: maxTotalBitrate = x + 1/4x + 1/16x
303+
// x - bitrate for base resolution
304+
// 1/4x- bitrate for resolution scaled down by 2 - we decrese total number of pixels by 4 (width/2*height/2)
305+
// 1/16x- bitrate for resolution scaled down by 4 - we decrese total number of pixels by 16 (width/4*height/4)
306+
const maxHBitrate = Math.floor((16 * maxTotalBitrate) / 21);
307+
const maxMBitrate = Math.floor(maxHBitrate / 4);
308+
const maxLBitrate = Math.floor(maxHBitrate / 16);
309+
310+
view.pc.addTransceiver(view.localStream.getVideoTracks()[0], {
311+
streams: [view.localStream],
312+
sendEncodings: [
313+
{ rid: "h", maxBitrate: maxHBitrate },
314+
{ rid: "m", scaleResolutionDownBy: 2, maxBitrate: maxMBitrate },
315+
{ rid: "l", scaleResolutionDownBy: 4, maxBitrate: maxLBitrate },
316+
],
317+
});
318+
},
319+
320+
addNormalVideo(view) {
282321
view.pc.addTrack(view.localStream.getVideoTracks()[0], view.localStream);
283322

284323
// set max bitrate
@@ -290,11 +329,6 @@ export function createPublisherHook(iceServers = []) {
290329
params.encodings[0].maxBitrate = view.bitrate.value * 1024;
291330
await sender.setParameters(params);
292331
});
293-
294-
const offer = await view.pc.createOffer();
295-
await view.pc.setLocalDescription(offer);
296-
297-
view.pushEventTo(view.el, "offer", offer);
298332
},
299333

300334
stopStreaming(view) {

lib/live_ex_webrtc/player.ex

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ defmodule LiveExWebRTC.Player do
5959
'''
6060
use Phoenix.LiveView
6161

62+
require Logger
6263
alias LiveExWebRTC.Player
6364

6465
@type on_connected() :: (publisher_id :: String.t() -> any())
@@ -87,7 +88,7 @@ defmodule LiveExWebRTC.Player do
8788
video_codecs: nil,
8889
pc_genserver_opts: nil
8990

90-
alias ExWebRTC.{ICECandidate, MediaStreamTrack, PeerConnection, SessionDescription}
91+
alias ExWebRTC.{ICECandidate, MediaStreamTrack, PeerConnection, RTP.Munger, SessionDescription}
9192
alias ExRTCP.Packet.PayloadFeedback.PLI
9293
alias Phoenix.PubSub
9394

@@ -193,7 +194,14 @@ defmodule LiveExWebRTC.Player do
193194
@impl true
194195
def render(assigns) do
195196
~H"""
197+
<div class="">
196198
<video id={@player.id} phx-hook="Player" class={@class} controls autoplay muted></video>
199+
<select id="lexp-video-quality">
200+
<option value="h" selected>High</option>
201+
<option value="m">Medium</option>
202+
<option value="l">Low</option>
203+
</select>
204+
</div>
197205
"""
198206
end
199207

@@ -208,7 +216,12 @@ defmodule LiveExWebRTC.Player do
208216
socket =
209217
receive do
210218
{^ref, %Player{publisher_id: ^pub_id} = player} ->
211-
assign(socket, player: player)
219+
assign(socket,
220+
player: player,
221+
munger: Munger.new(90_000),
222+
layer: "h",
223+
target_layer: "h"
224+
)
212225
after
213226
5000 -> exit(:timeout)
214227
end
@@ -221,9 +234,9 @@ defmodule LiveExWebRTC.Player do
221234

222235
@impl true
223236
def handle_info({:ex_webrtc, _pid, {:connection_state_change, :connected}}, socket) do
224-
%{player: player} = socket.assigns
237+
%{player: player, layer: layer} = socket.assigns
225238
PubSub.subscribe(player.pubsub, "streams:audio:#{player.publisher_id}")
226-
PubSub.subscribe(player.pubsub, "streams:video:#{player.publisher_id}")
239+
PubSub.subscribe(player.pubsub, "streams:video:#{player.publisher_id}:#{layer}")
227240
broadcast_keyframe_req(socket)
228241
if player.on_connected, do: player.on_connected.(player.publisher_id)
229242

@@ -256,16 +269,52 @@ defmodule LiveExWebRTC.Player do
256269
{:noreply, socket}
257270
end
258271

259-
def handle_info({:live_ex_webrtc, :video, packet}, socket) do
272+
def handle_info({:live_ex_webrtc, :video, rid, packet}, socket) do
260273
%{player: player} = socket.assigns
261274

262275
packet =
263276
if player.on_packet,
264277
do: player.on_packet.(player.publisher_id, :video, packet),
265278
else: packet
266279

267-
PeerConnection.send_rtp(player.pc, player.video_track_id, packet)
268-
{:noreply, socket}
280+
cond do
281+
rid == socket.assigns.layer ->
282+
{packet, munger} = Munger.munge(socket.assigns.munger, packet)
283+
socket = assign(socket, munger: munger)
284+
:ok = PeerConnection.send_rtp(player.pc, player.video_track_id, packet)
285+
{:noreply, socket}
286+
287+
rid == socket.assigns.target_layer ->
288+
if ExWebRTC.RTP.H264.keyframe?(packet) == true do
289+
munger = Munger.update(socket.assigns.munger)
290+
{packet, munger} = Munger.munge(munger, packet)
291+
292+
PeerConnection.send_rtp(player.pc, player.video_track_id, packet)
293+
294+
PubSub.unsubscribe(
295+
socket.assigns.player.pubsub,
296+
"streams:video:#{player.publisher_id}:#{socket.assigns.layer}"
297+
)
298+
299+
flush_layer(socket.assigns.layer)
300+
301+
socket = assign(socket, munger: munger, layer: rid)
302+
{:noreply, socket}
303+
else
304+
{:noreply, socket}
305+
end
306+
307+
true ->
308+
# this should never happen
309+
Logger.warning("unexpected unsubscribe")
310+
311+
PubSub.unsubscribe(
312+
socket.assigns.player.pubsub,
313+
"streams:video:#{player.publisher_id}:#{rid}"
314+
)
315+
316+
{:noreply, socket}
317+
end
269318
end
270319

271320
@impl true
@@ -334,6 +383,22 @@ defmodule LiveExWebRTC.Player do
334383
end
335384
end
336385

386+
@impl true
387+
def handle_event("layer", layer, socket) when layer in ["l", "m", "h"] do
388+
if socket.assigns.layer == layer do
389+
{:noreply, socket}
390+
else
391+
PubSub.subscribe(
392+
socket.assigns.player.pubsub,
393+
"streams:video:#{socket.assigns.player.publisher_id}:#{layer}"
394+
)
395+
396+
socket = assign(socket, target_layer: layer)
397+
broadcast_keyframe_req(socket)
398+
{:noreply, socket}
399+
end
400+
end
401+
337402
defp spawn_peer_connection(socket) do
338403
%{player: player} = socket.assigns
339404

@@ -363,10 +428,20 @@ defmodule LiveExWebRTC.Player do
363428
defp broadcast_keyframe_req(socket) do
364429
%{player: player} = socket.assigns
365430

431+
layer = socket.assigns.target_layer || socket.assigns.layer
432+
366433
PubSub.broadcast(
367434
player.pubsub,
368435
"publishers:#{player.publisher_id}",
369-
{:live_ex_webrtc, :keyframe_req}
436+
{:live_ex_webrtc, :keyframe_req, layer}
370437
)
371438
end
439+
440+
defp flush_layer(layer) do
441+
receive do
442+
{:live_ex_webrtc, :video, ^layer, _packet} -> flush_layer(layer)
443+
after
444+
0 -> :ok
445+
end
446+
end
372447
end

0 commit comments

Comments
 (0)