Skip to content
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

Improve the structs in the API, add docs #104

Merged
merged 5 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
461 changes: 336 additions & 125 deletions lib/ex_webrtc/peer_connection.ex

Large diffs are not rendered by default.

72 changes: 50 additions & 22 deletions lib/ex_webrtc/rtp_receiver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ defmodule ExWebRTC.RTPReceiver do
alias ExWebRTC.{MediaStreamTrack, Utils, RTPCodecParameters}
alias __MODULE__.{NACKGenerator, ReportRecorder}

@type t() :: %__MODULE__{
@type id() :: integer()

@typedoc false
@type receiver() :: %{
id: id(),
track: MediaStreamTrack.t(),
codec: RTPCodecParameters.t() | nil,
ssrc: non_neg_integer() | nil,
Expand All @@ -19,42 +23,64 @@ defmodule ExWebRTC.RTPReceiver do
nack_generator: NACKGenerator.t()
}

@enforce_keys [:track, :codec, :report_recorder]
defstruct [
ssrc: nil,
bytes_received: 0,
packets_received: 0,
markers_received: 0,
nack_generator: %NACKGenerator{}
] ++ @enforce_keys
@typedoc """
Struct representing a receiver.

The fields mostly match these of [RTCRtpReceiver](https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpReceiver),
except for:
* `id` - to uniquely identify the receiver.
* `codec` - codec this receiver is expected to receive.
"""
@type t() :: %__MODULE__{
id: id(),
track: MediaStreamTrack.t(),
codec: RTPCodecParameters.t() | nil
}

@enforce_keys [:id, :track, :codec]
defstruct @enforce_keys

@doc false
@spec to_struct(receiver()) :: t()
def to_struct(receiver) do
receiver
|> Map.take([:id, :track, :codec])
|> then(&struct!(__MODULE__, &1))
end

@doc false
@spec new(MediaStreamTrack.t(), RTPCodecParameters.t() | nil) :: t()
@spec new(MediaStreamTrack.t(), RTPCodecParameters.t() | nil) :: receiver()
def new(track, codec) do
report_recorder = %ReportRecorder{
clock_rate: codec && codec.clock_rate
}

%__MODULE__{
%{
id: Utils.generate_id(),
track: track,
codec: codec,
report_recorder: report_recorder
ssrc: nil,
bytes_received: 0,
packets_received: 0,
markers_received: 0,
report_recorder: report_recorder,
nack_generator: %NACKGenerator{}
}
end

@doc false
@spec update(t(), RTPCodecParameters.t() | nil) :: t()
@spec update(receiver(), RTPCodecParameters.t() | nil) :: receiver()
def update(receiver, codec) do
report_recorder = %ReportRecorder{
receiver.report_recorder
| clock_rate: codec && codec.clock_rate
}

%__MODULE__{receiver | codec: codec, report_recorder: report_recorder}
%{receiver | codec: codec, report_recorder: report_recorder}
end

@doc false
@spec receive_packet(t(), ExRTP.Packet.t(), non_neg_integer()) :: t()
@spec receive_packet(receiver(), ExRTP.Packet.t(), non_neg_integer()) :: receiver()
def receive_packet(receiver, packet, size) do
if packet.payload_type != receiver.codec.payload_type do
Logger.warning("Received packet with unexpected payload_type \
Expand All @@ -65,7 +91,7 @@ defmodule ExWebRTC.RTPReceiver do
nack_generator = NACKGenerator.record_packet(receiver.nack_generator, packet)

# TODO assign ssrc when applying local/remote description.
%__MODULE__{
%{
receiver
| ssrc: packet.ssrc,
bytes_received: receiver.bytes_received + size,
Expand All @@ -76,7 +102,8 @@ defmodule ExWebRTC.RTPReceiver do
}
end

@spec receive_rtx(t(), ExRTP.Packet.t(), non_neg_integer()) :: {:ok, ExRTP.Packet.t()} | :error
@spec receive_rtx(receiver(), ExRTP.Packet.t(), non_neg_integer()) ::
{:ok, ExRTP.Packet.t()} | :error
def receive_rtx(receiver, rtx_packet, apt) do
with <<seq_no::16, rest::binary>> <- rtx_packet.payload,
ssrc when ssrc != nil <- receiver.ssrc do
Expand All @@ -94,23 +121,24 @@ defmodule ExWebRTC.RTPReceiver do
end
end

@spec receive_report(t(), ExRTCP.Packet.SenderReport.t()) :: t()
@spec receive_report(receiver(), ExRTCP.Packet.SenderReport.t()) :: receiver()
def receive_report(receiver, report) do
report_recorder = ReportRecorder.record_report(receiver.report_recorder, report)

%__MODULE__{receiver | report_recorder: report_recorder}
%{receiver | report_recorder: report_recorder}
end

@doc false
@spec update_sender_ssrc(t(), non_neg_integer()) :: t()
@spec update_sender_ssrc(receiver(), non_neg_integer()) :: receiver()
def update_sender_ssrc(receiver, ssrc) do
report_recorder = %ReportRecorder{receiver.report_recorder | sender_ssrc: ssrc}
nack_generator = %NACKGenerator{receiver.nack_generator | sender_ssrc: ssrc}
%__MODULE__{receiver | report_recorder: report_recorder, nack_generator: nack_generator}

%{receiver | report_recorder: report_recorder, nack_generator: nack_generator}
end

@doc false
@spec get_stats(t(), non_neg_integer()) :: map()
@spec get_stats(receiver(), non_neg_integer()) :: map()
def get_stats(receiver, timestamp) do
%{
id: receiver.track.id,
Expand Down
5 changes: 1 addition & 4 deletions lib/ex_webrtc/rtp_receiver/nack_generator.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule ExWebRTC.RTPReceiver.NACKGenerator do
@moduledoc nil
@moduledoc false
# for now, it mimics the Pion implementation, but there's some issues and remarks
# 1) NACKs are send at constant interval
# 2) no timing rules (like rtt) are taken into account
Expand All @@ -25,9 +25,6 @@ defmodule ExWebRTC.RTPReceiver.NACKGenerator do
last_sn: nil,
max_nack: @max_nack

@doc """
Records incoming RTP Packet.
"""
@spec record_packet(t(), ExRTP.Packet.t()) :: t()
def record_packet(generator, packet)

Expand Down
17 changes: 4 additions & 13 deletions lib/ex_webrtc/rtp_receiver/report_recorder.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule ExWebRTC.RTPReceiver.ReportRecorder do
@moduledoc nil
@moduledoc false
# based on https://datatracker.ietf.org/doc/html/rfc3550#section-6.4.1

import Bitwise
Expand Down Expand Up @@ -39,10 +39,7 @@ defmodule ExWebRTC.RTPReceiver.ReportRecorder do
jitter: 0.0,
total_lost: 0

@doc """
Records incoming RTP Packet.
`time` parameter accepts output of `System.monotonic_time()` as a value.
"""
# `time` parameter accepts output of `System.monotonic_time()` as a value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the previous docs could stay

Copy link
Member Author

@LVala LVala May 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say to leave as a comment and in general use comments for developer information and docs for public stuff? The only useful thing I could think off was that the LSP would provide the doc info even when function is private, but it doesn't seem to be the case.

EDIT: nvm, LSP does provide this info (which is actually useful), so I'll leave it as a doc.

@spec record_packet(t(), ExRTP.Packet.t(), integer()) :: t()
def record_packet(recorder, packet, time \\ System.monotonic_time())

Expand All @@ -66,10 +63,7 @@ defmodule ExWebRTC.RTPReceiver.ReportRecorder do
|> record_jitter(packet.timestamp, time)
end

@doc """
Records incoming RTCP Sender Report.
`time` parameter accepts output of `System.monotonic_time()` as a value.
"""
# `time` parameter accepts output of `System.monotonic_time()` as a value.
@spec record_report(t(), ExRTCP.Packet.SenderReport.t(), integer()) :: t()
def record_report(recorder, sender_report, time \\ System.monotonic_time()) do
# we take the middle 32 bits of the NTP timestamp
Expand All @@ -78,10 +72,7 @@ defmodule ExWebRTC.RTPReceiver.ReportRecorder do
%__MODULE__{recorder | last_sr_ntp_timestamp: ntp_ts, last_sr_timestamp: time}
end

@doc """
Creates an RTCP Receiver Report.
`time` parameter accepts output of `System.monotonic_time()` as a value.
"""
# `time` parameter accepts output of `System.monotonic_time()` as a value.
@spec get_report(t(), integer()) :: {:ok, ReceiverReport.t(), t()} | {:error, term()}
def get_report(recorder, time \\ System.monotonic_time())

Expand Down
65 changes: 38 additions & 27 deletions lib/ex_webrtc/rtp_sender.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ defmodule ExWebRTC.RTPSender do

@type id() :: integer()

@type t() :: %__MODULE__{
@typedoc false
@type sender() :: %{
id: id(),
track: MediaStreamTrack.t() | nil,
codec: RTPCodecParameters.t() | nil,
Expand All @@ -28,21 +29,30 @@ defmodule ExWebRTC.RTPSender do
nack_responder: NACKResponder.t()
}

@enforce_keys [:id, :report_recorder, :nack_responder]
defstruct @enforce_keys ++
[
:track,
:codec,
:mid,
:pt,
:rtx_pt,
:ssrc,
:rtx_ssrc,
rtp_hdr_exts: %{},
packets_sent: 0,
bytes_sent: 0,
markers_sent: 0
]
@typedoc """
Struct representing a sender.

The fields mostly match these of [RTCRtpSender](https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender),
except for:
* `id` - to uniquely identify the sender.
* `codec` - codec this sender is going to send.
"""
@type t() :: %__MODULE__{
id: id(),
track: MediaStreamTrack.t() | nil,
codec: RTPCodecParameters.t() | nil
}

@enforce_keys [:id, :track, :codec]
defstruct @enforce_keys

@doc false
@spec to_struct(sender()) :: t()
def to_struct(sender) do
sender
|> Map.take([:id, :track, :codec])
|> then(&struct!(__MODULE__, &1))
end

@doc false
@spec new(
Expand All @@ -53,15 +63,15 @@ defmodule ExWebRTC.RTPSender do
String.t() | nil,
non_neg_integer() | nil,
non_neg_integer() | nil
) :: t()
) :: sender()
def new(track, codec, rtx_codec, rtp_hdr_exts, mid \\ nil, ssrc, rtx_ssrc) do
# convert to a map to be able to find extension id using extension uri
rtp_hdr_exts = Map.new(rtp_hdr_exts, fn extmap -> {extmap.uri, extmap} end)
# TODO: handle cases when codec == nil (no valid codecs after negotiation)
pt = if codec != nil, do: codec.payload_type, else: nil
rtx_pt = if rtx_codec != nil, do: rtx_codec.payload_type, else: nil

%__MODULE__{
%{
id: Utils.generate_id(),
track: track,
codec: codec,
Expand All @@ -71,15 +81,18 @@ defmodule ExWebRTC.RTPSender do
ssrc: ssrc,
rtx_ssrc: rtx_ssrc,
mid: mid,
packets_sent: 0,
bytes_sent: 0,
markers_sent: 0,
report_recorder: %ReportRecorder{clock_rate: codec && codec.clock_rate},
nack_responder: %NACKResponder{}
}
end

@doc false
@spec update(t(), String.t(), RTPCodecParameters.t() | nil, RTPCodecParameters.t() | nil, [
@spec update(sender(), String.t(), RTPCodecParameters.t() | nil, RTPCodecParameters.t() | nil, [
Extmap.t()
]) :: t()
]) :: sender()
def update(sender, mid, codec, rtx_codec, rtp_hdr_exts) do
if sender.mid != nil and mid != sender.mid, do: raise(ArgumentError)
# convert to a map to be able to find extension id using extension uri
Expand All @@ -93,7 +106,7 @@ defmodule ExWebRTC.RTPSender do
| clock_rate: codec && codec.clock_rate
}

%__MODULE__{
%{
sender
| mid: mid,
codec: codec,
Expand All @@ -104,11 +117,8 @@ defmodule ExWebRTC.RTPSender do
}
end

# Prepares packet for sending i.e.:
# * assigns SSRC, pt, mid
# * serializes to binary
@doc false
@spec send_packet(t(), ExRTP.Packet.t(), boolean()) :: {binary(), t()}
@spec send_packet(sender(), ExRTP.Packet.t(), boolean()) :: {binary(), sender()}
def send_packet(sender, packet, rtx?) do
%Extmap{} = mid_extmap = Map.fetch!(sender.rtp_hdr_exts, @mid_uri)

Expand Down Expand Up @@ -146,7 +156,8 @@ defmodule ExWebRTC.RTPSender do
end

@doc false
@spec receive_nack(t(), ExRTCP.Packet.TransportFeedback.NACK.t()) :: {[ExRTP.Packet.t()], t()}
@spec receive_nack(sender(), ExRTCP.Packet.TransportFeedback.NACK.t()) ::
{[ExRTP.Packet.t()], sender()}
def receive_nack(sender, nack) do
{packets, nack_responder} = NACKResponder.get_rtx(sender.nack_responder, nack)
sender = %{sender | nack_responder: nack_responder}
Expand All @@ -155,7 +166,7 @@ defmodule ExWebRTC.RTPSender do
end

@doc false
@spec get_stats(t(), non_neg_integer()) :: map()
@spec get_stats(sender(), non_neg_integer()) :: map()
def get_stats(sender, timestamp) do
%{
timestamp: timestamp,
Expand Down
4 changes: 1 addition & 3 deletions lib/ex_webrtc/rtp_sender/nack_responder.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule ExWebRTC.RTPSender.NACKResponder do
@moduledoc nil
@moduledoc false

alias ExRTP.Packet
alias ExRTCP.Packet.TransportFeedback.NACK
Expand All @@ -14,7 +14,6 @@ defmodule ExWebRTC.RTPSender.NACKResponder do
defstruct packets: %{},
seq_no: Enum.random(0..0xFFFF)

@doc false
@spec record_packet(t(), Packet.t()) :: t()
def record_packet(responder, packet) do
key = rem(packet.sequence_number, @max_packets)
Expand All @@ -23,7 +22,6 @@ defmodule ExWebRTC.RTPSender.NACKResponder do
%__MODULE__{responder | packets: packets}
end

@doc false
@spec get_rtx(t(), NACK.t()) :: {[ExRTP.Packet.t()], t()}
def get_rtx(responder, nack) do
seq_nos = NACK.to_sequence_numbers(nack)
Expand Down
17 changes: 5 additions & 12 deletions lib/ex_webrtc/rtp_sender/report_recorder.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule ExWebRTC.RTPSender.ReportRecorder do
@moduledoc nil
@moduledoc false

import Bitwise

Expand Down Expand Up @@ -29,10 +29,7 @@ defmodule ExWebRTC.RTPSender.ReportRecorder do
packet_count: 0,
octet_count: 0

@doc """
Records outgoing RTP Packet.
`time` parameter accepts output of `System.os_time(:native)` as a value (UNIX timestamp in :native units).
"""
# `time` parameter accepts output of `System.os_time(:native)` as a value (UNIX timestamp in :native units).
@spec record_packet(t(), ExRTP.Packet.t(), integer()) :: t()
def record_packet(recorder, packet, time \\ System.os_time(:native))

Expand Down Expand Up @@ -80,13 +77,9 @@ defmodule ExWebRTC.RTPSender.ReportRecorder do
}
end

@doc """
Creates an RTCP Sender Report.
`time` parameter accepts output of `System.os_time(:native)` as a value (UNIX timestamp in :native units).

This function can be called only if at least one packet has been recorded,
otherwise it will raise.
"""
# `time` parameter accepts output of `System.os_time(:native)` as a value (UNIX timestamp in :native units).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

# This function can be called only if at least one packet has been recorded,
# otherwise it will raise.
@spec get_report(t(), integer()) :: {:ok, SenderReport.t(), t()} | {:error, term()}
def get_report(recorder, time \\ System.os_time(:native))

Expand Down
Loading