Packages
membrane_hls_plugin
2.0.13
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.1.9
2.1.8
2.1.7
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.16
2.0.15
2.0.14
2.0.13
2.0.12
2.0.11
2.0.10
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
Adaptive live streaming (HLS) plugin for the Membrane Framework.
Current section
Files
Jump to
Current section
Files
lib/membrane/hls/webvtt/aggregator.ex
defmodule Membrane.HLS.WebVTT.Aggregator do
use Membrane.Filter
alias Membrane.{Buffer, Time}
alias Subtitle.WebVTT
def_input_pad(:input,
availability: :always,
accepted_format: Membrane.Text
)
def_output_pad(:output,
availability: :always,
accepted_format: Membrane.Text
)
def_options(
segment_duration: [spec: Time.t(), default: Time.seconds(6)],
headers: [
spec: [%WebVTT.HeaderLine{}],
default: [%Subtitle.WebVTT.HeaderLine{key: :description, original: "WEBVTT"}]
],
omit_repetition: [
spec: boolean(),
default: false,
description:
"When true, cues that span across segment boundaries are not repeated in both segments"
],
relative_mpeg_ts_timestamps: [
spec: boolean(),
default: false,
description:
"If true, each segment will have a X-TIMESTAMP-MAP header and its contents will be relative to that timing."
]
)
@impl true
def handle_init(_ctc, opts) do
{[],
%{
omit_repetition: opts.omit_repetition,
segment_duration: opts.segment_duration,
relative_mpeg_ts_timestamps: opts.relative_mpeg_ts_timestamps,
headers: opts.headers,
segment: nil
}}
end
@impl true
def handle_buffer(:input, buffer, ctx, state = %{segment: nil}) do
# If the segment is nil, it means this is the first buffer
# and the option resume was set.
from = buffer.pts
to = from + state.segment_duration
segment = new_segment(from, to)
state = put_in(state, [:segment], segment)
handle_buffer(:input, buffer, ctx, state)
end
def handle_buffer(:input, buffer, _ctx, state) do
{buffers, state} = put_and_get(state, buffer, [])
{[buffer: {:output, buffers}], state}
end
@impl true
def handle_end_of_stream(_, _ctx, state) do
case state.segment do
nil ->
{[end_of_stream: :output], state}
segment ->
buffer = segment_to_buffer(segment, state)
{[buffer: {:output, buffer}, end_of_stream: :output], put_in(state, [:segment], nil)}
end
end
defp buffer_to_cue(buffer) do
%Subtitle.Cue{
text: String.trim(buffer.payload),
from: Time.as_milliseconds(buffer.pts, :round),
to: Time.as_milliseconds(buffer.metadata.to, :round)
}
end
defp new_segment(from, to) do
%{from: from, to: to, queue: :queue.new()}
end
defp next_segment(state) do
previous = state.segment
next = new_segment(previous.to, previous.to + state.segment_duration)
put_in(state, [:segment], next)
end
defp segment_to_buffer(%{from: from_ns, to: to_ns, queue: queue}, state) do
[from, to] = Enum.map([from_ns, to_ns], &Membrane.Time.as_milliseconds(&1, :round))
fix_t = fn x ->
x
|> max(from)
|> min(to)
end
cues =
queue
|> :queue.to_list()
|> Enum.reject(&(&1.payload == ""))
|> Enum.map(&buffer_to_cue/1)
|> Enum.map(fn x ->
if state.omit_repetition do
x
else
# When repeating cues, ensure we cut them at segment's boundaries.
x
|> update_in([Access.key!(:from)], &fix_t.(&1))
|> update_in([Access.key!(:to)], &fix_t.(&1))
end
end)
{headers, cues} =
if state.relative_mpeg_ts_timestamps do
ts = round(from_ns / 1.0e9 * 90_000)
shift_t = fn x ->
max(x - from, 0)
end
headers =
state.headers ++
[
%WebVTT.HeaderLine{
key: :x_timestamp_map,
original: "X-TIMESTAMP-MAP=MPEGTS:#{ts},LOCAL:00:00:00.000"
}
]
cues =
Enum.map(cues, fn x ->
x
|> update_in([Access.key!(:from)], &shift_t.(&1))
|> update_in([Access.key!(:to)], &shift_t.(&1))
end)
{headers, cues}
else
{state.headers, cues}
end
webvtt =
%Subtitle.WebVTT{cues: cues, header: headers}
|> WebVTT.marshal!()
|> to_string()
%Buffer{
pts: from_ns,
payload: webvtt,
metadata: %{to: to_ns, duration: to_ns - from_ns}
}
end
defp put_and_get(state, nil, acc) do
buffers =
acc
|> Enum.reverse()
|> Enum.map(&segment_to_buffer(&1, state))
{buffers, state}
end
defp put_and_get(state = %{omit_repetition: true}, buffer, acc) do
segment = state.segment
from = buffer.pts
to = buffer.metadata.to
cond do
# The buffer starts after. Forward.
from >= segment.to ->
state
|> next_segment()
|> put_and_get(buffer, [segment | acc])
# The buffer starts here and ends in the next segment, meaning we have
# to store the buffer and proceed to the next segment.
from >= segment.from and to > segment.to ->
segment = update_in(segment, [:queue], fn q -> :queue.in(buffer, q) end)
state
|> next_segment()
|> put_and_get(nil, [segment | acc])
# Buffer starts and ends here, we shall not proceed to the next segment.
from >= segment.from and to <= segment.to ->
state
|> update_in([:segment, :queue], fn q -> :queue.in(buffer, q) end)
|> put_and_get(nil, acc)
true ->
# The buffer started before this segment and there is nothing we can
# do about it. Throw it away.
state
|> put_and_get(nil, acc)
end
end
defp put_and_get(state, buffer, acc) do
segment = state.segment
from = buffer.pts
to = buffer.metadata.to
cond do
# The buffer starts after. Forward.
from >= segment.to ->
state
|> next_segment()
|> put_and_get(buffer, [segment | acc])
# This buffer started in a previous segment and ends here,
# we're done with it.
to <= segment.to ->
state
|> update_in([:segment, :queue], fn q -> :queue.in(buffer, q) end)
|> put_and_get(nil, acc)
# The buffer started before and does not end here. Store and forward.
true ->
segment = update_in(segment, [:queue], fn q -> :queue.in(buffer, q) end)
state
|> next_segment()
|> put_and_get(buffer, [segment | acc])
end
end
end