Skip to content

Commit 0bf4ec8

Browse files
maslowalexNoarkhh
andauthored
AV1 muxer (#125)
* Support AV1 in Muxer * added membrane_av1_format library * removed redundant deps * fix: reverted unrelated changes, refactored code, use the membrane_av1_format dependency * fix: addressed the PR comments * chore: bump membrane_av1_format to 0.3.1 * chore: addressed naming suggestions and fixed credo * test: added muxer test * Satisfy credo and compiler --------- Co-authored-by: noarkhh <jakub.pryc@gmail.com>
1 parent 51f222e commit 0bf4ec8

14 files changed

Lines changed: 327 additions & 13 deletions

File tree

‎lib/membrane_mp4/container/schema_default.ex‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ defmodule Membrane.MP4.Container.Schema.Default do
5050
]
5151
]
5252

53+
@av1_schema [
54+
version: 0,
55+
fields: @visual_sample_entry,
56+
av1C: [
57+
black_box?: true
58+
],
59+
pasp: [
60+
fields: [
61+
h_spacing: :uint32,
62+
v_spacing: :uint32
63+
]
64+
]
65+
]
66+
5367
@schema_def ftyp: [
5468
fields: [
5569
major_brand: :str32,
@@ -197,6 +211,7 @@ defmodule Membrane.MP4.Container.Schema.Default do
197211
avc3: @avc_schema,
198212
hvc1: @hevc_schema,
199213
hev1: @hevc_schema,
214+
av01: @av1_schema,
200215
mp4a: [
201216
fields: [
202217
reserved: <<0::6*8>>,

‎lib/membrane_mp4/demuxer/isom.ex‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ defmodule Membrane.MP4.Demuxer.ISOM do
5050
stream_structure: {_hevc, _dcr},
5151
alignment: :au
5252
},
53-
%Membrane.Opus{self_delimiting?: false}
53+
%Membrane.Opus{self_delimiting?: false},
54+
%Membrane.AV1{alignment: :tu}
5455
),
5556
availability: :on_request,
5657
options: [
@@ -517,6 +518,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do
517518

518519
defp sample_description_to_kind(%Membrane.H264{}), do: :video
519520
defp sample_description_to_kind(%Membrane.H265{}), do: :video
521+
defp sample_description_to_kind(%Membrane.AV1{}), do: :video
520522
defp sample_description_to_kind(%Membrane.AAC{}), do: :audio
521523
defp sample_description_to_kind(%Membrane.Opus{}), do: :audio
522524

‎lib/membrane_mp4/demuxer/isom/samples_info.ex‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ defmodule Membrane.MP4.Demuxer.ISOM.SamplesInfo do
55
# generating output buffers using that structure.
66
# The samples' description is generated from the the `moov` box, which describes how the data is stored inside the `mdat` box.
77

8-
alias Membrane.{Buffer, Time}
8+
alias Membrane.{AV1, Buffer, Time}
99
alias Membrane.MP4.Container
1010
alias Membrane.MP4.MovieBox.SampleTableBox
1111
alias Membrane.MP4.Track.SampleTable
1212

13+
@av1_temporal_delimiter_obu <<0x12, 0x00>>
14+
1315
@enforce_keys [
1416
:samples,
1517
:tracks_number,
@@ -77,7 +79,7 @@ defmodule Membrane.MP4.Demuxer.ISOM.SamplesInfo do
7779

7880
buffer =
7981
{%Buffer{
80-
payload: payload,
82+
payload: maybe_prepend_temporal_delimiter(samples_info, track_id, payload),
8183
dts: dts,
8284
pts: pts
8385
}, track_id}
@@ -95,6 +97,16 @@ defmodule Membrane.MP4.Demuxer.ISOM.SamplesInfo do
9597
end
9698
end
9799

100+
# The AV1 ISOBMFF binding requires samples to be stored without temporal delimiter OBUs,
101+
# while `Membrane.AV1` with `alignment: :tu` requires each buffer to start with one,
102+
# so it is restored here.
103+
defp maybe_prepend_temporal_delimiter(samples_info, track_id, payload) do
104+
case samples_info.sample_tables[track_id].sample_description do
105+
%AV1{} -> @av1_temporal_delimiter_obu <> payload
106+
_other -> payload
107+
end
108+
end
109+
98110
defp get_dts_and_pts(samples_info, %{
99111
sample_delta: delta,
100112
track_id: track_id,

‎lib/membrane_mp4/helper.ex‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ defmodule Membrane.MP4.Helper do
1717
@spec key_frame?(Membrane.Buffer.metadata()) :: boolean()
1818
def key_frame?(%{h264: %{key_frame?: false}}), do: false
1919
def key_frame?(%{h265: %{key_frame?: false}}), do: false
20+
def key_frame?(%{av1: %{key_frame?: false}}), do: false
2021
def key_frame?(_metadata), do: true
2122
end

‎lib/membrane_mp4/movie_box/sample_table_box.ex‎

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Membrane.MP4.MovieBox.SampleTableBox do
33

44
require Membrane.H264
55
require Membrane.Logger
6-
alias Membrane.{AAC, H264, H265, Opus}
6+
alias Membrane.{AAC, AV1, H264, H265, Opus}
77
alias Membrane.MP4.{Container, Helper, Track.SampleTable}
88

99
@spec assemble(SampleTable.t()) :: Container.t()
@@ -184,6 +184,79 @@ defmodule Membrane.MP4.MovieBox.SampleTableBox do
184184
]
185185
end
186186

187+
defp assemble_sample_description(%AV1{width: width, height: height} = format) do
188+
av1c_content = build_av1c(format)
189+
190+
[
191+
{:av01,
192+
%{
193+
children: [
194+
av1C: %{
195+
content: av1c_content
196+
},
197+
pasp: %{
198+
children: [],
199+
fields: %{h_spacing: 1, v_spacing: 1}
200+
}
201+
],
202+
fields: %{
203+
compressor_name: <<0::size(32)-unit(8)>>,
204+
depth: 24,
205+
flags: 0,
206+
frame_count: 1,
207+
height: height || 0,
208+
horizresolution: {0, 0},
209+
num_of_entries: 1,
210+
version: 0,
211+
vertresolution: {0, 0},
212+
width: width || 0
213+
}
214+
}}
215+
]
216+
end
217+
218+
# Build av1C (AV1CodecConfigurationRecord) per ISO/IEC 14496-15
219+
defp build_av1c(%AV1{} = format) do
220+
config = av1c_config(format)
221+
222+
<<
223+
# Byte 0: marker (1) | version (7)
224+
1::1,
225+
1::7,
226+
# Byte 1: seq_profile (3) | seq_level_idx_0 (5)
227+
config.profile::3,
228+
config.level_idx::5,
229+
# Byte 2: seq_tier_0 (1) | high_bitdepth (1) | twelve_bit (1) | monochrome (1) |
230+
# chroma_subsampling_x (1) | chroma_subsampling_y (1) | chroma_sample_position (2)
231+
config.tier::1,
232+
config.high_bitdepth::1,
233+
config.twelve_bit::1,
234+
config.monochrome::1,
235+
config.chroma_subsampling_x::1,
236+
config.chroma_subsampling_y::1,
237+
config.chroma_sample_position::2,
238+
# Byte 3: reserved (3) | initial_presentation_delay_present (1) | reserved (4)
239+
0::3,
240+
0::1,
241+
0::4
242+
>>
243+
end
244+
245+
defp av1c_config(%AV1{} = format) do
246+
%{
247+
profile: AV1.profile_to_seq_profile(format.profile || :main),
248+
level_idx: AV1.level_to_seq_level_idx(format.level || :"4.0"),
249+
tier: AV1.tier_to_seq_tier(format.tier || :main),
250+
# Defaults for 8-bit, 4:2:0
251+
high_bitdepth: 0,
252+
twelve_bit: 0,
253+
monochrome: 0,
254+
chroma_subsampling_x: 1,
255+
chroma_subsampling_y: 1,
256+
chroma_sample_position: 0
257+
}
258+
end
259+
187260
defp assemble_sample_deltas(%{timescale: timescale, decoding_deltas: decoding_deltas}),
188261
do:
189262
Enum.map(decoding_deltas, fn %{sample_count: count, sample_delta: delta} ->
@@ -343,6 +416,20 @@ defmodule Membrane.MP4.MovieBox.SampleTableBox do
343416
%Opus{channels: boxes[:dOps].fields.output_channel_count, self_delimiting?: false}
344417
end
345418

419+
defp unpack_sample_description(%{children: [{:av01, %{children: boxes, fields: fields}}]}) do
420+
<<_marker::1, _version::7, seq_profile::3, seq_level_idx::5, seq_tier::1, _rest::bitstring>> =
421+
boxes[:av1C].content
422+
423+
%AV1{
424+
alignment: :tu,
425+
width: fields.width,
426+
height: fields.height,
427+
profile: AV1.seq_profile_to_profile(seq_profile),
428+
level: AV1.seq_level_idx_to_level(seq_level_idx),
429+
tier: AV1.seq_tier_to_tier(seq_tier)
430+
}
431+
end
432+
346433
defp unpack_sample_description(%{children: [{sample_type, _sample_metadata}]}) do
347434
Membrane.Logger.warning("Unknown sample type: #{inspect(sample_type)}")
348435
nil

‎lib/membrane_mp4/movie_box/track_box.ex‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ defmodule Membrane.MP4.MovieBox.TrackBox do
1313

1414
defguardp is_audio(track)
1515
when not is_struct(track.stream_format, Membrane.H264) and
16-
not is_struct(track.stream_format, Membrane.H265)
16+
not is_struct(track.stream_format, Membrane.H265) and
17+
not is_struct(track.stream_format, Membrane.AV1)
1718

1819
@spec assemble(Track.t()) :: Container.t()
1920
def assemble(track) do

‎lib/membrane_mp4/muxer/isom.ex‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ defmodule Membrane.MP4.Muxer.ISOM do
2424
stream_structure: {_hevc, _dcr},
2525
alignment: :au
2626
},
27-
%Membrane.Opus{self_delimiting?: false}
27+
%Membrane.Opus{self_delimiting?: false},
28+
%Membrane.AV1{alignment: :tu}
2829
),
2930
availability: :on_request
3031

@@ -182,11 +183,23 @@ defmodule Membrane.MP4.Muxer.ISOM do
182183
{[event: {:output, event}], state}
183184
end
184185

185-
defp handle_queue_item({Pad.ref(:input, pad_ref), {:buffer, buffer}}, state) do
186+
defp handle_queue_item({Pad.ref(:input, pad_ref), {:buffer, %Buffer{} = buffer}}, state) do
186187
# In case DTS is not set, use PTS. This is the case for audio tracks or H264 originated
187188
# from an RTP stream. ISO base media file format specification uses DTS for calculating
188189
# decoding deltas, and so is the implementation of sample table in this plugin.
189-
buffer = %{buffer | dts: Buffer.get_dts_or_pts(buffer)}
190+
buffer = %Buffer{buffer | dts: Buffer.get_dts_or_pts(buffer)}
191+
192+
# For AV1 tracks, strip temporal delimiter OBUs from the payload
193+
track = get_in(state, [:pad_to_track, pad_ref])
194+
195+
buffer =
196+
case track.stream_format do
197+
%Membrane.AV1{} ->
198+
%Buffer{buffer | payload: strip_av1_temporal_delimiter_obu(buffer.payload)}
199+
200+
_other ->
201+
buffer
202+
end
190203

191204
state
192205
|> update_in([:pad_to_track, pad_ref], &Track.store_sample(&1, buffer))
@@ -286,4 +299,11 @@ defmodule Membrane.MP4.Muxer.ISOM do
286299
defp shift_left([]), do: []
287300

288301
defp shift_left([first | rest]), do: rest ++ [first]
302+
303+
# Strip temporal delimiter OBUs from AV1 payload
304+
# Temporal delimiter OBU: header=0x12 (type=2, has_size=1), size=0x00
305+
defp strip_av1_temporal_delimiter_obu(<<0x12, 0x00, rest::binary>>),
306+
do: strip_av1_temporal_delimiter_obu(rest)
307+
308+
defp strip_av1_temporal_delimiter_obu(payload), do: payload
289309
end

‎lib/membrane_mp4/track.ex‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule Membrane.MP4.Track do
88
"""
99
require Membrane.{H264, H265}
1010
alias __MODULE__.SampleTable
11-
alias Membrane.{AAC, H264, H265}
11+
alias Membrane.{AAC, AV1, H264, H265}
1212
alias Membrane.MP4.Helper
1313

1414
@type t :: %__MODULE__{
@@ -90,6 +90,8 @@ defmodule Membrane.MP4.Track do
9090
| {:avc1, %{profile: binary(), compatibility: binary(), level: binary()}}
9191
| {:hvc1,
9292
%{profile: non_neg_integer(), tier: non_neg_integer(), level: non_neg_integer()}}
93+
| {:av01,
94+
%{profile: non_neg_integer(), level: non_neg_integer(), tier: non_neg_integer()}}
9395
| nil
9496

9597
def get_encoding_info(%__MODULE__{
@@ -139,6 +141,16 @@ defmodule Membrane.MP4.Track do
139141
{hevc, map}
140142
end
141143

144+
def get_encoding_info(%__MODULE__{stream_format: %AV1{} = format}) do
145+
map = %{
146+
profile: AV1.profile_to_seq_profile(format.profile || :main),
147+
level: AV1.level_to_seq_level_idx(format.level || :"4.0"),
148+
tier: AV1.tier_to_seq_tier(format.tier || :main)
149+
}
150+
151+
{:av01, map}
152+
end
153+
142154
def get_encoding_info(_unknown), do: nil
143155

144156
defp get_timescale(stream_format) do
@@ -149,13 +161,13 @@ defmodule Membrane.MP4.Track do
149161
%Membrane.AAC{sample_rate: sample_rate} ->
150162
sample_rate
151163

152-
%module{framerate: nil} when module in [H264, H265] ->
164+
%module{framerate: nil} when module in [AV1, H264, H265] ->
153165
30 * 1024
154166

155-
%module{framerate: {0, _denominator}} when module in [H264, H265] ->
167+
%module{framerate: {0, _denominator}} when module in [AV1, H264, H265] ->
156168
30 * 1024
157169

158-
%module{framerate: {nominator, _denominator}} when module in [H264, H265] ->
170+
%module{framerate: {nominator, _denominator}} when module in [AV1, H264, H265] ->
159171
nominator * 1024
160172
end
161173
end

‎lib/membrane_mp4/track/sample_table.ex‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ defmodule Membrane.MP4.Track.SampleTable do
177177
Map.update!(sample_table, :sync_samples, &[sample_table.sample_count | &1])
178178
end
179179

180+
defp maybe_store_sync_sample(sample_table, %Buffer{metadata: %{av1: av1}})
181+
when av1.key_frame? do
182+
Map.update!(sample_table, :sync_samples, &[sample_table.sample_count | &1])
183+
end
184+
180185
defp maybe_store_sync_sample(sample_table, _buffer), do: sample_table
181186

182187
defp store_last_dts(sample_table, %Buffer{dts: dts}),

‎mix.exs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ defmodule Membrane.MP4.Plugin.MixProject do
4545
{:membrane_h264_format, "~> 0.6.1"},
4646
{:membrane_h265_format, "~> 0.2.0"},
4747
{:membrane_opus_format, "~> 0.3.0"},
48+
{:membrane_av1_format, "~> 0.3.1"},
4849
{:membrane_file_plugin, "~> 0.17.0"},
4950
{:membrane_timestamp_queue, "~> 0.2.1"},
5051
{:bunch, "~> 1.5"},
@@ -53,6 +54,7 @@ defmodule Membrane.MP4.Plugin.MixProject do
5354
{:membrane_opus_plugin, "~> 0.19.0", only: :test},
5455
{:membrane_stream_plugin, "~> 0.4.0", only: :test},
5556
{:membrane_fake_plugin, "~> 0.11.0", only: :test},
57+
{:membrane_ivf_plugin, "~> 0.9.2", only: :test},
5658
{:ex_doc, ">= 0.40.0", only: :dev, runtime: false},
5759
{:dialyxir, ">= 0.0.0", only: :dev, runtime: false},
5860
{:credo, ">= 0.0.0", only: :dev, runtime: false}

0 commit comments

Comments
 (0)