Skip to content

Commit cf3142e

Browse files
authored
Ogg writer (#50)
1 parent 579773b commit cf3142e

File tree

9 files changed

+706
-107
lines changed

9 files changed

+706
-107
lines changed

examples/send_from_file/example.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ defmodule Peer do
161161
Process.send_after(self(), :send_audio_packet, duration)
162162
# values set to 0 are handled by PeerConnection.set_rtp
163163
rtp_packet = OpusPayloader.payload(packet)
164-
rtp_packet = %{rtp_packet | timestamp: state.last_audio_timestamp}
164+
rtp_packet = %{rtp_packet | timestamp: trunc(state.last_audio_timestamp)}
165165
PeerConnection.send_rtp(state.peer_connection, state.audio_track_id, rtp_packet)
166166

167167
# OggReader.next_packet/1 returns duration in ms

lib/ex_webrtc/media/ogg/header.ex

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
defmodule ExWebRTC.Media.Ogg.Header do
2+
@moduledoc false
3+
# based on RFC 7845, sec. 5
4+
5+
@id_signature "OpusHead"
6+
@comment_signature "OpusTags"
7+
8+
@vendor "elixir-webrtc"
9+
10+
@default_preskip 3840
11+
@default_gain 0
12+
# mono or stereo
13+
@channel_mapping 0
14+
15+
# for now, we ignore the Ogg/Opus header when decoding
16+
@spec decode_id(binary()) :: :ok | {:error, term()}
17+
def decode_id(<<@id_signature, _rest::binary>>), do: :ok
18+
def decode_id(_packet), do: {:error, :invalid_id_header}
19+
20+
@spec decode_id(binary()) :: :ok | {:error, term()}
21+
def decode_comment(<<@comment_signature, _rest::binary>>), do: :ok
22+
def decode_commend(_packet), do: {:error, :invalid_comment_header}
23+
24+
@spec create_id(non_neg_integer(), non_neg_integer()) :: binary()
25+
def create_id(sample_rate, channel_count) do
26+
<<
27+
@id_signature,
28+
1,
29+
channel_count,
30+
@default_preskip::little-16,
31+
sample_rate::little-32,
32+
@default_gain::little-16,
33+
@channel_mapping
34+
>>
35+
end
36+
37+
@spec create_comment() :: binary()
38+
def create_comment do
39+
<<
40+
@comment_signature,
41+
byte_size(@vendor)::little-32,
42+
@vendor,
43+
# no additional user comments
44+
0
45+
>>
46+
end
47+
end

lib/ex_webrtc/media/ogg/page.ex

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
defmodule ExWebRTC.Media.Ogg.Page do
2+
@moduledoc false
3+
# see RFC 3553, sec. 6 for description of the Ogg Page
4+
5+
import Bitwise
6+
7+
@crc_params %{
8+
extend: :crc_32,
9+
poly: 0x04C11DB7,
10+
init: 0x0,
11+
xorout: 0x0,
12+
refin: false,
13+
refout: false
14+
}
15+
16+
@signature "OggS"
17+
@version 0
18+
19+
@type t() :: %__MODULE__{
20+
continued?: boolean(),
21+
first?: boolean(),
22+
last?: boolean(),
23+
granule_pos: non_neg_integer(),
24+
serial_no: non_neg_integer(),
25+
sequence_no: non_neg_integer(),
26+
packets: [binary()],
27+
rest: binary()
28+
}
29+
30+
@enforce_keys [:granule_pos, :serial_no, :sequence_no]
31+
defstruct @enforce_keys ++
32+
[
33+
continued?: false,
34+
first?: false,
35+
last?: false,
36+
packets: [],
37+
rest: <<>>
38+
]
39+
40+
@spec read(File.io_device()) :: {:ok, t()} | {:error, term()}
41+
def read(file) do
42+
with <<@signature, @version, type, granule_pos::little-64, serial_no::little-32,
43+
sequence_no::little-32, _checksum::little-32,
44+
segment_no>> = header <- IO.binread(file, 27),
45+
raw_segment_table when is_binary(raw_segment_table) <- IO.binread(file, segment_no),
46+
segment_table <- :binary.bin_to_list(raw_segment_table),
47+
payload_length <- Enum.sum(segment_table),
48+
payload when is_binary(payload) <- IO.binread(file, payload_length),
49+
:ok <- verify_checksum(header <> raw_segment_table <> payload) do
50+
{packets, rest} = split_packets(segment_table, payload)
51+
52+
page = %__MODULE__{
53+
continued?: (type &&& 0x01) != 0,
54+
first?: (type &&& 0x02) != 0,
55+
last?: (type &&& 0x04) != 0,
56+
granule_pos: granule_pos,
57+
serial_no: serial_no,
58+
sequence_no: sequence_no,
59+
packets: packets,
60+
rest: rest
61+
}
62+
63+
{:ok, page}
64+
else
65+
data when is_binary(data) -> {:error, :invalid_page_header}
66+
:eof -> :eof
67+
{:error, _res} = err -> err
68+
end
69+
end
70+
71+
@spec write(File.io_device(), t()) :: :ok | {:error, term()}
72+
def write(file, %__MODULE__{} = page) do
73+
with {:ok, segment_table} <- create_segment_table(page.packets, page.rest) do
74+
continued = if page.continued?, do: 0x01, else: 0
75+
first = if page.first?, do: 0x02, else: 0
76+
last = if page.last?, do: 0x04, else: 0
77+
type = first ||| continued ||| last
78+
79+
before_crc = <<
80+
@signature,
81+
@version,
82+
type,
83+
page.granule_pos::little-64,
84+
page.serial_no::little-32,
85+
page.sequence_no::little-32
86+
>>
87+
88+
after_crc =
89+
<<length(segment_table)>> <>
90+
:binary.list_to_bin(segment_table) <>
91+
:binary.list_to_bin(page.packets) <>
92+
page.rest
93+
94+
checksum = CRC.calculate(<<before_crc::binary, 0::32, after_crc::binary>>, @crc_params)
95+
packet = <<before_crc::binary, checksum::little-32, after_crc::binary>>
96+
97+
IO.binwrite(file, packet)
98+
end
99+
end
100+
101+
defp verify_checksum(<<start::binary-22, checksum::little-32, rest::binary>>) do
102+
actual_checksum =
103+
<<start::binary, 0::32, rest::binary>>
104+
|> CRC.calculate(@crc_params)
105+
106+
if checksum == actual_checksum do
107+
:ok
108+
else
109+
{:error, :invalid_checksum}
110+
end
111+
end
112+
113+
defp split_packets(segment_table, payload, packets \\ [], packet \\ <<>>)
114+
defp split_packets([], <<>>, packets, packet), do: {Enum.reverse(packets), packet}
115+
116+
defp split_packets([segment_len | segment_table], payload, packets, packet) do
117+
<<segment::binary-size(segment_len), rest::binary>> = payload
118+
packet = packet <> segment
119+
120+
case segment_len do
121+
255 -> split_packets(segment_table, rest, packets, packet)
122+
_len -> split_packets(segment_table, rest, [packet | packets], <<>>)
123+
end
124+
end
125+
126+
defp create_segment_table(packets, rest) when rem(byte_size(rest), 255) == 0 do
127+
# normally packet of length that is a multiple of 255 would end with 0-lenght segment
128+
# for the rest (split packet) we don't want that
129+
rest_segments =
130+
case segment_packet(rest) do
131+
[0 | segments] -> segments
132+
[] -> []
133+
end
134+
135+
segment_table =
136+
packets
137+
|> Enum.reduce([], fn packet, segments ->
138+
segment_packet(packet) ++ segments
139+
end)
140+
|> then(&Enum.concat(rest_segments, &1))
141+
|> Enum.reverse()
142+
143+
if length(segment_table) > 255 do
144+
{:error, :too_many_segments}
145+
else
146+
{:ok, segment_table}
147+
end
148+
end
149+
150+
defp create_segment_table(_packets, _rest), do: {:error, :rest_too_short}
151+
152+
# returned segment table for the packet is reversed
153+
# thus the Enum.reverse/1 call in create_segment_table/2
154+
defp segment_packet(packet, acc \\ [])
155+
defp segment_packet(<<>>, [255 | _rest] = acc), do: [0 | acc]
156+
defp segment_packet(<<>>, acc), do: acc
157+
158+
defp segment_packet(<<_seg::binary-255, rest::binary>>, acc),
159+
do: segment_packet(rest, [255 | acc])
160+
161+
defp segment_packet(packet, acc) when is_binary(packet), do: [byte_size(packet) | acc]
162+
end

lib/ex_webrtc/media/ogg_reader.ex

Lines changed: 12 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule ExWebRTC.Media.OggReader do
22
@moduledoc """
3-
Defines Ogg reader.
3+
Reads Opus packets from an Ogg container file.
44
55
For now, works only with single Opus stream in the container.
66
@@ -10,21 +10,8 @@ defmodule ExWebRTC.Media.OggReader do
1010
* [RFC 6716: Definition of the Opus Audio Codec](https://www.rfc-editor.org/rfc/rfc6716.txt)
1111
"""
1212

13-
import Bitwise
14-
15-
@crc_params %{
16-
extend: :crc_32,
17-
poly: 0x04C11DB7,
18-
init: 0x0,
19-
xorout: 0x0,
20-
refin: false,
21-
refout: false
22-
}
23-
24-
@signature "OggS"
25-
@id_signature "OpusHead"
26-
@comment_signature "OpusTags"
27-
@version 0
13+
alias ExWebRTC.Media.Ogg.{Header, Page}
14+
alias ExWebRTC.Media.Opus
2815

2916
@opaque t() :: %{
3017
file: File.io_device(),
@@ -38,17 +25,18 @@ defmodule ExWebRTC.Media.OggReader do
3825
For now, works only with single Opus stream in the container.
3926
This function reads the ID and Comment Headers (and, for now, ignores them).
4027
"""
41-
@spec open(Path.t()) :: {:ok, t()} | {:error, File.posix() | :invalid_header}
28+
@spec open(Path.t()) :: {:ok, t()} | {:error, term()}
4229
def open(path) do
4330
with {:ok, file} <- File.open(path),
4431
reader <- %{file: file, packets: [], rest: <<>>},
45-
# for now, we ignore ID Header and Comment Header
46-
{:ok, <<@id_signature, _rest::binary>>, reader} <- do_next_packet(reader),
47-
{:ok, <<@comment_signature, _rest::binary>>, reader} <- do_next_packet(reader) do
32+
{:ok, id_header, reader} <- do_next_packet(reader),
33+
{:ok, comment_header, reader} <- do_next_packet(reader),
34+
:ok <- Header.decode_id(id_header),
35+
:ok <- Header.decode_comment(comment_header) do
4836
{:ok, reader}
4937
else
38+
:eof -> {:error, :invalid_file}
5039
{:error, _res} = err -> err
51-
_other -> {:error, :invalid_header}
5240
end
5341
end
5442

@@ -59,13 +47,10 @@ defmodule ExWebRTC.Media.OggReader do
5947
This function also returns the duration of the audio in milliseconds, based on Opus packet TOC sequence (see RFC 6716, sec. 3).
6048
It assumes that all of the Ogg packets belong to the same stream.
6149
"""
62-
@spec next_packet(t()) ::
63-
{:ok, {binary(), non_neg_integer()}, t()}
64-
| {:error, :invalid_page_header | :not_enough_data}
65-
| :eof
50+
@spec next_packet(t()) :: {:ok, {binary(), non_neg_integer()}, t()} | {:error, term()} | :eof
6651
def next_packet(reader) do
6752
with {:ok, packet, reader} <- do_next_packet(reader),
68-
{:ok, duration} <- get_packet_duration(packet) do
53+
{:ok, duration} <- Opus.duration(packet) do
6954
{:ok, {packet, duration}, reader}
7055
end
7156
end
@@ -75,7 +60,7 @@ defmodule ExWebRTC.Media.OggReader do
7560
end
7661

7762
defp do_next_packet(%{packets: []} = reader) do
78-
with {:ok, _header, packets, rest} <- read_page(reader.file) do
63+
with {:ok, %Page{packets: packets, rest: rest}} <- Page.read(reader.file) do
7964
case packets do
8065
[] ->
8166
do_next_packet(%{reader | packets: [], rest: reader.rest <> rest})
@@ -87,82 +72,4 @@ defmodule ExWebRTC.Media.OggReader do
8772
end
8873
end
8974
end
90-
91-
defp read_page(file) do
92-
with <<@signature, @version, type, granule_pos::little-64, serial_no::little-32,
93-
sequence_no::little-32, _checksum::little-32,
94-
segment_no>> = header <- IO.binread(file, 27),
95-
raw_segment_table when is_binary(raw_segment_table) <- IO.binread(file, segment_no),
96-
segment_table <- :binary.bin_to_list(raw_segment_table),
97-
payload_length <- Enum.sum(segment_table),
98-
payload when is_binary(payload) <- IO.binread(file, payload_length),
99-
:ok <- verify_checksum(header <> raw_segment_table <> payload) do
100-
{packets, rest} = split_packets(segment_table, payload)
101-
102-
type = %{
103-
fresh?: (type &&& 0x01) != 0,
104-
first?: (type &&& 0x02) != 0,
105-
last?: (type &&& 0x04) != 0
106-
}
107-
108-
{:ok,
109-
%{
110-
type: type,
111-
granule_pos: granule_pos,
112-
serial_no: serial_no,
113-
sequence_no: sequence_no
114-
}, packets, rest}
115-
else
116-
data when is_binary(data) -> {:error, :invalid_page_header}
117-
:eof -> :eof
118-
{:error, _res} = err -> err
119-
end
120-
end
121-
122-
defp verify_checksum(<<start::binary-22, checksum::little-32, rest::binary>>) do
123-
actual_checksum =
124-
<<start::binary, 0::32, rest::binary>>
125-
|> CRC.calculate(@crc_params)
126-
127-
if checksum == actual_checksum do
128-
:ok
129-
else
130-
{:error, :invalid_checksum}
131-
end
132-
end
133-
134-
defp split_packets(segment_table, payload, packets \\ [], packet \\ <<>>)
135-
defp split_packets([], <<>>, packets, packet), do: {Enum.reverse(packets), packet}
136-
137-
defp split_packets([segment_len | segment_table], payload, packets, packet) do
138-
<<segment::binary-size(segment_len), rest::binary>> = payload
139-
packet = packet <> segment
140-
141-
case segment_len do
142-
255 -> split_packets(segment_table, rest, packets, packet)
143-
_len -> split_packets(segment_table, rest, [packet | packets], <<>>)
144-
end
145-
end
146-
147-
# computes how much audio Opus packet contains (in ms), based on the TOC sequence
148-
# RFC 6716, sec. 3
149-
defp get_packet_duration(<<config::5, rest::bitstring>>) do
150-
with {:ok, frame_count} <- get_frame_count(rest) do
151-
{:ok, trunc(frame_count * get_frame_duration(config))}
152-
end
153-
end
154-
155-
defp get_packet_duration(_other), do: {:error, :not_enough_data}
156-
157-
defp get_frame_count(<<_s::1, 0::2, _rest::binary>>), do: {:ok, 1}
158-
defp get_frame_count(<<_s::1, c::2, _rest::binary>>) when c in 1..2, do: {:ok, 2}
159-
defp get_frame_count(<<_s::1, 3::2, _vp::2, frame_no::5, _rest::binary>>), do: {:ok, frame_no}
160-
defp get_frame_count(_other), do: {:error, :not_enough_data}
161-
162-
defp get_frame_duration(config) when config in [16, 20, 24, 28], do: 2.5
163-
defp get_frame_duration(config) when config in [17, 21, 25, 29], do: 5
164-
defp get_frame_duration(config) when config in [0, 4, 8, 12, 14, 18, 22, 26, 30], do: 10
165-
defp get_frame_duration(config) when config in [1, 5, 9, 13, 15, 19, 23, 27, 31], do: 20
166-
defp get_frame_duration(config) when config in [2, 6, 10], do: 40
167-
defp get_frame_duration(config) when config in [3, 7, 11], do: 60
16875
end

0 commit comments

Comments
 (0)