Skip to content

Add transcoder. Update vk-video dependency.#7

Merged
varsill merged 31 commits into
masterfrom
add_transcoder
Mar 27, 2026
Merged

Add transcoder. Update vk-video dependency.#7
varsill merged 31 commits into
masterfrom
add_transcoder

Conversation

@varsill
Copy link
Copy Markdown
Collaborator

@varsill varsill commented Mar 20, 2026

This PR:

  • adds Membrane.VKVideo.Transcoder along with Membrane.VKVideo.Transcoder.OutputConfig specifying desired output of the transcoder
  • adds transcoder.rs with transcoder-specific functions
  • adds new_transcoder(), transcoder and flush_transcoder() NIFs
  • updates the dependeny to vk-video, adjusts to its new API and update test fixtures
  • moves EncodedFrame struct from encoder.rs to common lib.rs
  • removes Cargo dependencies to wgpu and naga
  • adds transcoder integration tests
  • adds a helper mechanism to update reference files by setting TAKE_TEST_REFERENCES_SNAPSHOT=1 env

varsill and others added 2 commits March 20, 2026 15:56
@varsill varsill mentioned this pull request Mar 20, 2026
varsill added 3 commits March 24, 2026 10:41
…his behaviour. Make sure no output pads are added when playback is already playing. Add reference outputs. Bump version to v0.2.0
@varsill varsill requested review from jerzywilczek and mat-hek and removed request for jerzywilczek March 24, 2026 11:25
Comment thread lib/transcoder.ex Outdated
Comment on lines +46 to +54
def_options output_specs: [
spec: [Membrane.VKVideo.Transcoder.OutputSpec.t()],
description: """
List of output specifications. Each entry defines the target resolution,
framerate, encoder tune, rate control, and scaling algorithm for one output
stream. Output pads are referenced as `Pad.ref(:output, index)` where
`index` is the zero-based position in this list.
"""
]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make it a pad option

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread lib/transcoder/output_spec.ex Outdated
Fields:
* `width` - output frame width in pixels
* `height` - output frame height in pixels
* `frame_rate` - output frame rate as `{numerator, denominator}`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be called approx_framerate and optional, like in the encoder?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, especially that vk-video won't change the framerate. you need to pass the input framerate here. Perhaps not the best API on our part..

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed it, now there is a single approx_framerate element option which is propagated to all transcoder output configs.

Comment thread lib/transcoder.ex Outdated
Comment on lines +128 to +129
Unexpected :output pad was linked: \
#{inspect(pad_ref)}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Unexpected :output pad was linked: \
#{inspect(pad_ref)}
Pad #{inspect(pad_ref)} was linked when playback already changed to `:playing`. Make sure to link all transcoder pads in the spec where the transcoder is spawned.

Comment thread native/vkvideo/src/transcoder.rs Outdated
let transcoder = TranscoderResource { transcoder_mutex };

let resource = ResourceArc::new(Resource::Transcoder(transcoder));
Ok((ok(), resource))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we always raise on errors, what's the point of returning an ok tuple? Applies to all functions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't alwasy raise, but I think we should

Comment thread lib/transcoder/output_spec.ex Outdated
Fields:
* `width` - output frame width in pixels
* `height` - output frame height in pixels
* `frame_rate` - output frame rate as `{numerator, denominator}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, especially that vk-video won't change the framerate. you need to pass the input framerate here. Perhaps not the best API on our part..

Comment thread native/vkvideo/src/lib.rs
Comment on lines 19 to 24
pub enum Resource {
Encoder(EncoderResource),
Decoder(DecoderResource),
Transcoder(TranscoderResource),
Device(DeviceResource),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this instead of implementing Resource for each thing. A limitation of rustler? can you only have a single resource per crate?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK indeed I can define a single resource per crate 🤔

Comment thread native/vkvideo/src/transcoder.rs Outdated
.atom_to_string()?
.as_str()
{
"nearest_neighbour" => ScalingAlgorithm::NearestNeighbor,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

british spelling?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unified

Comment thread native/vkvideo/src/transcoder.rs Outdated
Comment on lines +36 to +37
let non_zero_width = std::num::NonZero::new(spec.width).ok_or(Error::BadArg)?;
let non_zero_height = std::num::NonZero::new(spec.height).ok_or(Error::BadArg)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe hard for someone to figure out which argument is bad based on the BadArg alone

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In line 52 etc you raise strings instead of returning simple errors, this should be unified I think

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added RaiseTerms with string reason instead

Comment thread native/vkvideo/src/transcoder.rs Outdated
let transcoder = TranscoderResource { transcoder_mutex };

let resource = ResourceArc::new(Resource::Transcoder(transcoder));
Ok((ok(), resource))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't alwasy raise, but I think we should

Comment thread native/vkvideo/src/transcoder.rs Outdated
.transcoder_mutex
.lock()
.map_err(|err| Error::RaiseTerm(Box::new(err.to_string())))?;
let transcoder = guard.as_mut().ok_or(Error::BadArg)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either rename guard to transcoder and remove this line, or add explicit typing in this line to unconfuse the language server. otherwise this line doesnt make sense, you can call guard.transcode() transparently

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed - we cannot get rid of this guard, I added an explicit typing

let mut guard = transcoder
.transcoder_mutex
.lock()
.map_err(|err| Error::RaiseTerm(Box::new(err.to_string())))?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this map_err makes sense here (it might, I'm just not certain). If this is an Err, it means that some thread panicked while having the mutex locked. maybe this is the correct way of handling this, if it happens in rust code the normal thing is to panic this thread too (to propagate panics to everyone who wants to access the mutex), since something hard to detect has gone very wrong

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I get it right that only if I panic when I encounter Err, other threads trying to get the lock will get Err too? (even though my thread didn't obtain the lock at all? 🤔)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, sorry explained it wrong. if any thread gets an Err, all others will too

Comment thread native/vkvideo/Cargo.toml Outdated
rustler = "0.37.0"
vk-video = { git = "https://github.com/software-mansion/smelter.git", features=["expose_parsers"] }
wgpu = "25.0.2"
termcolor = "1.4.1"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why termcolor? if you needed to add this it probably means that we need to fix something on our side, you shouldn't have to do that

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed, it was a leftover ;)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you have these tests, does that mean you have a CI runner with a GPU? can I have it too???

@varsill varsill requested a review from mat-hek March 26, 2026 08:29
Copy link
Copy Markdown
Contributor

@jerzywilczek jerzywilczek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you don't run any rust checks on CI. I'd run at least cargo fmt --check and cargo clippy -- -D warnings

Comment thread lib/transcoder.ex Outdated
Comment on lines +50 to +55
output_spec: [
spec: Membrane.VKVideo.Transcoder.OutputSpec.t(),
description: """
Output specification for this pad. Defines the target resolution,
encoder tune, rate control, and scaling algorithm for the output stream.
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather put the fields of the OutputSpec as separate pad options

Copy link
Copy Markdown
Collaborator Author

@varsill varsill Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to keep OutputSpec as a struct for the sake of convenience in passing it via rustler, but I will make it a private struct and create it based on pad options

Comment thread lib/transcoder.ex Outdated
Comment on lines 96 to 111
cond do
state.override_framerate? and is_nil(state.transcoder) ->
spawn_transcoder(state)

not state.override_framerate? ->
new_framerate = stream_format.framerate || @default_framerate

if is_nil(state.transcoder) or new_framerate != state.framerate do
%{state | framerate: new_framerate} |> spawn_transcoder()
else
{[], state}
end

true ->
{[], state}
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit convoluted, I'd first calculate the new framerate and then spawn the transcoder if it's not there or the framerate changed

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread lib/transcoder.ex Outdated
{buffer_actions ++ eos_actions, %{state | transcoder: nil}}
end

defp build_buffer_actions(outputs, state) do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outputs isn't a fortunate name, as it can mean 'output pads'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread lib/transcoder.ex Outdated
Comment on lines +156 to +158
Enum.with_index(frame_per_pads)
|> Enum.map(fn {frame, i} ->
{pad_ref, _spec} = Enum.at(state.output_specs, i)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Enum.with_index(frame_per_pads)
|> Enum.map(fn {frame, i} ->
{pad_ref, _spec} = Enum.at(state.output_specs, i)
Enum.zip(frame_per_pads, state.output_specs)
|> Enum.map(fn {frame, {pad_ref, _spec}} ->

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread lib/transcoder.ex Outdated
output_specs: %{},
device: nil,
override_framerate?: opts.approx_framerate != nil,
framerate: opts.approx_framerate
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe let's call it approx_framerate everywhere

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@varsill varsill requested a review from mat-hek March 26, 2026 13:30
Comment thread lib/transcoder.ex Outdated
def_input_pad :input,
alias Membrane.VKVideo.{DeviceServer, Native, Transcoder.OutputSpec}

def_options(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting issue 🕵️‍♀️

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread lib/transcoder.ex Outdated
def handle_init(_ctx, opts) do
state = %{
transcoder: nil,
output_specs: %{},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's change to list

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread lib/transcoder.ex Outdated
Comment on lines +135 to +145
new_framerate = state.approx_framerate || stream_format.framerate || @default_framerate

needs_spawn? =
is_nil(state.transcoder) or
(not state.override_framerate? and new_framerate != state.approx_framerate)

if needs_spawn? do
%{state | approx_framerate: new_framerate} |> spawn_transcoder()
else
{[], state}
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's improve as we spoke

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@varsill varsill requested a review from mat-hek March 26, 2026 15:22
Comment thread lib/transcoder.ex
Comment on lines 134 to 150
def handle_stream_format(:input, stream_format, _ctx, state) do
new_framerate = state.approx_framerate || stream_format.framerate || @default_framerate
new_framerate =
if not is_nil(state.approx_framerate_option) do
state.approx_framerate_option
else
stream_format.framerate || @default_framerate
end

needs_spawn? =
is_nil(state.transcoder) or
(not state.override_framerate? and new_framerate != state.approx_framerate)
old_framerate = state.approx_framerate

if needs_spawn? do
%{state | approx_framerate: new_framerate} |> spawn_transcoder()
state = %{state | approx_framerate: new_framerate}

if is_nil(state.transcoder) or new_framerate != old_framerate do
spawn_transcoder(state)
else
{[], state}
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beautiful ✨

@varsill varsill merged commit 5c96478 into master Mar 27, 2026
4 checks passed
@varsill varsill self-assigned this Mar 27, 2026
@varsill varsill added this to Smackore Mar 27, 2026
@varsill varsill moved this to Done in Smackore Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants