Skip to content

Commit 10d4108

Browse files
FelonEkonommat-hek
andauthored
Specify pad codec (#114)
* Allow specifing demuxer output pad codec * Remove leftowver * Fix doc * Rewrite pad option codec -> kind * Simplify spawning test pipeline * Fix lint * Implement CR comments * Add spec * Update lib/membrane_mp4/demuxer/isom.ex Co-authored-by: Mateusz Front <mateusz.front@swmansion.com> * Mix format: * Update credo --------- Co-authored-by: Mateusz Front <mateusz.front@swmansion.com>
1 parent 72d38a0 commit 10d4108

File tree

7 files changed

+213
-36
lines changed

7 files changed

+213
-36
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The package can be installed by adding `membrane_mp4_plugin` to your list of dep
1212
```elixir
1313
defp deps do
1414
[
15-
{:membrane_mp4_plugin, "~> 0.35.0"}
15+
{:membrane_mp4_plugin, "~> 0.35.1"}
1616
]
1717
end
1818
```

examples/demuxer_isom.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ defmodule Example do
2424
hackney_opts: [follow_redirect: true]
2525
})
2626
|> child(:demuxer, Membrane.MP4.Demuxer.ISOM)
27-
|> via_out(Pad.ref(:output, 1))
27+
|> via_out(:output, options: [kind: :video])
2828
|> child(:parser_video, %Membrane.H264.Parser{output_stream_structure: :annexb})
2929
|> child(:sink_video, %Membrane.File.Sink{location: @output_video}),
3030
get_child(:demuxer)
31-
|> via_out(Pad.ref(:output, 2))
31+
|> via_out(:output, options: [kind: :audio])
3232
|> child(:audio_parser, %Membrane.AAC.Parser{
3333
out_encapsulation: :ADTS
3434
})

lib/membrane_mp4/demuxer/isom.ex

Lines changed: 199 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@ defmodule Membrane.MP4.Demuxer.ISOM do
55
The MP4 must have `fast start` enabled, i.e. the `moov` box must precede the `mdat` box.
66
Once the Demuxer identifies the tracks in the MP4, `t:new_tracks_t/0` notification is sent for each of the tracks.
77
8-
All the tracks in the MP4 must have a corresponding output pad linked (`Pad.ref(:output, track_id)`).
8+
All pads has to be linked either before `handle_playing/2` callback or after the Element sends `{:new_tracks, ...}`
9+
notification.
10+
11+
Number of pads has to be equal to the number of demuxed tracks.
12+
13+
If the demuxed data contains only one track, linked pad doesn't have to specify `:kind` option.
14+
15+
If there are more than one track and pads are linked before `handle_playing/2`, every pad has to specify `:kind`
16+
option.
17+
18+
If any of pads isn't linked before `handle_playing/2`, #{inspect(__MODULE__)} will send `{:new_tracks, ...}`
19+
notification to the parent. Otherwise, if any of them is linked before `handle_playing/3`, this notification won't
20+
be sent.
21+
22+
If pads are linked after the `{:new_tracks, ...}` notfitaction, their references must match MP4 tracks ids
23+
(`Pad.ref(:output, track_id)`).
924
"""
1025
use Membrane.Filter
1126

@@ -35,7 +50,15 @@ defmodule Membrane.MP4.Demuxer.ISOM do
3550
%Membrane.Opus{self_delimiting?: false}
3651
),
3752
availability: :on_request,
38-
flow_control: :auto
53+
options: [
54+
kind: [
55+
spec: :video | :audio | nil,
56+
default: nil,
57+
description: """
58+
Specifies, what kind of data can be handled by a pad.
59+
"""
60+
]
61+
]
3962

4063
def_options optimize_for_non_fast_start?: [
4164
default: false,
@@ -82,7 +105,10 @@ defmodule Membrane.MP4.Demuxer.ISOM do
82105
boxes_size: 0,
83106
mdat_beginning: nil,
84107
mdat_size: nil,
85-
mdat_header_size: nil
108+
mdat_header_size: nil,
109+
track_to_pad_id: %{},
110+
track_notifications_sent?: false,
111+
pads_linked_before_notification?: false
86112
}
87113

88114
{[], state}
@@ -147,7 +173,7 @@ defmodule Membrane.MP4.Demuxer.ISOM do
147173
state.partial <> buffer.payload
148174
)
149175

150-
buffers = get_buffer_actions(samples)
176+
buffers = get_buffer_actions(samples, state)
151177

152178
{buffers, %{state | samples_info: samples_info, partial: rest}}
153179
end
@@ -356,22 +382,26 @@ defmodule Membrane.MP4.Demuxer.ISOM do
356382

357383
state = %{state | samples_info: samples_info, partial: rest}
358384

385+
state = match_tracks_with_pads(ctx, state)
386+
359387
all_pads_connected? = all_pads_connected?(ctx, state)
360388

361389
{buffers, state} =
362390
if all_pads_connected? do
363-
{get_buffer_actions(samples), state}
391+
{get_buffer_actions(samples, state), state}
364392
else
365393
{[], store_samples(state, samples)}
366394
end
367395

368-
notifications = get_track_notifications(state)
396+
notifications = maybe_get_track_notifications(state)
397+
369398
stream_format = if all_pads_connected?, do: get_stream_format(state), else: []
370399

371400
state =
372401
%{
373402
state
374-
| all_pads_connected?: all_pads_connected?
403+
| all_pads_connected?: all_pads_connected?,
404+
track_notifications_sent?: true
375405
}
376406
|> update_fsm_state()
377407

@@ -385,9 +415,10 @@ defmodule Membrane.MP4.Demuxer.ISOM do
385415
end)
386416
end
387417

388-
defp get_buffer_actions(samples) do
418+
defp get_buffer_actions(samples, state) do
389419
Enum.map(samples, fn {buffer, track_id} ->
390-
{:buffer, {Pad.ref(:output, track_id), buffer}}
420+
pad_id = state.track_to_pad_id[track_id]
421+
{:buffer, {Pad.ref(:output, pad_id), buffer}}
391422
end)
392423
end
393424

@@ -398,12 +429,98 @@ defmodule Membrane.MP4.Demuxer.ISOM do
398429
end
399430
end
400431

401-
defp get_track_notifications(state) do
432+
defp match_tracks_with_pads(ctx, state) do
433+
sample_tables = state.samples_info.sample_tables
434+
435+
output_pads_data =
436+
ctx.pads
437+
|> Map.values()
438+
|> Enum.filter(&(&1.direction == :output))
439+
440+
if length(output_pads_data) not in [0, map_size(sample_tables)] do
441+
raise_pads_not_matching_codecs_error!(ctx, state)
442+
end
443+
444+
track_to_pad_id =
445+
case output_pads_data do
446+
[] ->
447+
sample_tables
448+
|> Map.new(fn {track_id, _table} -> {track_id, track_id} end)
449+
450+
[pad_data] ->
451+
{track_id, table} = Enum.at(sample_tables, 0)
452+
453+
if pad_data.options.kind not in [
454+
nil,
455+
sample_description_to_kind(table.sample_description)
456+
] do
457+
raise_pads_not_matching_codecs_error!(ctx, state)
458+
end
459+
460+
%{track_id => pad_data_to_pad_id(pad_data)}
461+
462+
_many ->
463+
kind_to_pads_data = output_pads_data |> Enum.group_by(& &1.options.kind)
464+
465+
kind_to_tracks =
466+
sample_tables
467+
|> Enum.group_by(
468+
fn {_track_id, table} -> sample_description_to_kind(table.sample_description) end,
469+
fn {track_id, _table} -> track_id end
470+
)
471+
472+
raise? =
473+
Enum.any?(kind_to_pads_data, fn {kind, pads} ->
474+
length(pads) != length(kind_to_tracks[kind])
475+
end)
476+
477+
if raise?, do: raise_pads_not_matching_codecs_error!(ctx, state)
478+
479+
kind_to_tracks
480+
|> Enum.flat_map(fn {kind, tracks} ->
481+
pad_refs = kind_to_pads_data[kind] |> Enum.map(&pad_data_to_pad_id/1)
482+
Enum.zip(tracks, pad_refs)
483+
end)
484+
|> Map.new()
485+
end
486+
487+
%{state | track_to_pad_id: Map.new(track_to_pad_id)}
488+
end
489+
490+
defp pad_data_to_pad_id(%{ref: Pad.ref(_name, id)}), do: id
491+
492+
@spec raise_pads_not_matching_codecs_error!(map(), map()) :: no_return()
493+
defp raise_pads_not_matching_codecs_error!(ctx, state) do
494+
pads_kinds =
495+
ctx.pads
496+
|> Enum.flat_map(fn
497+
{:input, _pad_data} -> []
498+
{_pad_ref, %{options: %{kind: kind}}} -> [kind]
499+
end)
500+
501+
tracks_codecs =
502+
state.samples_info.sample_tables
503+
|> Enum.map(fn {_track, table} -> table.sample_description.__struct__ end)
504+
505+
raise """
506+
Pads kinds don't match with tracks codecs. Pads kinds are #{inspect(pads_kinds)}. \
507+
Tracks codecs are #{inspect(tracks_codecs)}
508+
"""
509+
end
510+
511+
defp sample_description_to_kind(%Membrane.H264{}), do: :video
512+
defp sample_description_to_kind(%Membrane.H265{}), do: :video
513+
defp sample_description_to_kind(%Membrane.AAC{}), do: :audio
514+
defp sample_description_to_kind(%Membrane.Opus{}), do: :audio
515+
516+
defp maybe_get_track_notifications(%{pads_linked_before_notification?: true}), do: []
517+
518+
defp maybe_get_track_notifications(%{pads_linked_before_notification?: false} = state) do
402519
new_tracks =
403520
state.samples_info.sample_tables
404521
|> Enum.map(fn {track_id, table} ->
405-
content = table.sample_description
406-
{track_id, content}
522+
pad_id = state.track_to_pad_id[track_id]
523+
{pad_id, table.sample_description}
407524
end)
408525

409526
[{:notify_parent, {:new_tracks, new_tracks}}]
@@ -412,7 +529,8 @@ defmodule Membrane.MP4.Demuxer.ISOM do
412529
defp get_stream_format(state) do
413530
state.samples_info.sample_tables
414531
|> Enum.map(fn {track_id, table} ->
415-
{:stream_format, {Pad.ref(:output, track_id), table.sample_description}}
532+
pad_id = state.track_to_pad_id[track_id]
533+
{:stream_format, {Pad.ref(:output, pad_id), table.sample_description}}
416534
end)
417535
end
418536

@@ -425,7 +543,23 @@ defmodule Membrane.MP4.Demuxer.ISOM do
425543
raise "All tracks have corresponding pad already connected"
426544
end
427545

428-
def handle_pad_added(Pad.ref(:output, _track_id), ctx, state) do
546+
def handle_pad_added(Pad.ref(:output, _track_id) = pad_ref, ctx, state) do
547+
state =
548+
case ctx.playback do
549+
:stopped ->
550+
%{state | pads_linked_before_notification?: true}
551+
552+
:playing when state.track_notifications_sent? ->
553+
state
554+
555+
:playing ->
556+
raise """
557+
Pads can be linked either before #{inspect(__MODULE__)} enters :playing playback or after it \
558+
sends {:new_tracks, ...} notification
559+
"""
560+
end
561+
562+
:ok = validate_pad_kind!(pad_ref, ctx.pad_options.kind, ctx, state)
429563
all_pads_connected? = all_pads_connected?(ctx, state)
430564

431565
{actions, state} =
@@ -444,6 +578,55 @@ defmodule Membrane.MP4.Demuxer.ISOM do
444578
{actions, state}
445579
end
446580

581+
defp validate_pad_kind!(pad_ref, pad_kind, ctx, state) do
582+
allowed_kinds = [nil, :audio, :video]
583+
584+
if pad_kind not in allowed_kinds do
585+
raise """
586+
Pad #{inspect(pad_ref)} has :kind option set to #{inspect(pad_kind)}, while it has te be one of \
587+
#{[:audio, :video] |> inspect()} or be unspecified.
588+
"""
589+
end
590+
591+
if not state.track_notifications_sent? and
592+
Enum.count(ctx.pads, &match?({Pad.ref(:output, _id), %{options: %{kind: nil}}}, &1)) > 1 do
593+
raise """
594+
If pads are linked before :new_tracks notifications and there are more then one of them, pad option \
595+
:kind has to be specyfied.
596+
"""
597+
end
598+
599+
if state.track_notifications_sent? do
600+
Pad.ref(:output, pad_id) = pad_ref
601+
602+
related_track =
603+
state.track_to_pad_id
604+
|> Map.keys()
605+
|> Enum.find(&(state.track_to_pad_id[&1] == pad_id))
606+
607+
if related_track == nil do
608+
raise """
609+
Pad #{inspect(pad_ref)} doesn't have a related track. If you link pads after #{inspect(__MODULE__)} \
610+
sent the track notification, you have to restrict yourself to the pad occuring in this notification. \
611+
Tracks, that occured in this notification are: #{Map.keys(state.track_to_pad_id) |> inspect()}
612+
"""
613+
end
614+
615+
track_kind =
616+
state.samples_info.sample_tables[related_track].sample_description
617+
|> sample_description_to_kind()
618+
619+
if pad_kind != nil and pad_kind != track_kind do
620+
raise """
621+
Pad option :kind must match with the kind of the related track or be equal nil, but pad #{inspect(pad_ref)} \
622+
kind is #{inspect(pad_kind)}, while the related track kind is #{inspect(track_kind)}
623+
"""
624+
end
625+
end
626+
627+
:ok
628+
end
629+
447630
@impl true
448631
def handle_end_of_stream(:input, _ctx, %{all_pads_connected?: false} = state) do
449632
{[], %{state | end_of_stream?: true}}
@@ -465,12 +648,6 @@ defmodule Membrane.MP4.Demuxer.ISOM do
465648
_pad -> []
466649
end)
467650

468-
Enum.each(pads, fn pad ->
469-
if pad not in tracks do
470-
raise "An output pad connected with #{pad} id, however no matching track exists"
471-
end
472-
end)
473-
474651
Range.size(tracks) == length(pads)
475652
end
476653

@@ -482,7 +659,8 @@ defmodule Membrane.MP4.Demuxer.ISOM do
482659
|> Enum.reverse()
483660
|> Enum.map(fn {buffer, ^track_id} -> buffer end)
484661

485-
{:buffer, {Pad.ref(:output, track_id), buffers}}
662+
pad_id = state.track_to_pad_id[track_id]
663+
{:buffer, {Pad.ref(:output, pad_id), buffers}}
486664
end)
487665

488666
state = %{state | buffered_samples: %{}}

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Membrane.MP4.Plugin.MixProject do
22
use Mix.Project
33

4-
@version "0.35.0"
4+
@version "0.35.1"
55
@github_url "https://github.com/membraneframework/membrane_mp4_plugin"
66

77
def project do

mix.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
77
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
88
"coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
9-
"credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
9+
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
1010
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
1111
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
1212
"elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
@@ -16,7 +16,7 @@
1616
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
1717
"heap": {:hex, :heap, "2.0.2", "d98cb178286cfeb5edbcf17785e2d20af73ca57b5a2cf4af584118afbcf917eb", [:mix], [], "hexpm", "ba9ea2fe99eb4bcbd9a8a28eaf71cbcac449ca1d8e71731596aace9028c9d429"},
1818
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
19-
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
19+
"jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
2020
"logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"},
2121
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
2222
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},

0 commit comments

Comments
 (0)