-
Notifications
You must be signed in to change notification settings - Fork 0
Handle TDEN frame in timed ID3v2 stream #13
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
Changes from 23 commits
519c5fc
103380c
cd15788
82cc8c2
ac7cf92
b66a899
5cebfc0
4a645f3
cd72f0d
894ee48
efc0a92
d914feb
4c785ff
182469e
02673d7
3ba5575
7ddf388
2d1b92c
6021a5c
c436d26
9744b31
da157a0
b573115
87aa082
66437b7
f56a314
da1b54e
c2a37a3
00525f5
33817ba
7ffa36e
0529892
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,7 +8,7 @@ defmodule ExHLS.DemuxingEngine.MPEGTS do | |||||||||||||||
| alias Membrane.{AAC, H264, RemoteStream} | ||||||||||||||||
| alias MPEG.TS.Demuxer | ||||||||||||||||
|
|
||||||||||||||||
| @enforce_keys [:demuxer] | ||||||||||||||||
| @enforce_keys [:demuxer, :last_tden_tag] | ||||||||||||||||
| defstruct @enforce_keys ++ [track_timestamps_data: %{}] | ||||||||||||||||
|
|
||||||||||||||||
| # using it a boundary expressed in nanoseconds, instead of the usual 90kHz clock ticks, | ||||||||||||||||
|
|
@@ -17,7 +17,8 @@ defmodule ExHLS.DemuxingEngine.MPEGTS do | |||||||||||||||
| @timestamp_range_size_ns div(2 ** 33 * 1_000_000_000, 90_000) | ||||||||||||||||
|
|
||||||||||||||||
| @type t :: %__MODULE__{ | ||||||||||||||||
| demuxer: Demuxer.t() | ||||||||||||||||
| demuxer: Demuxer.t(), | ||||||||||||||||
| last_tden_tag: String.t() | nil | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @impl true | ||||||||||||||||
|
|
@@ -32,7 +33,7 @@ defmodule ExHLS.DemuxingEngine.MPEGTS do | |||||||||||||||
| # TODO - figure out how to do it properly | ||||||||||||||||
| demuxer = %{demuxer | waiting_random_access_indicator: false} | ||||||||||||||||
|
|
||||||||||||||||
| %__MODULE__{demuxer: demuxer} | ||||||||||||||||
| %__MODULE__{demuxer: demuxer, last_tden_tag: nil} | ||||||||||||||||
| end | ||||||||||||||||
|
|
||||||||||||||||
| @impl true | ||||||||||||||||
|
|
@@ -79,8 +80,11 @@ defmodule ExHLS.DemuxingEngine.MPEGTS do | |||||||||||||||
| @impl true | ||||||||||||||||
| def pop_chunk(%__MODULE__{} = demuxing_engine, track_id) do | ||||||||||||||||
| with {[packet], demuxer} <- Demuxer.take(demuxing_engine.demuxer, track_id) do | ||||||||||||||||
| {maybe_tden_tag, demuxer} = maybe_read_tden_tag(demuxer, packet.pts) | ||||||||||||||||
| tden_tag = maybe_tden_tag || demuxing_engine.last_tden_tag | ||||||||||||||||
|
|
||||||||||||||||
| {demuxing_engine, packet} = | ||||||||||||||||
| %{demuxing_engine | demuxer: demuxer} | ||||||||||||||||
| %{demuxing_engine | demuxer: demuxer, last_tden_tag: tden_tag} | ||||||||||||||||
| |> handle_possible_timestamps_rollover(track_id, packet) | ||||||||||||||||
|
|
||||||||||||||||
| chunk = %ExHLS.Chunk{ | ||||||||||||||||
|
|
@@ -90,7 +94,8 @@ defmodule ExHLS.DemuxingEngine.MPEGTS do | |||||||||||||||
| track_id: track_id, | ||||||||||||||||
| metadata: %{ | ||||||||||||||||
| discontinuity: packet.discontinuity, | ||||||||||||||||
| is_aligned: packet.is_aligned | ||||||||||||||||
| is_aligned: packet.is_aligned, | ||||||||||||||||
| tden_tag: tden_tag | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -101,6 +106,33 @@ defmodule ExHLS.DemuxingEngine.MPEGTS do | |||||||||||||||
| end | ||||||||||||||||
| end | ||||||||||||||||
|
|
||||||||||||||||
| defp maybe_read_tden_tag(demuxer, packet_pts) do | ||||||||||||||||
| with {id3_track_id, _stream_description} <- | ||||||||||||||||
| demuxer.pmt.streams | ||||||||||||||||
| |> Enum.find(fn {_pid, stream_description} -> | ||||||||||||||||
| stream_description.stream_type == :METADATA_IN_PES | ||||||||||||||||
| end), | ||||||||||||||||
| {[id3], demuxer} <- Demuxer.take(demuxer, id3_track_id), | ||||||||||||||||
| true <- id3.pts <= packet_pts do | ||||||||||||||||
| {parse_tden_tag(id3.data), demuxer} | ||||||||||||||||
| else | ||||||||||||||||
| nil -> {nil, demuxer} | ||||||||||||||||
| {[], updated_demuxer} -> {nil, updated_demuxer} | ||||||||||||||||
| false -> {nil, demuxer} | ||||||||||||||||
| end | ||||||||||||||||
| end | ||||||||||||||||
|
|
||||||||||||||||
| defp parse_tden_tag(payload) do | ||||||||||||||||
| with {pos, len} <- :binary.match(payload, "TDEN"), | ||||||||||||||||
| trailing_bytes <- :binary.part(payload, pos + len, byte_size(payload) - (pos + len)), | ||||||||||||||||
| <<size::integer-size(4)-unit(8), _flags::16, rest::binary>> <- trailing_bytes, | ||||||||||||||||
| <<_3, text::binary-size(size - 2), 0, _rest::binary>> <- rest do | ||||||||||||||||
|
||||||||||||||||
| with {pos, len} <- :binary.match(payload, "TDEN"), | |
| trailing_bytes <- :binary.part(payload, pos + len, byte_size(payload) - (pos + len)), | |
| <<size::integer-size(4)-unit(8), _flags::16, rest::binary>> <- trailing_bytes, | |
| <<_3, text::binary-size(size - 2), 0, _rest::binary>> <- rest do | |
| with {pos, _len} <- :binary.match(payload, "TDEN"), | |
| <<_skip::binary-size(pos), "TDEN", tden::binary>> <- payload, | |
| <<size::integer-size(4)-unit(8), _flags::16, _3, text::binary-size(size - 2), 0, _rest::binary>> <- tden do |
btw, what is _3?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a flag indicating encoding of the timestamps string (3 stands for UTF-8)
I think it's good enough to support just UTF-8 encoded strings (so I will make it match for exact value of 3 in this binary) - WDYT?
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,7 +42,9 @@ defmodule ExHLS.Mixfile do | |
| {:qex, "~> 0.5.1"}, | ||
| {:membrane_mp4_plugin, "~> 0.36.0"}, | ||
| {:membrane_h26x_plugin, "~> 0.10.2"}, | ||
| {:mpeg_ts, "~> 2.0.0"}, | ||
| {:mpeg_ts, | ||
| github: "membraneframework-labs/kim_mpeg_ts", | ||
| branch: "varsill/fix_pes_optional_header_resolving"}, | ||
|
Comment on lines
+46
to
+48
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We either need to wait for backport of my bugfix on |
||
| {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, | ||
| {:dialyxir, ">= 0.0.0", only: :dev, runtime: false}, | ||
| {:credo, ">= 0.0.0", only: :dev, runtime: false}, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,11 +4,13 @@ defmodule ExHLS.Client.Test do | |
| alias ExHLS.Client | ||
| alias Membrane.{AAC, H264, RemoteStream} | ||
|
|
||
| @fixtures "https://raw.githubusercontent.com/membraneframework-labs/ex_hls/refs/heads/master/test/fixtures/" | ||
| @fixtures "https://raw.githubusercontent.com/membraneframework/ex_hls/refs/heads/master/test/fixtures/" | ||
| @fmp4_url @fixtures <> "fmp4/output.m3u8" | ||
| @fmp4_only_video_url @fixtures <> "fmp4_only_video/output.m3u8" | ||
| @mpegts_only_video_url @fixtures <> "mpeg_ts_only_video/output_playlist.m3u8" | ||
| @mpegts_with_tden_url "test/fixtures/mpeg_ts_with_tden/output_playlist.m3u8" | ||
| @mpegts_url "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" | ||
| @mpegts_live_url "./test/fixtures/mpeg_ts_live/output_playlist.m3u8" | ||
|
|
||
| describe "if client reads video and audio chunks of the HLS" do | ||
| test "(MPEGTS) stream" do | ||
|
|
@@ -128,7 +130,40 @@ defmodule ExHLS.Client.Test do | |
| 183, 150, 44, 216, 32, 217, 35, 238, 239, 120, 50, 54, 52, 32, 45, 32, 99, 111, 114, | ||
| 101, 32, 49, 54, 52, 32, 114>> <> _rest = video_chunk.payload | ||
|
|
||
| assert video_chunk.metadata == %{discontinuity: false, is_aligned: false} | ||
| assert video_chunk.metadata == %{discontinuity: false, is_aligned: false, tden_tag: nil} | ||
| end | ||
|
|
||
| @tag :sometag | ||
| test "(MPEGTS) stream with ID3v2.4 TDEN tag" do | ||
| client = Client.new(@mpegts_with_tden_url) | ||
|
|
||
| assert Client.get_variants(client) == %{} | ||
|
|
||
| chunks = Client.generate_stream(client) |> Enum.take(381) | ||
|
|
||
| first_audio_chunk_after_tden = | ||
| Enum.find( | ||
| chunks, | ||
| &(&1.metadata.tden_tag != nil and | ||
| &1.media_type == | ||
| :audio) | ||
| ) | ||
|
|
||
| first_video_chunk_after_tden = | ||
| Enum.find( | ||
| chunks, | ||
| &(&1.metadata.tden_tag != nil and | ||
| &1.media_type == | ||
| :video) | ||
| ) | ||
|
||
|
|
||
| assert first_audio_chunk_after_tden.pts_ms == 3328 | ||
| assert first_audio_chunk_after_tden.dts_ms == 3328 | ||
| assert first_audio_chunk_after_tden.metadata.tden_tag == "2025-10-21T08:07:50" | ||
|
|
||
| assert first_video_chunk_after_tden.pts_ms == 3233 | ||
| assert first_video_chunk_after_tden.dts_ms == 3233 | ||
| assert first_video_chunk_after_tden.metadata.tden_tag == "2025-10-21T08:07:50" | ||
| end | ||
|
|
||
| test "(fMP4) stream with only video" do | ||
|
|
@@ -203,6 +238,28 @@ defmodule ExHLS.Client.Test do | |
| 215, 198, 77, 184, 229, 170, 157, 115, 169, 223>> <> _rest = audio_chunk.payload | ||
| end | ||
|
|
||
| test "(MPEGTS) stream with ultra low latency mode" do | ||
| client = Client.new(@mpegts_live_url, ultra_low_latency?: true) | ||
|
|
||
| assert Client.get_variants(client) == %{} | ||
| assert {:ok, tracks_info, client} = Client.get_tracks_info(client) | ||
|
|
||
| assert [%Membrane.RemoteStream{content_format: Membrane.H264, type: :bytestream}] = | ||
| tracks_info |> Map.values() | ||
|
|
||
| chunks = Client.generate_stream(client) |> Enum.take(1) | ||
| [video_chunk | _rest_video_chunks] = chunks | ||
|
|
||
| assert %{pts_ms: 11_081, dts_ms: 11_001} = video_chunk | ||
| assert byte_size(video_chunk.payload) == 28_699 | ||
|
|
||
| assert <<0, 0, 0, 1, 9, 240, 0, 0, 0, 1, 103, 100, 0, 21, 172, 217, 65, 224, 143, 235, 1, 106, | ||
| 12, 2, 13, 110, 0, 0, 9, 154, 0, 1, 224, 0, 30, 44, 91, 44, 0, 0, 0, 1, 104, 234, | ||
| 225, 178, 200, 176, 0, 0>> <> _rest = video_chunk.payload | ||
|
|
||
| assert video_chunk.metadata == %{discontinuity: false, is_aligned: false} | ||
| end | ||
|
|
||
| defp assert_chunks_are_in_proper_order(chunks) do | ||
| iteration_state = %{ | ||
| last_dts: %{audio: nil, video: nil}, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| #EXTM3U | ||
| #EXT-X-VERSION:3 | ||
| #EXT-X-TARGETDURATION:6 | ||
| #EXT-X-MEDIA-SEQUENCE:0 | ||
| #EXTINF:5.760933, | ||
| video_segment_000.ts | ||
| #EXTINF:3.840633, | ||
| video_segment_001.ts | ||
| #EXTINF:0.400111, | ||
| video_segment_002.ts |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #EXTM3U | ||
| #EXT-X-VERSION:3 | ||
| #EXT-X-TARGETDURATION:2 | ||
| #EXT-X-MEDIA-SEQUENCE:0 | ||
| #EXT-X-PLAYLIST-TYPE:VOD | ||
| #EXTINF:2.000000, | ||
| output_playlist0.ts | ||
| #EXTINF:2.000000, | ||
| output_playlist1.ts | ||
| #EXTINF:2.000000, | ||
| output_playlist2.ts | ||
| #EXTINF:2.000000, | ||
| output_playlist3.ts | ||
| #EXTINF:2.000000, | ||
| output_playlist4.ts | ||
| #EXTINF:0.033333, | ||
| output_playlist5.ts | ||
| #EXT-X-ENDLIST |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can easily get messed up so that a wrong clause matches. Either use
withlor reshape it to be less ambiguous