Current section

Files

Jump to
membrane_hls_plugin lib membrane hls webvtt aggregator.ex
Raw

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