fix: hls partial sequencing
All checks were successful
continuous-integration/drone Build is passing

This commit is contained in:
2025-06-13 12:36:20 +01:00
parent fee5e77407
commit 047b3fec59
4 changed files with 55 additions and 23 deletions

2
Cargo.lock generated
View File

@ -2153,7 +2153,7 @@ checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]] [[package]]
name = "m3u8-rs" name = "m3u8-rs"
version = "6.0.0" version = "6.0.0"
source = "git+https://github.com/v0l/m3u8-rs.git?rev=d76ff96326814237a6d5e92288cdfe7060a43168#d76ff96326814237a6d5e92288cdfe7060a43168" source = "git+https://git.v0l.io/Kieran/m3u8-rs.git?rev=5b7aa0c65994b5ab2780b7ed27d84c03bc32d19f#5b7aa0c65994b5ab2780b7ed27d84c03bc32d19f"
dependencies = [ dependencies = [
"chrono", "chrono",
"nom", "nom",

View File

@ -24,6 +24,6 @@ url = "2.5.0"
itertools = "0.14.0" itertools = "0.14.0"
chrono = { version = "^0.4.38", features = ["serde"] } chrono = { version = "^0.4.38", features = ["serde"] }
hex = "0.4.3" hex = "0.4.3"
m3u8-rs = { git = "https://github.com/v0l/m3u8-rs.git", rev = "d76ff96326814237a6d5e92288cdfe7060a43168" } m3u8-rs = { git = "https://git.v0l.io/Kieran/m3u8-rs.git", rev = "5b7aa0c65994b5ab2780b7ed27d84c03bc32d19f" }
sha2 = "0.10.8" sha2 = "0.10.8"
data-encoding = "2.9.0" data-encoding = "2.9.0"

View File

@ -10,7 +10,7 @@ use ffmpeg_rs_raw::ffmpeg_sys_the_third::{
use ffmpeg_rs_raw::{cstr, Encoder, Muxer}; use ffmpeg_rs_raw::{cstr, Encoder, Muxer};
use itertools::Itertools; use itertools::Itertools;
use log::{info, trace, warn}; use log::{info, trace, warn};
use m3u8_rs::{ByteRange, MediaSegment, MediaSegmentType, Part, PartInf}; use m3u8_rs::{ByteRange, MediaSegment, MediaSegmentType, Part, PartInf, PreloadHint};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::fs::File; use std::fs::File;
@ -103,6 +103,8 @@ pub struct HlsVariant {
current_partial_index: u64, current_partial_index: u64,
/// HLS-LL: Current duration in this partial /// HLS-LL: Current duration in this partial
current_partial_duration: f64, current_partial_duration: f64,
/// HLS-LL: Whether the next partial segment should be marked as independent
next_partial_independent: bool,
} }
#[derive(PartialEq)] #[derive(PartialEq)]
@ -114,8 +116,8 @@ enum HlsSegment {
impl HlsSegment { impl HlsSegment {
fn to_media_segment(&self) -> MediaSegmentType { fn to_media_segment(&self) -> MediaSegmentType {
match self { match self {
HlsSegment::Full(s) => s.to_media_segment(), HlsSegment::Full(f) => f.to_media_segment(),
HlsSegment::Partial(s) => s.to_media_segment(), HlsSegment::Partial(p) => p.to_media_segment(),
} }
} }
} }
@ -168,6 +170,13 @@ impl PartialSegmentInfo {
fn filename(&self) -> String { fn filename(&self) -> String {
HlsVariant::segment_name(self.parent_kind, self.parent_index) HlsVariant::segment_name(self.parent_kind, self.parent_index)
} }
/// Byte offset where this partial segment ends
fn end_pos(&self) -> Option<u64> {
self.byte_range
.as_ref()
.map(|(len, start)| start.unwrap_or(0) + len)
}
} }
impl HlsVariant { impl HlsVariant {
@ -271,6 +280,7 @@ impl HlsVariant {
partial_target_duration: 0.33, partial_target_duration: 0.33,
current_partial_index: 0, current_partial_index: 0,
current_partial_duration: 0.0, current_partial_duration: 0.0,
next_partial_independent: false,
}) })
} }
@ -311,6 +321,16 @@ impl HlsVariant {
is_ref_pkt = false; is_ref_pkt = false;
} }
// HLS-LL: write prev partial segment
if self.current_partial_duration >= self.partial_target_duration as f64 {
self.create_partial_segment()?;
// HLS-LL: Mark next partial as independent if this packet is a keyframe
if can_split {
self.next_partial_independent = true;
}
}
// check if current packet is keyframe, flush current segment // check if current packet is keyframe, flush current segment
if self.packets_written > 1 && can_split && self.duration >= self.segment_length as f64 { if self.packets_written > 1 && can_split && self.duration >= self.segment_length as f64 {
result = self.split_next_seg()?; result = self.split_next_seg()?;
@ -334,11 +354,6 @@ impl HlsVariant {
self.mux.write_packet(pkt)?; self.mux.write_packet(pkt)?;
self.packets_written += 1; self.packets_written += 1;
// HLS-LL: write next partial segment
if is_ref_pkt && self.current_partial_duration >= self.partial_target_duration as f64 {
self.create_partial_segment(can_split)?;
}
Ok(result) Ok(result)
} }
@ -347,27 +362,30 @@ impl HlsVariant {
} }
/// Create a partial segment for LL-HLS /// Create a partial segment for LL-HLS
fn create_partial_segment(&mut self, independent: bool) -> Result<()> { fn create_partial_segment(&mut self) -> Result<()> {
let ctx = self.mux.context(); let ctx = self.mux.context();
let pos = unsafe { let end_pos = unsafe {
avio_flush((*ctx).pb); avio_flush((*ctx).pb);
avio_size((*ctx).pb) as u64 avio_size((*ctx).pb) as u64
}; };
let previous_partial_end = self.segments.last().and_then(|s| match &s { let previous_end_pos = self
HlsSegment::Partial(p) => p.byte_range.as_ref().map(|(len, start)| start.unwrap_or(0) + len), .segments
_ => None, .last()
}); .and_then(|s| match &s {
HlsSegment::Partial(p) => p.end_pos(),
_ => None,
})
.unwrap_or(0);
let independent = self.next_partial_independent;
let partial_size = end_pos - previous_end_pos;
let partial_info = PartialSegmentInfo { let partial_info = PartialSegmentInfo {
index: self.current_partial_index, index: self.current_partial_index,
parent_index: self.idx, parent_index: self.idx,
parent_kind: self.segment_type, parent_kind: self.segment_type,
duration: self.current_partial_duration, duration: self.current_partial_duration,
independent, independent,
byte_range: match previous_partial_end { byte_range: Some((partial_size, Some(previous_end_pos))),
Some(prev_end) => Some((pos - prev_end, Some(prev_end))),
_ => Some((pos, Some(0))),
},
}; };
trace!( trace!(
@ -380,6 +398,7 @@ impl HlsVariant {
self.segments.push(HlsSegment::Partial(partial_info)); self.segments.push(HlsSegment::Partial(partial_info));
self.current_partial_index += 1; self.current_partial_index += 1;
self.current_partial_duration = 0.0; self.current_partial_duration = 0.0;
self.next_partial_independent = false;
self.write_playlist()?; self.write_playlist()?;
@ -558,6 +577,17 @@ impl HlsVariant {
let mut pl = m3u8_rs::MediaPlaylist::default(); let mut pl = m3u8_rs::MediaPlaylist::default();
pl.target_duration = (self.segment_length.ceil() as u64).max(1); pl.target_duration = (self.segment_length.ceil() as u64).max(1);
pl.segments = self.segments.iter().map(|s| s.to_media_segment()).collect(); pl.segments = self.segments.iter().map(|s| s.to_media_segment()).collect();
// append segment preload for next part segment
if let Some(HlsSegment::Partial(partial)) = self.segments.last() {
// TODO: try to estimate if there will be another partial segment
pl.segments.push(MediaSegmentType::PreloadHint(PreloadHint {
hint_type: "PART".to_string(),
uri: partial.filename(),
byte_range_start: partial.end_pos(),
byte_range_length: None,
}));
}
pl.version = Some(6); pl.version = Some(6);
pl.part_inf = Some(PartInf { pl.part_inf = Some(PartInf {
part_target: self.partial_target_duration as f64, part_target: self.partial_target_duration as f64,

View File

@ -208,13 +208,15 @@ impl PipelineRunner {
unsafe fn generate_thumb_from_frame(&mut self, frame: *mut AVFrame) -> Result<()> { unsafe fn generate_thumb_from_frame(&mut self, frame: *mut AVFrame) -> Result<()> {
if self.thumb_interval > 0 && (self.frame_ctr % self.thumb_interval) == 0 { if self.thumb_interval > 0 && (self.frame_ctr % self.thumb_interval) == 0 {
let frame = av_frame_clone(frame).addr(); let frame = av_frame_clone(frame).addr();
let dst_pic = PathBuf::from(&self.out_dir) let dir = PathBuf::from(&self.out_dir).join(self.connection.id.to_string());
.join(self.connection.id.to_string()) if !dir.exists() {
.join("thumb.webp"); std::fs::create_dir_all(&dir)?;
}
std::thread::spawn(move || unsafe { std::thread::spawn(move || unsafe {
let mut frame = frame as *mut AVFrame; //TODO: danger?? let mut frame = frame as *mut AVFrame; //TODO: danger??
let thumb_start = Instant::now(); let thumb_start = Instant::now();
let dst_pic = dir.join("thumb.webp");
if let Err(e) = Self::save_thumb(frame, &dst_pic) { if let Err(e) = Self::save_thumb(frame, &dst_pic) {
warn!("Failed to save thumb: {}", e); warn!("Failed to save thumb: {}", e);
} }