Skip to content

Commit dbd0b7e

Browse files
committed
Support native acceleration with Vulkan for h264 videos
1 parent b43bc1f commit dbd0b7e

7 files changed

Lines changed: 353 additions & 70 deletions

File tree

examples/vp8_to_h264.exs

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
1-
Mix.install([
2-
:membrane_file_plugin,
3-
:membrane_ivf_plugin,
4-
{:membrane_transcoder_plugin, path: __DIR__ |> Path.join("..") |> Path.expand()}
5-
])
1+
use_native = System.argv() |> Enum.member?("--native")
2+
3+
vk_dep =
4+
if use_native &&
5+
match?({_, 0}, System.cmd("pkg-config", ["--exists", "vulkan"], stderr_to_stdout: true)) do
6+
[{:membrane_vk_video_plugin, "~> 0.2.0"}]
7+
else
8+
[]
9+
end
10+
11+
Mix.install(
12+
vk_dep ++
13+
[
14+
:membrane_file_plugin,
15+
:membrane_ivf_plugin,
16+
{:membrane_transcoder_plugin, path: __DIR__ |> Path.join("..") |> Path.expand()}
17+
]
18+
)
619

720
defmodule Example do
8-
alias Membrane.RCPipeline
9-
require Membrane.RCPipeline, as: RCPipeline
21+
alias Membrane.{H264, RCPipeline}
22+
require RCPipeline
23+
1024
import Membrane.ChildrenSpec
1125

12-
def convert(input_file, output_file) do
26+
def convert(input_file, output_file, native_acceleration) do
1327
pipeline = RCPipeline.start_link!()
1428

1529
spec =
1630
child(%Membrane.File.Source{
1731
location: input_file
1832
})
1933
|> child(:deserializer, Membrane.IVF.Deserializer)
20-
|> child(:transcoder, %Membrane.Transcoder{output_stream_format: Membrane.H264})
34+
|> child(:transcoder, %Membrane.Transcoder{
35+
output_stream_format: H264,
36+
native_acceleration: native_acceleration
37+
})
2138
|> child(:sink, %Membrane.File.Sink{location: output_file})
2239

2340
RCPipeline.subscribe(pipeline, _any)
@@ -27,5 +44,25 @@ defmodule Example do
2744
end
2845
end
2946

30-
File.mkdir("tmp")
31-
Example.convert(Path.join("./test/fixtures", "video_vp8.ivf"), Path.join("./tmp", "video.h264"))
47+
File.mkdir_p!("tmp")
48+
49+
if use_native && !Membrane.Transcoder.vulkan_available?() do
50+
raise "Vulkan is not available. Cannot run with --native flag."
51+
end
52+
53+
acceleration = if use_native, do: :if_available, else: :never
54+
55+
output_file = Path.join(__DIR__, "tmp/video.h264")
56+
57+
if use_native do
58+
IO.puts("Vulkan available: true")
59+
IO.puts("Using native_acceleration: :if_available")
60+
end
61+
62+
Example.convert(
63+
Path.join(__DIR__, "../test/fixtures/video_vp8.ivf"),
64+
output_file,
65+
acceleration
66+
)
67+
68+
IO.puts("Done. Output written to #{output_file}")

lib/transcoder.ex

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ defmodule Membrane.Transcoder do
2626
Now, the only supported stream parameters are:
2727
* `:pixel_format` in `Membrane.RawVideo`
2828
* `:alignment` and `:stream_structure` in `Membrane.H264` and `Membrane.H265`
29+
30+
When the `membrane_vk_video_plugin` dependency is present and Vulkan hardware is available,
31+
H.264 encode/decode can be offloaded to the GPU by setting `native_acceleration: :if_available`.
2932
"""
3033
use Membrane.Bin
3134

@@ -139,6 +142,17 @@ defmodule Membrane.Transcoder do
139142
140143
If nil or not set, the input stream format won't be overriden.
141144
"""
145+
],
146+
native_acceleration: [
147+
spec: :never | :if_available,
148+
default: :never,
149+
description: """
150+
Specifies whether to use Vulkan hardware acceleration for video transcoding.
151+
152+
Can be:
153+
* `:never` - Always use software-based transcoding (default)
154+
* `:if_available` - Use Vulkan acceleration when available on the system
155+
"""
142156
]
143157

144158
@impl true
@@ -155,12 +169,29 @@ defmodule Membrane.Transcoder do
155169
opts
156170
|> Map.from_struct()
157171
|> Map.merge(%{
158-
input_stream_format: nil
172+
input_stream_format: nil,
173+
use_vulkan?: should_use_vulkan?(opts.native_acceleration)
159174
})
160175

161176
{[spec: spec], state}
162177
end
163178

179+
defp should_use_vulkan?(:if_available), do: vulkan_available?()
180+
defp should_use_vulkan?(_native_acceleration), do: false
181+
182+
@doc """
183+
Returns `true` if Vulkan hardware acceleration is available on this machine.
184+
185+
Use this to guard code that sets `native_acceleration: :if_available`, for example
186+
to print a diagnostic or pick a different pipeline branch.
187+
"""
188+
@spec vulkan_available?() :: boolean()
189+
def vulkan_available?() do
190+
Code.ensure_loaded?(Membrane.VKVideo.Decoder) and
191+
Code.ensure_loaded?(Membrane.VKVideo.Encoder) and
192+
Code.ensure_loaded?(Membrane.VKVideo.Native)
193+
end
194+
164195
defp maybe_plug_stream_format_changer(builder, nil), do: builder
165196

166197
defp maybe_plug_stream_format_changer(builder, enforced_stream_format) do
@@ -187,7 +218,8 @@ defmodule Membrane.Transcoder do
187218
|> plug_transcoding(
188219
format,
189220
state.output_stream_format,
190-
state.transcoding_policy
221+
state.transcoding_policy,
222+
state.use_vulkan?
191223
)
192224
|> get_child(:output_funnel)
193225

@@ -232,15 +264,15 @@ defmodule Membrane.Transcoder do
232264
end
233265
end
234266

235-
defp plug_transcoding(builder, input_format, output_format, transcoding_policy)
267+
defp plug_transcoding(builder, input_format, output_format, transcoding_policy, _use_vulkan?)
236268
when Audio.is_audio_format(input_format) do
237269
builder
238270
|> Audio.plug_audio_transcoding(input_format, output_format, transcoding_policy)
239271
end
240272

241-
defp plug_transcoding(builder, input_format, output_format, transcoding_policy)
273+
defp plug_transcoding(builder, input_format, output_format, transcoding_policy, use_vulkan?)
242274
when Video.is_video_format(input_format) do
243275
builder
244-
|> Video.plug_video_transcoding(input_format, output_format, transcoding_policy)
276+
|> Video.plug_video_transcoding(input_format, output_format, transcoding_policy, use_vulkan?)
245277
end
246278
end

lib/transcoder/video.ex

Lines changed: 119 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,50 @@ defmodule Membrane.Transcoder.Video do
3333
ChildrenSpec.builder(),
3434
video_stream_format(),
3535
video_stream_format(),
36+
:always | :if_needed | :never,
3637
boolean()
3738
) :: ChildrenSpec.builder()
38-
def plug_video_transcoding(builder, input_format, output_format, transcoding_policy)
39+
def plug_video_transcoding(
40+
builder,
41+
input_format,
42+
output_format,
43+
transcoding_policy,
44+
use_vulkan?
45+
)
3946
when is_video_format(input_format) and is_video_format(output_format) do
40-
do_plug_video_transcoding(builder, input_format, output_format, transcoding_policy)
47+
do_plug_video_transcoding(
48+
builder,
49+
input_format,
50+
output_format,
51+
transcoding_policy,
52+
use_vulkan?
53+
)
4154
end
4255

4356
defp do_plug_video_transcoding(
4457
builder,
4558
%RemoteStream{content_format: h26x},
4659
output_format,
47-
transcoding_policy
60+
transcoding_policy,
61+
use_vulkan?
4862
)
4963
when h26x in [H264, H265] do
5064
do_plug_video_transcoding(
5165
builder,
5266
struct!(h26x),
5367
output_format,
54-
transcoding_policy
68+
transcoding_policy,
69+
use_vulkan?
5570
)
5671
end
5772

58-
defp do_plug_video_transcoding(builder, %H264{}, %H264{} = output_format, transcoding_policy)
73+
defp do_plug_video_transcoding(
74+
builder,
75+
%H264{},
76+
%H264{} = output_format,
77+
transcoding_policy,
78+
_use_vulkan?
79+
)
5980
when transcoding_policy in [:if_needed, :never] do
6081
builder
6182
|> child(:h264_parser, %H264.Parser{
@@ -64,7 +85,13 @@ defmodule Membrane.Transcoder.Video do
6485
})
6586
end
6687

67-
defp do_plug_video_transcoding(builder, %H265{}, %H265{} = output_format, transcoding_policy)
88+
defp do_plug_video_transcoding(
89+
builder,
90+
%H265{},
91+
%H265{} = output_format,
92+
transcoding_policy,
93+
_use_vulkan?
94+
)
6895
when transcoding_policy in [:if_needed, :never] do
6996
builder
7097
|> child(:h265_parser, %H265.Parser{
@@ -77,7 +104,19 @@ defmodule Membrane.Transcoder.Video do
77104
builder,
78105
%RawVideo{} = input_format,
79106
%RawVideo{} = output_format,
80-
_transcoding_policy
107+
_transcoding_policy,
108+
true
109+
) do
110+
builder
111+
|> maybe_plug_swscale_converter_vulkan(input_format, output_format)
112+
end
113+
114+
defp do_plug_video_transcoding(
115+
builder,
116+
%RawVideo{} = input_format,
117+
%RawVideo{} = output_format,
118+
_transcoding_policy,
119+
false
81120
) do
82121
builder
83122
|> maybe_plug_swscale_converter(input_format, output_format)
@@ -87,7 +126,8 @@ defmodule Membrane.Transcoder.Video do
87126
builder,
88127
%format_module{},
89128
%format_module{},
90-
transcoding_policy
129+
transcoding_policy,
130+
_use_vulkan?
91131
)
92132
when transcoding_policy in [:if_needed, :never] do
93133
Membrane.Logger.debug("""
@@ -97,20 +137,40 @@ defmodule Membrane.Transcoder.Video do
97137
builder
98138
end
99139

100-
defp do_plug_video_transcoding(_builder, input_format, output_format, :never) do
101-
raise """
102-
Cannot convert input format #{inspect(input_format)} to output format #{inspect(output_format)} \
103-
with :transcoding_policy option set to :never.
104-
"""
140+
defp do_plug_video_transcoding(_builder, input_format, output_format, :never, _use_vulkan?),
141+
do:
142+
raise("""
143+
Cannot convert input format #{inspect(input_format)} to output format #{inspect(output_format)} \
144+
with :transcoding_policy option set to :never.
145+
""")
146+
147+
defp do_plug_video_transcoding(builder, input_format, output_format, _transcoding_policy, true) do
148+
builder
149+
|> maybe_plug_parser_and_decoder_vulkan(input_format)
150+
|> maybe_plug_swscale_converter_vulkan(input_format, output_format)
151+
|> maybe_plug_encoder_and_parser_vulkan(output_format)
105152
end
106153

107-
defp do_plug_video_transcoding(builder, input_format, output_format, _transcoding_policy) do
154+
defp do_plug_video_transcoding(builder, input_format, output_format, _transcoding_policy, false) do
108155
builder
109156
|> maybe_plug_parser_and_decoder(input_format)
110157
|> maybe_plug_swscale_converter(input_format, output_format)
111158
|> maybe_plug_encoder_and_parser(output_format)
112159
end
113160

161+
# VK-specific decoder: child name :vk_h264_decoder distinguishes it from FFmpeg's :h264_decoder
162+
defp maybe_plug_parser_and_decoder_vulkan(builder, %H264{}) do
163+
builder
164+
|> child(:h264_input_parser, %H264.Parser{
165+
output_stream_structure: :annexb,
166+
output_alignment: :au
167+
})
168+
|> child(:vk_h264_decoder, Membrane.VKVideo.Decoder)
169+
end
170+
171+
defp maybe_plug_parser_and_decoder_vulkan(builder, format),
172+
do: maybe_plug_parser_and_decoder(builder, format)
173+
114174
defp maybe_plug_parser_and_decoder(builder, %H264{}) do
115175
builder
116176
|> child(:h264_input_parser, %H264.Parser{
@@ -143,10 +203,41 @@ defmodule Membrane.Transcoder.Video do
143203
builder |> child(:vp8_decoder, decoder_module)
144204
end
145205

146-
defp maybe_plug_parser_and_decoder(builder, %RawVideo{}) do
147-
builder
206+
defp maybe_plug_parser_and_decoder(builder, %RawVideo{}), do: builder
207+
208+
# VK decoder outputs NV12; these clauses handle the NV12 <-> other-format conversion
209+
# when mixing VK decode/encode with different pixel formats.
210+
211+
defp maybe_plug_swscale_converter_vulkan(builder, %H264{}, %RawVideo{pixel_format: nil}),
212+
do: builder
213+
214+
defp maybe_plug_swscale_converter_vulkan(builder, %H264{}, %RawVideo{pixel_format: :NV12}),
215+
do: builder
216+
217+
defp maybe_plug_swscale_converter_vulkan(builder, %H264{}, %RawVideo{} = output_format) do
218+
builder |> child(:raw_video_converter, %SWScale.Converter{format: output_format.pixel_format})
219+
end
220+
221+
defp maybe_plug_swscale_converter_vulkan(builder, %RawVideo{pixel_format: :NV12}, %H264{}),
222+
do: builder
223+
224+
defp maybe_plug_swscale_converter_vulkan(builder, %RawVideo{}, %H264{}) do
225+
builder |> child(:raw_video_converter, %SWScale.Converter{format: :NV12})
148226
end
149227

228+
defp maybe_plug_swscale_converter_vulkan(builder, %H264{}, %H264{}), do: builder
229+
230+
defp maybe_plug_swscale_converter_vulkan(builder, %H264{}, _output_format) do
231+
builder |> child(:raw_video_converter, %SWScale.Converter{format: :I420})
232+
end
233+
234+
defp maybe_plug_swscale_converter_vulkan(builder, _input_format, %H264{}) do
235+
builder |> child(:raw_video_converter, %SWScale.Converter{format: :NV12})
236+
end
237+
238+
defp maybe_plug_swscale_converter_vulkan(builder, input_format, output_format),
239+
do: maybe_plug_swscale_converter(builder, input_format, output_format)
240+
150241
defp maybe_plug_swscale_converter(builder, input_format, %RawVideo{} = output_format) do
151242
case input_format do
152243
_any when output_format.pixel_format == nil ->
@@ -175,10 +266,20 @@ defmodule Membrane.Transcoder.Video do
175266
end
176267
end
177268

178-
defp maybe_plug_swscale_converter(builder, _input_format, _output_format) do
269+
defp maybe_plug_swscale_converter(builder, _input_format, _output_format), do: builder
270+
271+
defp maybe_plug_encoder_and_parser_vulkan(builder, %H264{} = h264) do
179272
builder
273+
|> child(:vk_h264_encoder, Membrane.VKVideo.Encoder)
274+
|> child(:h264_output_parser, %H264.Parser{
275+
output_stream_structure: stream_structure_type(h264),
276+
output_alignment: h264.alignment
277+
})
180278
end
181279

280+
defp maybe_plug_encoder_and_parser_vulkan(builder, format),
281+
do: maybe_plug_encoder_and_parser(builder, format)
282+
182283
defp maybe_plug_encoder_and_parser(builder, %H264{} = h264) do
183284
builder
184285
|> child(:h264_encoder, %H264.FFmpeg.Encoder{preset: :ultrafast})
@@ -219,9 +320,7 @@ defmodule Membrane.Transcoder.Video do
219320
builder |> child(:vp8_encoder, %VP9.Encoder{g_threads: number_of_threads, cpu_used: 15})
220321
end
221322

222-
defp maybe_plug_encoder_and_parser(builder, %RawVideo{}) do
223-
builder
224-
end
323+
defp maybe_plug_encoder_and_parser(builder, %RawVideo{}), do: builder
225324

226325
defp stream_structure_type(%h26x{stream_structure: stream_structure})
227326
when h26x in [H264, H265] do

0 commit comments

Comments
 (0)