Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Packed Audio #33

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 102 additions & 3 deletions lib/membrane/aac/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ defmodule Membrane.AAC.Parser do
in_encapsulation: nil,
output_config: output_config,
avg_bit_rate: avg_bit_rate,
max_bit_rate: max_bit_rate
max_bit_rate: max_bit_rate,
# For id3 handling
start_pts: nil
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
start_pts: nil
pts_offset: nil

})

{[], state}
Expand Down Expand Up @@ -92,9 +94,26 @@ defmodule Membrane.AAC.Parser do
%{stream_format: stream_format} = ctx.pads.output
timestamp = buffer.pts || state.timestamp

case ADTS.parse_adts(state.leftover <> buffer.payload, stream_format, timestamp, state) do
{tags, payload} = parse_id3v4_tags(buffer.payload, [])

state =
if state.start_pts == nil do
Copy link
Member

Choose a reason for hiding this comment

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

it seems it's already checked in update_start_pts

update_start_pts(state, tags)
else
state
end

case ADTS.parse_adts(state.leftover <> payload, stream_format, timestamp, state) do
{:ok, {output, leftover, timestamp}} ->
actions = Enum.map(output, fn {action, value} -> {action, {:output, value}} end)
actions =
Enum.map(output, fn
{:buffer, buffer} ->
value = correct_pts(buffer, state.start_pts)
{:buffer, {:output, value}}

{action, value} ->
{action, {:output, value}}
end)
Comment on lines +108 to +116
Copy link
Member

Choose a reason for hiding this comment

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

Let's pass already corrected timestamp to ADTS.parse_adts instead


{actions ++ [redemand: :output], %{state | leftover: leftover, timestamp: timestamp}}

Expand Down Expand Up @@ -128,4 +147,84 @@ defmodule Membrane.AAC.Parser do
def handle_demand(:output, size, :buffers, _ctx, state) do
{[demand: {:input, size}], state}
end

defp update_start_pts(state, tags) do
start_pts =
Enum.find_value(tags, fn {id, val} ->
if(id == "com.apple.streaming.transportStreamTimestamp", do: val, else: nil)
end)

if start_pts != nil, do: %{state | start_pts: start_pts}, else: state
end
Comment on lines +151 to +158
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
defp update_start_pts(state, tags) do
start_pts =
Enum.find_value(tags, fn {id, val} ->
if(id == "com.apple.streaming.transportStreamTimestamp", do: val, else: nil)
end)
if start_pts != nil, do: %{state | start_pts: start_pts}, else: state
end
defp update_start_pts(%{start_pts: nil} = state, tags) do
start_pts =
Enum.find_value(tags, fn {id, val} ->
if id == "com.apple.streaming.transportStreamTimestamp", do: val
end)
%{state | start_pts: start_pts}
end


defp correct_pts(buffer, nil), do: %Buffer{buffer | pts: Ratio.to_float(buffer.pts) |> round()}
Copy link
Member

@mat-hek mat-hek Oct 4, 2023

Choose a reason for hiding this comment

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

PTS and DTS in buffers aren't ratios anymore, they're Membrane.Time.t (integers underneath)


defp correct_pts(buffer, start_pts),
do: %Buffer{buffer | pts: Ratio.add(buffer.pts, start_pts) |> Ratio.to_float() |> round()}

defp parse_id3v4_tags(data, acc) do
Copy link
Member

Choose a reason for hiding this comment

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

let's move all this ID3 parsing to a separate module

case parse_id3v4_tag(data) do
{[], rest} -> {acc, rest}
{tags, rest} -> parse_id3v4_tags(rest, acc ++ tags)
end
end

# https://github.com/id3/ID3v2.4/blob/master/id3v2.40-structure.txt
defp parse_id3v4_tag(
<<"ID3", version::binary-size(2), flags::binary-size(1), size::binary-size(4),
rest::binary>>
) do
# Here we pattern match the combo of version-flags supported.
<<4::8, _minor::8>> = version

# <<unsynchronisation::1, extended::1, experimental::1, footer::1, 0::4>> = flags
<<0::1, 0::1, 0::1, 0::1, 0::4>> = flags

size = decode_synchsafe_integer(size)
<<data::binary-size(size), rest::binary>> = rest
{parse_id3_frames(data, []), rest}
end

defp parse_id3v4_tag(<<"ID3", version::binary-size(2), _rest::binary>>) do
raise "Unsupported ID3 header version #{inspect(version)}"
Copy link
Member

Choose a reason for hiding this comment

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

can't we just skip the header in this case?

end

defp parse_id3v4_tag(data), do: {[], data}

defp parse_id3_frames(
<<"PRIV", size::binary-size(4), flags::binary-size(2), rest::binary>>,
acc
) do
<<_status::binary-size(1), _format::binary-size(1)>> = flags

size = decode_synchsafe_integer(size)
<<data::binary-size(size), rest::binary>> = rest

[owner_identifier, private_data] = :binary.split(data, <<0>>)
tag = parse_priv_tag(owner_identifier, private_data)
parse_id3_frames(rest, [tag | acc])
end

defp parse_id3_frames(<<>>, acc), do: Enum.reverse(acc)

defp parse_priv_tag(id = "com.apple.streaming.transportStreamTimestamp", data) do
# https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-19#section-3
rem = bit_size(data) - 33
<<_pad::size(rem), data::33-big>> = data
secs = data / 90_000
ns = Membrane.Time.nanoseconds(round(secs * 1_000_000_000))
{id, ns}
Comment on lines +215 to +216
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
ns = Membrane.Time.nanoseconds(round(secs * 1_000_000_000))
{id, ns}
time = round(secs * Membrane.Time.second()))
{id, time}

end

defp parse_priv_tag(id, data), do: {id, data}

defp decode_synchsafe_integer(binary) do
import Bitwise

binary
|> :binary.bin_to_list()
|> Enum.reverse()
|> Enum.with_index()
|> Enum.reduce(0, fn {el, index}, acc -> acc ||| el <<< (index * 7) end)
end
end
Binary file added test/fixtures/packed.aac
Binary file not shown.
28 changes: 28 additions & 0 deletions test/membrane/aac/parser_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,32 @@ defmodule Membrane.AAC.ParserTest do
perform_custom_sample_rate_test(:esds)
perform_custom_sample_rate_test(:audio_specific_config)
end

test "parses packed audio files shifting pts accordingly" do
structure =
child(:file, %Membrane.File.Source{location: "test/fixtures/packed.aac"})
|> child(:parser, %Parser{})
|> child(:sink, Testing.Sink)

pipeline = Testing.Pipeline.start_link_supervised!(structure: structure)

assert_pipeline_play(pipeline)
assert_sink_stream_format(pipeline, :sink, stream_format)

assert stream_format == %Membrane.AAC{
channels: 2,
encapsulation: :ADTS,
frames_per_buffer: 1,
mpeg_version: 4,
profile: :LC,
sample_rate: 48_000,
samples_per_frame: 1024
}

assert_sink_buffer(pipeline, :sink, %Membrane.Buffer{pts: pts, dts: nil})
assert pts == Membrane.Time.milliseconds(2100)

assert_end_of_stream(pipeline, :sink)
Testing.Pipeline.terminate(pipeline, blocking?: true)
end
end