-
Notifications
You must be signed in to change notification settings - Fork 14
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
Add support for RTCP Sender Reports #78
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3b3a022
Add `RTPSender.RecordReporter.record_packet/3`
LVala 5f737f2
Add `RTPSender.RecordReporter.get_report/2`
LVala 30e6511
Add tests for `RTPSender.ReportRecorder`
LVala 67c02ad
Improve docs
LVala 14e791a
Apply requested changes
LVala 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 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 |
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.
I would add a comment here, sth like
In ReceiverReports we have:
which is really nice and easy to to read but if someone didn't see this code, it might be pretty hard to get what's going on?