Skip to content

Commit

Permalink
Merge pull request #1 from membraneframework/initial_implementation
Browse files Browse the repository at this point in the history
Initial implementation
  • Loading branch information
varsill authored Dec 9, 2024
2 parents 896d9a4 + 3ce7acf commit 6454a00
Show file tree
Hide file tree
Showing 17 changed files with 651 additions and 77 deletions.
54 changes: 0 additions & 54 deletions .github/workflows/fetch_changes.yml

This file was deleted.

16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
# Membrane Template Plugin
# Membrane Transcoder Plugin

[![Hex.pm](https://img.shields.io/hexpm/v/membrane_template_plugin.svg)](https://hex.pm/packages/membrane_template_plugin)
[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_template_plugin)
[![CircleCI](https://circleci.com/gh/membraneframework/membrane_template_plugin.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_template_plugin)

This repository contains a template for new plugins.

Check out different branches for other flavors of this template.
This repository provides `Membrane.Transcoder` which is a bin that is capable
of transcoding the input audio or video stream into the descired one specified
with simple declarative API.

It's a part of the [Membrane Framework](https://membrane.stream).

## Installation

The package can be installed by adding `membrane_template_plugin` to your list of dependencies in `mix.exs`:
The package can be installed by adding `membrane_transcoder_plugin` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:membrane_template_plugin, "~> 0.1.0"}
{:membrane_transcoder_plugin, "~> 0.1.0"}
]
end
```

## Usage

TODO

## Copyright and License

Copyright 2020, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_template_plugin)
Expand Down
2 changes: 0 additions & 2 deletions lib/membrane_template.ex

This file was deleted.

118 changes: 118 additions & 0 deletions lib/transcoder.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
defmodule Membrane.Transcoder do
@moduledoc false
use Membrane.Bin

require __MODULE__.Audio
require __MODULE__.Video
require Membrane.Logger

alias __MODULE__.{Audio, ForwardingFilter, Video}
alias Membrane.{AAC, Funnel, H264, H265, Opus, RawAudio, RawVideo, RemoteStream, VP8}

@type stream_format ::
H264.t()
| H265.t()
| VP8.t()
| RawVideo.t()
| AAC.t()
| Opus.t()
| RemoteStream.t()
| RawAudio.t()

@type stream_format_module :: H264 | H265 | VP8 | RawVideo | AAC | Opus | RawAudio

@type stream_format_resolver :: (stream_format() -> stream_format() | stream_format_module())

def_input_pad :input,
accepted_format: format when Audio.is_audio_format(format) or Video.is_video_format(format)

def_output_pad :output,
accepted_format: format when Audio.is_audio_format(format) or Video.is_video_format(format)

def_options output_stream_format: [
spec: stream_format() | stream_format_module() | stream_format_resolver()
]

@impl true
def handle_init(_ctx, opts) do
spec = [
bin_input()
|> child(:forwarding_filter, ForwardingFilter),
child(:output_funnel, Funnel)
|> bin_output()
]

state =
opts
|> Map.from_struct()
|> Map.put(:input_stream_format, nil)

{[spec: spec], state}
end

@impl true
def handle_child_notification(
{:stream_format, format},
:forwarding_filter,
_ctx,
%{input_stream_format: nil} = state
) do
state =
%{state | input_stream_format: format}
|> resolve_output_stream_format()

spec =
get_child(:forwarding_filter)
|> plug_transcoding(format, state.output_stream_format)
|> get_child(:output_funnel)

{[spec: spec], state}
end

@impl true
def handle_child_notification(
{:stream_format, new_format},
:forwarding_filter,
_ctx,
%{input_stream_format: non_nil_stream_format} = state
) do
if new_format != non_nil_stream_format do
raise """
Received new stream format on transcoder's input: #{inspect(new_format)}
which doesn't match the first received input stream format: #{inspect(non_nil_stream_format)}
Transcoder doesn't support updating the input stream format.
"""
end

{[], state}
end

@impl true
def handle_child_notification(_notification, _element, _ctx, state) do
{[], state}
end

defp resolve_output_stream_format(state) do
case state.output_stream_format do
format when is_struct(format) ->
state

module when is_atom(module) ->
%{state | output_stream_format: struct(module)}

resolver when is_function(resolver) ->
%{state | output_stream_format: resolver.(state.input_stream_format)}
|> resolve_output_stream_format()
end
end

defp plug_transcoding(builder, input_format, output_format)
when Audio.is_audio_format(input_format) do
builder |> Audio.plug_audio_transcoding(input_format, output_format)
end

defp plug_transcoding(builder, input_format, output_format)
when Video.is_video_format(input_format) do
builder |> Video.plug_video_transcoding(input_format, output_format)
end
end
116 changes: 116 additions & 0 deletions lib/transcoder/audio.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
defmodule Membrane.Transcoder.Audio do
@moduledoc false

import Membrane.ChildrenSpec
require Membrane.Logger
alias Membrane.{AAC, ChildrenSpec, Opus, RawAudio, RemoteStream}

@opus_sample_rate 48_000
@aac_sample_rates [
96_000,
88_200,
64_000,
48_000,
44_100,
32_000,
24_000,
22_050,
16_000,
12_000,
11_025,
8000
]

@type audio_stream_format :: AAC.t() | Opus.t() | RawAudio.t()

defguard is_audio_format(format)
when is_struct(format) and
(format.__struct__ in [AAC, Opus, RawAudio] or
(format.__struct__ == RemoteStream and format.content_format == Opus and
format.type == :packetized))

@spec plug_audio_transcoding(
ChildrenSpec.builder(),
audio_stream_format() | RemoteStream.t(),
audio_stream_format()
) :: ChildrenSpec.builder()
def plug_audio_transcoding(builder, input_format, output_format)
when is_audio_format(input_format) and is_audio_format(output_format) do
do_plug_audio_transcoding(builder, input_format, output_format)
end

defp do_plug_audio_transcoding(builder, %format_module{}, %format_module{}) do
Membrane.Logger.debug("""
This bin will only forward buffers, as the input stream format is the same as the output stream format.
""")

builder
end

defp do_plug_audio_transcoding(builder, %RemoteStream{content_format: Opus}, %Opus{}) do
builder |> child(:opus_parser, Opus.Parser)
end

defp do_plug_audio_transcoding(builder, input_format, output_format) do
builder
|> maybe_plug_decoder(input_format)
|> maybe_plug_resampler(input_format, output_format)
|> maybe_plug_encoder(output_format)
end

defp maybe_plug_decoder(builder, %Opus{}) do
builder |> child(:opus_decoder, Opus.Decoder)
end

defp maybe_plug_decoder(builder, %RemoteStream{content_format: Opus, type: :packetized}) do
builder |> child(:opus_decoder, Opus.Decoder)
end

defp maybe_plug_decoder(builder, %AAC{}) do
builder |> child(:aac_decoder, AAC.FDK.Decoder)
end

defp maybe_plug_decoder(builder, %RawAudio{}) do
builder
end

defp maybe_plug_resampler(builder, %{sample_rate: sample_rate} = input_format, %Opus{})
when sample_rate != @opus_sample_rate do
builder
|> child(:resampler, %Membrane.FFmpeg.SWResample.Converter{
output_stream_format: %RawAudio{
sample_format: :s16le,
sample_rate: @opus_sample_rate,
channels: input_format.channels
}
})
end

defp maybe_plug_resampler(builder, %{sample_rate: sample_rate} = input_format, %AAC{})
when sample_rate not in @aac_sample_rates do
builder
|> child(:resampler, %Membrane.FFmpeg.SWResample.Converter{
output_stream_format: %RawAudio{
sample_format: :s16le,
sample_rate: 44_100,
channels: input_format.channels
}
})
end

defp maybe_plug_resampler(builder, _input_format, _output_format) do
builder
end

defp maybe_plug_encoder(builder, %Opus{}) do
builder |> child(:opus_encoder, Opus.Encoder)
end

defp maybe_plug_encoder(builder, %AAC{}) do
builder |> child(:aac_encoder, AAC.FDK.Encoder)
end

defp maybe_plug_encoder(builder, %RawAudio{}) do
builder
end
end
Loading

0 comments on commit 6454a00

Please sign in to comment.