Skip to content

Commit edf272f

Browse files
committed
v0
1 parent 6f202ce commit edf272f

File tree

9 files changed

+255
-143
lines changed

9 files changed

+255
-143
lines changed

lib/ex_webrtc/recorder.ex

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,17 @@ defmodule ExWebRTC.Recorder do
88
See `ExWebRTC.Recorder.S3` and `t:options/0` for more info.
99
"""
1010

11-
use GenServer
12-
1311
alias ExWebRTC.MediaStreamTrack
14-
alias __MODULE__.S3.UploadHandler
1512
alias __MODULE__.S3
1613

1714
require Logger
1815

16+
use GenServer
17+
1918
@default_base_dir "./recordings"
2019

2120
@type recorder :: GenServer.server()
2221

23-
@typep track_manifest :: %{
24-
start_time: DateTime.t(),
25-
kind: :video | :audio,
26-
streams: [MediaStreamTrack.stream_id()],
27-
rid_map: %{MediaStreamTrack.rid() => integer()},
28-
location: String.t()
29-
}
30-
31-
# XXX really opaque?
32-
@typedoc """
33-
Contains metadata about the recordings.
34-
WRITEME
35-
"""
36-
@opaque manifest :: %{MediaStreamTrack.id() => track_manifest()}
37-
3822
@typedoc """
3923
Options that can be passed to `start_link/1`.
4024
@@ -57,12 +41,12 @@ defmodule ExWebRTC.Recorder do
5741
Messages sent by the `ExWebRTC.Recorder` process to its controlling process.
5842
5943
* `:upload_complete`, `:upload_failed` - Sent after the completion of the upload task, identified by its reference.
60-
Contains the updated manifest with `s3://` schema URLs to uploaded files.
44+
Contains the updated manifest with `s3://` scheme URLs to uploaded files.
6145
"""
6246
@type message ::
6347
{:ex_webrtc_recorder, pid(),
64-
{:upload_complete, S3.upload_task_ref(), manifest()}
65-
| {:upload_failed, S3.upload_task_ref(), manifest()}}
48+
{:upload_complete, S3.upload_task_ref(), __MODULE__.Manifest.t()}
49+
| {:upload_failed, S3.upload_task_ref(), __MODULE__.Manifest.t()}}
6650

6751
# Necessary to start Recorder under a supervisor using `{Recorder, [recorder_opts, gen_server_opts]}`
6852
@doc false
@@ -107,9 +91,9 @@ defmodule ExWebRTC.Recorder do
10791
Adds new tracks to the recording.
10892
10993
Returns the part of the recording manifest that's relevant to the freshly added tracks.
110-
See `t:manifest/0` for more info.
94+
See `t:ExWebRTC.Recorder.Manifest.t/0` for more info.
11195
"""
112-
@spec add_tracks(recorder(), [MediaStreamTrack.t()]) :: {:ok, manifest()}
96+
@spec add_tracks(recorder(), [MediaStreamTrack.t()]) :: {:ok, __MODULE__.Manifest.t()}
11397
def add_tracks(recorder, tracks) do
11498
GenServer.call(recorder, {:add_tracks, tracks})
11599
end
@@ -143,18 +127,18 @@ defmodule ExWebRTC.Recorder do
143127
Finishes the recording for the given tracks and optionally uploads the result files.
144128
145129
Returns the part of the recording manifest that's relevant to the freshly ended tracks.
146-
See `t:manifest/0` for more info.
130+
See `t:ExWebRTC.Recorder.Manifest.t/0` for more info.
147131
148132
If uploads are configured:
149133
* Returns the reference to the upload task that was spawned.
150134
* Will send the `:upload_complete`/`:upload_failed` message with this reference
151135
to the controlling process when the task finishes.
152136
153137
Note that the manifest returned by this function always contains local paths to files.
154-
The updated manifest with `s3://` schema URLs is sent in the aforementioned message.
138+
The updated manifest with `s3://` scheme URLs is sent in the aforementioned message.
155139
"""
156140
@spec end_tracks(recorder(), [MediaStreamTrack.id()]) ::
157-
{:ok, manifest(), S3.upload_task_ref() | nil} | {:error, :tracks_not_found}
141+
{:ok, __MODULE__.Manifest.t(), S3.upload_task_ref() | nil} | {:error, :tracks_not_found}
158142
def end_tracks(recorder, track_ids) do
159143
GenServer.call(recorder, {:end_tracks, track_ids})
160144
end
@@ -172,7 +156,7 @@ defmodule ExWebRTC.Recorder do
172156
upload_handler =
173157
if config[:s3_upload_config] do
174158
Logger.info("Recordings will be uploaded to S3")
175-
UploadHandler.new(config[:s3_upload_config])
159+
S3.UploadHandler.new(config[:s3_upload_config])
176160
end
177161

178162
state = %{
@@ -264,7 +248,7 @@ defmodule ExWebRTC.Recorder do
264248
def handle_info({ref, _res} = task_result, state) when is_reference(ref) do
265249
if state.upload_handler do
266250
{result, manifest, handler} =
267-
UploadHandler.process_result(state.upload_handler, task_result)
251+
S3.UploadHandler.process_result(state.upload_handler, task_result)
268252

269253
case result do
270254
:ok ->
@@ -332,7 +316,7 @@ defmodule ExWebRTC.Recorder do
332316
{manifest_diff, nil, state}
333317

334318
handler ->
335-
{ref, handler} = UploadHandler.spawn_task(handler, manifest_diff)
319+
{ref, handler} = S3.UploadHandler.spawn_task(handler, manifest_diff)
336320

337321
{manifest_diff, ref, %{state | upload_handler: handler}}
338322
end

lib/ex_webrtc/recorder/converter.ex

Lines changed: 51 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule ExWebRTC.Recorder.Converter do
22
@moduledoc """
33
Processes RTP packet files saved by `ExWebRTC.Recorder`.
4+
45
Requires the `ffmpeg` binary with the relevant libraries present in `PATH`.
56
67
At the moment, `ExWebRTC.Recorder.Converter` works only with VP8 video and Opus audio.
@@ -9,14 +10,17 @@ defmodule ExWebRTC.Recorder.Converter do
910
See `ExWebRTC.Recorder.S3` and `t:options/0` for more info.
1011
"""
1112

12-
require Logger
13-
1413
alias ExWebRTC.RTP.JitterBuffer.PacketStore
1514
alias ExWebRTC.RTP.Depayloader
1615
alias ExWebRTC.Media.{IVF, Ogg}
16+
1717
alias ExWebRTC.Recorder.S3
1818
alias ExWebRTC.{Recorder, RTPCodecParameters}
1919

20+
alias __MODULE__.FFmpeg
21+
22+
require Logger
23+
2024
# TODO: Allow changing these values
2125
@ivf_header_opts [
2226
# <<fourcc::little-32>> = "VP80"
@@ -45,32 +49,23 @@ defmodule ExWebRTC.Recorder.Converter do
4549
@default_output_path "./converter/output"
4650
@default_download_path "./converter/download"
4751
@default_thumbnail_width 640
48-
# -1 means fit to aspect ratio
4952
@default_thumbnail_height -1
5053

51-
@typep file_manifest :: %{
52-
:location => String.t(),
53-
:duration_seconds => non_neg_integer(),
54-
optional(:thumbnail_location) => String.t()
55-
}
56-
57-
# XXX this probably shouldn't be opaque
58-
# WRITEME typedoc
59-
@opaque manifest :: %{ExWebRTC.MediaStreamTrack.stream_id() => file_manifest()}
60-
6154
@typedoc """
62-
Context for the thumbnail generation
55+
Context for the thumbnail generation.
6356
64-
WRITEME
57+
* `:width` - Thumbnail width. #{@default_thumbnail_width} by default.
58+
* `:height` - Thumbnail height. #{@default_thumbnail_height} by default.
59+
60+
Setting either of the values to `-1` will fit the size to the aspect ratio.
6561
"""
6662
@type thumbnails_ctx :: %{
6763
optional(:width) => pos_integer() | -1,
6864
optional(:height) => pos_integer() | -1
6965
}
7066

71-
# XXX as well as convert_from_file...?
7267
@typedoc """
73-
Options that can be passed to `convert_manifest!/2`.
68+
Options that can be passed to `convert_manifest!/2` and `convert_path!/2`.
7469
7570
* `:output_path` - Directory where Converter will save its artifacts. `#{@default_output_path}` by default.
7671
* `:s3_upload_config` - If passed, processed recordings will be uploaded to S3-compatible storage.
@@ -91,13 +86,12 @@ defmodule ExWebRTC.Recorder.Converter do
9186
@type options :: [option()]
9287

9388
@doc """
94-
Convert the saved dumps of tracks in the report to IVF and Ogg files.
89+
Loads the recording manifest from file, then proceeds with `convert_manifest!/2`.
9590
"""
96-
# REWRITEME
97-
@spec convert!(Path.t(), Path.t()) :: term() | no_return()
98-
def convert!(report_path, output_path \\ @default_output_path) do
99-
report_path =
100-
report_path
91+
@spec convert_path!(Path.t(), options()) :: __MODULE__.Manifest.t() | no_return()
92+
def convert_path!(recorder_manifest_path, options \\ []) do
93+
recorder_manifest_path =
94+
recorder_manifest_path
10195
|> Path.expand()
10296
|> then(
10397
&if(File.dir?(&1),
@@ -106,18 +100,21 @@ defmodule ExWebRTC.Recorder.Converter do
106100
)
107101
)
108102

109-
report =
110-
report_path
103+
recorder_manifest =
104+
recorder_manifest_path
111105
|> File.read!()
112106
|> Jason.decode!()
107+
|> Recorder.Manifest.from_json!()
113108

114-
# XXX no maikel this is so not right
115-
convert_manifest!(report, output_path)
109+
convert_manifest!(recorder_manifest, options)
116110
end
117111

118-
# XXX type options + docs
119-
@spec convert_manifest!(Recorder.manifest(), keyword()) :: manifest() | no_return()
120-
def convert_manifest!(manifest, options \\ [])
112+
@doc """
113+
Converts the saved dumps of tracks in the manifest to WEBM files.
114+
"""
115+
@spec convert_manifest!(Recorder.Manifest.t(), options()) ::
116+
__MODULE__.Manifest.t() | no_return()
117+
def convert_manifest!(recorder_manifest, options \\ [])
121118

122119
def convert_manifest!(manifest, options) when map_size(manifest) > 0 do
123120
thumbnails_ctx =
@@ -154,19 +151,18 @@ defmodule ExWebRTC.Recorder.Converter do
154151
if upload_handler != nil do
155152
{ref, upload_handler} =
156153
output_manifest
157-
|> prepare_upload_handler_manifest()
154+
|> __MODULE__.Manifest.to_upload_handler_manifest()
158155
|> then(&S3.UploadHandler.spawn_task(upload_handler, &1))
159156

160-
# XXX What if upload fails?
157+
# FIXME: Add descriptive errors
161158
{:ok, upload_handler_result_manifest, _handler} =
162159
receive do
163160
{^ref, _res} = task_result ->
164161
S3.UploadHandler.process_result(upload_handler, task_result)
165162
end
166163

167-
# XXX this naming of functions and variables is tragic and extremely shitty
168164
upload_handler_result_manifest
169-
|> prepare_result_manifest(output_manifest)
165+
|> __MODULE__.Manifest.from_upload_handler_manifest(output_manifest)
170166
else
171167
output_manifest
172168
end
@@ -176,37 +172,6 @@ defmodule ExWebRTC.Recorder.Converter do
176172

177173
def convert_manifest!(_empty_manifest, _options), do: %{}
178174

179-
# def convert_report!(report, output_path \\ @default_output_path) do
180-
# output_path = Path.expand(output_path)
181-
# File.mkdir_p!(output_path)
182-
183-
defp prepare_upload_handler_manifest(converter_result_manifest) do
184-
Enum.reduce(converter_result_manifest, %{}, fn
185-
{id, %{location: file, thumbnail_location: thumbnail}}, acc ->
186-
acc
187-
|> Map.put(id, %{location: file})
188-
|> Map.put("thumbnail_#{id}", %{location: thumbnail})
189-
190-
{id, %{location: file}}, acc ->
191-
Map.put(acc, id, %{location: file})
192-
end)
193-
end
194-
195-
defp prepare_result_manifest(upload_handler_result_manifest, original_output_manifest) do
196-
Enum.reduce(upload_handler_result_manifest, original_output_manifest, fn
197-
{"thumbnail_" <> id, %{location: thumbnail}}, acc ->
198-
Map.update(
199-
acc,
200-
id,
201-
%{thumbnail_location: thumbnail},
202-
&Map.put(&1, :thumbnail_location, thumbnail)
203-
)
204-
205-
{id, %{location: file}}, acc ->
206-
Map.update(acc, id, %{location: file}, &Map.put(&1, :location, file))
207-
end)
208-
end
209-
210175
defp fetch_remote_files!(manifest, dl_path, dl_config) do
211176
Map.new(manifest, fn {track_id, %{location: location} = track_data} ->
212177
scheme = URI.parse(location).scheme || "file"
@@ -229,7 +194,7 @@ defmodule ExWebRTC.Recorder.Converter do
229194
{:ok, _result} <- S3.Utils.fetch_file(bucket_name, s3_path, out_path, dl_config) do
230195
{:ok, out_path}
231196
else
232-
# XXX descriptive errors
197+
# FIXME: Add descriptive errors
233198
_other -> :error
234199
end
235200
end
@@ -268,49 +233,38 @@ defmodule ExWebRTC.Recorder.Converter do
268233
|> Map.update!(stream_id, &Map.put(&1, kind, output_metadata))
269234
end)
270235

236+
# FIXME: This won't work if we have audio/video only
271237
for {stream_id, %{video: video_files, audio: audio_files}} <- stream_map,
272238
{rid, %{filename: video_file, start_time: video_start}} <- video_files,
273239
{nil, %{filename: audio_file, start_time: audio_start}} <- audio_files,
274240
into: %{} do
275-
{video_start_time, audio_start_time} = calculate_start_times(video_start, audio_start)
276241
output_id = if rid == nil, do: stream_id, else: "#{stream_id}_#{rid}"
277-
278242
output_file = Path.join(output_path, "#{output_id}.webm")
279243

280-
{_io, 0} =
281-
System.cmd(
282-
"ffmpeg",
283-
~w(-ss #{video_start_time} -i #{Path.join(output_path, video_file)} -ss #{audio_start_time} -i #{Path.join(output_path, audio_file)} -c:v copy -c:a copy -shortest #{output_file}),
284-
stderr_to_stdout: true
285-
)
286-
287-
{duration, 0} =
288-
System.cmd(
289-
"ffprobe",
290-
~w(-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 #{output_file})
291-
)
244+
FFmpeg.combine_av!(
245+
Path.join(output_path, video_file),
246+
video_start,
247+
Path.join(output_path, audio_file),
248+
audio_start,
249+
output_file
250+
)
292251

293-
{duration_seconds, _rest} = Float.parse(duration)
252+
# TODO: Consider deleting the `.ivf` and `.ogg` files at this point
294253

295254
stream_manifest = %{
296255
location: output_file,
297-
duration_seconds: round(duration_seconds)
256+
duration_seconds: FFmpeg.get_duration_in_seconds!(output_file)
298257
}
299258

300-
if thumbnails_ctx do
301-
thumbnail_file = "#{output_file}_thumbnail.jpg"
302-
303-
{_io, 0} =
304-
System.cmd(
305-
"ffmpeg",
306-
~w(-i #{output_file} -vf thumbnail,scale=#{thumbnails_ctx.width}:#{thumbnails_ctx.height} -frames:v 1 #{thumbnail_file}),
307-
stderr_to_stdout: true
308-
)
259+
stream_manifest =
260+
if thumbnails_ctx do
261+
thumbnail_file = FFmpeg.generate_thumbnail!(output_file, thumbnails_ctx)
262+
Map.put(stream_manifest, :thumbnail_location, thumbnail_file)
263+
else
264+
stream_manifest
265+
end
309266

310-
{output_id, Map.put(stream_manifest, :thumbnail_location, thumbnail_file)}
311-
else
312-
{output_id, stream_manifest}
313-
end
267+
{output_id, stream_manifest}
314268
end
315269
end
316270

@@ -331,6 +285,7 @@ defmodule ExWebRTC.Recorder.Converter do
331285
frames_cnt: 0
332286
}
333287

288+
# Returns the timestamp (in milliseconds) at which the first frame was received
334289
start_time = do_convert_video_track(packets[rid_idx], conversion_state)
335290

336291
{rid, %{filename: filename, start_time: start_time}}
@@ -373,7 +328,7 @@ defmodule ExWebRTC.Recorder.Converter do
373328

374329
{:ok, depayloader} = Depayloader.new(@audio_codec_params)
375330

376-
# XXX ugleh
331+
# Same behaviour as in `convert_video_track/4`
377332
start_time = do_convert_audio_track(packets, %{depayloader: depayloader, writer: writer})
378333

379334
%{filename: filename, start_time: start_time}
@@ -452,15 +407,4 @@ defmodule ExWebRTC.Recorder.Converter do
452407
store
453408
end
454409
end
455-
456-
defp calculate_start_times(video_start_ms, audio_start_ms) do
457-
diff = abs(video_start_ms - audio_start_ms)
458-
s = div(diff, 1000)
459-
ms = rem(diff, 1000)
460-
delayed_start_time = :io_lib.format("00:00:~2..0w.~3..0w", [s, ms]) |> to_string()
461-
462-
if video_start_ms > audio_start_ms,
463-
do: {"00:00:00.000", delayed_start_time},
464-
else: {delayed_start_time, "00:00:00.000"}
465-
end
466410
end

0 commit comments

Comments
 (0)