///|
pub(all) enum DeltaKind {
SegmentAdded
SegmentRemoved
SegmentChanged
SegmentReordered
SequenceAdvanced
SequenceRegressed
TargetDurationChanged
EndListChanged
VariantAdded
VariantRemoved
VariantChanged
RenditionAdded
RenditionRemoved
RenditionChanged
} derive(Eq, @debug.Debug)
///|
pub(all) struct DeltaChange {
kind : DeltaKind
code : String
message : String
old_line : Int
new_line : Int
} derive(Eq, @debug.Debug)
///|
pub(all) struct DeltaReport {
compatible : Bool
breaking_changes : Int
additive_changes : Int
informational_changes : Int
old_sequence : Int
new_sequence : Int
changes : Array[DeltaChange]
} derive(Eq, @debug.Debug)
///|
pub fn compare_playlists(
old_playlist : HlsPlaylist,
new_playlist : HlsPlaylist,
) -> DeltaReport {
let changes : Array[DeltaChange] = []
compare_media_header(old_playlist, new_playlist, changes)
compare_segments(old_playlist, new_playlist, changes)
compare_variants(old_playlist, new_playlist, changes)
compare_renditions(old_playlist, new_playlist, changes)
report_from_changes(old_playlist, new_playlist, changes)
}
///|
pub fn format_delta_report(report : DeltaReport) -> String {
let out = StringBuilder()
out.write_string("delta=compatible=")
out.write_string(report.compatible.to_string())
out.write_string(", breaking=")
out.write_string(report.breaking_changes.to_string())
out.write_string(", additive=")
out.write_string(report.additive_changes.to_string())
out.write_string(", info=")
out.write_string(report.informational_changes.to_string())
out.write_string(", seq=")
out.write_string(report.old_sequence.to_string())
out.write_string("->")
out.write_string(report.new_sequence.to_string())
for change in report.changes {
out.write_char('\n')
out.write_string(delta_kind_to_string(change.kind))
out.write_string(" ")
out.write_string(change.code)
if change.old_line > 0 {
out.write_string(" old_line=")
out.write_string(change.old_line.to_string())
}
if change.new_line > 0 {
out.write_string(" new_line=")
out.write_string(change.new_line.to_string())
}
out.write_string(": ")
out.write_string(change.message)
}
out.to_string()
}
///|
pub fn delta_is_append_only(report : DeltaReport) -> Bool {
if !report.compatible {
return false
}
for change in report.changes {
match change.kind {
SegmentAdded | SequenceAdvanced | EndListChanged => ()
_ => return false
}
}
true
}
///|
pub fn count_delta_kind(report : DeltaReport, kind : DeltaKind) -> Int {
let mut count = 0
for change in report.changes {
if change.kind == kind {
count += 1
}
}
count
}
///|
fn compare_media_header(
old_playlist : HlsPlaylist,
new_playlist : HlsPlaylist,
changes : Array[DeltaChange],
) -> Unit {
if new_playlist.media_sequence > old_playlist.media_sequence {
push_change(
changes,
SequenceAdvanced,
"media-sequence-advanced",
"media sequence advanced by \{new_playlist.media_sequence - old_playlist.media_sequence}",
0,
0,
)
} else if new_playlist.media_sequence < old_playlist.media_sequence {
push_change(
changes,
SequenceRegressed,
"media-sequence-regressed",
"media sequence moved backward from \{old_playlist.media_sequence} to \{new_playlist.media_sequence}",
0,
0,
)
}
if old_playlist.target_duration != new_playlist.target_duration {
push_change(
changes,
TargetDurationChanged,
"target-duration-changed",
"target duration changed from \{old_playlist.target_duration} to \{new_playlist.target_duration}",
0,
0,
)
}
if old_playlist.end_list != new_playlist.end_list {
push_change(
changes,
EndListChanged,
"endlist-changed",
"EXT-X-ENDLIST changed from \{old_playlist.end_list} to \{new_playlist.end_list}",
0,
0,
)
}
}
///|
fn compare_segments(
old_playlist : HlsPlaylist,
new_playlist : HlsPlaylist,
changes : Array[DeltaChange],
) -> Unit {
for old_index, old_segment in old_playlist.segments {
let old_sequence = old_playlist.media_sequence + old_index
match find_segment_by_sequence(new_playlist, old_sequence) {
Some(new_segment) =>
if old_segment.uri != new_segment.uri {
push_change(
changes,
SegmentReordered,
"segment-uri-at-sequence-changed",
"sequence \{old_sequence} changed URI from \{old_segment.uri} to \{new_segment.uri}",
old_segment.line,
new_segment.line,
)
} else if segment_signature(old_segment) !=
segment_signature(new_segment) {
push_change(
changes,
SegmentChanged,
"segment-metadata-changed",
"segment \{old_segment.uri} changed duration, key, map, byte range, or discontinuity metadata",
old_segment.line,
new_segment.line,
)
}
None =>
if !sequence_before_playlist(new_playlist, old_sequence) {
push_change(
changes,
SegmentRemoved,
"segment-removed",
"segment sequence \{old_sequence} disappeared from the new playlist",
old_segment.line,
0,
)
}
}
}
for new_index, new_segment in new_playlist.segments {
let new_sequence = new_playlist.media_sequence + new_index
if find_segment_by_sequence(old_playlist, new_sequence) == None {
push_change(
changes,
SegmentAdded,
"segment-added",
"segment sequence \{new_sequence} was added: \{new_segment.uri}",
0,
new_segment.line,
)
}
}
}
///|
fn compare_variants(
old_playlist : HlsPlaylist,
new_playlist : HlsPlaylist,
changes : Array[DeltaChange],
) -> Unit {
for old_variant in old_playlist.variants {
match find_variant_by_uri(new_playlist, old_variant.uri) {
Some(new_variant) =>
if variant_signature(old_variant) != variant_signature(new_variant) {
push_change(
changes,
VariantChanged,
"variant-changed",
"variant \{old_variant.uri} changed bandwidth, codecs, groups, or display attributes",
old_variant.line,
new_variant.line,
)
}
None =>
push_change(
changes,
VariantRemoved,
"variant-removed",
"variant was removed: \{old_variant.uri}",
old_variant.line,
0,
)
}
}
for new_variant in new_playlist.variants {
if find_variant_by_uri(old_playlist, new_variant.uri) == None {
push_change(
changes,
VariantAdded,
"variant-added",
"variant was added: \{new_variant.uri}",
0,
new_variant.line,
)
}
}
}
///|
fn compare_renditions(
old_playlist : HlsPlaylist,
new_playlist : HlsPlaylist,
changes : Array[DeltaChange],
) -> Unit {
for old_rendition in old_playlist.renditions {
match find_rendition(new_playlist, old_rendition) {
Some(new_rendition) =>
if rendition_signature(old_rendition) !=
rendition_signature(new_rendition) {
push_change(
changes,
RenditionChanged,
"rendition-changed",
"rendition \{rendition_key(old_rendition)} changed URI or selection flags",
old_rendition.line,
new_rendition.line,
)
}
None =>
push_change(
changes,
RenditionRemoved,
"rendition-removed",
"rendition was removed: \{rendition_key(old_rendition)}",
old_rendition.line,
0,
)
}
}
for new_rendition in new_playlist.renditions {
if find_rendition(old_playlist, new_rendition) == None {
push_change(
changes,
RenditionAdded,
"rendition-added",
"rendition was added: \{rendition_key(new_rendition)}",
0,
new_rendition.line,
)
}
}
}
///|
fn report_from_changes(
old_playlist : HlsPlaylist,
new_playlist : HlsPlaylist,
changes : Array[DeltaChange],
) -> DeltaReport {
let mut breaking_changes = 0
let mut additive_changes = 0
let mut informational_changes = 0
for change in changes {
match delta_bucket(change.kind) {
"breaking" => breaking_changes += 1
"additive" => additive_changes += 1
_ => informational_changes += 1
}
}
{
compatible: breaking_changes == 0,
breaking_changes,
additive_changes,
informational_changes,
old_sequence: old_playlist.media_sequence,
new_sequence: new_playlist.media_sequence,
changes,
}
}
///|
fn find_segment_by_sequence(playlist : HlsPlaylist, sequence : Int) -> Segment? {
let index = sequence - playlist.media_sequence
if index < 0 || index >= playlist.segments.length() {
None
} else {
Some(playlist.segments[index])
}
}
///|
fn sequence_before_playlist(playlist : HlsPlaylist, sequence : Int) -> Bool {
sequence < playlist.media_sequence
}
///|
fn find_variant_by_uri(playlist : HlsPlaylist, uri : String) -> VariantStream? {
for variant in playlist.variants {
if variant.uri == uri {
return Some(variant)
}
}
None
}
///|
fn find_rendition(playlist : HlsPlaylist, probe : Rendition) -> Rendition? {
for rendition in playlist.renditions {
if rendition.rendition_type == probe.rendition_type &&
rendition.group_id == probe.group_id &&
rendition.name == probe.name {
return Some(rendition)
}
}
None
}
///|
fn push_change(
changes : Array[DeltaChange],
kind : DeltaKind,
code : String,
message : String,
old_line : Int,
new_line : Int,
) -> Unit {
changes.push({ kind, code, message, old_line, new_line })
}
///|
fn segment_signature(segment : Segment) -> String {
segment.uri +
"|" +
segment.duration_ms.to_string() +
"|" +
option_range_signature(segment.byte_range) +
"|" +
option_key_signature(segment.key) +
"|" +
option_map_signature(segment.map) +
"|" +
segment.discontinuity.to_string() +
"|" +
option_string(segment.program_date_time)
}
///|
fn option_range_signature(range : ByteRange?) -> String {
match range {
Some(value) => byte_range_to_string(value)
None => "-"
}
}
///|
fn option_key_signature(key : KeyInfo?) -> String {
match key {
Some(value) =>
value.key_method +
":" +
option_string(value.uri) +
":" +
option_string(value.iv) +
":" +
option_string(value.key_format) +
":" +
option_string(value.key_format_versions)
None => "-"
}
}
///|
fn option_map_signature(map : SegmentMap?) -> String {
match map {
Some(value) => value.uri + ":" + option_range_signature(value.byte_range)
None => "-"
}
}
///|
fn variant_signature(variant : VariantStream) -> String {
variant.bandwidth.to_string() +
"|" +
option_int(variant.average_bandwidth) +
"|" +
option_string(variant.codecs) +
"|" +
option_resolution(variant.resolution) +
"|" +
option_int(variant.frame_rate_milli) +
"|" +
option_string(variant.audio_group) +
"|" +
option_string(variant.subtitles_group) +
"|" +
option_string(variant.closed_captions)
}
///|
fn rendition_signature(rendition : Rendition) -> String {
rendition_key(rendition) +
"|" +
option_string(rendition.language) +
"|" +
option_string(rendition.uri) +
"|" +
rendition.default.to_string() +
"|" +
rendition.autoselect.to_string() +
"|" +
rendition.forced.to_string()
}
///|
fn rendition_key(rendition : Rendition) -> String {
rendition.rendition_type + ":" + rendition.group_id + ":" + rendition.name
}
///|
fn option_string(value : String?) -> String {
match value {
Some(text) => text
None => "-"
}
}
///|
fn option_int(value : Int?) -> String {
match value {
Some(number) => number.to_string()
None => "-"
}
}
///|
fn option_resolution(value : Resolution?) -> String {
match value {
Some(resolution) => resolution_to_string(resolution)
None => "-"
}
}
///|
fn delta_bucket(kind : DeltaKind) -> String {
match kind {
SegmentRemoved => "breaking"
SegmentChanged => "breaking"
SegmentReordered => "breaking"
SequenceRegressed => "breaking"
TargetDurationChanged => "info"
VariantRemoved => "breaking"
VariantChanged => "breaking"
RenditionRemoved => "breaking"
RenditionChanged => "breaking"
SegmentAdded => "additive"
SequenceAdvanced => "info"
EndListChanged => "info"
VariantAdded => "additive"
RenditionAdded => "additive"
}
}
///|
fn delta_kind_to_string(kind : DeltaKind) -> String {
match kind {
SegmentAdded => "segment-added"
SegmentRemoved => "segment-removed"
SegmentChanged => "segment-changed"
SegmentReordered => "segment-reordered"
SequenceAdvanced => "sequence-advanced"
SequenceRegressed => "sequence-regressed"
TargetDurationChanged => "target-duration-changed"
EndListChanged => "endlist-changed"
VariantAdded => "variant-added"
VariantRemoved => "variant-removed"
VariantChanged => "variant-changed"
RenditionAdded => "rendition-added"
RenditionRemoved => "rendition-removed"
RenditionChanged => "rendition-changed"
}
}