From 7028ae277d7bf2f28470f12e65d285956e1f38a7 Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:33:53 +0200 Subject: [PATCH 1/3] Recorder fixes --- lib/ex_webrtc_recorder/converter.ex | 348 +++++++++++-------- lib/ex_webrtc_recorder/converter/ffmpeg.ex | 44 ++- lib/ex_webrtc_recorder/converter/pipeline.ex | 95 +++++ mix.exs | 7 + mix.lock | 29 +- 5 files changed, 373 insertions(+), 150 deletions(-) create mode 100644 lib/ex_webrtc_recorder/converter/pipeline.ex diff --git a/lib/ex_webrtc_recorder/converter.ex b/lib/ex_webrtc_recorder/converter.ex index 27dc01e..910ba76 100644 --- a/lib/ex_webrtc_recorder/converter.ex +++ b/lib/ex_webrtc_recorder/converter.ex @@ -12,26 +12,14 @@ defmodule ExWebRTC.Recorder.Converter do alias ExWebRTC.RTP.JitterBuffer.PacketStore alias ExWebRTC.RTP.Depayloader - alias ExWebRTC.Media.{IVF, Ogg} alias ExWebRTC.Recorder.S3 alias ExWebRTC.{Recorder, RTPCodecParameters} - alias __MODULE__.FFmpeg + alias __MODULE__.{FFmpeg, Pipeline} require Logger - # TODO: Allow changing these values - @ivf_header_opts [ - # <> = "VP80" - fourcc: 808_996_950, - height: 720, - width: 1280, - num_frames: 1024, - timebase_denum: 30, - timebase_num: 1 - ] - # TODO: Support codecs other than VP8/Opus @video_codec_params %RTPCodecParameters{ payload_type: 96, @@ -50,6 +38,10 @@ defmodule ExWebRTC.Recorder.Converter do @default_download_path "./converter/download" @default_thumbnail_width 640 @default_thumbnail_height -1 + @default_reorder_buffer_size 100 + @default_reencode_bitrate "1.5M" + @default_reencode_gop_size 125 + @default_reencode_cues_to_front true @typedoc """ Context for the thumbnail generation. @@ -64,6 +56,22 @@ defmodule ExWebRTC.Recorder.Converter do optional(:height) => pos_integer() | -1 } + @typedoc """ + Context for the video reencoding. See `man ffmpeg` for more details. + + * `:threads` - How many threads to use. Unlimited by default. + * `:bitrate` - Video bitrate the VP8 encoder shall strive for. `#{@default_reencode_bitrate}` by default. + * `:gop_size` - Keyframe interval. #{@default_reencode_gop_size} by default. + * `:cues_to_front` - Whether the muxer should put MKV Cues element at the front of the file, + to aid with seeking e.g. when streaming the result file. #{@default_reencode_cues_to_front} by default. + """ + @type reencode_ctx :: %{ + optional(:threads) => pos_integer(), + optional(:bitrate) => String.t(), + optional(:gop_size) => pos_integer(), + optional(:cues_to_front) => boolean() + } + @typedoc """ Options that can be passed to `convert!/2`. @@ -80,6 +88,11 @@ defmodule ExWebRTC.Recorder.Converter do the layers with RIDs present in this list. E.g. if you want to receive a single video file from the layer `"h"`, pass `["h"]`. For single-layer tracks RID is set to `nil`, so if you want to handle both simulcast and regular tracks, pass `["h", nil]`. + * `:reorder_buffer_size` - Size of the buffer used for reordering late packets. `#{@default_reorder_buffer_size}` by default. + Increasing this value may help with "Decoded late RTP packet" warnings, + but keep in mind that larger values slow the conversion process considerably. + * `:reencode_ctx` - If passed, Converter will reencode the video using FFmpeg. + See `t:reencode_ctx/0` for more info. """ @type option :: {:output_path, Path.t()} @@ -88,6 +101,8 @@ defmodule ExWebRTC.Recorder.Converter do | {:s3_download_config, keyword()} | {:thumbnails_ctx, thumbnails_ctx()} | {:only_rids, [ExWebRTC.MediaStreamTrack.rid() | nil]} + | {:reorder_buffer_size, pos_integer()} + | {:reencode_ctx, reencode_ctx()} @type options :: [option()] @@ -121,17 +136,8 @@ defmodule ExWebRTC.Recorder.Converter do end def convert!(recorder_manifest, options) when map_size(recorder_manifest) > 0 do - thumbnails_ctx = - case Keyword.get(options, :thumbnails_ctx) do - nil -> - nil - - ctx -> - %{ - width: ctx[:width] || @default_thumbnail_width, - height: ctx[:height] || @default_thumbnail_height - } - end + thumbnails_ctx = get_thumbnails_ctx(options) + reencode_ctx = get_reencode_ctx(options) rid_allowed? = case Keyword.get(options, :only_rids) do @@ -142,6 +148,8 @@ defmodule ExWebRTC.Recorder.Converter do fn rid -> Enum.member?(allowed_rids, rid) end end + reorder_buffer_size = Keyword.get(options, :reorder_buffer_size, @default_reorder_buffer_size) + output_path = Keyword.get(options, :output_path, @default_output_path) |> Path.expand() download_path = Keyword.get(options, :download_path, @default_download_path) |> Path.expand() File.mkdir_p!(output_path) @@ -155,41 +163,47 @@ defmodule ExWebRTC.Recorder.Converter do S3.UploadHandler.new(options[:s3_upload_config]) end - output_manifest = - recorder_manifest - |> fetch_remote_files!(download_path, download_config) - |> do_convert_manifest!(output_path, thumbnails_ctx, rid_allowed?) - - result_manifest = - if upload_handler != nil do - {ref, upload_handler} = - output_manifest - |> __MODULE__.Manifest.to_upload_handler_manifest() - |> then(&S3.UploadHandler.spawn_task(upload_handler, &1)) - - # FIXME: Add descriptive errors - {:ok, upload_handler_result_manifest, _handler} = - receive do - {^ref, _res} = task_result -> - S3.UploadHandler.process_result(upload_handler, task_result) - end + recorder_manifest + |> fetch_remote_files!(download_path, download_config) + |> do_convert_manifest!( + output_path, + thumbnails_ctx, + rid_allowed?, + reorder_buffer_size, + reencode_ctx + ) + |> maybe_upload_result!(upload_handler) + end - # UploadHandler spawns a task that gets auto-monitored - # We don't want to propagate this message - receive do - {:DOWN, ^ref, :process, _pid, _reason} -> :ok - end + def convert!(_empty_manifest, _options), do: %{} - upload_handler_result_manifest - |> __MODULE__.Manifest.from_upload_handler_manifest(output_manifest) - else - output_manifest - end + defp get_thumbnails_ctx(options) do + case Keyword.get(options, :thumbnails_ctx) do + nil -> + nil - result_manifest + ctx -> + %{ + width: ctx[:width] || @default_thumbnail_width, + height: ctx[:height] || @default_thumbnail_height + } + end end - def convert!(_empty_manifest, _options), do: %{} + defp get_reencode_ctx(options) do + case Keyword.get(options, :reencode_ctx) do + nil -> + nil + + ctx -> + %{ + threads: ctx[:threads], + bitrate: ctx[:bitrate] || @default_reencode_bitrate, + gop_size: ctx[:gop_size] || @default_reencode_gop_size, + cues_to_front: ctx[:cues_to_front] || @default_reencode_cues_to_front + } + end + end defp fetch_remote_files!(manifest, dl_path, dl_config) do Map.new(manifest, fn {track_id, %{location: location} = track_data} -> @@ -218,9 +232,16 @@ defmodule ExWebRTC.Recorder.Converter do end end - defp do_convert_manifest!(manifest, output_path, thumbnails_ctx, rid_allowed?) do + defp do_convert_manifest!( + manifest, + output_path, + thumbnails_ctx, + rid_allowed?, + reorder_buffer_size, + reencode_ctx + ) do stream_map = - Enum.reduce(manifest, %{}, fn {id, track}, stream_map -> + Enum.reduce(manifest, %{}, fn {_id, track}, stream_map -> %{ location: path, kind: kind, @@ -233,44 +254,58 @@ defmodule ExWebRTC.Recorder.Converter do packets = read_packets( file, - Map.new(rid_map, fn {_rid, rid_idx} -> {rid_idx, %PacketStore{}} end) + Map.new(rid_map, fn {_rid, rid_idx} -> + {rid_idx, %{store: %PacketStore{}, acc: [], packets_in_store: 0}} + end), + reorder_buffer_size ) - output_metadata = + track_contexts = case kind do :video -> rid_map = filter_rids(rid_map, rid_allowed?) - convert_video_track(id, rid_map, output_path, packets) + get_video_track_contexts(rid_map, packets) :audio -> - %{nil: convert_audio_track(id, output_path, packets |> Map.values() |> hd())} + %{nil: get_audio_track_context(packets |> Map.values() |> hd())} end stream_id = List.first(streams) stream_map |> Map.put_new(stream_id, %{video: %{}, audio: %{}}) - |> Map.update!(stream_id, &Map.put(&1, kind, output_metadata)) + |> Map.update!(stream_id, &Map.put(&1, kind, track_contexts)) end) # FIXME: This won't work if we have audio/video only - for {stream_id, %{video: video_files, audio: audio_files}} <- stream_map, - {rid, %{filename: video_file, start_time: video_start}} <- video_files, - {nil, %{filename: audio_file, start_time: audio_start}} <- audio_files, + for {stream_id, %{video: video_contexts, audio: audio_contexts}} <- stream_map, + {rid, video_ctx} <- video_contexts, + {nil, audio_ctx} <- audio_contexts, into: %{} do output_id = if rid == nil, do: stream_id, else: "#{stream_id}_#{rid}" output_file = Path.join(output_path, "#{output_id}.webm") + video_stream = make_stream(self(), :video) + audio_stream = make_stream(self(), :audio) + + {:ok, _sup, pid} = Pipeline.start_link(video_stream, audio_stream, output_file) + Process.monitor(pid) + + # FIXME: Possible RC here? + emit_packets(pid, video_ctx.packets, audio_ctx.packets) + FFmpeg.combine_av!( - Path.join(output_path, video_file), - video_start, - Path.join(output_path, audio_file), - audio_start, - output_file + output_file <> "_video.webm", + video_ctx.start_time, + output_file <> "_audio.webm", + audio_ctx.start_time, + output_file, + reencode_ctx ) - # TODO: Consider deleting the `.ivf` and `.ogg` files at this point + File.rm!(output_file <> "_video.webm") + File.rm!(output_file <> "_audio.webm") stream_manifest = %{ location: output_file, @@ -289,94 +324,106 @@ defmodule ExWebRTC.Recorder.Converter do end end - defp convert_video_track(id, rid_map, output_path, packets) do - for {rid, rid_idx} <- rid_map, into: %{} do - filename = if rid == nil, do: "#{id}.ivf", else: "#{id}_#{rid}.ivf" - - {:ok, writer} = - output_path - |> Path.join(filename) - |> IVF.Writer.open(@ivf_header_opts) - - {:ok, depayloader} = Depayloader.new(@video_codec_params) - - conversion_state = %{ - depayloader: depayloader, - writer: writer, - frames_cnt: 0 - } + defp maybe_upload_result!(output_manifest, nil) do + output_manifest + end - # Returns the timestamp (in milliseconds) at which the first frame was received - start_time = do_convert_video_track(packets[rid_idx], conversion_state) + defp maybe_upload_result!(output_manifest, upload_handler) do + {ref, upload_handler} = + output_manifest + |> __MODULE__.Manifest.to_upload_handler_manifest() + |> then(&S3.UploadHandler.spawn_task(upload_handler, &1)) + + # FIXME: Add descriptive errors + {:ok, upload_handler_result_manifest, _handler} = + receive do + {^ref, _res} = task_result -> + S3.UploadHandler.process_result(upload_handler, task_result) + end - {rid, %{filename: filename, start_time: start_time}} + # UploadHandler spawns a task that gets auto-monitored + # We don't want to propagate this message + receive do + {:DOWN, ^ref, :process, _pid, _reason} -> :ok end - end - defp do_convert_video_track([], %{writer: writer} = state) do - IVF.Writer.close(writer) - - state[:first_frame_recv_time] + upload_handler_result_manifest + |> __MODULE__.Manifest.from_upload_handler_manifest(output_manifest) end - defp do_convert_video_track([packet | rest], state) do - case Depayloader.depayload(state.depayloader, packet) do - {nil, depayloader} -> - do_convert_video_track(rest, %{state | depayloader: depayloader}) + defp make_stream(pid, kind) do + Stream.resource( + fn -> pid end, + fn pid -> + send(pid, {:demand, kind, self()}) - {vp8_frame, depayloader} -> - {:ok, %ExRTP.Packet.Extension{id: 1, data: <>}} = - ExRTP.Packet.fetch_extension(packet, 1) + receive do + {^kind, nil} -> + {:halt, pid} - frame = %IVF.Frame{timestamp: state.frames_cnt, data: vp8_frame} - {:ok, writer} = IVF.Writer.write_frame(state.writer, frame) + {^kind, packet} -> + {[packet], pid} + end + end, + fn _pid -> :ok end + ) + end - state = - %{state | depayloader: depayloader, writer: writer, frames_cnt: state.frames_cnt + 1} - |> Map.put_new(:first_frame_recv_time, recv_time) + defp emit_packets(pipeline_pid, video_packets, audio_packets) do + receive do + {:demand, :video, pid} -> + {p, video_packets} = List.pop_at(video_packets, 0) + send(pid, {:video, p}) + emit_packets(pipeline_pid, video_packets, audio_packets) - do_convert_video_track(rest, state) + {:demand, :audio, pid} -> + {p, audio_packets} = List.pop_at(audio_packets, 0) + send(pid, {:audio, p}) + emit_packets(pipeline_pid, video_packets, audio_packets) + + {:DOWN, _monitor, :process, ^pipeline_pid, _reason} -> + :ok end end - defp convert_audio_track(id, output_path, packets) do - filename = "#{id}.ogg" - - {:ok, writer} = - output_path - |> Path.join(filename) - |> Ogg.Writer.open() + defp get_video_track_contexts(rid_map, packets) do + for {rid, rid_idx} <- rid_map, into: %{} do + {:ok, depayloader} = Depayloader.new(@video_codec_params) - {:ok, depayloader} = Depayloader.new(@audio_codec_params) + start_time = get_start_time(packets[rid_idx], depayloader) - # Same behaviour as in `convert_video_track/4` - start_time = do_convert_audio_track(packets, %{depayloader: depayloader, writer: writer}) + video_ctx = %{ + packets: packets[rid_idx], + start_time: start_time + } - %{filename: filename, start_time: start_time} + {rid, video_ctx} + end end - defp do_convert_audio_track([], %{writer: writer} = state) do - Ogg.Writer.close(writer) - - state[:first_frame_recv_time] - end + defp get_audio_track_context(packets) do + {:ok, depayloader} = Depayloader.new(@audio_codec_params) - defp do_convert_audio_track([packet | rest], state) do - {opus_packet, depayloader} = Depayloader.depayload(state.depayloader, packet) + start_time = get_start_time(packets, depayloader) - {:ok, %ExRTP.Packet.Extension{id: 1, data: <>}} = - ExRTP.Packet.fetch_extension(packet, 1) + %{packets: packets, start_time: start_time} + end - {:ok, writer} = Ogg.Writer.write_packet(state.writer, opus_packet) + # Returns the timestamp (in milliseconds) at which the first frame was received + defp get_start_time([packet | rest], depayloader) do + case Depayloader.depayload(depayloader, packet) do + {nil, depayloader} -> + get_start_time(rest, depayloader) - state = - %{state | depayloader: depayloader, writer: writer} - |> Map.put_new(:first_frame_recv_time, recv_time) + {_frame, _depayloader} -> + {:ok, %ExRTP.Packet.Extension{id: 1, data: <>}} = + ExRTP.Packet.fetch_extension(packet, 1) - do_convert_audio_track(rest, state) + recv_time + end end - defp read_packets(file, stores) do + defp read_packets(file, state, reorder_buffer_size) do case read_packet(file) do {:ok, rid_idx, recv_time, packet} -> packet = @@ -385,16 +432,28 @@ defmodule ExWebRTC.Recorder.Converter do data: <> }) - stores = Map.update!(stores, rid_idx, &insert_packet_to_store(&1, packet)) - read_packets(file, stores) + state = + if state[rid_idx][:packets_in_store] == reorder_buffer_size do + Map.update!(state, rid_idx, &flush_packet_from_store(&1)) + else + state + end + + state = Map.update!(state, rid_idx, &insert_packet_to_store(&1, packet)) + read_packets(file, state, reorder_buffer_size) {:error, reason} -> Logger.warning("Error decoding RTP packet: #{inspect(reason)}") - read_packets(file, stores) + read_packets(file, state, reorder_buffer_size) :eof -> - Map.new(stores, fn {rid_idx, store} -> - {rid_idx, store |> PacketStore.dump() |> Enum.reject(&is_nil/1)} + Map.new(state, fn {rid_idx, %{store: store, acc: acc}} -> + recent_packets = PacketStore.dump(store) + + packets = + acc |> Enum.reverse() |> Stream.concat(recent_packets) |> Enum.reject(&is_nil/1) + + {rid_idx, packets} end) end end @@ -418,17 +477,24 @@ defmodule ExWebRTC.Recorder.Converter do end end - defp insert_packet_to_store(store, packet) do + defp insert_packet_to_store(%{store: store, packets_in_store: n} = layer_state, packet) do case PacketStore.insert(store, packet) do {:ok, store} -> - store + %{layer_state | store: store, packets_in_store: n + 1} {:error, :late_packet} -> Logger.warning("Decoded late RTP packet") - store + layer_state end end + defp flush_packet_from_store(%{store: store, packets_in_store: n, acc: acc} = layer_state) do + {entry, store} = PacketStore.flush_one(store) + packet = if is_nil(entry), do: nil, else: entry.packet + + %{layer_state | store: store, packets_in_store: n - 1, acc: [packet | acc]} + end + defp filter_rids(rid_map, rid_allowed?) do Map.filter(rid_map, fn {rid, _rid_idx} -> rid_allowed?.(rid) end) end diff --git a/lib/ex_webrtc_recorder/converter/ffmpeg.ex b/lib/ex_webrtc_recorder/converter/ffmpeg.ex index 910e04f..14f6a6d 100644 --- a/lib/ex_webrtc_recorder/converter/ffmpeg.ex +++ b/lib/ex_webrtc_recorder/converter/ffmpeg.ex @@ -3,22 +3,47 @@ defmodule ExWebRTC.Recorder.Converter.FFmpeg do alias ExWebRTC.Recorder.Converter - @spec combine_av!(Path.t(), integer(), Path.t(), integer(), Path.t()) :: Path.t() | no_return() + @spec combine_av!( + Path.t(), + integer(), + Path.t(), + integer(), + Path.t(), + Converter.reencode_ctx() | nil + ) :: Path.t() | no_return() def combine_av!( video_file, video_start_timestamp_ms, audio_file, audio_start_timestamp_ms, - output_file + output_file, + reencode_ctx \\ nil ) do {video_start_time, audio_start_time} = calculate_start_times(video_start_timestamp_ms, audio_start_timestamp_ms) + reencode_flags = + case reencode_ctx do + nil -> + ~w(-c:v copy) + + %{ + threads: threads, + bitrate: bitrate, + gop_size: gop_size, + cues_to_front: cues_to_front + } -> + if(threads == nil, do: ~w(), else: ~w(-threads #{threads})) ++ + ~w(-c:v vp8 -b:v #{bitrate} -g #{gop_size}) ++ + if cues_to_front, do: ~w(-cues_to_front 1), else: ~w() + end + {_io, 0} = System.cmd( "ffmpeg", - # FIXME: we're assuming a lot here - ~w(-ss #{video_start_time} -i #{video_file} -ss #{audio_start_time} -i #{audio_file} -c:v vp8 -threads 8 -b:v 1.5M -cues_to_front 1 -g 150 -c:a copy -shortest #{output_file}), + ~w(-nostdin -ss #{video_start_time} -i #{video_file} -ss #{audio_start_time} -i #{audio_file}) ++ + reencode_flags ++ + ~w(-c:a copy -shortest #{output_file}), stderr_to_stdout: true ) @@ -32,7 +57,7 @@ defmodule ExWebRTC.Recorder.Converter.FFmpeg do {_io, 0} = System.cmd( "ffmpeg", - ~w(-i #{file} -vf thumbnail,scale=#{thumbnails_ctx.width}:#{thumbnails_ctx.height} -frames:v 1 #{thumbnail_file}), + ~w(-nostdin -i #{file} -vf thumbnail,scale=#{thumbnails_ctx.width}:#{thumbnails_ctx.height} -frames:v 1 #{thumbnail_file}), stderr_to_stdout: true ) @@ -58,9 +83,12 @@ defmodule ExWebRTC.Recorder.Converter.FFmpeg do defp calculate_start_times(video_start_ms, audio_start_ms) do diff = abs(video_start_ms - audio_start_ms) - s = div(diff, 1000) - ms = rem(diff, 1000) - delayed_start_time = :io_lib.format("00:00:~2..0w.~3..0w", [s, ms]) |> to_string() + millis = rem(diff, 1000) + seconds = div(diff, 1000) |> rem(60) + minutes = div(diff, 60_000) + + delayed_start_time = + :io_lib.format("00:~2..0w:~2..0w.~3..0w", [minutes, seconds, millis]) |> to_string() if video_start_ms > audio_start_ms, do: {"00:00:00.000", delayed_start_time}, diff --git a/lib/ex_webrtc_recorder/converter/pipeline.ex b/lib/ex_webrtc_recorder/converter/pipeline.ex new file mode 100644 index 0000000..e1a1ce5 --- /dev/null +++ b/lib/ex_webrtc_recorder/converter/pipeline.ex @@ -0,0 +1,95 @@ +defmodule ExWebRTC.Recorder.Converter.Pipeline do + defmodule Source do + @moduledoc false + use Membrane.Source + + def_options( + stream: [ + spec: Enumerable.t() + ] + ) + + def_output_pad(:output, accepted_format: Membrane.RTP, flow_control: :manual) + + @impl true + def handle_init(_ctx, %__MODULE__{stream: stream}) do + {[], %{stream: stream}} + end + + @impl true + def handle_playing(_ctx, state) do + {[stream_format: {:output, %Membrane.RTP{}}], state} + end + + @impl true + def handle_demand(:output, size, :buffers, _ctx, state) do + {actions, state} = + case Enum.take(state.stream, size) do + [] -> + {[end_of_stream: :output], state} + + packets -> + {[buffer: {:output, Enum.map(packets, &to_membrane_buffer/1)}, redemand: :output], + state} + end + + {actions, state} + end + + defp to_membrane_buffer(packet) do + %Membrane.Buffer{payload: packet.payload, metadata: %{rtp: %{packet | payload: <<>>}}} + end + end + + @moduledoc false + use Membrane.Pipeline + + def start_link(video_stream, audio_stream, output_path) do + Membrane.Pipeline.start_link(__MODULE__, %{ + video_stream: video_stream, + audio_stream: audio_stream, + output_path: output_path + }) + end + + @impl true + def handle_init(_ctx, opts) do + # TODO: Support codecs other than VP8/Opus + spec = [ + child(:video_source, %Source{stream: opts.video_stream}) + |> child(:video_depayloader, %Membrane.RTP.DepayloaderBin{ + clock_rate: 90_000, + depayloader: Membrane.RTP.VP8.Depayloader + }) + |> child(:video_muxer, Membrane.Matroska.Muxer) + |> child(:video_sink, %Membrane.File.Sink{location: opts.output_path <> "_video.webm"}), + child(:audio_source, %Source{stream: opts.audio_stream}) + |> child(:audio_depayloader, %Membrane.RTP.DepayloaderBin{ + clock_rate: 48_000, + depayloader: Membrane.RTP.Opus.Depayloader + }) + |> child(:opus_parser, Membrane.Opus.Parser) + |> child(:audio_muxer, Membrane.Matroska.Muxer) + |> child(:audio_sink, %Membrane.File.Sink{location: opts.output_path <> "_audio.webm"}) + ] + + {[spec: spec], %{}} + end + + @impl true + def handle_element_end_of_stream(sink, _pad, _ctx, state) + when sink in [:video_sink, :audio_sink] do + state = Map.update(state, :sinks_done, 1, &(&1 + 1)) + + if state.sinks_done == 2 do + {[terminate: :normal], state} + else + {[], state} + end + end + + @impl true + def handle_element_end_of_stream(_element, _pad, _ctx, state) do + {[], state} + end +end diff --git a/mix.exs b/mix.exs index c4ac97e..6688970 100644 --- a/mix.exs +++ b/mix.exs @@ -58,6 +58,13 @@ defmodule ExWebRTC.Recorder.MixProject do [ {:ex_webrtc, "~> 0.13.0"}, {:jason, "~> 1.4"}, + {:membrane_core, "~> 1.2"}, + {:membrane_rtp_plugin, "~> 0.31.0"}, + {:membrane_rtp_vp8_plugin, "~> 0.9.0"}, + {:membrane_rtp_opus_plugin, "~> 0.10.0"}, + {:membrane_vpx_plugin, "~> 0.4.0"}, + {:membrane_opus_plugin, "~> 0.20.0"}, + {:membrane_matroska_plugin, "~> 0.6.1"}, {:ex_aws_s3, "~> 2.5", optional: true}, {:ex_aws, "~> 2.5", optional: true}, {:sweet_xml, "~> 0.7", optional: true}, diff --git a/mix.lock b/mix.lock index 70e4bca..600e337 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,10 @@ %{ + "bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"}, "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, "bundlex": {:hex, :bundlex, "1.5.4", "3726acd463f4d31894a59bbc177c17f3b574634a524212f13469f41c4834a1d9", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "e745726606a560275182a8ac1c8ebd5e11a659bb7460d8abf30f397e59b4c5d2"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, "crc": {:hex, :crc, "0.10.5", "ee12a7c056ac498ef2ea985ecdc9fa53c1bfb4e53a484d9f17ff94803707dfd8", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3e673b6495a9525c5c641585af1accba59a1eb33de697bedf341e247012c2c7f"}, "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, @@ -25,22 +27,47 @@ "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, + "heap": {:hex, :heap, "2.0.2", "d98cb178286cfeb5edbcf17785e2d20af73ca57b5a2cf4af584118afbcf917eb", [:mix], [], "hexpm", "ba9ea2fe99eb4bcbd9a8a28eaf71cbcac449ca1d8e71731596aace9028c9d429"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "membrane_common_c": {:hex, :membrane_common_c, "0.16.0", "caf3f29d2f5a1d32d8c2c122866110775866db2726e4272be58e66dfdf4bce40", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "a3c7e91de1ce1f8b23b9823188a5d13654d317235ea0ca781c05353ed3be9b1c"}, + "membrane_core": {:hex, :membrane_core, "1.2.3", "0e23f50b2e7dfe95dd6047cc341807991f9d0349cd98455cc5cbfab41ba5233c", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0 or ~> 4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e3099dcb52d136a4aef84b6fbb20905ea55d9f0d2d6726f7b589e8d169a55cd"}, + "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.17.2", "650e134c2345d946f930082fac8bac9f5aba785a7817d38a9a9da41ffc56fa92", [:mix], [{:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "df50c6040004cd7b901cf057bd7e99c875bbbd6ae574efc93b2c753c96f43b9d"}, + "membrane_funnel_plugin": {:hex, :membrane_funnel_plugin, "0.9.2", "2b2e840dbb232ce29aaff2d55bd329d9978766518dbeb6e8dba7aba7115fadcc", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "865ac9d84f86698e2cfeb7904d3b12ab74855a38ca651a880db1505965fa77cc"}, + "membrane_h264_format": {:hex, :membrane_h264_format, "0.6.1", "44836cd9de0abe989b146df1e114507787efc0cf0da2368f17a10c47b4e0738c", [:mix], [], "hexpm", "4b79be56465a876d2eac2c3af99e115374bbdc03eb1dea4f696ee9a8033cd4b0"}, + "membrane_ivf_plugin": {:hex, :membrane_ivf_plugin, "0.8.0", "0495a4fd34a1b9841d288b3a078d2b09c48020294a6452ce08e8954711648ac8", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_file_plugin, "~> 0.17.0", [hex: :membrane_file_plugin, repo: "hexpm", optional: false]}, {:membrane_vp8_format, "~> 0.5.0", [hex: :membrane_vp8_format, repo: "hexpm", optional: false]}, {:membrane_vp9_format, "~> 0.5.0", [hex: :membrane_vp9_format, repo: "hexpm", optional: false]}], "hexpm", "b17805a451f1066dab68c19577efad7819200c20970e3ba5f7d6fbbc751240e9"}, + "membrane_matroska_format": {:hex, :membrane_matroska_format, "0.1.0", "99902bac6c46cc783776038d91d7bb035a917933b260343204fd6261d911f039", [:mix], [], "hexpm", "8bf180165ea9bb4094673818df5989fe6bd44b752a86dc071daafe611af1f3cc"}, + "membrane_matroska_plugin": {:hex, :membrane_matroska_plugin, "0.6.1", "0759be00e35ae9a38b63c31eb9c6d40392a34fc8a1a532dbc9074387f0b7fd23", [:mix], [{:bimap, "~> 1.2", [hex: :bimap, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_file_plugin, "~> 0.17.0", [hex: :membrane_file_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.1", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_matroska_format, "~> 0.1.0", [hex: :membrane_matroska_format, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}, {:membrane_vp8_format, "~> 0.5.0", [hex: :membrane_vp8_format, repo: "hexpm", optional: false]}, {:membrane_vp9_format, "~> 0.5.0", [hex: :membrane_vp9_format, repo: "hexpm", optional: false]}, {:qex, "~> 0.5.1", [hex: :qex, repo: "hexpm", optional: false]}], "hexpm", "9db2a7ea8056c4c7ac6b0e61dc4913f556962e8489691ed971e1bfd6d2cd3cd5"}, + "membrane_opus_format": {:hex, :membrane_opus_format, "0.3.0", "3804d9916058b7cfa2baa0131a644d8186198d64f52d592ae09e0942513cb4c2", [:mix], [], "hexpm", "8fc89c97be50de23ded15f2050fe603dcce732566fe6fdd15a2de01cb6b81afe"}, + "membrane_opus_plugin": {:hex, :membrane_opus_plugin, "0.20.5", "aa344bb9931c8e22b2286778cce0658e0d4aa071a503c18c55e1b161e17ab337", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.2", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "94fd4447b6576780afc6144dbb0520b43bd399c86a10bf5df1fa878a91798cf6"}, "membrane_precompiled_dependency_provider": {:hex, :membrane_precompiled_dependency_provider, "0.1.2", "8af73b7dc15ba55c9f5fbfc0453d4a8edfb007ade54b56c37d626be0d1189aba", [:mix], [{:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "7fe3e07361510445a29bee95336adde667c4162b76b7f4c8af3aeb3415292023"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "membrane_raw_audio_format": {:hex, :membrane_raw_audio_format, "0.12.0", "b574cd90f69ce2a8b6201b0ccf0826ca28b0fbc8245b8078d9f11cef65f7d5d5", [:mix], [{:bimap, "~> 1.1", [hex: :bimap, repo: "hexpm", optional: false]}, {:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "6e6c98e3622a2b9df19eab50ba65d7eb45949b1ba306fa8423df6cdb12fd0b44"}, + "membrane_raw_video_format": {:hex, :membrane_raw_video_format, "0.4.1", "d7344499c2d80f236a7ef962b5490c651341a501052ee43dec56cf0319fa3936", [:mix], [], "hexpm", "9920b7d445b5357608a364fec5685acdfce85334c647f745045237a0d296c442"}, + "membrane_rtp_format": {:hex, :membrane_rtp_format, "0.11.0", "0091465530ab946bd4d8affe849e9a01a79e46b750f6ec420521bcb806d5be49", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "bea52c2f286d5edb90d647bf3916f4e9e0270d5c6eddb708b65880570c1e42d2"}, + "membrane_rtp_opus_plugin": {:hex, :membrane_rtp_opus_plugin, "0.10.1", "7c81d3edb8dfe3c53dd35061f402ee6278d07678f8518aa9014f8c5406a475f6", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}, {:membrane_rtp_format, "~> 0.11.0", [hex: :membrane_rtp_format, repo: "hexpm", optional: false]}], "hexpm", "161db3d03e9db3d5d2b1c850942ae3f37500ccf769afbfb9d38dca2eb1d09726"}, + "membrane_rtp_plugin": {:hex, :membrane_rtp_plugin, "0.31.0", "f2f3147de028f5b8f0839f22015c5d1e54f3a6f3a26886c878ed6d8c98245bc2", [:mix], [{:bimap, "~> 1.2", [hex: :bimap, repo: "hexpm", optional: false]}, {:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:ex_libsrtp, "~> 0.6.0 or ~> 0.7.0", [hex: :ex_libsrtp, repo: "hexpm", optional: true]}, {:ex_rtcp, "~> 0.4.0", [hex: :ex_rtcp, repo: "hexpm", optional: false]}, {:ex_rtp, "~> 0.4.0", [hex: :ex_rtp, repo: "hexpm", optional: false]}, {:heap, "~> 2.0.2", [hex: :heap, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_funnel_plugin, "~> 0.9.0", [hex: :membrane_funnel_plugin, repo: "hexpm", optional: false]}, {:membrane_rtp_format, "~> 0.11.0", [hex: :membrane_rtp_format, repo: "hexpm", optional: false]}, {:membrane_telemetry_metrics, "~> 0.1.0", [hex: :membrane_telemetry_metrics, repo: "hexpm", optional: false]}, {:qex, "~> 0.5.1", [hex: :qex, repo: "hexpm", optional: false]}], "hexpm", "7dc0d1b2e6e4eea6027bb645859134949a4a6e17c403926fd427eb333407a236"}, + "membrane_rtp_vp8_plugin": {:hex, :membrane_rtp_vp8_plugin, "0.9.5", "61072297ed4737fd0e6e0d791ebbb19c7ae920f9b2db3487a2f7d185560791d5", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_rtp_format, "~> 0.11.0", [hex: :membrane_rtp_format, repo: "hexpm", optional: false]}, {:membrane_vp8_format, "~> 0.4.0 or ~> 0.5.0", [hex: :membrane_vp8_format, repo: "hexpm", optional: false]}], "hexpm", "5c60fa6979b84d436969b134ff41d6353eb61caffdc11312101b99de56a47231"}, + "membrane_telemetry_metrics": {:hex, :membrane_telemetry_metrics, "0.1.1", "57917e72012f9ebe124eab54f29ca74c9d9eb3ae2207f55c95618ee51280eb4f", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "69392966e6bd51937244758c2b3d835c5ff47d8953d25431a9d37059737afc11"}, + "membrane_vp8_format": {:hex, :membrane_vp8_format, "0.5.0", "a589c20bb9d97ddc9b717684d00cefc84e2500ce63a0c33c4b9618d9b2f9b2ea", [:mix], [], "hexpm", "d29e0dae4bebc6838e82e031c181fe626d168c687e4bc617c1d0772bdeed19d5"}, + "membrane_vp9_format": {:hex, :membrane_vp9_format, "0.5.0", "c6a4f2cbfc39dba5d80ad8287162c52b5cf6488676bd64435c1ac957bd16e66f", [:mix], [], "hexpm", "68752d8cbe7270ec222fc84a7d1553499f0d8ff86ef9d9e89f8955d49e20278e"}, + "membrane_vpx_plugin": {:hex, :membrane_vpx_plugin, "0.4.0", "911df2079ef20c28c945001b1f048556690ff3dd4b2d586a0c03d3ba03d83a34", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_video_format, "~> 0.4.0", [hex: :membrane_raw_video_format, repo: "hexpm", optional: false]}, {:membrane_vp8_format, "~> 0.5.0", [hex: :membrane_vp8_format, repo: "hexpm", optional: false]}, {:membrane_vp9_format, "~> 0.5.0", [hex: :membrane_vp9_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.2", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "921f86af2ff0efe072fc1bce7873845fa9f02eebd27df5ed658c42d4d655b402"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, + "ratio": {:hex, :ratio, "4.0.1", "3044166f2fc6890aa53d3aef0c336f84b2bebb889dc57d5f95cc540daa1912f8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "c60cbb3ccdff9ffa56e7d6d1654b5c70d9f90f4d753ab3a43a6bf40855b881ce"}, "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, "shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "unifex": {:hex, :unifex, "1.2.1", "6841c170a6e16509fac30b19e4e0a19937c33155a59088b50c15fc2c36251b6b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "8c9d2e3c48df031e9995dd16865bab3df402c0295ba3a31f38274bb5314c7d37"}, "zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"}, } From 2030d670e776a1d94b50422a099b6c3eafc7c190 Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:03:59 +0200 Subject: [PATCH 2/3] Review fixes --- .formatter.exs | 3 ++- lib/ex_webrtc_recorder/converter.ex | 24 ++++++++++++++------ lib/ex_webrtc_recorder/converter/pipeline.ex | 18 +++++++++++---- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index d2cda26..4bd914b 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,5 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + import_deps: [:membrane_core] ] diff --git a/lib/ex_webrtc_recorder/converter.ex b/lib/ex_webrtc_recorder/converter.ex index 910ba76..6b65cd6 100644 --- a/lib/ex_webrtc_recorder/converter.ex +++ b/lib/ex_webrtc_recorder/converter.ex @@ -61,9 +61,9 @@ defmodule ExWebRTC.Recorder.Converter do * `:threads` - How many threads to use. Unlimited by default. * `:bitrate` - Video bitrate the VP8 encoder shall strive for. `#{@default_reencode_bitrate}` by default. - * `:gop_size` - Keyframe interval. #{@default_reencode_gop_size} by default. + * `:gop_size` - Keyframe interval. `#{@default_reencode_gop_size}` by default. * `:cues_to_front` - Whether the muxer should put MKV Cues element at the front of the file, - to aid with seeking e.g. when streaming the result file. #{@default_reencode_cues_to_front} by default. + to aid with seeking e.g. when streaming the result file. `#{@default_reencode_cues_to_front}` by default. """ @type reencode_ctx :: %{ optional(:threads) => pos_integer(), @@ -92,6 +92,9 @@ defmodule ExWebRTC.Recorder.Converter do Increasing this value may help with "Decoded late RTP packet" warnings, but keep in mind that larger values slow the conversion process considerably. * `:reencode_ctx` - If passed, Converter will reencode the video using FFmpeg. + The keyframe interval of video tracks sent over WebRTC may vary, so this is helpful when + you want to generate additional ones, to facilitate accurate seeking in the result file during playback. + Keep in mind that reenncoding is slow and resource-intensive. See `t:reencode_ctx/0` for more info. """ @type option :: @@ -240,6 +243,10 @@ defmodule ExWebRTC.Recorder.Converter do reorder_buffer_size, reencode_ctx ) do + # What's happening here: + # 1. Read tracks + # 2. Convert tracks to WEBM files + # 3. Mux WEBM files into a single file stream_map = Enum.reduce(manifest, %{}, fn {_id, track}, stream_map -> %{ @@ -264,11 +271,10 @@ defmodule ExWebRTC.Recorder.Converter do case kind do :video -> rid_map = filter_rids(rid_map, rid_allowed?) - get_video_track_contexts(rid_map, packets) :audio -> - %{nil: get_audio_track_context(packets |> Map.values() |> hd())} + get_audio_track_context(packets) end stream_id = List.first(streams) @@ -289,10 +295,14 @@ defmodule ExWebRTC.Recorder.Converter do video_stream = make_stream(self(), :video) audio_stream = make_stream(self(), :audio) + # FIXME: Possible RC here: when the pipeline playback starts, the `Stream`s will start sending + # `{:demand, kind, self()}` messages to this process. + # There's no guarantee we'll start listening for these messages (in `emit_packets/3`) soon enough, + # so we may reach a deadlock. + # For now, it seems to work just fine, though. {:ok, _sup, pid} = Pipeline.start_link(video_stream, audio_stream, output_file) Process.monitor(pid) - # FIXME: Possible RC here? emit_packets(pid, video_ctx.packets, audio_ctx.packets) FFmpeg.combine_av!( @@ -401,12 +411,12 @@ defmodule ExWebRTC.Recorder.Converter do end end - defp get_audio_track_context(packets) do + defp get_audio_track_context(%{nil: packets}) do {:ok, depayloader} = Depayloader.new(@audio_codec_params) start_time = get_start_time(packets, depayloader) - %{packets: packets, start_time: start_time} + %{nil: %{packets: packets, start_time: start_time}} end # Returns the timestamp (in milliseconds) at which the first frame was received diff --git a/lib/ex_webrtc_recorder/converter/pipeline.ex b/lib/ex_webrtc_recorder/converter/pipeline.ex index e1a1ce5..393c4ef 100644 --- a/lib/ex_webrtc_recorder/converter/pipeline.ex +++ b/lib/ex_webrtc_recorder/converter/pipeline.ex @@ -1,13 +1,20 @@ defmodule ExWebRTC.Recorder.Converter.Pipeline do + # This pipeline: + # - takes the sorted RTP packets from Converter, + # - depayloads the VP8/Opus media within, + # - and dumps them into two separate WEBM files. + # + # The result file is then generated using FFmpeg. + # We have to do it this way, because at the moment `Membrane.Matroska.Muxer` ignores the difference in PTS + # of the audio and video tracks, which means we're unable to synchronize them using only Membrane. + defmodule Source do @moduledoc false use Membrane.Source - def_options( - stream: [ - spec: Enumerable.t() - ] - ) + def_options stream: [ + spec: Enumerable.t() + ] def_output_pad(:output, accepted_format: Membrane.RTP, flow_control: :manual) @@ -55,6 +62,7 @@ defmodule ExWebRTC.Recorder.Converter.Pipeline do @impl true def handle_init(_ctx, opts) do # TODO: Support codecs other than VP8/Opus + # TODO: Use a single muxer + sink once `Membrane.Matroska.Muxer` supports synchronizing AV spec = [ child(:video_source, %Source{stream: opts.video_stream}) |> child(:video_depayloader, %Membrane.RTP.DepayloaderBin{ From 75336e123eee79e4f89f67ebb10d7b63d2d03958 Mon Sep 17 00:00:00 2001 From: Jakub Pisarek <99591440+sgfn@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:56:06 +0200 Subject: [PATCH 3/3] Bump ex_webrtc to 0.14. Release 0.3 --- README.md | 4 ++-- mix.exs | 4 ++-- mix.lock | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d4dbe3e..5e22fb8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Add `:ex_webrtc_recorder` to your list of dependencies ```elixir def deps do [ - {:ex_webrtc_recorder, "~> 0.2.1"} + {:ex_webrtc_recorder, "~> 0.3.0"} ] end ``` @@ -28,7 +28,7 @@ but it must be explicitly turned on by adding the following dependencies: ```elixir def deps do [ - {:ex_webrtc_recorder, "~> 0.2.1"}, + {:ex_webrtc_recorder, "~> 0.3.0"}, {:ex_aws_s3, "~> 2.5"}, {:ex_aws, "~> 2.5"}, {:sweet_xml, "~> 0.7"}, diff --git a/mix.exs b/mix.exs index 6688970..320e31e 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule ExWebRTC.Recorder.MixProject do use Mix.Project - @version "0.2.1" + @version "0.3.0" @source_url "https://github.com/elixir-webrtc/ex_webrtc_recorder" def project do @@ -56,7 +56,7 @@ defmodule ExWebRTC.Recorder.MixProject do defp deps do [ - {:ex_webrtc, "~> 0.13.0"}, + {:ex_webrtc, "~> 0.14.0"}, {:jason, "~> 1.4"}, {:membrane_core, "~> 1.2"}, {:membrane_rtp_plugin, "~> 0.31.0"}, diff --git a/mix.lock b/mix.lock index 600e337..efe8a92 100644 --- a/mix.lock +++ b/mix.lock @@ -6,15 +6,15 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, "crc": {:hex, :crc, "0.10.5", "ee12a7c056ac498ef2ea985ecdc9fa53c1bfb4e53a484d9f17ff94803707dfd8", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3e673b6495a9525c5c641585af1accba59a1eb33de697bedf341e247012c2c7f"}, - "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, - "ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.6", "d135983bbd8b6df6350dfd83999437725527c1bea151e5055760bfc9b2d17c20", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "9874e12847e469ca2f13a5689be04e546c16f63caf6380870b7f25bf7cb98875"}, - "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, + "ex_aws": {:hex, :ex_aws, "2.5.10", "d3f8ca8959dad6533a2a934dfdf380df1b1bef425feeb215a47a5176dee8736c", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "88fcd9cc1b2e0fcea65106bdaa8340ac56c6e29bf72f46cf7ef174027532d3da"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.7", "e571424d2f345299753382f3a01b005c422b1a460a8bc3ed47659b3d3ef91e9e", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "858e51241e50181e29aa2bc128fef548873a3a9cd580471f57eda5b64dec937f"}, + "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, "ex_dtls": {:hex, :ex_dtls, "0.17.0", "dbe1d494583a307c26148cb5ea5d7c14e65daa8ec96cc73002cc3313ce4b9a81", [:mix], [{:bundlex, "~> 1.5.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "3eaa7221ec08fa9e4bc9430e426cbd5eb4feb8d8f450b203cf39b2114a94d713"}, "ex_ice": {:hex, :ex_ice, "0.12.0", "b52ec3ff878d5fb632ef9facc7657dfdf59e2ff9f23e634b0918e6ce1a05af48", [:mix], [{:elixir_uuid, "~> 1.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}, {:ex_turn, "~> 0.2.0", [hex: :ex_turn, repo: "hexpm", optional: false]}], "hexpm", "a86024a5fbf9431082784be4bb3606d3cde9218fb325a9f208ccd6e0abfd0d73"}, "ex_libsrtp": {:hex, :ex_libsrtp, "0.7.2", "211bd89c08026943ce71f3e2c0231795b99cee748808ed3ae7b97cd8d2450b6b", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2e20645d0d739a4ecdcf8d4810a0c198120c8a2f617f2b75b2e2e704d59f492a"}, @@ -23,7 +23,7 @@ "ex_sdp": {:hex, :ex_sdp, "1.1.1", "1a7b049491e5ec02dad9251c53d960835dc5631321ae978ec331831f3e4f6d5f", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}], "hexpm", "1b13a72ac9c5c695b8824dbdffc671be8cbb4c0d1ccb4ff76a04a6826759f233"}, "ex_stun": {:hex, :ex_stun, "0.2.0", "feb1fc7db0356406655b2a617805e6c712b93308c8ea2bf0ba1197b1f0866deb", [:mix], [], "hexpm", "1e01ba8290082ccbf37acaa5190d1f69b51edd6de2026a8d6d51368b29d115d0"}, "ex_turn": {:hex, :ex_turn, "0.2.0", "4e1f9b089e9a5ee44928d12370cc9ea7a89b84b2f6256832de65271212eb80de", [:mix], [{:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "08e884f0af2c4a147e3f8cd4ffe33e3452a256389f0956e55a8c4d75bf0e74cd"}, - "ex_webrtc": {:hex, :ex_webrtc, "0.13.0", "17e9c4954cb19ae67db51eaeea4dec35f5aab399fdaf9d340f86b85750b0e7ff", [:mix], [{:crc, "~> 0.10", [hex: :crc, repo: "hexpm", optional: false]}, {:ex_dtls, "~> 0.17.0", [hex: :ex_dtls, repo: "hexpm", optional: false]}, {:ex_ice, "~> 0.12.0", [hex: :ex_ice, repo: "hexpm", optional: false]}, {:ex_libsrtp, "~> 0.7.1", [hex: :ex_libsrtp, repo: "hexpm", optional: false]}, {:ex_rtcp, "~> 0.4.0", [hex: :ex_rtcp, repo: "hexpm", optional: false]}, {:ex_rtp, "~> 0.4.0", [hex: :ex_rtp, repo: "hexpm", optional: false]}, {:ex_sctp, "0.1.2", [hex: :ex_sctp, repo: "hexpm", optional: true]}, {:ex_sdp, "~> 1.0", [hex: :ex_sdp, repo: "hexpm", optional: false]}], "hexpm", "714a918afd476b1e9b6b6b25cd6498345bf2b94aa7ed683506b8428144bca7e0"}, + "ex_webrtc": {:hex, :ex_webrtc, "0.14.0", "47d3d100fb2294ac82e06269335b702a3f5c5c55cae90444c38e7b78ed6ee79d", [:mix], [{:crc, "~> 0.10", [hex: :crc, repo: "hexpm", optional: false]}, {:ex_dtls, "~> 0.17.0", [hex: :ex_dtls, repo: "hexpm", optional: false]}, {:ex_ice, "~> 0.12.0", [hex: :ex_ice, repo: "hexpm", optional: false]}, {:ex_libsrtp, "~> 0.7.1", [hex: :ex_libsrtp, repo: "hexpm", optional: false]}, {:ex_rtcp, "~> 0.4.0", [hex: :ex_rtcp, repo: "hexpm", optional: false]}, {:ex_rtp, "~> 0.4.0", [hex: :ex_rtp, repo: "hexpm", optional: false]}, {:ex_sctp, "0.1.2", [hex: :ex_sctp, repo: "hexpm", optional: true]}, {:ex_sdp, "~> 1.0", [hex: :ex_sdp, repo: "hexpm", optional: false]}], "hexpm", "280d66c8fedefd78e1a38b734fb25e1fd7ddf4f2daa897ee5afd11aa20d311f8"}, "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},