1
1
defmodule ExWebRTC.Recorder.Converter do
2
2
@ moduledoc """
3
3
Processes RTP packet files saved by `ExWebRTC.Recorder`.
4
+
4
5
Requires the `ffmpeg` binary with the relevant libraries present in `PATH`.
5
6
6
7
At the moment, `ExWebRTC.Recorder.Converter` works only with VP8 video and Opus audio.
@@ -9,14 +10,17 @@ defmodule ExWebRTC.Recorder.Converter do
9
10
See `ExWebRTC.Recorder.S3` and `t:options/0` for more info.
10
11
"""
11
12
12
- require Logger
13
-
14
13
alias ExWebRTC.RTP.JitterBuffer.PacketStore
15
14
alias ExWebRTC.RTP.Depayloader
16
15
alias ExWebRTC.Media . { IVF , Ogg }
16
+
17
17
alias ExWebRTC.Recorder.S3
18
18
alias ExWebRTC . { Recorder , RTPCodecParameters }
19
19
20
+ alias __MODULE__ . FFmpeg
21
+
22
+ require Logger
23
+
20
24
# TODO: Allow changing these values
21
25
@ ivf_header_opts [
22
26
# <<fourcc::little-32>> = "VP80"
@@ -45,32 +49,23 @@ defmodule ExWebRTC.Recorder.Converter do
45
49
@ default_output_path "./converter/output"
46
50
@ default_download_path "./converter/download"
47
51
@ default_thumbnail_width 640
48
- # -1 means fit to aspect ratio
49
52
@ default_thumbnail_height - 1
50
53
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
-
61
54
@ typedoc """
62
- Context for the thumbnail generation
55
+ Context for the thumbnail generation.
63
56
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.
65
61
"""
66
62
@ type thumbnails_ctx :: % {
67
63
optional ( :width ) => pos_integer ( ) | - 1 ,
68
64
optional ( :height ) => pos_integer ( ) | - 1
69
65
}
70
66
71
- # XXX as well as convert_from_file...?
72
67
@ 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` .
74
69
75
70
* `:output_path` - Directory where Converter will save its artifacts. `#{ @ default_output_path } ` by default.
76
71
* `:s3_upload_config` - If passed, processed recordings will be uploaded to S3-compatible storage.
@@ -91,13 +86,12 @@ defmodule ExWebRTC.Recorder.Converter do
91
86
@ type options :: [ option ( ) ]
92
87
93
88
@ 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` .
95
90
"""
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
101
95
|> Path . expand ( )
102
96
|> then (
103
97
& if ( File . dir? ( & 1 ) ,
@@ -106,18 +100,21 @@ defmodule ExWebRTC.Recorder.Converter do
106
100
)
107
101
)
108
102
109
- report =
110
- report_path
103
+ recorder_manifest =
104
+ recorder_manifest_path
111
105
|> File . read! ( )
112
106
|> Jason . decode! ( )
107
+ |> Recorder.Manifest . from_json! ( )
113
108
114
- # XXX no maikel this is so not right
115
- convert_manifest! ( report , output_path )
109
+ convert_manifest! ( recorder_manifest , options )
116
110
end
117
111
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 \\ [ ] )
121
118
122
119
def convert_manifest! ( manifest , options ) when map_size ( manifest ) > 0 do
123
120
thumbnails_ctx =
@@ -154,19 +151,18 @@ defmodule ExWebRTC.Recorder.Converter do
154
151
if upload_handler != nil do
155
152
{ ref , upload_handler } =
156
153
output_manifest
157
- |> prepare_upload_handler_manifest ( )
154
+ |> __MODULE__ . Manifest . to_upload_handler_manifest ( )
158
155
|> then ( & S3.UploadHandler . spawn_task ( upload_handler , & 1 ) )
159
156
160
- # XXX What if upload fails?
157
+ # FIXME: Add descriptive errors
161
158
{ :ok , upload_handler_result_manifest , _handler } =
162
159
receive do
163
160
{ ^ ref , _res } = task_result ->
164
161
S3.UploadHandler . process_result ( upload_handler , task_result )
165
162
end
166
163
167
- # XXX this naming of functions and variables is tragic and extremely shitty
168
164
upload_handler_result_manifest
169
- |> prepare_result_manifest ( output_manifest )
165
+ |> __MODULE__ . Manifest . from_upload_handler_manifest ( output_manifest )
170
166
else
171
167
output_manifest
172
168
end
@@ -176,37 +172,6 @@ defmodule ExWebRTC.Recorder.Converter do
176
172
177
173
def convert_manifest! ( _empty_manifest , _options ) , do: % { }
178
174
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
-
210
175
defp fetch_remote_files! ( manifest , dl_path , dl_config ) do
211
176
Map . new ( manifest , fn { track_id , % { location: location } = track_data } ->
212
177
scheme = URI . parse ( location ) . scheme || "file"
@@ -229,7 +194,7 @@ defmodule ExWebRTC.Recorder.Converter do
229
194
{ :ok , _result } <- S3.Utils . fetch_file ( bucket_name , s3_path , out_path , dl_config ) do
230
195
{ :ok , out_path }
231
196
else
232
- # XXX descriptive errors
197
+ # FIXME: Add descriptive errors
233
198
_other -> :error
234
199
end
235
200
end
@@ -268,49 +233,38 @@ defmodule ExWebRTC.Recorder.Converter do
268
233
|> Map . update! ( stream_id , & Map . put ( & 1 , kind , output_metadata ) )
269
234
end )
270
235
236
+ # FIXME: This won't work if we have audio/video only
271
237
for { stream_id , % { video: video_files , audio: audio_files } } <- stream_map ,
272
238
{ rid , % { filename: video_file , start_time: video_start } } <- video_files ,
273
239
{ nil , % { filename: audio_file , start_time: audio_start } } <- audio_files ,
274
240
into: % { } do
275
- { video_start_time , audio_start_time } = calculate_start_times ( video_start , audio_start )
276
241
output_id = if rid == nil , do: stream_id , else: "#{ stream_id } _#{ rid } "
277
-
278
242
output_file = Path . join ( output_path , "#{ output_id } .webm" )
279
243
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
+ )
292
251
293
- { duration_seconds , _rest } = Float . parse ( duration )
252
+ # TODO: Consider deleting the `.ivf` and `.ogg` files at this point
294
253
295
254
stream_manifest = % {
296
255
location: output_file ,
297
- duration_seconds: round ( duration_seconds )
256
+ duration_seconds: FFmpeg . get_duration_in_seconds! ( output_file )
298
257
}
299
258
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
309
266
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 }
314
268
end
315
269
end
316
270
@@ -331,6 +285,7 @@ defmodule ExWebRTC.Recorder.Converter do
331
285
frames_cnt: 0
332
286
}
333
287
288
+ # Returns the timestamp (in milliseconds) at which the first frame was received
334
289
start_time = do_convert_video_track ( packets [ rid_idx ] , conversion_state )
335
290
336
291
{ rid , % { filename: filename , start_time: start_time } }
@@ -373,7 +328,7 @@ defmodule ExWebRTC.Recorder.Converter do
373
328
374
329
{ :ok , depayloader } = Depayloader . new ( @ audio_codec_params )
375
330
376
- # XXX ugleh
331
+ # Same behaviour as in `convert_video_track/4`
377
332
start_time = do_convert_audio_track ( packets , % { depayloader: depayloader , writer: writer } )
378
333
379
334
% { filename: filename , start_time: start_time }
@@ -452,15 +407,4 @@ defmodule ExWebRTC.Recorder.Converter do
452
407
store
453
408
end
454
409
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
466
410
end
0 commit comments