Preserve native broadcast video and add patient shared stream startup

This commit is contained in:
2026-10-25 16:59:56 +01:00
parent bd7395f045
commit 11a34a5195
27 changed files with 708 additions and 139 deletions
+25 -7
View File
@@ -11,19 +11,22 @@ public sealed class ContinuityTimeline
{
// 112 frames at 50 Hz == 105 AAC frames at 48 kHz: exact AV boundary.
public const double SegmentSeconds = 2.24;
public sealed record Segment(long Sequence, long DiscontinuitiesBefore, bool Discontinuity, double Duration, byte[] Video, string Slate, long Start90k);
public sealed record Segment(long Sequence, long DiscontinuitiesBefore, bool Discontinuity, double Duration, byte[] Video, string Slate, long Start90k, long? ClockOffset90k = null);
private readonly double slateSeconds;
private readonly List<Segment> segments = new();
private readonly object gate = new();
private long next, discontinuities;
private long clock90k = 126000;
private string previousEpoch;
private string clockEpoch;
private long sourceOffset90k, sourceWrap90k, lastDecode90k;
private DateTime lastAppend, lastLive;
public ContinuityTimeline(DateTime now, bool withSlates = true) { lastAppend = now; lastLive = DateTime.MinValue; if (!withSlates) return; Add(null, "waiting", "slate", SegmentSeconds, now); Add(null, "waiting", "slate", SegmentSeconds, now); }
private void Add(byte[] video, string slate, string epoch, double duration, DateTime now)
public ContinuityTimeline(DateTime now, bool withSlates = true, double slateSeconds = SegmentSeconds) { this.slateSeconds = slateSeconds; lastAppend = now; lastLive = DateTime.MinValue; if (!withSlates) return; Add(null, "waiting", "slate", slateSeconds, now); Add(null, "waiting", "slate", slateSeconds, now); }
private void Add(byte[] video, string slate, string epoch, double duration, DateTime now, long? offset = null, long? start = null, long? end = null)
{
var discontinuity = next > 0 && (slate != null || previousEpoch != epoch);
segments.Add(new(next++, discontinuities, discontinuity, duration, video, slate, clock90k));
clock90k += (long)Math.Round(duration * 90000);
segments.Add(new(next++, discontinuities, discontinuity, duration, video, slate, start ?? clock90k, offset));
clock90k = end ?? clock90k + (long)Math.Round(duration * 90000);
if (discontinuity) discontinuities++;
previousEpoch = epoch; lastAppend = now;
while (segments.Count > 8) segments.RemoveAt(0);
@@ -31,13 +34,28 @@ public sealed class ContinuityTimeline
public void Live(byte[] video, string epoch, double duration, DateTime now)
{
if (video == null || video.Length == 0 || video.Length > 32 * 1024 * 1024 || duration <= 0 || duration > 30) throw new ArgumentException("Invalid live segment");
lock (gate) { lastLive = now; Add(video, null, epoch, duration, now); }
lock (gate)
{
lastLive = now;
var bounds = TransportStreamClock.ClockBounds(video);
if (bounds == null) { Add(video, null, epoch, duration, now); return; }
var times = bounds.Value;
// Keep one offset for an entire producer epoch. Anchoring each segment
// to its first PTS corrupts DTS order when copied video has B-frames.
if (clockEpoch != epoch || previousEpoch != epoch)
{ clockEpoch = epoch; sourceWrap90k = 0; sourceOffset90k = clock90k + 9000 - times.Earliest; }
else if (times.Decode + sourceWrap90k < lastDecode90k - (1L << 32)) sourceWrap90k += 1L << 33;
lastDecode90k = times.Decode + sourceWrap90k;
var offset = sourceOffset90k + sourceWrap90k;
var end = Math.Max(times.Decode + (long)Math.Round(duration * 90000), times.Latest + 9000) + offset;
Add(video, null, epoch, duration, now, offset, times.Decode + offset, end);
}
}
public void Tick(DateTime now, bool retrying)
{
lock (gate)
if ((now - lastAppend).TotalSeconds >= 2 && (now - lastLive).TotalSeconds >= 5)
Add(null, retrying ? "retrying" : "waiting", "slate", SegmentSeconds, now);
Add(null, retrying ? "retrying" : "waiting", "slate", slateSeconds, now);
}
public Segment Find(long sequence) { lock (gate) return segments.FirstOrDefault(s => s.Sequence == sequence); }
public string Playlist()