///|
pub(all) struct TimelineEntry {
sequence : Int
uri : String
start_ms : Int
end_ms : Int
duration_ms : Int
discontinuity_group : Int
encrypted : Bool
line : Int
} derive(Eq, @debug.Debug)
///|
pub(all) struct TimelineReport {
entries : Array[TimelineEntry]
first_sequence : Int
last_sequence : Int
total_duration_ms : Int
discontinuity_groups : Int
encrypted_segments : Int
longest_segment_ms : Int
shortest_segment_ms : Int
average_segment_ms : Int
open_ended : Bool
} derive(Eq, @debug.Debug)
///|
pub(all) struct WindowPlan {
start_index : Int
dropped_segments : Int
kept_segments : Int
kept_duration_ms : Int
first_sequence : Int
last_sequence : Int
} derive(Eq, @debug.Debug)
///|
pub fn build_timeline(playlist : HlsPlaylist) -> TimelineReport {
let entries : Array[TimelineEntry] = []
let mut cursor = 0
let mut group = playlist.discontinuity_sequence
let mut encrypted_segments = 0
let mut longest_segment_ms = 0
let mut shortest_segment_ms = 0
for index, segment in playlist.segments {
if segment.discontinuity {
group += 1
}
let encrypted = segment_is_encrypted(segment)
if encrypted {
encrypted_segments += 1
}
if index == 0 || segment.duration_ms < shortest_segment_ms {
shortest_segment_ms = segment.duration_ms
}
if segment.duration_ms > longest_segment_ms {
longest_segment_ms = segment.duration_ms
}
let start_ms = cursor
cursor += segment.duration_ms
entries.push({
sequence: playlist.media_sequence + index,
uri: segment.uri,
start_ms,
end_ms: cursor,
duration_ms: segment.duration_ms,
discontinuity_group: group,
encrypted,
line: segment.line,
})
}
let first_sequence = if entries.length() == 0 {
playlist.media_sequence
} else {
entries[0].sequence
}
let last_sequence = if entries.length() == 0 {
playlist.media_sequence - 1
} else {
entries[entries.length() - 1].sequence
}
let average_segment_ms = if entries.length() == 0 {
0
} else {
cursor / entries.length()
}
{
entries,
first_sequence,
last_sequence,
total_duration_ms: cursor,
discontinuity_groups: count_discontinuity_groups(entries),
encrypted_segments,
longest_segment_ms,
shortest_segment_ms,
average_segment_ms,
open_ended: !playlist.end_list,
}
}
///|
pub fn format_timeline_report(report : TimelineReport) -> String {
"timeline=segments=\{report.entries.length()}, seq=\{report.first_sequence}-\{report.last_sequence}, duration=\{millis_to_string(report.total_duration_ms)}, groups=\{report.discontinuity_groups}, encrypted=\{report.encrypted_segments}, min=\{millis_to_string(report.shortest_segment_ms)}, max=\{millis_to_string(report.longest_segment_ms)}, avg=\{millis_to_string(report.average_segment_ms)}, open=\{report.open_ended}"
}
///|
pub fn timeline_entry_at(
playlist : HlsPlaylist,
position_ms : Int,
) -> TimelineEntry? {
let report = build_timeline(playlist)
for entry in report.entries {
if position_ms >= entry.start_ms && position_ms < entry.end_ms {
return Some(entry)
}
}
if report.entries.length() > 0 && position_ms == report.total_duration_ms {
Some(report.entries[report.entries.length() - 1])
} else {
None
}
}
///|
pub fn timeline_entry_by_sequence(
playlist : HlsPlaylist,
sequence : Int,
) -> TimelineEntry? {
let report = build_timeline(playlist)
for entry in report.entries {
if entry.sequence == sequence {
return Some(entry)
}
}
None
}
///|
pub fn live_window_plan(
playlist : HlsPlaylist,
min_window_ms : Int,
) -> WindowPlan {
let mut start_index = 0
let mut kept_duration_ms = 0
let count = playlist.segments.length()
let mut index = count
while index > 0 {
index -= 1
kept_duration_ms += playlist.segments[index].duration_ms
start_index = index
if kept_duration_ms >= min_window_ms {
break
}
}
let kept_segments = count - start_index
let first_sequence = playlist.media_sequence + start_index
let last_sequence = if kept_segments == 0 {
first_sequence - 1
} else {
first_sequence + kept_segments - 1
}
{
start_index,
dropped_segments: start_index,
kept_segments,
kept_duration_ms,
first_sequence,
last_sequence,
}
}
///|
pub fn trim_live_window(
playlist : HlsPlaylist,
min_window_ms : Int,
) -> HlsPlaylist {
let plan = live_window_plan(playlist, min_window_ms)
trim_from_index(playlist, plan.start_index)
}
///|
pub fn trim_before_sequence(
playlist : HlsPlaylist,
first_sequence : Int,
) -> HlsPlaylist {
if first_sequence <= playlist.media_sequence {
return playlist
}
let offset = first_sequence - playlist.media_sequence
if offset >= playlist.segments.length() {
{ ..playlist, media_sequence: first_sequence, segments: [] }
} else {
trim_from_index(playlist, offset)
}
}
///|
pub fn format_window_plan(plan : WindowPlan) -> String {
"window=drop=\{plan.dropped_segments}, keep=\{plan.kept_segments}, seq=\{plan.first_sequence}-\{plan.last_sequence}, duration=\{millis_to_string(plan.kept_duration_ms)}"
}
///|
pub fn segment_sequence(playlist : HlsPlaylist, index : Int) -> Int {
playlist.media_sequence + index
}
///|
pub fn segment_end_sequence(playlist : HlsPlaylist) -> Int {
playlist.media_sequence + playlist.segments.length() - 1
}
///|
pub fn playlist_duration_seconds_floor(playlist : HlsPlaylist) -> Int {
build_timeline(playlist).total_duration_ms / 1000
}
///|
pub fn playlist_duration_seconds_ceil(playlist : HlsPlaylist) -> Int {
let total = build_timeline(playlist).total_duration_ms
if total == 0 {
0
} else {
(total + 999) / 1000
}
}
///|
pub fn discontinuity_sequence_for_segment(
playlist : HlsPlaylist,
sequence : Int,
) -> Int? {
match timeline_entry_by_sequence(playlist, sequence) {
Some(entry) => Some(entry.discontinuity_group)
None => None
}
}
///|
pub fn sequence_for_playhead(playlist : HlsPlaylist, position_ms : Int) -> Int? {
match timeline_entry_at(playlist, position_ms) {
Some(entry) => Some(entry.sequence)
None => None
}
}
///|
pub fn time_range_for_sequence(
playlist : HlsPlaylist,
sequence : Int,
) -> (Int, Int)? {
match timeline_entry_by_sequence(playlist, sequence) {
Some(entry) => Some((entry.start_ms, entry.end_ms))
None => None
}
}
///|
fn trim_from_index(playlist : HlsPlaylist, start_index : Int) -> HlsPlaylist {
let segments : Array[Segment] = []
for index, segment in playlist.segments {
if index >= start_index {
segments.push(segment)
}
}
{
..playlist,
media_sequence: playlist.media_sequence + start_index,
segments,
}
}
///|
fn segment_is_encrypted(segment : Segment) -> Bool {
match segment.key {
Some(key) => key.key_method != "NONE"
None => false
}
}
///|
fn count_discontinuity_groups(entries : Array[TimelineEntry]) -> Int {
if entries.length() == 0 {
return 0
}
let mut groups = 1
let mut last = entries[0].discontinuity_group
for entry in entries {
if entry.discontinuity_group != last {
groups += 1
last = entry.discontinuity_group
}
}
groups
}