Skip to content

Commit d0b2778

Browse files
committed
Add IVF writer
1 parent a8709d5 commit d0b2778

File tree

8 files changed

+198
-61
lines changed

8 files changed

+198
-61
lines changed

examples/echo/example.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
const pcConfig = {
2-
'iceServers': [
3-
{'urls': 'stun:stun.stunprotocol.org:3478'},
4-
{'urls': 'stun:stun.l.google.com:19302'},
5-
]
6-
};
1+
const pcConfig = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' },] };
72

83
const start_connection = async (ws) => {
94
const pc = new RTCPeerConnection(pcConfig);
@@ -25,11 +20,11 @@ const start_connection = async (ws) => {
2520
console.log("New local ICE candidate:", event.candidate);
2621

2722
if (event.candidate !== null) {
28-
ws.send(JSON.stringify({type: "ice", data: event.candidate}));
23+
ws.send(JSON.stringify({ type: "ice", data: event.candidate }));
2924
}
3025
};
31-
32-
const localStream = await navigator.mediaDevices.getUserMedia({video: true});
26+
27+
const localStream = await navigator.mediaDevices.getUserMedia({ video: true });
3328
const localVideoPlayer = document.createElement("video");
3429
localVideoPlayer.srcObject = localStream;
3530
localVideoPlayer.onloadedmetadata = () => {

examples/send_from_file/example.exs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,7 @@ defmodule Peer do
134134

135135
:eof ->
136136
Logger.info("video.ivf ended. Looping...")
137-
{:ok, ivf_reader} = IVFReader.open("./video.ivf")
138-
{:ok, _header} = IVFReader.read_header(ivf_reader)
137+
{:ok, _header, ivf_reader} = IVFReader.open("./video.ivf")
139138
state = %{state | ivf_reader: ivf_reader}
140139
{:noreply, state}
141140
end
@@ -203,8 +202,7 @@ defmodule Peer do
203202
defp handle_webrtc_message({:connection_state_change, :connected} = msg, state) do
204203
Logger.info("#{inspect(msg)}")
205204
Logger.info("Starting sending video.ivf")
206-
{:ok, ivf_reader} = IVFReader.open("./video.ivf")
207-
{:ok, _header} = IVFReader.read_header(ivf_reader)
205+
{:ok, _header, ivf_reader} = IVFReader.open("./video.ivf")
208206
payloader = VP8Payloader.new(800)
209207

210208
Process.send_after(self(), :send_frame, 30)

examples/send_from_file/example.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
const pcConfig = {
2-
'iceServers': [
3-
{'urls': 'stun:stun.stunprotocol.org:3478'},
4-
{'urls': 'stun:stun.l.google.com:19302'},
5-
]
6-
};
1+
const pcConfig = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' },] };
72

83
const start_connection = async (ws) => {
94
const pc = new RTCPeerConnection(pcConfig);
@@ -21,10 +16,10 @@ const start_connection = async (ws) => {
2116
console.log("New local ICE candidate:", event.candidate);
2217

2318
if (event.candidate !== null) {
24-
ws.send(JSON.stringify({type: "ice", data: event.candidate}));
19+
ws.send(JSON.stringify({ type: "ice", data: event.candidate }));
2520
}
2621
};
27-
22+
2823
ws.onmessage = async event => {
2924
const msg = JSON.parse(event.data);
3025

lib/ex_webrtc/media/ivf_reader.ex

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ defmodule ExWebRTC.Media.IVFHeader do
3131
width: non_neg_integer(),
3232
height: non_neg_integer(),
3333
timebase_denum: non_neg_integer(),
34-
timebase_num: non_neg_integer(),
34+
timebase_num: pos_integer(),
3535
num_frames: non_neg_integer(),
3636
unused: non_neg_integer()
3737
}
@@ -73,7 +73,7 @@ end
7373

7474
defmodule ExWebRTC.Media.IVFReader do
7575
@moduledoc """
76-
Defines IVF reader.
76+
Reads video frames from an IVF file.
7777
7878
Based on:
7979
* https://formats.kaitai.io/vp8_ivf/
@@ -84,37 +84,34 @@ defmodule ExWebRTC.Media.IVFReader do
8484

8585
@opaque t() :: File.io_device()
8686

87-
@spec open(Path.t()) :: {:ok, t()} | {:error, File.posix()}
88-
def open(path), do: File.open(path)
89-
90-
@spec read_header(t()) :: {:ok, IVFHeader.t()} | {:error, :invalid_file} | :eof
91-
def read_header(reader) do
92-
case IO.binread(reader, 32) do
93-
<<"DKIF", 0::little-integer-size(16), 32::little-integer-size(16),
94-
fourcc::little-integer-size(32), width::little-integer-size(16),
95-
height::little-integer-size(16), timebase_denum::little-integer-size(32),
96-
timebase_num::little-integer-size(32), num_frames::little-integer-size(32),
97-
unused::little-integer-size(32)>> ->
98-
{:ok,
99-
%IVFHeader{
100-
signature: "DKIF",
101-
version: 0,
102-
header_size: 32,
103-
fourcc: fourcc,
104-
width: width,
105-
height: height,
106-
timebase_denum: timebase_denum,
107-
timebase_num: timebase_num,
108-
num_frames: num_frames,
109-
unused: unused
110-
}}
111-
112-
_other ->
113-
{:error, :invalid_file}
87+
@spec open(Path.t()) :: {:ok, IVFHeader.t(), t()} | {:error, term()}
88+
def open(path) do
89+
with {:ok, file} <- File.open(path),
90+
<<"DKIF", 0::little-16, 32::little-16, fourcc::little-32, width::little-16,
91+
height::little-16, timebase_denum::little-32, timebase_num::little-32,
92+
num_frames::little-32, unused::little-32>> <- IO.binread(file, 32) do
93+
header = %IVFHeader{
94+
signature: "DKIF",
95+
version: 0,
96+
header_size: 32,
97+
fourcc: fourcc,
98+
width: width,
99+
height: height,
100+
timebase_denum: timebase_denum,
101+
timebase_num: timebase_num,
102+
num_frames: num_frames,
103+
unused: unused
104+
}
105+
106+
{:ok, header, file}
107+
else
108+
{:error, _reason} = error -> error
109+
# eof or invalid pattern matching
110+
_other -> {:error, :invalid_file}
114111
end
115112
end
116113

117-
@spec next_frame(t()) :: {:ok, IVFFrame.t()} | {:error, :invalid_file} | :eof
114+
@spec next_frame(t()) :: {:ok, IVFFrame.t()} | {:error, term()} | :eof
118115
def next_frame(reader) do
119116
with <<len_frame::little-integer-size(32), timestamp::little-integer-size(64)>> <-
120117
IO.binread(reader, 12),
@@ -123,6 +120,7 @@ defmodule ExWebRTC.Media.IVFReader do
123120
{:ok, %IVFFrame{timestamp: timestamp, data: data}}
124121
else
125122
:eof -> :eof
123+
{:error, _reason} = error -> error
126124
_other -> {:error, :invalid_file}
127125
end
128126
end

lib/ex_webrtc/media/ivf_writer.ex

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
defmodule ExWebRTC.Media.IVFWriter do
2+
@moduledoc """
3+
Writes video frames as an IVF file.
4+
"""
5+
6+
alias ExWebRTC.Media.IVFFrame
7+
8+
@opaque t() :: %__MODULE__{
9+
file: File.io_device(),
10+
frames_cnt: non_neg_integer()
11+
}
12+
13+
@enforce_keys [:file]
14+
defstruct @enforce_keys ++ [update_header_after: 0, frames_cnt: 0]
15+
16+
defguard update_header?(writer)
17+
when writer.frames_cnt >= writer.update_header_after and
18+
rem(writer.frames_cnt, writer.update_header_after) == 0
19+
20+
@doc """
21+
Creates a new IVF writer.
22+
23+
Initially, IVF header is written with `num_frames` and
24+
is updated every `num_frames` by `num_frames`.
25+
"""
26+
@spec open(Path.t(),
27+
fourcc: non_neg_integer(),
28+
height: non_neg_integer(),
29+
width: non_neg_integer(),
30+
num_frames: pos_integer(),
31+
timebase_denum: non_neg_integer(),
32+
timebase_num: pos_integer()
33+
) :: {:ok, t()} | {:error, File.posix() | term()}
34+
def open(path,
35+
fourcc: fourcc,
36+
height: height,
37+
width: width,
38+
num_frames: num_frames,
39+
timebase_denum: timebase_denum,
40+
timebase_num: timebase_num
41+
)
42+
when num_frames > 0 do
43+
header =
44+
<<"DKIF", 0::little-16, 32::little-16, fourcc::little-32, width::little-16,
45+
height::little-16, timebase_denum::little-32, timebase_num::little-32,
46+
num_frames::little-32, 0::little-32>>
47+
48+
with {:ok, file} <- File.open(path, [:write]),
49+
:ok <- IO.binwrite(file, header) do
50+
writer = %__MODULE__{file: file, update_header_after: num_frames}
51+
{:ok, writer}
52+
end
53+
end
54+
55+
@doc """
56+
Writes IVF frame into a file.
57+
"""
58+
@spec write_frame(t(), IVFFrame.t()) :: {:ok, t()} | {:error, term()}
59+
def write_frame(writer, frame) when update_header?(writer) and frame.data != <<>> do
60+
case update_header(writer) do
61+
:ok -> do_write_frame(writer, frame)
62+
{:error, _reason} = error -> error
63+
end
64+
end
65+
66+
def write_frame(writer, frame) when frame.data != <<>>, do: do_write_frame(writer, frame)
67+
68+
defp update_header(writer) do
69+
num_frames = <<writer.frames_cnt + writer.update_header_after::little-32>>
70+
ret = :file.pwrite(writer.file, 24, num_frames)
71+
{:ok, _position} = :file.position(writer.file, :eof)
72+
ret
73+
end
74+
75+
defp do_write_frame(writer, frame) do
76+
len_frame = byte_size(frame.data)
77+
serialized_frame = <<len_frame::little-32, frame.timestamp::little-64, frame.data::binary>>
78+
79+
case IO.binwrite(writer.file, serialized_frame) do
80+
:ok ->
81+
writer = %{writer | frames_cnt: writer.frames_cnt + 1}
82+
{:ok, writer}
83+
84+
{:error, _reason} = error ->
85+
error
86+
end
87+
end
88+
end

test/ex_webrtc/media/ivf_reader_test.exs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ defmodule ExWebRTC.Media.IVFReaderTest do
44
alias ExWebRTC.Media.{IVFFrame, IVFHeader, IVFReader}
55

66
test "correct file" do
7-
assert {:ok, reader} = IVFReader.open("test/fixtures/ivf/vp8_correct.ivf")
8-
97
assert {:ok,
108
%IVFHeader{
119
signature: "DKIF",
@@ -18,7 +16,7 @@ defmodule ExWebRTC.Media.IVFReaderTest do
1816
timebase_num: 1000,
1917
num_frames: 29,
2018
unused: 0
21-
}} == IVFReader.read_header(reader)
19+
}, reader} = IVFReader.open("test/fixtures/ivf/vp8_correct.ivf")
2220

2321
for i <- 0..28 do
2422
assert {:ok, %IVFFrame{} = frame} = IVFReader.next_frame(reader)
@@ -31,13 +29,10 @@ defmodule ExWebRTC.Media.IVFReaderTest do
3129
end
3230

3331
test "empty file" do
34-
assert {:ok, reader} = IVFReader.open("test/fixtures/ivf/empty.ivf")
35-
assert {:error, :invalid_file} == IVFReader.read_header(reader)
32+
assert {:error, :invalid_file} = IVFReader.open("test/fixtures/ivf/empty.ivf")
3633
end
3734

3835
test "invalid last frame" do
39-
assert {:ok, reader} = IVFReader.open("test/fixtures/ivf/vp8_invalid_last_frame.ivf")
40-
4136
assert {:ok,
4237
%IVFHeader{
4338
signature: "DKIF",
@@ -50,7 +45,7 @@ defmodule ExWebRTC.Media.IVFReaderTest do
5045
timebase_num: 1000,
5146
num_frames: 29,
5247
unused: 0
53-
}} == IVFReader.read_header(reader)
48+
}, reader} = IVFReader.open("test/fixtures/ivf/vp8_invalid_last_frame.ivf")
5449

5550
for i <- 0..27 do
5651
assert {:ok, %IVFFrame{} = frame} = IVFReader.next_frame(reader)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
defmodule ExWebRTC.Media.IVFWritertTest do
2+
use ExUnit.Case, async: true
3+
4+
alias ExWebRTC.Media.{IVFFrame, IVFHeader, IVFReader, IVFWriter}
5+
6+
@tag :tmp_dir
7+
test "IVF writer", %{tmp_dir: tmp_dir} do
8+
path = Path.join([tmp_dir, "output.ivf"])
9+
<<fourcc::little-32>> = "VP80"
10+
num_frames = 2
11+
12+
{:ok, writer} =
13+
IVFWriter.open(path,
14+
fourcc: fourcc,
15+
height: 640,
16+
width: 480,
17+
num_frames: num_frames,
18+
timebase_denum: 30,
19+
timebase_num: 1
20+
)
21+
22+
writer =
23+
for i <- 0..(num_frames - 1), reduce: writer do
24+
writer ->
25+
frame = %IVFFrame{timestamp: i, data: <<0, 1, 2, 3, 4>>}
26+
assert {:ok, writer} = IVFWriter.write_frame(writer, frame)
27+
writer
28+
end
29+
30+
assert {:ok,
31+
%IVFHeader{
32+
fourcc: ^fourcc,
33+
height: 640,
34+
width: 480,
35+
num_frames: ^num_frames,
36+
timebase_denum: 30,
37+
timebase_num: 1,
38+
unused: 0
39+
}, _reader} = IVFReader.open(path)
40+
41+
# check if we update IVF header after writing
42+
# `num_frames + 1` frame
43+
expected_num_frames = 2 * num_frames
44+
frame = %IVFFrame{timestamp: num_frames, data: <<0, 1, 2, 3, 4>>}
45+
assert {:ok, _writer} = IVFWriter.write_frame(writer, frame)
46+
47+
assert {:ok,
48+
%IVFHeader{
49+
fourcc: ^fourcc,
50+
height: 640,
51+
width: 480,
52+
num_frames: ^expected_num_frames,
53+
timebase_denum: 30,
54+
timebase_num: 1,
55+
unused: 0
56+
}, reader} = IVFReader.open(path)
57+
58+
# check if we raise when trying to write an empty frame
59+
empty_frame = %IVFFrame{timestamp: num_frames + 1, data: <<>>}
60+
assert_raise FunctionClauseError, fn -> IVFWriter.write_frame(writer, empty_frame) end
61+
62+
# assert written frames are correct
63+
for i <- 0..num_frames do
64+
assert {:ok, %IVFFrame{} = frame} = IVFReader.next_frame(reader)
65+
assert frame.timestamp == i
66+
assert frame.data == <<0, 1, 2, 3, 4>>
67+
end
68+
end
69+
end

test/ex_webrtc/rtp/vp8_payloader_test.exs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ defmodule ExWebRTC.RTP.VP8PayloaderTest do
77
test "payload vp8 video" do
88
# video frames in the fixture are mostly 500+ bytes
99
vp8_payloader = VP8Payloader.new(200)
10-
{:ok, ivf_reader} = IVFReader.open("test/fixtures/ivf/vp8_correct.ivf")
11-
{:ok, _header} = IVFReader.read_header(ivf_reader)
10+
{:ok, _header, ivf_reader} = IVFReader.open("test/fixtures/ivf/vp8_correct.ivf")
1211

1312
for _i <- 0..28, reduce: vp8_payloader do
1413
vp8_payloader ->

0 commit comments

Comments
 (0)