-
Notifications
You must be signed in to change notification settings - Fork 4
Stream recordings #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,12 +6,14 @@ defmodule LiveExWebRTC.Publisher do | |
* renders: | ||
* audio and video device selects | ||
* audio and video stream configs | ||
* stream recording toggle (with recordings enabled) | ||
* stream preview | ||
* transmission stats | ||
* on clicking "Start Streaming", creates WebRTC PeerConnection both on the client and server side | ||
* connects those two peer connections negotiatiing a single audio and video track | ||
* sends audio and video from selected devices to the live view process | ||
* publishes received audio and video packets to the configured PubSub | ||
* can optionally use the [ExWebRTC Recorder](https://github.com/elixir-webrtc/ex_webrtc_recorder) to record the stream | ||
|
||
When `LiveExWebRTC.Player` is used, audio and video packets are delivered automatically, | ||
assuming both components are configured with the same PubSub. | ||
|
@@ -68,28 +70,44 @@ defmodule LiveExWebRTC.Publisher do | |
use Phoenix.LiveView | ||
|
||
alias LiveExWebRTC.Publisher | ||
alias ExWebRTC.{ICECandidate, PeerConnection, SessionDescription} | ||
alias ExWebRTC.{ICECandidate, PeerConnection, Recorder, SessionDescription} | ||
alias Phoenix.PubSub | ||
|
||
@type on_connected() :: (publisher_id :: String.t() -> any()) | ||
@type on_connected :: (publisher_id :: String.t() -> any()) | ||
|
||
@type on_packet() :: | ||
@typedoc """ | ||
Function signature of the `on_disconnected` callback. | ||
|
||
* If `recorder` was passed to `attach/2` and the "Record stream?" checkbox ticked, | ||
the second argument contains the result of calling `ExWebRTC.Recorder.end_tracks/2`. | ||
See `t:ExWebRTC.Recorder.end_tracks_ok_result/0` for more info. | ||
* Otherwise, the second argument is `nil` and can be ignored. | ||
""" | ||
@type on_disconnected :: (publisher_id :: String.t(), | ||
ExWebRTC.Recorder.end_tracks_ok_result() | ||
| nil -> | ||
any()) | ||
|
||
@type on_packet :: | ||
(publisher_id :: String.t(), | ||
packet_type :: :audio | :video, | ||
packet :: ExRTP.Packet.t(), | ||
socket :: Phoenix.LiveView.Socket.t() -> | ||
packet :: ExRTP.Packet.t()) | ||
|
||
@type t() :: struct() | ||
@type t :: struct() | ||
|
||
defstruct id: nil, | ||
pc: nil, | ||
streaming?: false, | ||
record?: false, | ||
audio_track_id: nil, | ||
video_track_id: nil, | ||
on_packet: nil, | ||
on_connected: nil, | ||
on_disconnected: nil, | ||
pubsub: nil, | ||
recorder: nil, | ||
ice_servers: nil, | ||
ice_ip_filter: nil, | ||
ice_port_range: nil, | ||
|
@@ -125,8 +143,11 @@ defmodule LiveExWebRTC.Publisher do | |
Options: | ||
* `id` - publisher id. This is typically your user id (if there is users database). | ||
It is used to identify live view and generated HTML elements. | ||
* `pubsub` - a pubsub that publisher live view will use for broadcasting audio and video packets received from a browser. See module doc for more. | ||
* `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. | ||
* `recorder` - optional `ExWebRTC.Recorder` instance that publisher live view will use for recording the stream. | ||
See module doc and `t:on_disconnected/0` for more info. | ||
* `on_connected` - callback called when the underlying peer connection changes its state to the `:connected`. See `t:on_connected/0`. | ||
* `on_disconnected` - callback called when the underlying peer connection process terminates. See `t:on_disconnected/0`. | ||
* `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`. | ||
* `ice_servers` - a list of `t:ExWebRTC.PeerConnection.Configuration.ice_server/0`, | ||
* `ice_ip_filter` - `t:ExICE.ICEAgent.ip_filter/0`, | ||
|
@@ -142,8 +163,10 @@ defmodule LiveExWebRTC.Publisher do | |
:id, | ||
:name, | ||
:pubsub, | ||
:recorder, | ||
:on_packet, | ||
:on_connected, | ||
:on_disconnected, | ||
:ice_servers, | ||
:ice_ip_filter, | ||
:ice_port_range, | ||
|
@@ -155,8 +178,10 @@ defmodule LiveExWebRTC.Publisher do | |
publisher = %Publisher{ | ||
id: Keyword.fetch!(opts, :id), | ||
pubsub: Keyword.fetch!(opts, :pubsub), | ||
recorder: Keyword.get(opts, :recorder), | ||
on_packet: Keyword.get(opts, :on_packet), | ||
on_connected: Keyword.get(opts, :on_connected), | ||
on_disconnected: Keyword.get(opts, :on_disconnected), | ||
ice_servers: Keyword.get(opts, :ice_servers, [%{urls: "stun:stun.l.google.com:19302"}]), | ||
ice_ip_filter: Keyword.get(opts, :ice_ip_filter), | ||
ice_port_range: Keyword.get(opts, :ice_port_range), | ||
|
@@ -165,8 +190,11 @@ defmodule LiveExWebRTC.Publisher do | |
pc_genserver_opts: Keyword.get(opts, :pc_genserver_opts, []) | ||
} | ||
|
||
# Check the "Record stream?" checkbox by default if recorder was configured | ||
record? = publisher.recorder != nil | ||
|
||
socket | ||
|> assign(publisher: publisher) | ||
|> assign(publisher: %Publisher{publisher | record?: record?}) | ||
|> attach_hook(:handshake, :handle_info, &handshake/2) | ||
end | ||
|
||
|
@@ -255,7 +283,7 @@ defmodule LiveExWebRTC.Publisher do | |
<input | ||
type="text" | ||
id="lex-fps" | ||
value="24" | ||
value="30" | ||
class="rounded-lg disabled:text-gray-400 disabled:border-gray-400 focus:border-brand focus:outline-none focus:ring-0" | ||
/> | ||
</div> | ||
|
@@ -271,6 +299,10 @@ defmodule LiveExWebRTC.Publisher do | |
</div> | ||
<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> | ||
</details> | ||
<div :if={@publisher.recorder} class="flex gap-2.5 items-center"> | ||
<label for="lex-record-stream">Record stream:</label> | ||
<input type="checkbox" phx-click="record-stream-change" id="lex-record-stream" class="rounded-full" checked={@publisher.record?} /> | ||
</div> | ||
<div id="lex-videoplayer-wrapper" class="flex flex-1 flex-col min-h-0 pt-2.5"> | ||
<video id="lex-preview-player" class="m-auto rounded-lg bg-black h-full" autoplay controls muted> | ||
</video> | ||
|
@@ -358,37 +390,43 @@ defmodule LiveExWebRTC.Publisher do | |
def handle_info({:ex_webrtc, _pc, {:rtp, track_id, nil, packet}}, socket) do | ||
%{publisher: publisher} = socket.assigns | ||
|
||
case publisher do | ||
%Publisher{video_track_id: ^track_id} -> | ||
packet = | ||
if publisher.on_packet, | ||
do: publisher.on_packet.(publisher.id, :video, packet, socket), | ||
else: packet | ||
|
||
PubSub.broadcast( | ||
publisher.pubsub, | ||
"streams:video:#{publisher.id}", | ||
{:live_ex_webrtc, :video, packet} | ||
) | ||
kind = | ||
case publisher do | ||
%Publisher{video_track_id: ^track_id} -> :video | ||
%Publisher{audio_track_id: ^track_id} -> :audio | ||
end | ||
|
||
{:noreply, socket} | ||
packet = | ||
if publisher.on_packet, | ||
do: publisher.on_packet.(publisher.id, kind, packet, socket), | ||
else: packet | ||
|
||
%Publisher{audio_track_id: ^track_id} -> | ||
PubSub.broadcast( | ||
publisher.pubsub, | ||
"streams:audio:#{publisher.id}", | ||
{:live_ex_webrtc, :audio, packet} | ||
) | ||
if publisher.record?, do: Recorder.record(publisher.recorder, track_id, nil, packet) | ||
|
||
if publisher.on_packet, do: publisher.on_packet.(publisher.id, :audio, packet, socket) | ||
{:noreply, socket} | ||
end | ||
PubSub.broadcast( | ||
publisher.pubsub, | ||
"streams:#{kind}:#{publisher.id}", | ||
{:live_ex_webrtc, kind, packet} | ||
) | ||
|
||
{:noreply, socket} | ||
end | ||
|
||
@impl true | ||
def handle_info({:ex_webrtc, _pid, {:connection_state_change, :connected}}, socket) do | ||
%{publisher: pub} = socket.assigns | ||
|
||
if pub.record? do | ||
[ | ||
%{kind: :audio, receiver: %{track: audio_track}}, | ||
%{kind: :video, receiver: %{track: video_track}} | ||
] = PeerConnection.get_transceivers(pub.pc) | ||
|
||
Recorder.add_tracks(pub.recorder, [audio_track, video_track]) | ||
end | ||
|
||
if pub.on_connected, do: pub.on_connected.(pub.id) | ||
|
||
{:noreply, socket} | ||
end | ||
|
||
|
@@ -397,6 +435,23 @@ defmodule LiveExWebRTC.Publisher do | |
{:noreply, socket} | ||
end | ||
|
||
@impl true | ||
def handle_info( | ||
{:DOWN, _ref, :process, pc, _reason}, | ||
%{assigns: %{publisher: %{pc: pc} = pub}} = socket | ||
) do | ||
recorder_result = | ||
if pub.record? do | ||
Recorder.end_tracks(pub.recorder, [pub.audio_track_id, pub.video_track_id]) | ||
end | ||
|
||
if pub.on_disconnected, do: pub.on_disconnected.(pub.id, recorder_result) | ||
|
||
{:noreply, | ||
socket | ||
|> assign(publisher: %Publisher{pub | streaming?: false})} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if setting |
||
end | ||
|
||
@impl true | ||
def handle_event("start-streaming", _, socket) do | ||
{:noreply, | ||
|
@@ -413,11 +468,21 @@ defmodule LiveExWebRTC.Publisher do | |
|> push_event("stop-streaming", %{})} | ||
end | ||
|
||
@impl true | ||
def handle_event("record-stream-change", params, socket) do | ||
record? = params["value"] == "on" | ||
|
||
{:noreply, | ||
socket | ||
|> assign(publisher: %Publisher{socket.assigns.publisher | record?: record?})} | ||
end | ||
|
||
@impl true | ||
def handle_event("offer", unsigned_params, socket) do | ||
%{publisher: publisher} = socket.assigns | ||
offer = SessionDescription.from_json(unsigned_params) | ||
{:ok, pc} = spawn_peer_connection(socket) | ||
Process.monitor(pc) | ||
|
||
:ok = PeerConnection.set_remote_description(pc, offer) | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a hack:
Recorder.Converter
assumes we're using 30 FPS and there's currently no logic that would allow changing/overriding this assumptionI think adding it is a must, I'll get to work on that