Skip to content

Commit e3fba41

Browse files
committed
Add option to record streams using ex_webrtc_recorder
1 parent 31e4cc7 commit e3fba41

File tree

5 files changed

+112
-37
lines changed

5 files changed

+112
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Hex.pm](https://img.shields.io/hexpm/v/live_ex_webrtc.svg)](https://hex.pm/packages/live_ex_webrtc)
44
[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/live_ex_webrtc)
55

6-
Phoenix Live Components for Elixir WebRTC.
6+
Phoenix Live Components for [Elixir WebRTC](https://github.com/elixir-webrtc/ex_webrtc).
77

88
## Installation
99

assets/publisher.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export function createPublisherHook(iceServers = []) {
1818
view.fps = document.getElementById("lex-fps");
1919
view.bitrate = document.getElementById("lex-bitrate");
2020

21+
view.recordStream = document.getElementById("lex-record-stream");
22+
2123
view.previewPlayer = document.getElementById("lex-preview-player");
2224

2325
view.audioBitrate = document.getElementById("lex-audio-bitrate");
@@ -93,6 +95,8 @@ export function createPublisherHook(iceServers = []) {
9395
view.audioApplyButton.disabled = true;
9496
view.videoApplyButton.disabled = true;
9597
view.bitrate.disabled = true;
98+
// Button present only when Recorder is used
99+
if (view.recordStream) view.recordStream.disabled = true;
96100
},
97101

98102
enableControls(view) {
@@ -107,6 +111,8 @@ export function createPublisherHook(iceServers = []) {
107111
view.audioApplyButton.disabled = false;
108112
view.videoApplyButton.disabled = false;
109113
view.bitrate.disabled = false;
114+
// See above
115+
if (view.recordStream) view.recordStream.disabled = false;
110116
},
111117

112118
async findDevices(view) {

lib/live_ex_webrtc/publisher.ex

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ defmodule LiveExWebRTC.Publisher do
66
* renders:
77
* audio and video device selects
88
* audio and video stream configs
9+
* stream recording toggle (with recordings enabled)
910
* stream preview
1011
* transmission stats
1112
* on clicking "Start Streaming", creates WebRTC PeerConnection both on the client and server side
1213
* connects those two peer connections negotiatiing a single audio and video track
1314
* sends audio and video from selected devices to the live view process
1415
* publishes received audio and video packets to the configured PubSub
16+
* can optionally use the [ExWebRTC Recorder](https://github.com/elixir-webrtc/ex_webrtc_recorder) to record the stream
1517
1618
When `LiveExWebRTC.Player` is used, audio and video packets are delivered automatically,
1719
assuming both components are configured with the same PubSub.
@@ -68,28 +70,43 @@ defmodule LiveExWebRTC.Publisher do
6870
use Phoenix.LiveView
6971

7072
alias LiveExWebRTC.Publisher
71-
alias ExWebRTC.{ICECandidate, PeerConnection, SessionDescription}
73+
alias ExWebRTC.{ICECandidate, PeerConnection, Recorder, SessionDescription}
7274
alias Phoenix.PubSub
7375

74-
@type on_connected() :: (publisher_id :: String.t() -> any())
76+
@type on_connected :: (publisher_id :: String.t() -> any())
7577

76-
@type on_packet() ::
78+
@typedoc """
79+
Function signature of the `on_disconnected` callback.
80+
81+
* If `recorder` was passed to `attach/2`, the second argument contains the result of calling `ExWebRTC.Recorder.end_tracks/2`.
82+
See `t:ExWebRTC.Recorder.end_tracks_ok_result/0` for more info.
83+
* Otherwise, the second argument is `nil` and can be ignored.
84+
"""
85+
@type on_disconnected :: (publisher_id :: String.t(),
86+
ExWebRTC.Recorder.end_tracks_ok_result()
87+
| nil ->
88+
any())
89+
90+
@type on_packet ::
7791
(publisher_id :: String.t(),
7892
packet_type :: :audio | :video,
7993
packet :: ExRTP.Packet.t(),
8094
socket :: Phoenix.LiveView.Socket.t() ->
8195
packet :: ExRTP.Packet.t())
8296

83-
@type t() :: struct()
97+
@type t :: struct()
8498

8599
defstruct id: nil,
86100
pc: nil,
87101
streaming?: false,
102+
record?: false,
88103
audio_track_id: nil,
89104
video_track_id: nil,
90105
on_packet: nil,
91106
on_connected: nil,
107+
on_disconnected: nil,
92108
pubsub: nil,
109+
recorder: nil,
93110
ice_servers: nil,
94111
ice_ip_filter: nil,
95112
ice_port_range: nil,
@@ -125,8 +142,11 @@ defmodule LiveExWebRTC.Publisher do
125142
Options:
126143
* `id` - publisher id. This is typically your user id (if there is users database).
127144
It is used to identify live view and generated HTML elements.
128-
* `pubsub` - a pubsub that publisher live view will use for broadcasting audio and video packets received from a browser. See module doc for more.
145+
* `pubsub` - a pubsub that publisher live view will use for broadcasting audio and video packets received from a browser. See module doc for more info.
146+
* `recorder` - optional `ExWebRTC.Recorder` instance that publisher live view will use for recording the stream.
147+
See module doc and `t:on_disconnected/0` for more info.
129148
* `on_connected` - callback called when the underlying peer connection changes its state to the `:connected`. See `t:on_connected/0`.
149+
* `on_disconnected` - callback called when the underlying peer connection process terminates. See `t:on_disconnected/0`.
130150
* `on_packet` - callback called for each audio and video RTP packet. Can be used to modify the packet before publishing it on a pubsub. See `t:on_packet/0`.
131151
* `ice_servers` - a list of `t:ExWebRTC.PeerConnection.Configuration.ice_server/0`,
132152
* `ice_ip_filter` - `t:ExICE.ICEAgent.ip_filter/0`,
@@ -142,8 +162,10 @@ defmodule LiveExWebRTC.Publisher do
142162
:id,
143163
:name,
144164
:pubsub,
165+
:recorder,
145166
:on_packet,
146167
:on_connected,
168+
:on_disconnected,
147169
:ice_servers,
148170
:ice_ip_filter,
149171
:ice_port_range,
@@ -155,8 +177,10 @@ defmodule LiveExWebRTC.Publisher do
155177
publisher = %Publisher{
156178
id: Keyword.fetch!(opts, :id),
157179
pubsub: Keyword.fetch!(opts, :pubsub),
180+
recorder: Keyword.get(opts, :recorder),
158181
on_packet: Keyword.get(opts, :on_packet),
159182
on_connected: Keyword.get(opts, :on_connected),
183+
on_disconnected: Keyword.get(opts, :on_disconnected),
160184
ice_servers: Keyword.get(opts, :ice_servers, [%{urls: "stun:stun.l.google.com:19302"}]),
161185
ice_ip_filter: Keyword.get(opts, :ice_ip_filter),
162186
ice_port_range: Keyword.get(opts, :ice_port_range),
@@ -165,8 +189,11 @@ defmodule LiveExWebRTC.Publisher do
165189
pc_genserver_opts: Keyword.get(opts, :pc_genserver_opts, [])
166190
}
167191

192+
# Check the "Record stream?" checkbox by default if recorder was configured
193+
record? = publisher.recorder != nil
194+
168195
socket
169-
|> assign(publisher: publisher)
196+
|> assign(publisher: %Publisher{publisher | record?: record?})
170197
|> attach_hook(:handshake, :handle_info, &handshake/2)
171198
end
172199

@@ -255,7 +282,7 @@ defmodule LiveExWebRTC.Publisher do
255282
<input
256283
type="text"
257284
id="lex-fps"
258-
value="24"
285+
value="30"
259286
class="rounded-lg disabled:text-gray-400 disabled:border-gray-400 focus:border-brand focus:outline-none focus:ring-0"
260287
/>
261288
</div>
@@ -271,6 +298,10 @@ defmodule LiveExWebRTC.Publisher do
271298
</div>
272299
<button id="lex-video-apply-button" class="rounded-lg px-10 py-2.5 bg-brand disabled:bg-brand/50 hover:bg-brand/90 text-white font-bold" disabled>Apply</button>
273300
</details>
301+
<div :if={@publisher.recorder} class="flex gap-2.5 items-center">
302+
<label for="lex-record-stream">Record stream:</label>
303+
<input type="checkbox" phx-click="record-stream-change" id="lex-record-stream" class="rounded-full" checked={@publisher.record?} />
304+
</div>
274305
<div id="lex-videoplayer-wrapper" class="flex flex-1 flex-col min-h-0 pt-2.5">
275306
<video id="lex-preview-player" class="m-auto rounded-lg bg-black h-full" autoplay controls muted>
276307
</video>
@@ -358,37 +389,43 @@ defmodule LiveExWebRTC.Publisher do
358389
def handle_info({:ex_webrtc, _pc, {:rtp, track_id, nil, packet}}, socket) do
359390
%{publisher: publisher} = socket.assigns
360391

361-
case publisher do
362-
%Publisher{video_track_id: ^track_id} ->
363-
packet =
364-
if publisher.on_packet,
365-
do: publisher.on_packet.(publisher.id, :video, packet, socket),
366-
else: packet
367-
368-
PubSub.broadcast(
369-
publisher.pubsub,
370-
"streams:video:#{publisher.id}",
371-
{:live_ex_webrtc, :video, packet}
372-
)
392+
kind =
393+
case publisher do
394+
%Publisher{video_track_id: ^track_id} -> :video
395+
%Publisher{audio_track_id: ^track_id} -> :audio
396+
end
373397

374-
{:noreply, socket}
398+
packet =
399+
if publisher.on_packet,
400+
do: publisher.on_packet.(publisher.id, kind, packet, socket),
401+
else: packet
375402

376-
%Publisher{audio_track_id: ^track_id} ->
377-
PubSub.broadcast(
378-
publisher.pubsub,
379-
"streams:audio:#{publisher.id}",
380-
{:live_ex_webrtc, :audio, packet}
381-
)
403+
if publisher.record?, do: Recorder.record(publisher.recorder, track_id, nil, packet)
382404

383-
if publisher.on_packet, do: publisher.on_packet.(publisher.id, :audio, packet, socket)
384-
{:noreply, socket}
385-
end
405+
PubSub.broadcast(
406+
publisher.pubsub,
407+
"streams:#{kind}:#{publisher.id}",
408+
{:live_ex_webrtc, kind, packet}
409+
)
410+
411+
{:noreply, socket}
386412
end
387413

388414
@impl true
389415
def handle_info({:ex_webrtc, _pid, {:connection_state_change, :connected}}, socket) do
390416
%{publisher: pub} = socket.assigns
417+
418+
if pub.record? do
419+
[
420+
%{kind: :audio, receiver: %{track: audio_track}},
421+
%{kind: :video, receiver: %{track: video_track}}
422+
] = PeerConnection.get_transceivers(pub.pc)
423+
424+
Recorder.add_tracks(pub.recorder, [audio_track, video_track])
425+
end
426+
391427
if pub.on_connected, do: pub.on_connected.(pub.id)
428+
392429
{:noreply, socket}
393430
end
394431

@@ -397,6 +434,23 @@ defmodule LiveExWebRTC.Publisher do
397434
{:noreply, socket}
398435
end
399436

437+
@impl true
438+
def handle_info(
439+
{:DOWN, _ref, :process, pc, _reason},
440+
%{assigns: %{publisher: %{pc: pc} = pub}} = socket
441+
) do
442+
recorder_result =
443+
if pub.record? do
444+
Recorder.end_tracks(pub.recorder, [pub.audio_track_id, pub.video_track_id])
445+
end
446+
447+
if pub.on_disconnected, do: pub.on_disconnected.(pub.id, recorder_result)
448+
449+
{:noreply,
450+
socket
451+
|> assign(publisher: %Publisher{pub | streaming?: false})}
452+
end
453+
400454
@impl true
401455
def handle_event("start-streaming", _, socket) do
402456
{:noreply,
@@ -413,11 +467,21 @@ defmodule LiveExWebRTC.Publisher do
413467
|> push_event("stop-streaming", %{})}
414468
end
415469

470+
@impl true
471+
def handle_event("record-stream-change", params, socket) do
472+
record? = params["value"] == "on"
473+
474+
{:noreply,
475+
socket
476+
|> assign(publisher: %Publisher{socket.assigns.publisher | record?: record?})}
477+
end
478+
416479
@impl true
417480
def handle_event("offer", unsigned_params, socket) do
418481
%{publisher: publisher} = socket.assigns
419482
offer = SessionDescription.from_json(unsigned_params)
420483
{:ok, pc} = spawn_peer_connection(socket)
484+
Process.monitor(pc)
421485

422486
:ok = PeerConnection.set_remote_description(pc, offer)
423487

mix.exs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule LiveExWebrtc.MixProject do
1+
defmodule LiveExWebRTC.MixProject do
22
use Mix.Project
33

44
@version "0.6.0"
@@ -42,7 +42,11 @@ defmodule LiveExWebrtc.MixProject do
4242
[
4343
{:phoenix_live_view, "~> 1.0"},
4444
{:jason, "~> 1.0"},
45-
{:ex_webrtc, "~> 0.8.0"},
45+
# {:ex_webrtc, "~> 0.8.0"},
46+
{:ex_webrtc, github: "elixir-webrtc/ex_webrtc", override: true},
47+
{:ex_webrtc_recorder, github: "elixir-webrtc/ex_webrtc_recorder"},
48+
49+
# Dev deps
4650
{:ex_doc, "~> 0.31", only: :dev, runtime: false}
4751
]
4852
end

0 commit comments

Comments
 (0)