-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for RTCP Sender Reports (#78)
- Loading branch information
Showing
2 changed files
with
219 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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 |
---|---|---|
@@ -0,0 +1,115 @@ | ||
defmodule ExWebRTC.RTPSender.ReportRecorder do | ||
@moduledoc false | ||
|
||
import Bitwise | ||
|
||
alias ExRTCP.Packet.SenderReport | ||
|
||
@breakpoint 0x7FFF | ||
# NTP epoch is 1/1/1900 vs UNIX epoch is 1/1/1970 | ||
# so there's offset of 70 years (inc. 17 leap years) in seconds | ||
@ntp_offset (70 * 365 + 17) * 86_400 | ||
@micro_in_sec 1_000_000 | ||
|
||
@type t() :: %__MODULE__{ | ||
sender_ssrc: non_neg_integer(), | ||
clock_rate: non_neg_integer(), | ||
last_rtp_timestamp: ExRTP.Packet.uint32(), | ||
last_seq_no: ExRTP.Packet.uint16(), | ||
last_timestamp: integer(), | ||
packet_count: non_neg_integer(), | ||
octet_count: non_neg_integer() | ||
} | ||
|
||
@enforce_keys [:sender_ssrc, :clock_rate] | ||
defstruct [ | ||
last_rtp_timestamp: nil, | ||
last_seq_no: nil, | ||
last_timestamp: nil, | ||
packet_count: 0, | ||
octet_count: 0 | ||
] ++ @enforce_keys | ||
|
||
@doc """ | ||
Records outgoing RTP Packet. | ||
`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(%{last_timestamp: nil} = recorder, packet, time) do | ||
%__MODULE__{ | ||
recorder | ||
| last_rtp_timestamp: packet.timestamp, | ||
last_seq_no: packet.sequence_number, | ||
last_timestamp: time, | ||
packet_count: 1, | ||
octet_count: byte_size(packet.payload) | ||
} | ||
end | ||
|
||
def record_packet(recorder, packet, time) do | ||
%__MODULE__{ | ||
last_seq_no: last_seq_no, | ||
packet_count: packet_count, | ||
octet_count: octet_count | ||
} = recorder | ||
|
||
# a packet is in order when it is from the next cycle, or from current cycle with delta > 0 | ||
delta = packet.sequence_number - last_seq_no | ||
in_order? = delta < -@breakpoint or (delta > 0 and delta < @breakpoint) | ||
|
||
recorder = | ||
if in_order? do | ||
%__MODULE__{ | ||
recorder | ||
| last_seq_no: packet.sequence_number, | ||
last_rtp_timestamp: packet.timestamp, | ||
last_timestamp: time | ||
} | ||
else | ||
recorder | ||
end | ||
|
||
%__MODULE__{ | ||
recorder | ||
| packet_count: packet_count + 1, | ||
octet_count: octet_count + byte_size(packet.payload) | ||
} | ||
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. | ||
""" | ||
@spec get_report(t(), integer()) :: SenderReport.t() | ||
def get_report(%{last_timestamp: nil}, _time), do: raise("No packet has been recorded yet") | ||
|
||
def get_report(recorder, time) do | ||
ntp_time = to_ntp(time) | ||
rtp_delta = delay_since(time, recorder.last_timestamp) * recorder.clock_rate | ||
|
||
%SenderReport{ | ||
ssrc: recorder.sender_ssrc, | ||
packet_count: recorder.packet_count, | ||
octet_count: recorder.octet_count, | ||
ntp_timestamp: ntp_time, | ||
rtp_timestamp: round(recorder.last_rtp_timestamp + rtp_delta) | ||
} | ||
end | ||
|
||
defp to_ntp(time) do | ||
seconds = System.convert_time_unit(time, :native, :second) | ||
micros = System.convert_time_unit(time, :native, :microsecond) - seconds * @micro_in_sec | ||
|
||
frac = div(micros <<< 32, @micro_in_sec) | ||
|
||
(seconds + @ntp_offset) <<< 32 ||| frac | ||
end | ||
|
||
defp delay_since(cur_ts, last_ts) do | ||
native_in_sec = System.convert_time_unit(1, :second, :native) | ||
(cur_ts - last_ts) / native_in_sec | ||
end | ||
end |
This file contains 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 |
---|---|---|
@@ -0,0 +1,104 @@ | ||
defmodule ExWebRTC.RTPSender.ReportRecorderTest do | ||
use ExUnit.Case, async: true | ||
|
||
import Bitwise | ||
|
||
alias ExWebRTC.RTPSender.ReportRecorder | ||
|
||
@rand_ts System.os_time(:native) | ||
@seq_no 11_534 | ||
@rtp_ts 234_444 | ||
@clock_rate 90_000 | ||
@packet ExRTP.Packet.new(<<>>, sequence_number: @seq_no, timestamp: @rtp_ts) | ||
@recorder %ReportRecorder{sender_ssrc: 123_467, clock_rate: @clock_rate} | ||
|
||
@ntp_offset 2_208_988_800 | ||
@max_u32 0xFFFFFFFF | ||
|
||
describe "record_packet/3" do | ||
test "keeps track of packet counts and sizes" do | ||
recorder = | ||
@recorder | ||
|> ReportRecorder.record_packet(@packet, @rand_ts) | ||
|> ReportRecorder.record_packet(%{@packet | payload: <<1, 2, 3>>}, @rand_ts) | ||
|> ReportRecorder.record_packet(%{@packet | payload: <<1, 2, 3, 4, 5>>}, @rand_ts) | ||
|
||
assert %ReportRecorder{ | ||
packet_count: 3, | ||
octet_count: 8 | ||
} = recorder | ||
end | ||
|
||
test "remembers last timestamps" do | ||
last_ts = @rand_ts - 100 | ||
|
||
recorder = | ||
@recorder | ||
|> ReportRecorder.record_packet( | ||
%{@packet | timestamp: @rtp_ts - 200, sequence_number: @seq_no - 2}, | ||
@rand_ts - 200 | ||
) | ||
|> ReportRecorder.record_packet(@packet, last_ts) | ||
|> ReportRecorder.record_packet( | ||
%{@packet | timestamp: @rtp_ts - 100, sequence_number: @seq_no - 1}, | ||
@rand_ts | ||
) | ||
|
||
assert %ReportRecorder{ | ||
last_rtp_timestamp: @rtp_ts, | ||
last_seq_no: @seq_no, | ||
last_timestamp: ^last_ts | ||
} = recorder | ||
end | ||
|
||
test "handles wrapping sequence numbers" do | ||
recorder = | ||
@recorder | ||
|> ReportRecorder.record_packet(%{@packet | sequence_number: 65_534}, @rand_ts - 300) | ||
|> ReportRecorder.record_packet(%{@packet | sequence_number: 65_535}, @rand_ts - 200) | ||
|> ReportRecorder.record_packet(%{@packet | sequence_number: 0}, @rand_ts - 100) | ||
|> ReportRecorder.record_packet(%{@packet | sequence_number: 1}, @rand_ts) | ||
|
||
assert %ReportRecorder{ | ||
last_seq_no: 1, | ||
last_timestamp: @rand_ts | ||
} = recorder | ||
end | ||
end | ||
|
||
describe "get_report/2" do | ||
test "properly calculates NTP timestamp" do | ||
report = | ||
@recorder | ||
|> ReportRecorder.record_packet(@packet, 0) | ||
|> ReportRecorder.get_report(0) | ||
|
||
assert report.ntp_timestamp >>> 32 == @ntp_offset | ||
assert (report.ntp_timestamp &&& @max_u32) == 0 | ||
|
||
native_in_sec = System.convert_time_unit(1, :second, :native) | ||
seconds = 89_934 | ||
# 1/8, so 0.001 in binary | ||
frac = 0.125 | ||
|
||
report = | ||
@recorder | ||
|> ReportRecorder.record_packet(@packet, 0) | ||
|> ReportRecorder.get_report(trunc((seconds + frac) * native_in_sec)) | ||
|
||
assert report.ntp_timestamp >>> 32 == @ntp_offset + seconds | ||
assert (report.ntp_timestamp &&& @max_u32) == 1 <<< 29 | ||
end | ||
|
||
test "properly calculates delay since last packet" do | ||
delta = System.convert_time_unit(250, :millisecond, :native) | ||
|
||
report = | ||
@recorder | ||
|> ReportRecorder.record_packet(@packet, @rand_ts) | ||
|> ReportRecorder.get_report(@rand_ts + delta) | ||
|
||
assert report.rtp_timestamp == @rtp_ts + 0.25 * @clock_rate | ||
end | ||
end | ||
end |