62 lines
3.2 KiB
C#
62 lines
3.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
namespace Jellyfin.Plugin.Enigma2;
|
|
|
|
/// <summary>Bounded, never-ending HLS timeline shared by language-specific viewers.</summary>
|
|
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);
|
|
private readonly List<Segment> segments = new();
|
|
private readonly object gate = new();
|
|
private long next, discontinuities;
|
|
private long clock90k = 126000;
|
|
private string previousEpoch;
|
|
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)
|
|
{
|
|
var discontinuity = next > 0 && (slate != null || previousEpoch != epoch);
|
|
segments.Add(new(next++, discontinuities, discontinuity, duration, video, slate, clock90k));
|
|
clock90k += (long)Math.Round(duration * 90000);
|
|
if (discontinuity) discontinuities++;
|
|
previousEpoch = epoch; lastAppend = now;
|
|
while (segments.Count > 8) segments.RemoveAt(0);
|
|
}
|
|
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); }
|
|
}
|
|
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);
|
|
}
|
|
public Segment Find(long sequence) { lock (gate) return segments.FirstOrDefault(s => s.Sequence == sequence); }
|
|
public string Playlist()
|
|
{
|
|
lock (gate)
|
|
{
|
|
var window = segments.TakeLast(4).ToArray();
|
|
var text = new StringBuilder($"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:{Math.Max(5, Math.Ceiling(window.Select(s => s.Duration).DefaultIfEmpty(5).Max()))}\n");
|
|
if (window.Length == 0) return text.ToString();
|
|
text.Append("#EXT-X-MEDIA-SEQUENCE:").Append(window[0].Sequence).Append('\n');
|
|
text.Append("#EXT-X-DISCONTINUITY-SEQUENCE:").Append(window[0].DiscontinuitiesBefore).Append('\n');
|
|
foreach (var segment in window)
|
|
{
|
|
if (segment.Discontinuity) text.Append("#EXT-X-DISCONTINUITY\n");
|
|
text.Append("#EXTINF:").Append(segment.Duration.ToString("0.000000", CultureInfo.InvariantCulture)).Append(",\n");
|
|
text.Append(segment.Sequence).Append(".ts\n");
|
|
}
|
|
return text.ToString(); // Never ENDLIST: only the viewer closes playback.
|
|
}
|
|
}
|
|
}
|