Skip to content

Commit 4a7246d

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

File tree

5 files changed

+113
-37
lines changed

5 files changed

+113
-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: 94 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,44 @@ 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` and the "Record stream?" checkbox ticked,
82+
the second argument contains the result of calling `ExWebRTC.Recorder.end_tracks/2`.
83+
See `t:ExWebRTC.Recorder.end_tracks_ok_result/0` for more info.
84+
* Otherwise, the second argument is `nil` and can be ignored.
85+
"""
86+
@type on_disconnected :: (publisher_id :: String.t(),
87+
ExWebRTC.Recorder.end_tracks_ok_result()
88+
| nil ->
89+
any())
90+
91+
@type on_packet ::
7792
(publisher_id :: String.t(),
7893
packet_type :: :audio | :video,
7994
packet :: ExRTP.Packet.t(),
8095
socket :: Phoenix.LiveView.Socket.t() ->
8196
packet :: ExRTP.Packet.t())
8297

83-
@type t() :: struct()
98+
@type t :: struct()
8499

85100
defstruct id: nil,
86101
pc: nil,
87102
streaming?: false,
103+
record?: false,
88104
audio_track_id: nil,
89105
video_track_id: nil,
90106
on_packet: nil,
91107
on_connected: nil,
108+
on_disconnected: nil,
92109
pubsub: nil,
110+
recorder: nil,
93111
ice_servers: nil,
94112
ice_ip_filter: nil,
95113
ice_port_range: nil,
@@ -125,8 +143,11 @@ defmodule LiveExWebRTC.Publisher do
125143
Options:
126144
* `id` - publisher id. This is typically your user id (if there is users database).
127145
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.
146+
* `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.
147+
* `recorder` - optional `ExWebRTC.Recorder` instance that publisher live view will use for recording the stream.
148+
See module doc and `t:on_disconnected/0` for more info.
129149
* `on_connected` - callback called when the underlying peer connection changes its state to the `:connected`. See `t:on_connected/0`.
150+
* `on_disconnected` - callback called when the underlying peer connection process terminates. See `t:on_disconnected/0`.
130151
* `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`.
131152
* `ice_servers` - a list of `t:ExWebRTC.PeerConnection.Configuration.ice_server/0`,
132153
* `ice_ip_filter` - `t:ExICE.ICEAgent.ip_filter/0`,
@@ -142,8 +163,10 @@ defmodule LiveExWebRTC.Publisher do
142163
:id,
143164
:name,
144165
:pubsub,
166+
:recorder,
145167
:on_packet,
146168
:on_connected,
169+
:on_disconnected,
147170
:ice_servers,
148171
:ice_ip_filter,
149172
:ice_port_range,
@@ -155,8 +178,10 @@ defmodule LiveExWebRTC.Publisher do
155178
publisher = %Publisher{
156179
id: Keyword.fetch!(opts, :id),
157180
pubsub: Keyword.fetch!(opts, :pubsub),
181+
recorder: Keyword.get(opts, :recorder),
158182
on_packet: Keyword.get(opts, :on_packet),
159183
on_connected: Keyword.get(opts, :on_connected),
184+
on_disconnected: Keyword.get(opts, :on_disconnected),
160185
ice_servers: Keyword.get(opts, :ice_servers, [%{urls: "stun:stun.l.google.com:19302"}]),
161186
ice_ip_filter: Keyword.get(opts, :ice_ip_filter),
162187
ice_port_range: Keyword.get(opts, :ice_port_range),
@@ -165,8 +190,11 @@ defmodule LiveExWebRTC.Publisher do
165190
pc_genserver_opts: Keyword.get(opts, :pc_genserver_opts, [])
166191
}
167192

193+
# Check the "Record stream?" checkbox by default if recorder was configured
194+
record? = publisher.recorder != nil
195+
168196
socket
169-
|> assign(publisher: publisher)
197+
|> assign(publisher: %Publisher{publisher | record?: record?})
170198
|> attach_hook(:handshake, :handle_info, &handshake/2)
171199
end
172200

@@ -255,7 +283,7 @@ defmodule LiveExWebRTC.Publisher do
255283
<input
256284
type="text"
257285
id="lex-fps"
258-
value="24"
286+
value="30"
259287
class="rounded-lg disabled:text-gray-400 disabled:border-gray-400 focus:border-brand focus:outline-none focus:ring-0"
260288
/>
261289
</div>
@@ -271,6 +299,10 @@ defmodule LiveExWebRTC.Publisher do
271299
</div>
272300
<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>
273301
</details>
302+
<div :if={@publisher.recorder} class="flex gap-2.5 items-center">
303+
<label for="lex-record-stream">Record stream:</label>
304+
<input type="checkbox" phx-click="record-stream-change" id="lex-record-stream" class="rounded-full" checked={@publisher.record?} />
305+
</div>
274306
<div id="lex-videoplayer-wrapper" class="flex flex-1 flex-col min-h-0 pt-2.5">
275307
<video id="lex-preview-player" class="m-auto rounded-lg bg-black h-full" autoplay controls muted>
276308
</video>
@@ -358,37 +390,43 @@ defmodule LiveExWebRTC.Publisher do
358390
def handle_info({:ex_webrtc, _pc, {:rtp, track_id, nil, packet}}, socket) do
359391
%{publisher: publisher} = socket.assigns
360392

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-
)
393+
kind =
394+
case publisher do
395+
%Publisher{video_track_id: ^track_id} -> :video
396+
%Publisher{audio_track_id: ^track_id} -> :audio
397+
end
373398

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

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-
)
404+
if publisher.record?, do: Recorder.record(publisher.recorder, track_id, nil, packet)
382405

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

388415
@impl true
389416
def handle_info({:ex_webrtc, _pid, {:connection_state_change, :connected}}, socket) do
390417
%{publisher: pub} = socket.assigns
418+
419+
if pub.record? do
420+
[
421+
%{kind: :audio, receiver: %{track: audio_track}},
422+
%{kind: :video, receiver: %{track: video_track}}
423+
] = PeerConnection.get_transceivers(pub.pc)
424+
425+
Recorder.add_tracks(pub.recorder, [audio_track, video_track])
426+
end
427+
391428
if pub.on_connected, do: pub.on_connected.(pub.id)
429+
392430
{:noreply, socket}
393431
end
394432

@@ -397,6 +435,23 @@ defmodule LiveExWebRTC.Publisher do
397435
{:noreply, socket}
398436
end
399437

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

471+
@impl true
472+
def handle_event("record-stream-change", params, socket) do
473+
record? = params["value"] == "on"
474+
475+
{:noreply,
476+
socket
477+
|> assign(publisher: %Publisher{socket.assigns.publisher | record?: record?})}
478+
end
479+
416480
@impl true
417481
def handle_event("offer", unsigned_params, socket) do
418482
%{publisher: publisher} = socket.assigns
419483
offer = SessionDescription.from_json(unsigned_params)
420484
{:ok, pc} = spawn_peer_connection(socket)
485+
Process.monitor(pc)
421486

422487
:ok = PeerConnection.set_remote_description(pc, offer)
423488

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)