Skip to content

Commit 9d097e9

Browse files
Add encoder (#2)
* Return error message from all vk-video errors. Update typespecs. Add handling of tune in the encoder * Add RateControl option * Add test checking different configuration of the encoder. Raise errors instead of returning the error tuple * Use DirtyIo scheduler for all NIF functions. Adjust tests to new filename * Add :requires_gpu tag to encoder tests * Add encoder element * Update typespecs. Add tests for encoder. Add support for RateControl * Add more tests for encoder * Add docs for rate control structs * Put encoder and decoder into a single NIF. (#3) * Make Resource an Enum * Add device server (#4) * Add create_device * Add Device server and destroy function * Fallback to 30 and warn if the framerate is needed but not provided via stream format nor options --------- Co-authored-by: Jerzy Wilczek <jerzy.wilczek@swmansion.com>
1 parent 6c30fba commit 9d097e9

28 files changed

Lines changed: 925 additions & 157 deletions

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[workspace]
22
resolver = "2"
3-
members = ["native/vkvideo_decoder"]
3+
members = ["native/vkvideo"]

lib/decoder.ex

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ defmodule Membrane.VKVideo.Decoder do
55
"""
66
use Membrane.Filter
77

8-
alias __MODULE__.Native
8+
alias Membrane.VKVideo.{DeviceServer, Native}
99

1010
def_input_pad :input, accepted_format: %Membrane.H264{stream_structure: :annexb, alignment: :au}
11-
def_output_pad :output, accepted_format: Membrane.RawVideo
11+
def_output_pad :output, accepted_format: %Membrane.RawVideo{pixel_format: :NV12}
1212

1313
@impl true
1414
def handle_init(_ctx, _opts) do
@@ -18,7 +18,8 @@ defmodule Membrane.VKVideo.Decoder do
1818

1919
@impl true
2020
def handle_setup(_ctx, state) do
21-
{:ok, decoder} = Native.new()
21+
{:ok, device} = DeviceServer.get_device()
22+
{:ok, decoder} = Native.new_decoder(device)
2223
state = %{state | decoder: decoder}
2324
{[], state}
2425
end
@@ -70,8 +71,9 @@ defmodule Membrane.VKVideo.Decoder do
7071

7172
@impl true
7273
def handle_end_of_stream(:input, _ctx, state) do
73-
{:ok, flushed_frames} = Native.flush(state.decoder)
74+
{:ok, flushed_frames} = Native.flush_decoder(state.decoder)
75+
:ok = Native.destroy(state.decoder)
7476
{actions, state} = Enum.flat_map_reduce(flushed_frames, state, &prepare_actions(&1, &2))
75-
{actions ++ [end_of_stream: :output], state}
77+
{actions ++ [end_of_stream: :output], %{state | decoder: nil}}
7678
end
7779
end

lib/decoder/native.ex

Lines changed: 0 additions & 30 deletions
This file was deleted.

lib/device_server.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Membrane.VKVideo.DeviceServer do
2+
@moduledoc false
3+
use GenServer
4+
alias Membrane.VKVideo.Native
5+
6+
@impl true
7+
def init(_opts) do
8+
{:ok, %{device: nil}}
9+
end
10+
11+
@impl true
12+
def handle_call(:get_device, _from, state) do
13+
state = maybe_create_device(state)
14+
{:reply, state.device, state}
15+
end
16+
17+
defp maybe_create_device(%{device: nil} = state) do
18+
device = Native.create_device()
19+
%{state | device: device}
20+
end
21+
22+
defp maybe_create_device(state), do: state
23+
24+
@spec get_device() :: {:ok, Native.t()} | no_return()
25+
def get_device() do
26+
GenServer.call(__MODULE__, :get_device)
27+
end
28+
end

lib/encoder.ex

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
defmodule Membrane.VKVideo.Encoder do
2+
@moduledoc """
3+
H.264 encoder taking advantage of hardware acceleration provided
4+
by Vulkan video extensions.
5+
"""
6+
use Membrane.Filter
7+
8+
require Membrane.Logger
9+
10+
alias Membrane.VKVideo.{DeviceServer, Native}
11+
12+
def_input_pad :input, accepted_format: %Membrane.RawVideo{pixel_format: :NV12}
13+
14+
def_output_pad :output,
15+
accepted_format: %Membrane.H264{stream_structure: :annexb, alignment: :au}
16+
17+
def_options tune: [
18+
spec: :low_latency | :high_quality,
19+
default: :low_latency,
20+
description: """
21+
Specifies whether the encoder should be optimized for minimal latency (which is
22+
important in case of livestreams) or for higher quality (applicable to offline encoding).
23+
"""
24+
],
25+
approx_framerate: [
26+
spec: {non_neg_integer(), pos_integer()} | nil,
27+
default: nil,
28+
description: """
29+
Framerate of the stream expressed in number of frames per second.
30+
It's only used by the rate control mechanism and therefore it does not need to be an exact
31+
value. If nil, the framerate will be read from the stream format's structure or set
32+
to fixed value of 30 frames per second if framerate is not provided by the stream format.
33+
"""
34+
],
35+
rate_control: [
36+
spec:
37+
:encoder_default
38+
| :disabled
39+
| {:variable_bitrate, __MODULE__.VariableBitrate.t()}
40+
| {:constant_bitrate, __MODULE__.ConstantBitrate.t()},
41+
default: :encoder_default,
42+
description: """
43+
Specifies which rate control mechanism should by used by the encoder.
44+
"""
45+
]
46+
47+
@impl true
48+
def handle_init(_ctx, opts) do
49+
state = %{
50+
encoder: nil,
51+
width: nil,
52+
height: nil,
53+
override_framerate?: opts.approx_framerate != nil,
54+
framerate: opts.approx_framerate,
55+
rate_control: opts.rate_control,
56+
tune: opts.tune
57+
}
58+
59+
{[], state}
60+
end
61+
62+
@impl true
63+
def handle_stream_format(:input, stream_format, _ctx, state) do
64+
cond do
65+
state.override_framerate? and
66+
(stream_format.width != state.width or
67+
stream_format.height != state.height) ->
68+
%{
69+
state
70+
| width: stream_format.width,
71+
height: stream_format.height
72+
}
73+
|> spawn_encoder()
74+
75+
not state.override_framerate? and
76+
(stream_format.width != state.width or stream_format.height != state.height or
77+
stream_format.framerate != state.framerate) ->
78+
%{
79+
state
80+
| width: stream_format.width,
81+
height: stream_format.height,
82+
framerate: resolve_framerate(stream_format, state)
83+
}
84+
|> spawn_encoder()
85+
86+
true ->
87+
{[], state}
88+
end
89+
end
90+
91+
defp spawn_encoder(state) do
92+
{:ok, device} = DeviceServer.get_device()
93+
94+
{:ok, encoder} =
95+
Native.new_encoder(
96+
device,
97+
state.width,
98+
state.height,
99+
state.framerate,
100+
state.tune,
101+
state.rate_control
102+
)
103+
104+
state = put_in(state, [:encoder], encoder)
105+
106+
stream_format =
107+
%Membrane.H264{
108+
stream_structure: :annexb,
109+
alignment: :au,
110+
width: state.width,
111+
height: state.height
112+
}
113+
114+
stream_format =
115+
if state.override_framerate? do
116+
stream_format
117+
else
118+
%{stream_format | framerate: state.framerate}
119+
end
120+
121+
{[stream_format: {:output, stream_format}], state}
122+
end
123+
124+
defp resolve_framerate(stream_format, state) do
125+
if is_nil(stream_format.framerate) and requires_framerate?(state.rate_control) do
126+
Membrane.Logger.warning("""
127+
Framerate is required when using #{inspect(elem(state.rate_control, 0))} rate control but it
128+
wasn't provided in the stream format nor via options. Please provide approximate framerate
129+
using `approx_framerate` option of the element.
130+
""")
131+
end
132+
133+
stream_format.framerate || {30, 1}
134+
end
135+
136+
defp requires_framerate?({:constant_bitrate, _constant_bitrate}), do: true
137+
defp requires_framerate?({:variable_bitrate, _variable_bitrate}), do: true
138+
defp requires_framerate?(_other_rate_control), do: false
139+
140+
@impl true
141+
def handle_buffer(:input, buffer, _ctx, state) do
142+
{:ok, encoded_frame} = Native.encode(state.encoder, buffer.payload, buffer.pts)
143+
144+
pts =
145+
if encoded_frame.pts_ns != nil,
146+
do: Membrane.Time.nanoseconds(encoded_frame.pts_ns),
147+
else: nil
148+
149+
{[
150+
buffer:
151+
{:output,
152+
%Membrane.Buffer{
153+
payload: encoded_frame.payload,
154+
pts: pts,
155+
dts: buffer.dts
156+
}}
157+
], state}
158+
end
159+
160+
@impl true
161+
def handle_end_of_stream(:input, _ctx, state) do
162+
:ok = Native.destroy(state.encoder)
163+
state = %{state | encoder: nil}
164+
{[end_of_stream: :output], state}
165+
end
166+
end

lib/encoder/constant_bitrate.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
defmodule Membrane.VKVideo.Encoder.ConstantBitrate do
2+
@moduledoc """
3+
Defines encoder setting for constant bitrate rate control algorithm.
4+
The following fields need to be specified:
5+
* bitrate - desired bitrate of the stream; expressed in bits per second.
6+
* virtual_buffer_size_ms - virtual buffer duration for rate control smoothing; larger values increase bitrate stability, smaller values improve responsiveness to scene changes; expressed in milliseconds, defaults to 2 seconds.
7+
"""
8+
9+
@type t :: %__MODULE__{bitrate: non_neg_integer(), virtual_buffer_size_ms: non_neg_integer()}
10+
@enforce_keys [:bitrate]
11+
defstruct @enforce_keys ++ [virtual_buffer_size_ms: 2000]
12+
end

lib/encoder/variable_bitrate.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
defmodule Membrane.VKVideo.Encoder.VariableBitrate do
2+
@moduledoc """
3+
Defines encoder setting for variable bitrate rate control algorithm.
4+
5+
The following fields need to be specified:
6+
* average_bitrate - Target average bitrate for VBR encoding; the encoder will try to meet this
7+
average over the sequence; expressed in bits per second.
8+
* max_bitrate - Maximum allowed bitrate in VBR encoding; caps peak bitrate to prevent excessive
9+
spikes while maintaining average bitrate constraints; expressed in bits per second.
10+
* virtual_buffer_size_ms - virtual buffer duration for rate control smoothing; larger values increase bitrate stability, smaller values improve responsiveness to scene changes; expressed in milliseconds, defaults to 2 seconds.
11+
"""
12+
13+
@type t :: %__MODULE__{
14+
average_bitrate: non_neg_integer(),
15+
max_bitrate: non_neg_integer(),
16+
virtual_buffer_size_ms: non_neg_integer()
17+
}
18+
@enforce_keys [:average_bitrate, :max_bitrate, :virtual_buffer_size_ms]
19+
defstruct @enforce_keys
20+
end

lib/native.ex

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
defmodule Membrane.VKVideo.Native do
2+
@moduledoc false
3+
use Rustler, otp_app: :membrane_vk_video_plugin, crate: :vkvideo
4+
5+
@type t :: reference()
6+
@type raw_frame :: %{
7+
payload: binary(),
8+
pts: non_neg_integer() | nil,
9+
width: non_neg_integer(),
10+
height: non_neg_integer()
11+
}
12+
13+
@type encoded_frame :: %{
14+
payload: binary(),
15+
pts: non_neg_integer() | nil
16+
}
17+
18+
@spec create_device() :: {:ok, t()} | no_return()
19+
def create_device(), do: :erlang.nif_error(:nif_not_loaded)
20+
21+
@spec new_decoder(t()) :: {:ok, t()} | no_return()
22+
def new_decoder(_device), do: :erlang.nif_error(:nif_not_loaded)
23+
24+
@spec decode(t(), binary(), pts_ns :: non_neg_integer() | nil) ::
25+
{:ok, raw_frame()} | no_return()
26+
def decode(_decoder, _frame, _pts \\ nil), do: :erlang.nif_error(:nif_not_loaded)
27+
28+
@spec flush_decoder(t()) ::
29+
{:ok, raw_frame()} | no_return()
30+
def flush_decoder(_decoder), do: :erlang.nif_error(:nif_not_loaded)
31+
32+
@spec new_encoder(
33+
t(),
34+
non_neg_integer(),
35+
non_neg_integer(),
36+
{non_neg_integer(), non_neg_integer()},
37+
:low_latency | :high_quality,
38+
:encoder_default
39+
| :disabled
40+
| {:variable_bitrate, Membrane.VKVideo.Encoder.VariableBitrate.t()}
41+
| {:constant_bitrate, Membrane.VKVideo.Encoder.ConstantBitrate.t()}
42+
) :: {:ok, t()} | no_return()
43+
def new_encoder(_device, _width, _height, _framerate, _tune, _rate_control),
44+
do: :erlang.nif_error(:nif_not_loaded)
45+
46+
@spec encode(t(), binary(), pts_ns :: non_neg_integer() | nil) ::
47+
{:ok, encoded_frame()} | no_return()
48+
def encode(_encoder, _raw_frame, _pts \\ nil), do: :erlang.nif_error(:nif_not_loaded)
49+
50+
@spec destroy(t()) :: :ok
51+
def destroy(_resource), do: :erlang.nif_error(:nif_not_loaded)
52+
end

lib/vk_video.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Membrane.VKVideo do
2+
@moduledoc false
3+
use Application
4+
5+
alias Membrane.VKVideo.DeviceServer
6+
7+
@impl true
8+
def start(_start_type, _start_args) do
9+
GenServer.start_link(DeviceServer, nil, name: DeviceServer)
10+
end
11+
end

0 commit comments

Comments
 (0)