Packages
membrane_mpeg_ts_plugin
2.3.17
2.4.9
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.18
2.3.17
2.3.16
2.3.15
2.3.14
2.3.12
2.3.11
2.3.10
2.3.9
2.3.8
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.1
2.2.0
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.2
2.0.1
2.0.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
MPEG-TS Demuxer that implements the Membrane.Filter behaviour.
Current section
Files
Jump to
Current section
Files
lib/membrane/mpeg/ts/demuxer.ex
defmodule Membrane.MPEG.TS.Demuxer do
@moduledoc """
Demuxes MPEG TS stream.
After transition into playing state, this element will wait for [Program
Association Table](https://en.wikipedia.org/wiki/MPEG_transport_stream#PAT)
and [Program Mapping
Table](https://en.wikipedia.org/wiki/MPEG_transport_stream#PMT).
Use `profile:` to select streams that require descriptors and depacketization
(e.g. Opus in MPEG-TS). For custom payloads, match `stream_type:` and
`registration_descriptor:` explicitly.
"""
use Membrane.Filter
require Membrane.Logger
alias MPEG.TS
alias MPEG.TS.Demuxer.Container
def_input_pad(:input,
accepted_format: %Membrane.RemoteStream{},
flow_control: :auto
)
def_output_pad(:output,
availability: :on_request,
accepted_format: %Membrane.RemoteStream{},
flow_control: :auto,
options: [
pid: [
spec: pos_integer() | nil,
default: nil,
description:
"The PID of the target stream. If specified, all other selection options are ignored"
],
stream_type: [
spec: atom() | nil,
default: nil,
description: """
More generic selector than pid, allows to specify the stream type of the target stream, e.g. AAC_ADTS.
The first stream that matches the selector will be returned.
"""
],
profile: [
spec: atom() | nil,
default: nil,
description: "Well-known stream profile (e.g. :opus_mpeg_ts, :scte35)."
],
registration_descriptor: [
spec: String.t() | nil,
default: nil,
description: """
Matches a registration descriptor (tag 0x05) format identifier, e.g. "Opus".
"""
],
stream_category: [
spec: atom() | nil,
default: nil,
description: """
Even more generic, select either :video, :audio, :subtitles, :cues, :metadata target streams.
The first stream that matches the selector will be returned.
"""
]
]
)
def_options(
strict?: [
spec: boolean(),
default: false,
desciption: "If true, demuxing errors will raise"
],
wait_rai?: [
spec: boolean(),
default: true,
desciption: "If true, first packet buffer is going to be a keyframe (in case of video)"
]
)
@impl true
def handle_init(_ctx, opts) do
state = %{
demuxer: TS.Demuxer.new(strict?: opts.strict?, wait_rai?: opts.wait_rai?),
subscribers: %{},
pid_to_pads: %{}
}
{[], state}
end
@impl true
def handle_stream_format(_pad, _format, _ctx, state) do
{[], state}
end
@impl true
def handle_pad_added(pad, ctx, state) do
state =
state
|> put_in([:subscribers, pad], %{opts: ctx.pad_options, bound: false})
|> refresh_pid_to_pad()
if ctx.playback == :playing do
maybe_forward_stream_format(ctx, state)
else
{[], state}
end
end
@impl true
def handle_pad_removed(pad, _ctx, state) do
state
|> update_in([:subscribers], &Map.delete(&1, pad))
|> update_in([:pid_to_pads], fn mapping ->
mapping
|> Enum.map(fn {pid, pads} -> {pid, Enum.filter(pads, fn x -> x != pad end)} end)
|> Map.new()
end)
end
@impl true
def handle_buffer(:input, buffer, ctx, state) do
{units, state} =
get_and_update_in(state, [:demuxer], fn demuxer ->
TS.Demuxer.demux(demuxer, buffer.payload)
end)
Enum.flat_map_reduce(units, state, fn unit, state ->
handle_demuxed(unit, ctx, state)
end)
end
@impl true
def handle_end_of_stream(:input, ctx, state) do
{units, state} =
get_and_update_in(state, [:demuxer], fn demuxer -> TS.Demuxer.flush(demuxer) end)
{actions, state} =
Enum.flat_map_reduce(units, state, fn unit, state ->
handle_demuxed(unit, ctx, state)
end)
eos_actions =
Enum.filter(ctx.pads, fn
{{Membrane.Pad, :output, _id}, _pad} ->
true
{_other, _pad} ->
false
end)
|> Enum.map(fn {pad_name, _pad} -> {:end_of_stream, pad_name} end)
actions = actions ++ eos_actions
{actions, state}
end
defp handle_demuxed(%Container{payload: %TS.PSI{table_type: type, table: table}}, ctx, state)
when type in [:pmt, :pat] do
actions = [notify_parent: {type, table}]
state = if type == :pmt, do: refresh_pid_to_pad(state), else: state
{format_actions, state} = maybe_forward_stream_format(ctx, state)
{actions ++ format_actions, state}
end
defp handle_demuxed(unit = %Container{pid: pid}, _ctx, state) do
pads = get_in(state, [:pid_to_pads, Access.key(pid, [])])
stream = state.demuxer |> TS.Demuxer.available_streams() |> Map.get(pid)
buffer = ts_unit_to_buffer(unit, stream)
discontinue? = get_in(buffer, [Access.key!(:metadata), Access.key(:discontinuity, false)])
actions =
Enum.flat_map(pads, fn pad ->
extras = if discontinue?, do: [{:event, {pad, %Membrane.Event.Discontinuity{}}}], else: []
extras ++ [{:buffer, {pad, buffer}}]
end)
{actions, state}
end
defp refresh_pid_to_pad(state) do
avail = TS.Demuxer.available_streams(state.demuxer)
state.subscribers
|> Enum.filter(fn {_pad, %{bound: x}} -> not x end)
|> Enum.reduce(state, fn {pad, %{opts: opts}}, state ->
# Now we shall check if there is a stream in the PMT table that
# can satisfy the stream connected stream.
cond do
opts[:pid] != nil ->
# The user specified the PID directly.
if Map.has_key?(avail, opts[:pid]), do: bind(pad, opts[:pid], state), else: state
opts[:profile] != nil ->
pid =
Enum.find_value(avail, fn {pid, x} ->
if profile_match?(x, opts[:profile]), do: pid
end)
if pid, do: bind(pad, pid, state), else: state
opts[:stream_type] != nil ->
pid =
Enum.find_value(avail, fn {pid, x} ->
if x.stream_type == opts[:stream_type] and
registration_descriptor_match?(x, opts[:registration_descriptor]) do
pid
end
end)
if pid, do: bind(pad, pid, state), else: state
opts[:stream_category] != nil ->
pid =
Enum.find_value(avail, fn {pid, x} ->
if stream_category_for(x) == opts[:stream_category] and
registration_descriptor_match?(x, opts[:registration_descriptor]) do
pid
end
end)
if pid, do: bind(pad, pid, state), else: state
opts[:registration_descriptor] != nil ->
pid =
Enum.find_value(avail, fn {pid, x} ->
if registration_descriptor_match?(x, opts[:registration_descriptor]), do: pid
end)
if pid, do: bind(pad, pid, state), else: state
true ->
state
end
end)
end
defp bind(pad, pid, state) do
avail = TS.Demuxer.available_streams(state.demuxer)
info = Map.fetch!(avail, pid)
Membrane.Logger.info("Binding #{inspect(pad)} to #{pid} (#{inspect(info.stream_type)})")
state
|> update_in([:pid_to_pads, pid], fn
nil -> [pad]
acc -> [pad | acc]
end)
|> put_in([:subscribers, pad, :bound], true)
end
defp maybe_forward_stream_format(ctx, state) do
actions =
state.pid_to_pads
|> Enum.flat_map(fn {pid, pads} ->
pads
|> Enum.filter(fn pad -> ctx.pads[pad].stream_format == nil end)
|> Enum.map(fn pad ->
stream =
state.demuxer
|> TS.Demuxer.available_streams()
|> Map.fetch!(pid)
format = %Membrane.RemoteStream{
content_format: %Membrane.MPEG.TS.StreamFormat{
stream_type: stream.stream_type,
descriptors: stream.descriptors
}
}
{:stream_format, {pad, format}}
end)
end)
{actions, state}
end
defp ts_unit_to_buffer(%Container{payload: x = %TS.PES{}}, stream) do
payload = maybe_depacketize_opus(x.data, stream)
%Membrane.Buffer{
payload: payload,
pts: x.pts,
dts: x.dts,
metadata: %{
stream_id: x.stream_id,
is_aligned: x.is_aligned,
discontinuity: x.discontinuity
}
}
end
defp ts_unit_to_buffer(%Container{payload: x = %TS.PSI{}, t: t}, _stream) do
%Membrane.Buffer{
payload: TS.Marshaler.marshal(x),
dts: t,
metadata: %{
psi: x
}
}
end
defp registration_descriptor_match?(_stream, nil), do: true
defp registration_descriptor_match?(%{descriptors: descriptors}, format_identifier) do
Enum.any?(descriptors, fn
%{tag: 0x05, data: ^format_identifier} -> true
_ -> false
end)
end
defp registration_descriptor_match?(_stream, _format_identifier), do: false
defp stream_category_for(stream) do
case resolve_profile_for_stream(stream) do
%{stream_category: category} -> category
_ -> TS.PMT.get_stream_category(stream.stream_type)
end
end
defp resolve_profile_for_stream(stream) do
Enum.find_value(Membrane.MPEG.TS.Profiles.list(), fn profile ->
profile_data = Membrane.MPEG.TS.Profiles.fetch!(profile)
if stream.stream_type == profile_data.stream_type and
descriptors_match?(stream.descriptors, profile_data.descriptors) do
profile_data
else
false
end
end)
end
defp profile_match?(stream, profile) do
profile_data = Membrane.MPEG.TS.Profiles.fetch!(profile)
stream.stream_type == profile_data.stream_type and
descriptors_match?(stream.descriptors, profile_data.descriptors)
end
defp descriptors_match?(_stream_descriptors, nil), do: true
defp descriptors_match?(stream_descriptors, required_descriptors) do
Enum.all?(required_descriptors, fn required ->
Enum.any?(stream_descriptors, &(&1 == required))
end)
end
defp maybe_depacketize_opus(payload, stream) do
case resolve_profile_for_stream(stream) do
%{packetizer: nil} ->
payload
%{packetizer: packetizer} ->
case packetizer.depacketize(payload) do
{:ok, raw} ->
raw
{:error, reason} ->
Membrane.Logger.warning("Failed to depacketize payload: #{inspect(reason)}")
payload
end
_ ->
payload
end
end
end