Skip to content

Commit eee06d1

Browse files
authored
Improve fragments (moof) generation in low latency hls (#14)
* Improve fragments (moof) generation in low latency hls We keep two buffers when using ll-hls, one for the parts and the other for the whole segment (this means that we create only one moof at the end of the segment). As an improvement, when generating a moof as a part, we store the whole moof as part of the whole segment. So at the end of the segment we'll have as much as moofs as the number of generated parts. * mix format
1 parent 50b123f commit eee06d1

2 files changed

Lines changed: 21 additions & 65 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
* Use max part target duration of all renditions to calculate part hold back.
66
* Add ci GitHub action
7+
* Bump ex_m3u8 dependency.
8+
* Improve fragments (moof/mdat) generation in low latency HLS.
79

810
## v0.5.0 - 2025-12-24
911

lib/hlx/muxer/cmaf.ex

Lines changed: 19 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ defmodule HLX.Muxer.CMAF do
1313
@type t :: %__MODULE__{
1414
tracks: %{non_neg_integer() => ExMP4.Track.t()},
1515
header: ExMP4.Box.t(),
16-
segments: map(),
17-
fragments: map(),
16+
current_fragments: map(),
17+
fragments: [binary()],
1818
part_duration: map()
1919
}
2020

21-
defstruct [:tracks, :header, :segments, :fragments, :part_duration]
21+
defstruct [:tracks, :header, :current_fragments, :fragments, :part_duration]
2222

2323
@impl true
2424
def init(tracks) do
@@ -27,8 +27,8 @@ defmodule HLX.Muxer.CMAF do
2727
%__MODULE__{
2828
tracks: tracks,
2929
header: build_header(Map.values(tracks)),
30-
segments: new_segments(tracks),
31-
fragments: new_fragments(tracks),
30+
current_fragments: new_fragments(tracks),
31+
fragments: [],
3232
part_duration: Map.new(tracks, fn {id, _track} -> {id, 0} end)
3333
}
3434
end
@@ -41,11 +41,11 @@ defmodule HLX.Muxer.CMAF do
4141
@impl true
4242
def push(sample, state) do
4343
fragments =
44-
Map.update!(state.fragments, sample.track_id, fn {traf, data} ->
44+
Map.update!(state.current_fragments, sample.track_id, fn {traf, data} ->
4545
{Box.Traf.store_sample(traf, sample), [sample.payload | data]}
4646
end)
4747

48-
%{state | fragments: fragments}
48+
%{state | current_fragments: fragments}
4949
end
5050

5151
@impl true
@@ -86,20 +86,15 @@ defmodule HLX.Muxer.CMAF do
8686
mdat = %{mdat | content: Enum.reverse(mdat.content)}
8787

8888
moof = Box.Moof.update_base_offsets(moof, Box.size(moof) + @mdat_header_size, true)
89+
fragment = Box.serialize([moof, mdat])
8990

90-
# push samples to main segments
91-
state =
92-
Enum.reduce(parts, state, fn {_track_id, samples}, state ->
93-
Enum.reduce(samples, state, &push/2)
94-
end)
95-
96-
{Box.serialize([moof, mdat]), part_duration_s, %{state | part_duration: parts_duration}}
91+
{fragment, part_duration_s,
92+
%{state | part_duration: parts_duration, fragments: [fragment | state.fragments]}}
9793
end
9894

9995
@impl true
100-
def flush_segment(state) do
96+
def flush_segment(%{fragments: []} = state) do
10197
{moof, mdat} = build_moof_and_mdat(state)
102-
segments = finalize_segments(state.segments, moof, mdat)
10398

10499
base_data_offset = Box.size(moof) + @mdat_header_size
105100

@@ -114,18 +109,16 @@ defmodule HLX.Muxer.CMAF do
114109
)
115110
end)
116111

117-
segment_data = Box.serialize([segments, moof, mdat])
118-
119-
state = %{
120-
state
121-
| tracks: tracks,
122-
fragments: new_fragments(tracks),
123-
segments: new_segments(tracks)
124-
}
112+
segment_data = Box.serialize([moof, mdat])
113+
state = %{state | tracks: tracks, current_fragments: new_fragments(tracks)}
125114

126115
{segment_data, state}
127116
end
128117

118+
def flush_segment(%{fragments: fragments} = state) do
119+
{Enum.reverse(fragments), %{state | fragments: []}}
120+
end
121+
129122
defp build_header(tracks) do
130123
%Box.Moov{
131124
mvhd: %Box.Mvhd{
@@ -140,20 +133,6 @@ defmodule HLX.Muxer.CMAF do
140133
}
141134
end
142135

143-
defp new_segments(tracks) do
144-
Map.new(tracks, fn {track_id, track} ->
145-
sidx = %Box.Sidx{
146-
reference_id: track_id,
147-
timescale: track.timescale,
148-
earliest_presentation_time: track.duration,
149-
first_offset: 0,
150-
entries: []
151-
}
152-
153-
{track_id, sidx}
154-
end)
155-
end
156-
157136
defp new_fragments(tracks) do
158137
Map.new(tracks, fn {id, track} ->
159138
traf = %Box.Traf{
@@ -171,7 +150,8 @@ defmodule HLX.Muxer.CMAF do
171150
mdat = %Box.Mdat{content: []}
172151

173152
{moof, mdat} =
174-
Enum.reduce(state.fragments, {moof, mdat}, fn {_track_id, {traf, data}}, {moof, mdat} ->
153+
Enum.reduce(state.current_fragments, {moof, mdat}, fn {_track_id, {traf, data}},
154+
{moof, mdat} ->
175155
traf = Box.Traf.finalize(traf, true)
176156
data = Enum.reverse(data)
177157

@@ -186,30 +166,4 @@ defmodule HLX.Muxer.CMAF do
186166

187167
{moof, mdat}
188168
end
189-
190-
defp finalize_segments(segments, moof, mdat) do
191-
{segments, _size} =
192-
Enum.map_reduce(moof.traf, 0, fn traf, acc ->
193-
%Box.Sidx{} = segment = segments[traf.tfhd.track_id]
194-
195-
segment = %Box.Sidx{
196-
segment
197-
| first_offset: acc,
198-
entries: [
199-
%{
200-
reference_type: 0,
201-
referenced_size: Box.size(moof) + Box.size(mdat),
202-
subsegment_duration: Box.Traf.duration(traf),
203-
starts_with_sap: 1,
204-
sap_type: 0,
205-
sap_delta_time: 0
206-
}
207-
]
208-
}
209-
210-
{segment, acc + Box.size(segment)}
211-
end)
212-
213-
Enum.reverse(segments)
214-
end
215169
end

0 commit comments

Comments
 (0)