Skip to content

Commit

Permalink
Sink single track (#54)
Browse files Browse the repository at this point in the history
* Single track poc

* Fix tests

* Tests

* Refactor tests

* Bump version

* Use bool in native code
  • Loading branch information
roznawsk committed Jun 15, 2023
1 parent 810dd3f commit f83b811
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 59 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The package can be installed by adding `membrane_rtmp_plugin` to your list of de
```elixir
def deps do
[
{:membrane_rtmp_plugin, "~> 0.12.1"}
{:membrane_rtmp_plugin, "~> 0.13.0"}
]
end
```
Expand Down Expand Up @@ -56,6 +56,12 @@ Streaming implementation example is provided with the following [`examples/sink.
elixir examples/sink.exs
```

If you are interested in streaming only a single track. e.g. video, use [`examples/sink_video.exs`](examples/sink_video.exs) instead:

```bash
elixir examples/sink_video.exs
```

It will connect to RTMP server provided via URL and stream H264 video and AAC audio.
RTMP server that will receive this stream can be launched with ffmpeg by running the following commands:

Expand Down
17 changes: 11 additions & 6 deletions c_src/membrane_rtmp_plugin/sink/rtmp_sink.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ const AVRational MEMBRANE_TIME_BASE = (AVRational){1, 1000000000};

void handle_init_state(State *state);

void handle_destroy_state(UnifexEnv *env, State *state);
static bool is_ready(State *state);

UNIFEX_TERM create(UnifexEnv *env, char *rtmp_url) {
UNIFEX_TERM create(UnifexEnv *env, char *rtmp_url, int audio_present, int video_present) {
State *state = unifex_alloc_state(env);
handle_init_state(state);

state->audio_present = audio_present;
state->video_present = video_present;

UNIFEX_TERM create_result;
avformat_alloc_output_context2(&state->output_ctx, NULL, "flv", rtmp_url);
if (!state->output_ctx) {
Expand Down Expand Up @@ -74,8 +77,7 @@ UNIFEX_TERM init_video_stream(UnifexEnv *env, State *state, int width,
}
memcpy(video_stream->codecpar->extradata, avc_config->data, avc_config->size);

bool ready =
(state->video_stream_index != -1 && state->audio_stream_index != -1);
bool ready = is_ready(state);
if (ready && !state->header_written) {
if (avformat_write_header(state->output_ctx, NULL) < 0) {
return unifex_raise(env, "Failed writing header");
Expand Down Expand Up @@ -111,8 +113,7 @@ UNIFEX_TERM init_audio_stream(UnifexEnv *env, State *state, int channels,
}
memcpy(audio_stream->codecpar->extradata, aac_config->data, aac_config->size);

bool ready =
(state->video_stream_index != -1 && state->audio_stream_index != -1);
bool ready = is_ready(state);
if (ready && !state->header_written) {
if (avformat_write_header(state->output_ctx, NULL) < 0) {
return unifex_raise(env, "Failed writing header");
Expand Down Expand Up @@ -248,3 +249,7 @@ void handle_destroy_state(UnifexEnv *env, State *state) {
avformat_free_context(state->output_ctx);
}
}

bool is_ready(State *state) {
return (!state->audio_present || state->audio_stream_index != -1) && (!state->video_present || state->video_stream_index != -1);
}
3 changes: 3 additions & 0 deletions c_src/membrane_rtmp_plugin/sink/rtmp_sink.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ typedef struct State State;
struct State {
AVFormatContext *output_ctx;

bool audio_present;
bool video_present;

int video_stream_index;
int64_t current_video_dts;

Expand Down
4 changes: 3 additions & 1 deletion c_src/membrane_rtmp_plugin/sink/rtmp_sink.spec.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ module Membrane.RTMP.Sink.Native
state_type "State"
interface [NIF]

spec create(rtmp_url :: string) :: {:ok :: label, state} | {:error :: label, reason :: string}
spec create(rtmp_url :: string, audio_present :: bool, video_present :: bool) ::
{:ok :: label, state} | {:error :: label, reason :: string}

# WARN: connect will conflict with POSIX function name
spec try_connect(state) ::
(:ok :: label)
Expand Down
62 changes: 62 additions & 0 deletions examples/sink_video.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Before running this example, make sure that target RTMP server is live.
# If you are streaming to eg. Youtube, you don't need to worry about it.
# If you want to test it locally, you can run the FFmpeg server with:
# ffmpeg -y -listen 1 -f flv -i rtmp://localhost:1935 -c copy dest.flv

Mix.install([
:membrane_realtimer_plugin,
:membrane_hackney_plugin,
{:membrane_rtmp_plugin, path: __DIR__ |> Path.join("..") |> Path.expand()}
])

defmodule Example do
use Membrane.Pipeline

@samples_url "https://raw.githubusercontent.com/membraneframework/static/gh-pages/samples/big-buck-bunny/bun33s_480x270.h264"

@impl true
def handle_init(_ctx, destination: destination) do
structure = [
child(:video_source, %Membrane.Hackney.Source{
location: @video_url,
hackney_opts: [follow_redirect: true]
})
|> child(:video_parser, %Membrane.H264.FFmpeg.Parser{
framerate: {25, 1},
alignment: :au,
attach_nalus?: true,
skip_until_keyframe?: true
})
|> child(:video_realtimer, Membrane.Realtimer)
|> child(:video_payloader, Membrane.MP4.Payloader.H264)
|> via_in(Pad.ref(:video, 0))
|> child(:rtmp_sink, %Membrane.RTMP.Sink{rtmp_url: destination, tracks: [:video]})
]

{[spec: structure, playback: :playing], %{}}
end

# The rest of the example module is only used for self-termination of the pipeline after processing finishes
@impl true
def handle_element_end_of_stream(:rtmp_sink, _pad, _ctx, state) do
{[terminate: :shutdown], state}
end

@impl true
def handle_element_end_of_stream(_child, _pad, _ctx, state) do
{[], state}
end
end

destination = System.get_env("RTMP_URL", "rtmp://localhost:1935")

# Initialize the pipeline and start it
{:ok, _supervisor, pipeline} = Example.start_link(destination: destination)

monitor_ref = Process.monitor(pipeline)

# Wait for the pipeline to finish
receive do
{:DOWN, ^monitor_ref, :process, _pid, _reason} ->
:ok
end
56 changes: 44 additions & 12 deletions lib/membrane_rtmp_plugin/rtmp/sink/sink.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Membrane.RTMP.Sink do
@moduledoc """
Membrane element being client-side of RTMP streams.
To work successfuly it requires to receive both audio and video streams in AAC and H264 format respectively.
It needs to receive at least one of: video stream in H264 format or audio in AAC format.
Currently it supports only:
- RTMP proper - "plain" RTMP protocol
- RTMPS - RTMP over TLS/SSL
Expand All @@ -17,15 +17,16 @@ defmodule Membrane.RTMP.Sink do

@supported_protocols ["rtmp://", "rtmps://"]
@connection_attempt_interval 500
@type track_type :: :audio | :video

def_input_pad :audio,
availability: :always,
availability: :on_request,
accepted_format: AAC,
mode: :pull,
demand_unit: :buffers

def_input_pad :video,
availability: :always,
availability: :on_request,
accepted_format: MP4.Payload,
mode: :pull,
demand_unit: :buffers
Expand All @@ -44,6 +45,13 @@ defmodule Membrane.RTMP.Sink do
Maximum number of connection attempts before failing with an error.
The attempts will happen every #{@connection_attempt_interval} ms
"""
],
tracks: [
spec: [track_type()],
default: [:audio, :video],
description: """
A list of tracks, which will be sent. Can be `:audio`, `:video` or both.
"""
]

@impl true
Expand All @@ -57,27 +65,41 @@ defmodule Membrane.RTMP.Sink do
raise ArgumentError, "Invalid max_attempts option value: #{options.max_attempts}"
end

options = %{options | tracks: Enum.uniq(options.tracks)}

unless length(options.tracks) > 0 and
Enum.all?(options.tracks, &Kernel.in(&1, [:audio, :video])) do
raise ArgumentError, "All track have to be either :audio or :video"
end

single_track? = length(options.tracks) == 1
frame_buffer = Enum.map(options.tracks, &{Pad.ref(&1, 0), nil}) |> Enum.into(%{})

state =
options
|> Map.from_struct()
|> Map.merge(%{
attempts: 0,
native: nil,
# Keys here are the pad names.
frame_buffer: %{audio: nil, video: nil},
frame_buffer: frame_buffer,
ready?: false,
# Activated when one of the source inputs gets closed. Interleaving is
# disabled, frame buffer is flushed and from that point buffers on the
# remaining pad are simply forwarded to the output.
forward_mode?: false
# Always on if a single track is connected
forward_mode?: single_track?
})

{[], state}
end

@impl true
def handle_setup(_ctx, state) do
{:ok, native} = Native.create(state.rtmp_url)
audio? = :audio in state.tracks
video? = :video in state.tracks

{:ok, native} = Native.create(state.rtmp_url, audio?, video?)

state
|> Map.put(:native, native)
Expand All @@ -90,9 +112,18 @@ defmodule Membrane.RTMP.Sink do
{build_demand(state), state}
end

@impl true
def handle_pad_added(Pad.ref(_type, stream_id), _ctx, _state) when stream_id != 0,
do: raise(ArgumentError, message: "Stream id must always be 0")

@impl true
def handle_pad_added(_pad, _ctx, state) do
{[], state}
end

@impl true
def handle_stream_format(
:video,
Pad.ref(:video, 0),
%MP4.Payload{content: %MP4.Payload.AVC1{avcc: avc_config}} = stream_format,
_ctx,
state
Expand All @@ -117,7 +148,7 @@ defmodule Membrane.RTMP.Sink do
end

@impl true
def handle_stream_format(:audio, %Membrane.AAC{} = stream_format, _ctx, state) do
def handle_stream_format(Pad.ref(:audio, 0), %Membrane.AAC{} = stream_format, _ctx, state) do
profile = AAC.profile_to_aot_id(stream_format.profile)
sr_index = AAC.sample_rate_to_sampling_frequency_id(stream_format.sample_rate)
channel_configuration = AAC.channels_to_channel_config_id(stream_format.channels)
Expand Down Expand Up @@ -161,18 +192,19 @@ defmodule Membrane.RTMP.Sink do
end

@impl true
def handle_end_of_stream(pad, _ctx, state) do
def handle_end_of_stream(Pad.ref(type, 0), _ctx, state) do
if state.forward_mode? do
Native.finalize_stream(state.native)
{[], state}
else
# The interleave logic does not work if either one of the inputs does not
# produce buffers. From this point on we act as a "forward" filter.
other_pad =
case pad do
case type do
:audio -> :video
:video -> :audio
end
|> then(&Pad.ref(&1, 0))

state = flush_frame_buffer(state)
{[demand: other_pad], %{state | forward_mode?: true}}
Expand Down Expand Up @@ -258,7 +290,7 @@ defmodule Membrane.RTMP.Sink do
end)
end

defp write_frame(state, :audio, buffer) do
defp write_frame(state, Pad.ref(:audio, 0), buffer) do
buffer_pts = buffer.pts |> Ratio.ceil()

case Native.write_audio_frame(state.native, buffer.payload, buffer_pts) do
Expand All @@ -270,7 +302,7 @@ defmodule Membrane.RTMP.Sink do
end
end

defp write_frame(state, :video, buffer) do
defp write_frame(state, Pad.ref(:video, 0), buffer) do
case Native.write_video_frame(
state.native,
buffer.payload,
Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Membrane.RTMP.Mixfile do
use Mix.Project

@version "0.12.1"
@version "0.13.0"
@github_url "https://github.com/membraneframework/membrane_rtmp_plugin"

def project do
Expand Down Expand Up @@ -42,7 +42,7 @@ defmodule Membrane.RTMP.Mixfile do
{:unifex, "~> 1.1.0"},
{:membrane_h264_ffmpeg_plugin, "~> 0.25.4"},
{:membrane_aac_plugin, "~> 0.13.0"},
{:membrane_mp4_plugin, "~> 0.19.0"},
{:membrane_mp4_plugin, "~> 0.23.0"},
{:membrane_flv_plugin, "~> 0.5.0"},
{:membrane_file_plugin, "~> 0.13.2"},
# testing
Expand Down
7 changes: 4 additions & 3 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@
"membrane_aac_plugin": {:hex, :membrane_aac_plugin, "0.13.0", "ea3bd7105abca13897be6da5ef325e9dfa55e6b83b83cee7b71d5703f108344f", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:crc, "~> 0.10.2", [hex: :crc, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.7.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.11.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "91bc75536319b1a5c962bb5c211811898553c10a2210da905de035227e9f279f"},
"membrane_cmaf_format": {:hex, :membrane_cmaf_format, "0.6.1", "89d130b5f8786d4285d395697b0f7763a2c82a02de1658cdeb4f8e37e6a6c85c", [:mix], [], "hexpm", "e916e3c8216f3bf999b069ffda94da48c9bdbe3181ce7155a458d1ccf1a97b3d"},
"membrane_common_c": {:hex, :membrane_common_c, "0.14.0", "35621d9736829bf675062dc0af66e931b0d82ff8361c2088576d63d4d002692e", [:mix], [{:membrane_core, "~> 0.11.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", "262b06e93fe3f1be57a111fa19f5cba4f26000a442ac32a59f3678e83cbb001f"},
"membrane_core": {:hex, :membrane_core, "0.11.3", "c7cedbbc5af216ce00f173b1e30cbb2ed686b0a86cac9d90bc9cff3c02f938fd", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 2.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de878caa5c5043ccd244f716a31afc9496f626013728881756cacaede72b4649"},
"membrane_file_plugin": {:hex, :membrane_file_plugin, "0.13.2", "220aa341b8d707b65c93cf8d42700a4c67340717ee4fbce3f8fc6503d182b0f8", [:mix], [{:membrane_core, "~> 0.11", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "d04159943f4f07fa45156487d9e7c19b9975678c1cda7957a6b0cca59442fae3"},
"membrane_core": {:hex, :membrane_core, "0.11.4", "1edfb8ffb1fca7f6ceff36fb51b89f02cb9a7f5ee3ee994f729b2b3ff8c5b831", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 2.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3626c29ffdfaf40514fc2bf9c62f41b52bd2088568bbcb23206ffec1d45a1f92"},
"membrane_file_plugin": {:hex, :membrane_file_plugin, "0.13.3", "aaf40a72e5fccf6da47ec85ef525234acbec828425f1300f74c464bca1487f40", [:mix], [{:membrane_core, "~> 0.11", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "1c1acf610d4fc0279b7fd65a5db06c8bc3ef6a276bf40fb033c2c735c25839ba"},
"membrane_flv_plugin": {:hex, :membrane_flv_plugin, "0.5.0", "003dca9a7a45013a0cca9dea9d45fce3747091aa61afe463a55dc103243abe56", [:mix], [{:bimap, "~> 1.2", [hex: :bimap, repo: "hexpm", optional: false]}, {:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.7.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.11.2", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.5.0", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_mp4_format, "~> 0.7.0", [hex: :membrane_mp4_format, repo: "hexpm", optional: false]}], "hexpm", "fa1938fac0308b11893092246fec80ed15e0418e9fd82af0ca1bc143bc1229e0"},
"membrane_h264_ffmpeg_plugin": {:hex, :membrane_h264_ffmpeg_plugin, "0.25.4", "7486583caae894a3bb38cce585a3546280af5c8cf22fe6a11af5b7ced9fc55e3", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.14.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.11.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.5.0", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_raw_video_format, "~> 0.2.0", [hex: :membrane_raw_video_format, repo: "hexpm", optional: false]}, {:ratio, "~> 2.4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "3590a8f608de799c45404cbaecf52da23b8cfe81aeb9fec70ead0b9220839888"},
"membrane_h264_format": {:hex, :membrane_h264_format, "0.5.0", "95c1cc00896dab6de322cee3be0c9b0b0840537b4cc49e82a34e901b4e4763f9", [:mix], [], "hexpm", "7b44e0b02d86b45d24699de3c970186dffbca6d51aca80bbed2f6a3fe5c9eba1"},
"membrane_h264_plugin": {:hex, :membrane_h264_plugin, "0.2.1", "d1643644afd45e45f5bd9b47e52fa02c6e17a6371d3045417096f6a2dea69a13", [:mix], [{:bunch, "~> 1.4", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.11.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.5.0", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}], "hexpm", "875f9ccbee6cada3181b7b54ee2e89a22816e6372dfa9ea190725dc7908a57fe"},
"membrane_hackney_plugin": {:hex, :membrane_hackney_plugin, "0.9.0", "9736e48c3213295f5060e755133465535578a51fe5f01e073696dbe53903478e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.11.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:mockery, "~> 2.3", [hex: :mockery, repo: "hexpm", optional: false]}], "hexpm", "719971528cf23167b07a344eadc2b97ecdc648c2ea5937f87d98aa3c7e9fa3cf"},
"membrane_mp4_format": {:hex, :membrane_mp4_format, "0.7.0", "0cc33f21dc571b43b4d2db66a056e2b7eecdc7ada71a9e0e923ab7a1554f15f2", [:mix], [], "hexpm", "7653a20e7b0c048ea05ffad6df88249abf4c1b63772c14dee843acffcad6b038"},
"membrane_mp4_plugin": {:hex, :membrane_mp4_plugin, "0.19.0", "83ea8d0734d1be638c21c5e5a774c33c8ea6afcaa9cc2b77d6b13d38eb414de0", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.7.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_cmaf_format, "~> 0.6.0", [hex: :membrane_cmaf_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.11.2", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_file_plugin, "~> 0.13.2", [hex: :membrane_file_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.5.0", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_mp4_format, "~> 0.7.0", [hex: :membrane_mp4_format, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}], "hexpm", "19bc8663348a204dc1489493c571372775e52a1afecb869fc0fe7faa918917bc"},
"membrane_mp4_plugin": {:hex, :membrane_mp4_plugin, "0.23.0", "e2af987f957d2f35c8ae8a89339fdabd4c775bf9a042b19cc5fd0d8114efc611", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.7.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_cmaf_format, "~> 0.6.0", [hex: :membrane_cmaf_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.11.2", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_file_plugin, "~> 0.13.3", [hex: :membrane_file_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.5.0", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_h264_plugin, "~> 0.2.1", [hex: :membrane_h264_plugin, repo: "hexpm", optional: false]}, {:membrane_mp4_format, "~> 0.7.0", [hex: :membrane_mp4_format, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}], "hexpm", "7db1130b5627074837a99248e70fd69ff37675ca514ab23c1f6281b2c77ba3c0"},
"membrane_opus_format": {:hex, :membrane_opus_format, "0.3.0", "3804d9916058b7cfa2baa0131a644d8186198d64f52d592ae09e0942513cb4c2", [:mix], [], "hexpm", "8fc89c97be50de23ded15f2050fe603dcce732566fe6fdd15a2de01cb6b81afe"},
"membrane_raw_video_format": {:hex, :membrane_raw_video_format, "0.2.0", "cda8eb207cf65c93690a19001aba3edbb2ba5d22abc8068a1f6a785ba871e8cf", [:mix], [], "hexpm", "6b716fc24f60834323637c95aaaa0f99be23fcc6a84a21af70195ef50185b634"},
"membrane_stream_plugin": {:hex, :membrane_stream_plugin, "0.2.0", "002323fbb69efd1d82bc467023fdf08e9fca9136569e761c9195e85257d8ce22", [:mix], [{:membrane_core, "~> 0.11.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "68a62fa4fd9a4519ba621e197ffaa5838e293dfd64398e78dfdfdc927dae432a"},
Expand Down
Binary file added test/fixtures/bun33s_audio.flv
Binary file not shown.
Binary file added test/fixtures/bun33s_video.flv
Binary file not shown.
Loading

0 comments on commit f83b811

Please sign in to comment.