///|
pub fn parse_playlist(input : String) -> Result[HlsPlaylist, ParseError] {
let lines = split_char(normalize_newlines(input), '\n')
let segments : Array[Segment] = []
let variants : Array[VariantStream] = []
let renditions : Array[Rendition] = []
let unknown_tags : Array[String] = []
let mut seen_header = false
let mut version = 0
let mut target_duration = 0
let mut media_sequence = 0
let mut discontinuity_sequence = 0
let mut start_offset_ms : Int? = None
let mut end_list = false
let mut independent_segments = false
let mut current_key : KeyInfo? = None
let mut current_map : SegmentMap? = None
let mut pending_duration = -1
let mut pending_title = ""
let mut pending_byte_range : ByteRange? = None
let mut pending_discontinuity = false
let mut pending_program_date_time : String? = None
let mut pending_variant_attrs : Array[HlsAttribute]? = None
let mut pending_variant_line = 0
for index, raw_line in lines {
let line = index + 1
let text = clean(raw_line[:])
if text == "" {
continue
}
if text == "#EXTM3U" {
seen_header = true
continue
}
if !seen_header {
return Err(MissingHeader(line))
}
if text.has_prefix("#EXTINF:") {
match parse_extinf(text, line) {
Ok((duration, title)) => {
pending_duration = duration
pending_title = title
}
Err(err) => return Err(err)
}
} else if text.has_prefix("#EXT-X-VERSION:") {
match parse_unsigned_int(after_colon(text), line, "EXT-X-VERSION") {
Ok(value) => version = value
Err(err) => return Err(err)
}
} else if text.has_prefix("#EXT-X-TARGETDURATION:") {
match parse_unsigned_int(
after_colon(text),
line,
"EXT-X-TARGETDURATION",
) {
Ok(value) => target_duration = value
Err(err) => return Err(err)
}
} else if text.has_prefix("#EXT-X-MEDIA-SEQUENCE:") {
match parse_unsigned_int(
after_colon(text),
line,
"EXT-X-MEDIA-SEQUENCE",
) {
Ok(value) => media_sequence = value
Err(err) => return Err(err)
}
} else if text.has_prefix("#EXT-X-DISCONTINUITY-SEQUENCE:") {
match parse_unsigned_int(
after_colon(text),
line,
"EXT-X-DISCONTINUITY-SEQUENCE",
) {
Ok(value) => discontinuity_sequence = value
Err(err) => return Err(err)
}
} else if text.has_prefix("#EXT-X-START:") {
match parse_start_offset(text, line) {
Ok(value) => start_offset_ms = Some(value)
Err(err) => return Err(err)
}
} else if text == "#EXT-X-ENDLIST" {
end_list = true
} else if text == "#EXT-X-INDEPENDENT-SEGMENTS" {
independent_segments = true
} else if text.has_prefix("#EXT-X-KEY:") {
match parse_key(after_colon(text), line) {
Ok(value) => current_key = Some(value)
Err(err) => return Err(err)
}
} else if text.has_prefix("#EXT-X-MAP:") {
match parse_map(after_colon(text), line) {
Ok(value) => current_map = Some(value)
Err(err) => return Err(err)
}
} else if text.has_prefix("#EXT-X-BYTERANGE:") {
match parse_byte_range(after_colon(text), line) {
Ok(value) => pending_byte_range = Some(value)
Err(err) => return Err(err)
}
} else if text == "#EXT-X-DISCONTINUITY" {
pending_discontinuity = true
} else if text.has_prefix("#EXT-X-PROGRAM-DATE-TIME:") {
pending_program_date_time = Some(after_colon(text))
} else if text.has_prefix("#EXT-X-STREAM-INF:") {
match parse_attribute_list(after_colon(text), line~) {
Ok(attrs) => {
pending_variant_attrs = Some(attrs)
pending_variant_line = line
}
Err(err) => return Err(err)
}
} else if text.has_prefix("#EXT-X-MEDIA:") {
match parse_rendition(after_colon(text), line) {
Ok(rendition) => renditions.push(rendition)
Err(err) => return Err(err)
}
} else if text.has_prefix("#") {
push_unknown(unknown_tags, line, text)
} else {
match pending_variant_attrs {
Some(attrs) => {
match parse_variant(attrs, text, pending_variant_line) {
Ok(variant) => {
variants.push(variant)
pending_variant_attrs = None
pending_variant_line = 0
}
Err(err) => return Err(err)
}
}
None =>
if pending_duration >= 0 {
segments.push({
uri: text,
title: pending_title,
duration_ms: pending_duration,
byte_range: pending_byte_range,
key: current_key,
map: current_map,
discontinuity: pending_discontinuity,
program_date_time: pending_program_date_time,
line,
})
pending_duration = -1
pending_title = ""
pending_byte_range = None
pending_discontinuity = false
pending_program_date_time = None
} else {
return Err(UriWithoutContext(line, text))
}
}
}
}
if pending_variant_attrs is Some(_) {
return Err(MissingVariantUri(pending_variant_line))
}
if pending_duration >= 0 {
return Err(MissingSegmentUri(lines.length()))
}
let kind = classify_playlist(
segments.length(),
variants.length(),
renditions.length(),
)
Ok({
version,
target_duration,
media_sequence,
discontinuity_sequence,
start_offset_ms,
end_list,
independent_segments,
segments,
variants,
renditions,
unknown_tags,
kind,
})
}
///|
fn classify_playlist(
segment_count : Int,
variant_count : Int,
rendition_count : Int,
) -> PlaylistKind {
if segment_count > 0 && (variant_count > 0 || rendition_count > 0) {
Hybrid
} else if segment_count > 0 {
Media
} else if variant_count > 0 || rendition_count > 0 {
Master
} else {
Empty
}
}
///|
fn after_colon(text : String) -> String {
match text.split_once(":") {
Some((_, value)) => value.to_owned()
None => ""
}
}
///|
fn parse_extinf(text : String, line : Int) -> Result[(Int, String), ParseError] {
let body = after_colon(text)
match body.split_once(",") {
Some((duration, title)) =>
match parse_millis(clean(duration), line, "EXTINF.duration") {
Ok(ms) => Ok((ms, clean(title)))
Err(err) => Err(err)
}
None =>
match parse_millis(body, line, "EXTINF.duration") {
Ok(ms) => Ok((ms, ""))
Err(err) => Err(err)
}
}
}
///|
fn parse_start_offset(text : String, line : Int) -> Result[Int, ParseError] {
match parse_attribute_list(after_colon(text), line~) {
Ok(attrs) =>
match maybe_attr(attrs, "TIME-OFFSET") {
Some(value) => parse_signed_millis(value, line, "TIME-OFFSET")
None => Err(InvalidAttribute(line, "EXT-X-START requires TIME-OFFSET"))
}
Err(err) => Err(err)
}
}
///|
fn parse_key(raw : String, line : Int) -> Result[KeyInfo, ParseError] {
match parse_attribute_list(raw, line~) {
Ok(attrs) =>
match required_attr(attrs, "METHOD", line) {
Ok(key_method) =>
Ok({
key_method,
uri: maybe_attr(attrs, "URI"),
iv: maybe_attr(attrs, "IV"),
key_format: maybe_attr(attrs, "KEYFORMAT"),
key_format_versions: maybe_attr(attrs, "KEYFORMATVERSIONS"),
line,
})
Err(err) => Err(err)
}
Err(err) => Err(err)
}
}
///|
fn parse_map(raw : String, line : Int) -> Result[SegmentMap, ParseError] {
match parse_attribute_list(raw, line~) {
Ok(attrs) =>
match required_attr(attrs, "URI", line) {
Ok(uri) => {
let byte_range = match maybe_attr(attrs, "BYTERANGE") {
Some(value) =>
match parse_byte_range(value, line) {
Ok(range) => Some(range)
Err(err) => return Err(err)
}
None => None
}
Ok({ uri, byte_range, line })
}
Err(err) => Err(err)
}
Err(err) => Err(err)
}
}
///|
fn parse_variant(
attrs : Array[HlsAttribute],
uri : String,
line : Int,
) -> Result[VariantStream, ParseError] {
let bandwidth = match maybe_int_attr(attrs, "BANDWIDTH", line) {
Ok(Some(value)) => value
Ok(None) =>
return Err(InvalidAttribute(line, "EXT-X-STREAM-INF requires BANDWIDTH"))
Err(err) => return Err(err)
}
let average_bandwidth = match maybe_int_attr(
attrs,
"AVERAGE-BANDWIDTH",
line,
) {
Ok(value) => value
Err(err) => return Err(err)
}
let resolution = match maybe_attr(attrs, "RESOLUTION") {
Some(value) =>
match parse_resolution(value, line) {
Ok(parsed) => Some(parsed)
Err(err) => return Err(err)
}
None => None
}
let frame_rate_milli = match maybe_attr(attrs, "FRAME-RATE") {
Some(value) =>
match parse_millis(value, line, "FRAME-RATE") {
Ok(parsed) => Some(parsed)
Err(err) => return Err(err)
}
None => None
}
Ok({
uri,
bandwidth,
average_bandwidth,
codecs: maybe_attr(attrs, "CODECS"),
resolution,
frame_rate_milli,
audio_group: maybe_attr(attrs, "AUDIO"),
subtitles_group: maybe_attr(attrs, "SUBTITLES"),
closed_captions: maybe_attr(attrs, "CLOSED-CAPTIONS"),
line,
})
}
///|
fn parse_rendition(raw : String, line : Int) -> Result[Rendition, ParseError] {
match parse_attribute_list(raw, line~) {
Ok(attrs) => {
let rendition_type = match required_attr(attrs, "TYPE", line) {
Ok(value) => value
Err(err) => return Err(err)
}
let group_id = match required_attr(attrs, "GROUP-ID", line) {
Ok(value) => value
Err(err) => return Err(err)
}
let name = match required_attr(attrs, "NAME", line) {
Ok(value) => value
Err(err) => return Err(err)
}
let default = match maybe_attr(attrs, "DEFAULT") {
Some(value) => parse_yes_no(value)
None => false
}
let autoselect = match maybe_attr(attrs, "AUTOSELECT") {
Some(value) => parse_yes_no(value)
None => false
}
let forced = match maybe_attr(attrs, "FORCED") {
Some(value) => parse_yes_no(value)
None => false
}
Ok({
rendition_type,
group_id,
name,
language: maybe_attr(attrs, "LANGUAGE"),
uri: maybe_attr(attrs, "URI"),
default,
autoselect,
forced,
line,
})
}
Err(err) => Err(err)
}
}