Skip to content

Commit

Permalink
Add support for MediaStreams (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
LVala authored Jun 13, 2024
1 parent fb1c71d commit e6c8dad
Show file tree
Hide file tree
Showing 11 changed files with 79 additions and 26 deletions.
5 changes: 3 additions & 2 deletions examples/echo/lib/echo/peer_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ defmodule Echo.PeerHandler do
audio_codecs: @audio_codecs
)

video_track = MediaStreamTrack.new(:video)
audio_track = MediaStreamTrack.new(:audio)
stream_id = MediaStreamTrack.generate_stream_id()
video_track = MediaStreamTrack.new(:video, [stream_id])
audio_track = MediaStreamTrack.new(:audio, [stream_id])

{:ok, _sender} = PeerConnection.add_track(pc, video_track)
{:ok, _sender} = PeerConnection.add_track(pc, audio_track)
Expand Down
6 changes: 2 additions & 4 deletions examples/echo/priv/static/script.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
const pcConfig = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' },] };
// we set the resolution manually in order to give simulcast enough bitrate to create 3 encodings
const mediaConstraints = {video: {width: {ideal: 1280}, height: {ideal: 720}, frameRate: {ideal: 24}}, audio: true}
const videoPlayer = document.getElementById("videoPlayer");

const proto = window.location.protocol === "https:" ? "wss:" : "ws:"
const ws = new WebSocket(`${proto}//${window.location.host}/ws`);
ws.onopen = _ => start_connection(ws);
ws.onclose = event => console.log("WebSocket connection was terminated:", event);

const start_connection = async (ws) => {
const videoPlayer = document.getElementById("videoPlayer");
videoPlayer.srcObject = new MediaStream();

const pc = new RTCPeerConnection(pcConfig);
pc.ontrack = event => videoPlayer.srcObject.addTrack(event.track);
pc.ontrack = event => videoPlayer.srcObject = event.streams[0];
pc.onicecandidate = event => {
if (event.candidate === null) return;

Expand Down
5 changes: 3 additions & 2 deletions examples/send_from_file/lib/send_from_file/peer_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ defmodule SendFromFile.PeerHandler do
audio_codecs: @audio_codecs
)

video_track = MediaStreamTrack.new(:video)
audio_track = MediaStreamTrack.new(:audio)
stream_id = MediaStreamTrack.generate_stream_id()
video_track = MediaStreamTrack.new(:video, [stream_id])
audio_track = MediaStreamTrack.new(:audio, [stream_id])

{:ok, _sender} = PeerConnection.add_track(pc, video_track)
{:ok, _sender} = PeerConnection.add_track(pc, audio_track)
Expand Down
6 changes: 2 additions & 4 deletions examples/send_from_file/priv/static/script.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const pcConfig = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' },] };
const videoPlayer = document.getElementById("videoPlayer");


const proto = window.location.protocol === "https:" ? "wss:" : "ws:"
Expand All @@ -7,11 +8,8 @@ ws.onopen = _ => start_connection(ws);
ws.onclose = event => console.log("WebSocket connection was terminated:", event);

const start_connection = async (ws) => {
const videoPlayer = document.getElementById("videoPlayer");
videoPlayer.srcObject = new MediaStream();

const pc = new RTCPeerConnection(pcConfig);
pc.ontrack = event => videoPlayer.srcObject.addTrack(event.track);
pc.ontrack = event => videoPlayer.srcObject = event.streams[0];
pc.onicecandidate = event => {
if (event.candidate === null) return;

Expand Down
5 changes: 3 additions & 2 deletions examples/whip_whep/lib/whip_whep/peer_supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ defmodule WhipWhep.PeerSupervisor do

defp setup_transceivers(pc, direction) do
if direction == :sendonly do
{:ok, _sender} = PeerConnection.add_track(pc, MediaStreamTrack.new(:audio))
{:ok, _sender} = PeerConnection.add_track(pc, MediaStreamTrack.new(:video))
stream_id = MediaStreamTrack.generate_stream_id()
{:ok, _sender} = PeerConnection.add_track(pc, MediaStreamTrack.new(:audio, [stream_id]))
{:ok, _sender} = PeerConnection.add_track(pc, MediaStreamTrack.new(:video, [stream_id]))
end

transceivers = PeerConnection.get_transceivers(pc)
Expand Down
17 changes: 13 additions & 4 deletions lib/ex_webrtc/media_stream_track.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,27 @@ defmodule ExWebRTC.MediaStreamTrack do
alias ExWebRTC.Utils

@type id() :: integer()
@type stream_id() :: String.t()
@type kind() :: :audio | :video

@type t() :: %__MODULE__{
kind: kind(),
id: id()
id: id(),
streams: [stream_id()]
}

@enforce_keys [:id, :kind]
defstruct @enforce_keys
defstruct @enforce_keys ++ [streams: []]

@spec new(kind()) :: t()
def new(kind) when kind in [:audio, :video] do
%__MODULE__{kind: kind, id: Utils.generate_id()}
def new(kind, streams \\ []) when kind in [:audio, :video] do
%__MODULE__{kind: kind, id: Utils.generate_id(), streams: streams}
end

@spec generate_stream_id() :: stream_id()
def generate_stream_id() do
20
|> :crypto.strong_rand_bytes()
|> Base.encode32()
end
end
6 changes: 4 additions & 2 deletions lib/ex_webrtc/peer_connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,8 @@ defmodule ExWebRTC.PeerConnection do
offer
|> ExSDP.add_attributes([
%ExSDP.Attribute.Group{semantics: "BUNDLE", mids: mids},
"extmap-allow-mixed"
"extmap-allow-mixed",
{"msid-semantic", "WMS *"}
])
|> ExSDP.add_media(mlines)

Expand Down Expand Up @@ -586,7 +587,8 @@ defmodule ExWebRTC.PeerConnection do
%ExSDP.Attribute.Group{semantics: "BUNDLE", mids: mids},
# always allow for mixing one- and two-byte RTP header extensions
# TODO ensure this was also offered
"extmap-allow-mixed"
"extmap-allow-mixed",
{"msid-semantic", "WMS *"}
])
|> ExSDP.add_media(mlines)

Expand Down
10 changes: 7 additions & 3 deletions lib/ex_webrtc/rtp_receiver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,13 @@ defmodule ExWebRTC.RTPReceiver do
end

@doc false
@spec update(receiver(), RTPCodecParameters.t() | nil, [ExSDP.Attribute.Extmap.t()]) ::
@spec update(receiver(), RTPCodecParameters.t() | nil, [ExSDP.Attribute.Extmap.t()], [
String.t()
]) ::
receiver()
def update(receiver, codec, rtp_hdr_exts) do
def update(receiver, codec, rtp_hdr_exts, stream_ids) do
simulcast_demuxer = SimulcastDemuxer.update(receiver.simulcast_demuxer, rtp_hdr_exts)
track = %MediaStreamTrack{receiver.track | streams: stream_ids}

layers =
Map.new(receiver.layers, fn {rid, layer} ->
Expand All @@ -85,7 +88,8 @@ defmodule ExWebRTC.RTPReceiver do
receiver
| codec: codec,
simulcast_demuxer: simulcast_demuxer,
layers: layers
layers: layers,
track: track
}
end

Expand Down
20 changes: 17 additions & 3 deletions lib/ex_webrtc/rtp_transceiver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ defmodule ExWebRTC.RTPTransceiver do
rtp_hdr_exts = get_rtp_hdr_extensions(mline, config)
{:mid, mid} = ExSDP.get_attribute(mline, :mid)

track = MediaStreamTrack.new(mline.type)
stream_ids = SDPUtils.get_stream_ids(mline)
track = MediaStreamTrack.new(mline.type, stream_ids)
codec = get_codec(codecs)
rtx_codec = get_rtx(codecs, codec)

Expand Down Expand Up @@ -238,8 +239,9 @@ defmodule ExWebRTC.RTPTransceiver do
rtp_hdr_exts = get_rtp_hdr_extensions(mline, config)
codec = get_codec(codecs)
rtx_codec = get_rtx(codecs, codec)
stream_ids = SDPUtils.get_stream_ids(mline)

receiver = RTPReceiver.update(transceiver.receiver, codec, rtp_hdr_exts)
receiver = RTPReceiver.update(transceiver.receiver, codec, rtp_hdr_exts, stream_ids)
sender = RTPSender.update(transceiver.sender, mid, codec, rtx_codec, rtp_hdr_exts)

%{
Expand Down Expand Up @@ -457,6 +459,18 @@ defmodule ExWebRTC.RTPTransceiver do
[rtp_mapping, codec.sdp_fmtp_line, codec.rtcp_fbs]
end)

msids =
case transceiver.sender.track do
nil ->
[]

%MediaStreamTrack{id: id, streams: streams} ->
case Enum.map(streams, &ExSDP.Attribute.MSID.new(&1, id)) do
[] -> [ExSDP.Attribute.MSID.new("-", id)]
other -> other
end
end

attributes =
if(Keyword.get(opts, :rtcp, false), do: [{"rtcp", "9 IN IP4 0.0.0.0"}], else: []) ++
Keyword.get(opts, :simulcast, []) ++
Expand All @@ -469,7 +483,7 @@ defmodule ExWebRTC.RTPTransceiver do
{:fingerprint, Keyword.fetch!(opts, :fingerprint)},
{:setup, Keyword.fetch!(opts, :setup)},
:rtcp_mux
] ++ transceiver.rtp_hdr_exts
] ++ transceiver.rtp_hdr_exts ++ msids

%ExSDP.Media{
ExSDP.Media.new(transceiver.kind, 9, "UDP/TLS/RTP/SAVPF", pt)
Expand Down
7 changes: 7 additions & 0 deletions lib/ex_webrtc/sdp_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ defmodule ExWebRTC.SDPUtils do
|> Enum.reject(&(&1 == nil))
end

@spec get_stream_ids(ExSDP.Media.t()) :: [String.t()]
def get_stream_ids(media) do
ExSDP.get_attributes(media, :msid)
|> Enum.reject(fn msid -> msid.id == "-" end)
|> Enum.map(fn msid -> msid.id end)
end

@spec get_ice_credentials(ExSDP.t()) ::
{:ok, {binary(), binary()}}
| {:error,
Expand Down
18 changes: 18 additions & 0 deletions test/ex_webrtc/peer_connection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,24 @@ defmodule ExWebRTC.PeerConnectionTest do
assert_receive {:ex_webrtc, ^pc2, {:track, %MediaStreamTrack{kind: :video}}}
refute_receive {:ex_webrtc, ^pc2, {:track, %MediaStreamTrack{}}}
end

test "with media stream" do
{:ok, pc1} = PeerConnection.start_link()
{:ok, pc2} = PeerConnection.start_link()

# only track2 is assigned a stream
stream_id = MediaStreamTrack.generate_stream_id()
track1 = MediaStreamTrack.new(:audio)
track2 = MediaStreamTrack.new(:audio, [stream_id])

{:ok, _sender} = PeerConnection.add_track(pc1, track1)
{:ok, _sender} = PeerConnection.add_track(pc1, track2)

negotiate(pc1, pc2)

assert_receive {:ex_webrtc, ^pc2, {:track, %MediaStreamTrack{streams: []}}}
assert_receive {:ex_webrtc, ^pc2, {:track, %MediaStreamTrack{streams: [^stream_id]}}}
end
end

describe "replace_track/3" do
Expand Down

0 comments on commit e6c8dad

Please sign in to comment.