///|
pub fn playlist_stats(playlist : HlsPlaylist) -> PlaylistStats {
let mut total_duration_ms = 0
let mut max_segment_ms = 0
let mut encrypted_segments = 0
let mut discontinuities = 0
for segment in playlist.segments {
total_duration_ms += segment.duration_ms
if segment.duration_ms > max_segment_ms {
max_segment_ms = segment.duration_ms
}
if segment.discontinuity {
discontinuities += 1
}
match segment.key {
Some(key) =>
if key.key_method != "NONE" {
encrypted_segments += 1
}
None => ()
}
}
{
kind: playlist.kind,
segment_count: playlist.segments.length(),
variant_count: playlist.variants.length(),
rendition_count: playlist.renditions.length(),
total_duration_ms,
max_segment_ms,
encrypted_segments,
discontinuities,
}
}
///|
pub fn summarize_playlist(playlist : HlsPlaylist) -> String {
let stats = playlist_stats(playlist)
"kind=\{kind_to_string(stats.kind)}, segments=\{stats.segment_count}, variants=\{stats.variant_count}, renditions=\{stats.rendition_count}, duration=\{millis_to_string(stats.total_duration_ms)}"
}
///|
pub fn summarize_pipeline_readiness(playlist : HlsPlaylist) -> String {
let validation = validate_playlist(playlist)
let uris = audit_uris(playlist)
let ladder = ladder_report(playlist)
summarize_playlist(playlist) +
"; " +
format_validation_report(validation).split_once("\n").map_or(
format_validation_report(validation),
pair => pair.0.to_owned(),
) +
"; " +
format_uri_audit(uris) +
"; " +
format_ladder_report(ladder)
}
///|
fn kind_to_string(kind : PlaylistKind) -> String {
match kind {
Empty => "empty"
Media => "media"
Master => "master"
Hybrid => "hybrid"
}
}
///|
pub fn format_playlist(playlist : HlsPlaylist) -> String {
let out = StringBuilder()
out.write_string("#EXTM3U\n")
if playlist.version > 0 {
out.write_string("#EXT-X-VERSION:")
out.write_string(playlist.version.to_string())
out.write_char('\n')
}
if playlist.independent_segments {
out.write_string("#EXT-X-INDEPENDENT-SEGMENTS\n")
}
match playlist.start_offset_ms {
Some(offset) => {
out.write_string("#EXT-X-START:TIME-OFFSET=")
out.write_string(millis_to_string(offset))
out.write_char('\n')
}
None => ()
}
for rendition in playlist.renditions {
out.write_string(format_rendition(rendition))
out.write_char('\n')
}
for variant in playlist.variants {
out.write_string(format_variant(variant))
out.write_char('\n')
out.write_string(variant.uri)
out.write_char('\n')
}
if playlist.segments.length() > 0 {
if playlist.target_duration > 0 {
out.write_string("#EXT-X-TARGETDURATION:")
out.write_string(playlist.target_duration.to_string())
out.write_char('\n')
}
if playlist.media_sequence > 0 {
out.write_string("#EXT-X-MEDIA-SEQUENCE:")
out.write_string(playlist.media_sequence.to_string())
out.write_char('\n')
}
if playlist.discontinuity_sequence > 0 {
out.write_string("#EXT-X-DISCONTINUITY-SEQUENCE:")
out.write_string(playlist.discontinuity_sequence.to_string())
out.write_char('\n')
}
write_segments(out, playlist.segments)
if playlist.end_list {
out.write_string("#EXT-X-ENDLIST\n")
}
}
out.to_string()
}
///|
fn write_segments(out : StringBuilder, segments : Array[Segment]) -> Unit {
let mut last_key : KeyInfo? = None
let mut last_map : SegmentMap? = None
for segment in segments {
if segment.discontinuity {
out.write_string("#EXT-X-DISCONTINUITY\n")
}
match segment.program_date_time {
Some(value) => {
out.write_string("#EXT-X-PROGRAM-DATE-TIME:")
out.write_string(value)
out.write_char('\n')
}
None => ()
}
match segment.key {
Some(key) =>
if last_key != Some(key) {
out.write_string(format_key(key))
out.write_char('\n')
last_key = Some(key)
}
None => ()
}
match segment.map {
Some(map) =>
if last_map != Some(map) {
out.write_string(format_map(map))
out.write_char('\n')
last_map = Some(map)
}
None => ()
}
match segment.byte_range {
Some(range) => {
out.write_string("#EXT-X-BYTERANGE:")
out.write_string(byte_range_to_string(range))
out.write_char('\n')
}
None => ()
}
out.write_string("#EXTINF:")
out.write_string(millis_to_string(segment.duration_ms))
out.write_char(',')
out.write_string(segment.title)
out.write_char('\n')
out.write_string(segment.uri)
out.write_char('\n')
}
}
///|
fn format_key(key : KeyInfo) -> String {
let attrs : Array[String] = []
write_attr(attrs, "METHOD", key.key_method)
match key.uri {
Some(uri) => write_attr(attrs, "URI", uri, quoted=true)
None => ()
}
match key.iv {
Some(iv) => write_attr(attrs, "IV", iv)
None => ()
}
match key.key_format {
Some(value) => write_attr(attrs, "KEYFORMAT", value, quoted=true)
None => ()
}
match key.key_format_versions {
Some(value) => write_attr(attrs, "KEYFORMATVERSIONS", value, quoted=true)
None => ()
}
"#EXT-X-KEY:" + join_attrs(attrs)
}
///|
fn format_map(map : SegmentMap) -> String {
let attrs : Array[String] = []
write_attr(attrs, "URI", map.uri, quoted=true)
match map.byte_range {
Some(range) =>
write_attr(attrs, "BYTERANGE", byte_range_to_string(range), quoted=true)
None => ()
}
"#EXT-X-MAP:" + join_attrs(attrs)
}
///|
fn format_variant(variant : VariantStream) -> String {
let attrs : Array[String] = []
write_attr(attrs, "BANDWIDTH", variant.bandwidth.to_string())
match variant.average_bandwidth {
Some(value) => write_attr(attrs, "AVERAGE-BANDWIDTH", value.to_string())
None => ()
}
match variant.codecs {
Some(value) => write_attr(attrs, "CODECS", value, quoted=true)
None => ()
}
match variant.resolution {
Some(value) => write_attr(attrs, "RESOLUTION", resolution_to_string(value))
None => ()
}
match variant.frame_rate_milli {
Some(value) => write_attr(attrs, "FRAME-RATE", millis_to_string(value))
None => ()
}
match variant.audio_group {
Some(value) => write_attr(attrs, "AUDIO", value, quoted=true)
None => ()
}
match variant.subtitles_group {
Some(value) => write_attr(attrs, "SUBTITLES", value, quoted=true)
None => ()
}
match variant.closed_captions {
Some(value) => write_attr(attrs, "CLOSED-CAPTIONS", value, quoted=true)
None => ()
}
"#EXT-X-STREAM-INF:" + join_attrs(attrs)
}
///|
fn format_rendition(rendition : Rendition) -> String {
let attrs : Array[String] = []
write_attr(attrs, "TYPE", rendition.rendition_type)
write_attr(attrs, "GROUP-ID", rendition.group_id, quoted=true)
write_attr(attrs, "NAME", rendition.name, quoted=true)
match rendition.language {
Some(value) => write_attr(attrs, "LANGUAGE", value, quoted=true)
None => ()
}
match rendition.uri {
Some(value) => write_attr(attrs, "URI", value, quoted=true)
None => ()
}
write_attr(attrs, "DEFAULT", yes_no(rendition.default))
write_attr(attrs, "AUTOSELECT", yes_no(rendition.autoselect))
if rendition.forced {
write_attr(attrs, "FORCED", "YES")
}
"#EXT-X-MEDIA:" + join_attrs(attrs)
}
///|
pub fn format_validation_report(report : ValidationReport) -> String {
let out = StringBuilder()
out.write_string("ok=")
out.write_string(report.ok.to_string())
out.write_string(", errors=")
out.write_string(report.errors.to_string())
out.write_string(", warnings=")
out.write_string(report.warnings.to_string())
for issue in report.issues {
out.write_char('\n')
out.write_string(severity_to_string(issue.severity))
out.write_string(" ")
out.write_string(issue.code)
if issue.line > 0 {
out.write_string(" line=")
out.write_string(issue.line.to_string())
}
out.write_string(": ")
out.write_string(issue.message)
}
out.to_string()
}
///|
fn severity_to_string(severity : Severity) -> String {
match severity {
Error => "error"
Warning => "warning"
Info => "info"
}
}