Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
519c5fc
Bump ex_m3u8 to v0.15.4. Properly handle absolute URIs for variant pl…
varsill Oct 8, 2025
103380c
Merge branch 'handle-weird-segment-extensions' into varsill/handle_ab…
varsill Oct 8, 2025
cd15788
Add segment_format option to the client to override segments demuxer …
varsill Oct 8, 2025
82cc8c2
Improve warning message
varsill Oct 8, 2025
ac7cf92
Pass segment_format to reader
varsill Oct 8, 2025
b66a899
Start playing with the last segment
varsill Oct 8, 2025
5cebfc0
Set proper segment number
varsill Oct 8, 2025
4a645f3
Filter out non-segments
varsill Oct 8, 2025
cd72f0d
Handle TDEN
varsill Oct 14, 2025
894ee48
Synchronize ID3 tag with stream
varsill Oct 16, 2025
efc0a92
Fix bug with id3 generation
varsill Oct 27, 2025
d914feb
Refactor TDEN specific code. Update dependency to mpeg_ts to fix buf …
varsill Nov 3, 2025
4c785ff
Format the code
varsill Nov 5, 2025
182469e
Fix credo warnings
varsill Nov 5, 2025
02673d7
Merge branch 'master' into varsill/ultra_low_latency
varsill Nov 6, 2025
3ba5575
Remove h264 parser commited by accident
varsill Nov 6, 2025
7ddf388
Add ultra_low_latency? option to the client
varsill Nov 6, 2025
2d1b92c
Improve option description
varsill Nov 6, 2025
6021a5c
Add test of the ultra low latency mode. Improve description. Make sur…
varsill Nov 6, 2025
c436d26
Improve option description
varsill Nov 6, 2025
9744b31
Fix credo warning
varsill Nov 6, 2025
da157a0
Merge branch 'varsill/ultra_low_latency' into varsill/support_tden
varsill Nov 6, 2025
b573115
Format the code
varsill Nov 6, 2025
87aa082
Update lib/ex_hls/demuxing_engine/mpeg_ts.ex
varsill Nov 7, 2025
66437b7
Fix mpeg_ts demuxer version to v2. Update tests
varsill Nov 13, 2025
f56a314
Use withl
varsill Nov 13, 2025
da1b54e
improve formatting
varsill Nov 13, 2025
c2a37a3
Make sure encoding is equal to 3
varsill Nov 14, 2025
00525f5
Add size specifier
varsill Nov 14, 2025
33817ba
Remove tag
varsill Nov 14, 2025
7ffa36e
Merge branch 'master' into varsill/support_tden
varsill Nov 17, 2025
0529892
Remove redundant should_start_playing clause
varsill Nov 17, 2025
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
47 changes: 42 additions & 5 deletions lib/ex_hls/demuxing_engine/mpeg_ts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ defmodule ExHLS.DemuxingEngine.MPEGTS do
@behaviour ExHLS.DemuxingEngine

use Bunch.Access
use Bunch

require Logger
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,
Expand All @@ -17,7 +18,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
Expand All @@ -32,7 +34,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
Expand Down Expand Up @@ -79,8 +81,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{
Expand All @@ -90,7 +95,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
}
}

Expand All @@ -101,6 +107,37 @@ defmodule ExHLS.DemuxingEngine.MPEGTS do
end
end

defp maybe_read_tden_tag(demuxer, packet_pts) do
withl no_id3_stream:
{id3_track_id, _stream_description} <-
demuxer.pmt.streams
|> Enum.find(fn {_pid, stream_description} ->
stream_description.stream_type == :METADATA_IN_PES
end),
no_id3_data: {[id3], demuxer} <- Demuxer.take(demuxer, id3_track_id),
id3_not_in_timerange: true <- id3.pts <= packet_pts do
{parse_tden_tag(id3.data), demuxer}
else
no_id3_stream: nil -> {nil, demuxer}
no_id3_data: {[], updated_demuxer} -> {nil, updated_demuxer}
id3_not_in_timerange: false -> {nil, demuxer}
end
end

defp parse_tden_tag(payload) do
# UTF-8 encoding
encoding = 3

with {pos, _len} <- :binary.match(payload, "TDEN"),
<<_skip::binary-size(pos), "TDEN", tden::binary>> <- payload,
<<size::integer-size(4)-unit(8), _flags::16, ^encoding::8, text::binary-size(size - 2),
0::8, _rest::binary>> <- tden do
Copy link
Member

Choose a reason for hiding this comment

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

NIT: 8 bits is the default for an integer, so 0::8 can become 0

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, but as discussed - I think it's easier for me to read it with these ::8 as they are surrounded by ::16 etc. :D

text
else
_error -> nil
end
end

# value returned by Demuxer is represented in nanoseconds
defp packet_ts_to_millis(ts), do: div(ts, 1_000_000)

Expand Down
5 changes: 4 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ defmodule ExHLS.Mixfile do
{:ex_m3u8, "~> 0.15.4"},
{:req, "~> 0.5.10"},
{:qex, "~> 0.5.1"},
{:bunch, "~> 1.6"},
{: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
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We either need to wait for backport of my bugfix on mpeg_ts v2 or update our dependency to mpeg_ts v3

{: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},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"membrane_timestamp_queue": {:hex, :membrane_timestamp_queue, "0.2.2", "1c831b2273d018a6548654aa9f7fa7c4b683f71d96ffe164934ef55f9d11f693", [:mix], [{:heap, "~> 2.0", [hex: :heap, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "7c830e760baaced0988421671cd2c83c7cda8d1bd2b61fd05332711675d1204f"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mpeg_ts": {:git, "https://github.com/membraneframework-labs/kim_mpeg_ts.git", "c8c770e0e7714c72b3faa7f20088fdbd76f5bade", [branch: "varsill/fix_pes_optional_header_resolving"]},
"mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"},
"mpeg_ts": {:hex, :mpeg_ts, "2.0.2", "87f7d3b38c962fc367edbb4b1419f5f314be41a0d512b95437f11c4f60c931f4", [:mix], [], "hexpm", "5b7f1245a945de647c29abc9453e3d9d7eca1b0001d3d582f4feb11fc09b2792"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
Expand Down
35 changes: 32 additions & 3 deletions test/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ 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"

Expand Down Expand Up @@ -129,7 +130,35 @@ 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

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
Expand Down Expand Up @@ -223,7 +252,7 @@ defmodule ExHLS.Client.Test do
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}
assert video_chunk.metadata == %{discontinuity: false, is_aligned: false, tden_tag: nil}
end

defp assert_chunks_are_in_proper_order(chunks) do
Expand Down
3 changes: 2 additions & 1 deletion test/demuxing_engine_mpegts_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ defmodule ExHLS.DemuxingEngine.MPEGTS.Test do

demuxer = %{
waiting_random_access_indicator: nil,
packet_buffers: %{1 => packets, 2 => packets}
packet_buffers: %{1 => packets, 2 => packets},
pmt: %{streams: %{}}
}

new = fn -> demuxer end
Expand Down
18 changes: 18 additions & 0 deletions test/fixtures/mpeg_ts_with_tden/output_playlist.m3u8
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.