Add opt-in FFmpeg continuity slates with per-viewer localization

This commit is contained in:
2026-10-23 22:35:40 +02:00
parent d097aad032
commit 5f6b0b5ff6
22 changed files with 1407 additions and 30 deletions
+30 -4
View File
@@ -36,6 +36,8 @@ namespace Jellyfin.Plugin.Enigma2
private readonly LiveStreamProbe _streamProbe;
private readonly LiveStreamRelay _streamRelay;
private readonly ILibraryManager _library;
private readonly PlaybackFeedback _feedback;
private readonly ContinuityStreams _continuity;
private string tvBouquetSRef;
private List<ChannelInfo> tvChannelInfos = new List<ChannelInfo>();
@@ -46,13 +48,15 @@ namespace Jellyfin.Plugin.Enigma2
public static LiveTvService Instance { get; private set; }
public LiveTvService(ILogger<LiveTvService> logger, IHttpClientFactory httpClientFactory, InternetEpg internetEpg, LiveStreamProbe streamProbe, LiveStreamRelay streamRelay, ILibraryManager library)
public LiveTvService(ILogger<LiveTvService> logger, IHttpClientFactory httpClientFactory, InternetEpg internetEpg, LiveStreamProbe streamProbe, LiveStreamRelay streamRelay, ILibraryManager library, PlaybackFeedback feedback = null, ContinuityStreams continuity = null)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_streamProbe = streamProbe;
_streamRelay = streamRelay;
_library = library;
_feedback = feedback;
_continuity = continuity;
Instance = this;
}
@@ -1143,7 +1147,15 @@ namespace Jellyfin.Plugin.Enigma2
public async Task<MediaSourceInfo> GetChannelStream(string channelOid, string mediaSourceId, CancellationToken cancellationToken)
{
_logger.LogInformation("[Enigma2] Start GetChannelStream");
PlaybackFeedback.Attempt feedback = null;
if (_feedback != null)
{
try { feedback = await _feedback.Begin(channelOid, cancellationToken).ConfigureAwait(false); }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch { /* Feedback is optional and must not affect playback. */ }
}
try
{
var requestedReference = channelOid;
IEnumerable<ChannelInfo> currentChannels = Array.Empty<ChannelInfo>();
using (var lookupDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
@@ -1214,14 +1226,23 @@ namespace Jellyfin.Plugin.Enigma2
var streamUrl = string.Format("{0}/{1}{2}", baseUrl, channelOid, trancodingUrl);
streamUrl = _streamRelay.Register(streamUrl);
// Background recordings keep the original source: never record an
// endless status slate or guess a viewer language for a timer.
if (Plugin.Instance.Configuration.EnableContinuitySlates && _continuity != null && feedback != null)
{
feedback.Report("waiting_for_stream");
return await _continuity.Open(streamUrl, feedback.Language, cancellationToken).ConfigureAwait(false);
}
var fields = channelOid.Split(':');
var programme = !Plugin.Instance.Configuration.TranscodedStream && fields.Length > 3 && int.TryParse(fields[3], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var sid) ? sid : (int?)null;
var streams = await _streamProbe.Get(streamUrl, cancellationToken, programme).ConfigureAwait(false);
var probe = _streamProbe.Get(streamUrl, cancellationToken, programme);
var streams = await (_feedback == null ? probe : _feedback.AwaitStream(probe, feedback, cancellationToken)).ConfigureAwait(false);
// A live TS may enumerate its EPG/data PID before AV on the next open.
// Jellyfin maps positions in MediaStreams rather than MPEG-TS PID values.
// Its supported unknown-index path selects AV automatically and omits data.
streams = streams.Select(s => LiveStreamProbe.ParseLiveMetadata(s)).ToList();
feedback?.Report("ready");
return new MediaSourceInfo
{
@@ -1235,7 +1256,11 @@ namespace Jellyfin.Plugin.Enigma2
SupportsDirectPlay = false,
MediaStreams = streams
};
throw new ResourceNotFoundException(string.Format("Could not stream channel {0}", channelOid));
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{ feedback?.Report("cancelled"); throw; }
catch
{ feedback?.Report("failed"); throw; }
}
@@ -1628,6 +1653,7 @@ namespace Jellyfin.Plugin.Enigma2
public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
{
_continuity?.Close(id);
await Task.Factory.StartNew<string>(() =>
{
_logger.LogDebug("[Enigma2] LiveTvService.CloseLiveStream: closed stream for subscriptionId: {id}", id);