working livestreams!
🏗️ Build Plugin / build (push) Successful in 3m27s
🧪 Test Plugin / test (push) Successful in 1m38s
🚀 Release Plugin / build-and-release (push) Successful in 3m29s

This commit is contained in:
2025-11-22 14:14:43 +01:00
parent b8ac466c90
commit 89a911b9c4
5 changed files with 268 additions and 54 deletions
@@ -0,0 +1,134 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.SRFPlay.Services;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SRFPlay.Providers;
/// <summary>
/// Live stream wrapper for SRF Play streams to handle transcoding sessions.
/// </summary>
internal sealed class SRFLiveStream : ILiveStream
{
private readonly ILogger _logger;
private readonly StreamProxyService _proxyService;
private readonly string _originalItemId;
private MediaSourceInfo? _mediaSource;
/// <summary>
/// Initializes a new instance of the <see cref="SRFLiveStream"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="proxyService">The stream proxy service.</param>
/// <param name="originalItemId">The original item ID.</param>
/// <param name="openToken">The open token.</param>
/// <param name="loggerFactory">The logger factory.</param>
public SRFLiveStream(
ILogger logger,
StreamProxyService proxyService,
string originalItemId,
string openToken,
ILoggerFactory loggerFactory)
{
_logger = logger;
_proxyService = proxyService;
_originalItemId = originalItemId;
OriginalStreamId = openToken;
UniqueId = openToken;
}
/// <inheritdoc />
public int ConsumerCount { get; set; }
/// <inheritdoc />
public string OriginalStreamId { get; set; }
/// <inheritdoc />
public string UniqueId { get; }
/// <inheritdoc />
public string TunerHostId => string.Empty;
/// <inheritdoc />
public bool EnableStreamSharing => false;
/// <inheritdoc />
public MediaSourceInfo MediaSource
{
get => _mediaSource ?? throw new InvalidOperationException("MediaSource not set");
set
{
_mediaSource = value;
_logger.LogInformation(
"SRFLiveStream MediaSource set - Id: {MediaSourceId}, Path: {Path}, OriginalItemId: {OriginalItemId}",
value.Id,
value.Path,
_originalItemId);
// When Jellyfin assigns a live stream ID (for transcoding), register the stream with that ID too
if (value.Id != _originalItemId)
{
_logger.LogInformation(
"Transcoding session detected - LiveStream ID {LiveStreamId} differs from original item ID {OriginalItemId}. Registering stream with both IDs.",
value.Id,
_originalItemId);
// Get the authenticated URL from the original registration
var authenticatedUrl = _proxyService.GetAuthenticatedUrl(_originalItemId);
if (authenticatedUrl != null)
{
// Register the same stream URL with the transcoding session ID
_proxyService.RegisterStream(value.Id, authenticatedUrl);
_logger.LogInformation("Registered stream for transcoding session ID: {LiveStreamId}", value.Id);
}
else
{
_logger.LogWarning("Could not find authenticated URL for original item {OriginalItemId}", _originalItemId);
}
}
}
}
/// <inheritdoc />
public Task Close()
{
_logger.LogInformation("Closing SRF live stream for item {OriginalItemId}", _originalItemId);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task Open(CancellationToken cancellationToken)
{
_logger.LogInformation("Opening SRF live stream for item {OriginalItemId}", _originalItemId);
return Task.CompletedTask;
}
/// <inheritdoc />
public Stream GetStream()
{
throw new NotSupportedException("Direct stream access not supported for SRF streams");
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases the unmanaged resources used by the SRFLiveStream and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
private void Dispose(bool disposing)
{
if (disposing)
{
_logger.LogDebug("Disposing SRF live stream for item {OriginalItemId}", _originalItemId);
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -26,6 +27,7 @@ public class SRFMediaProvider : IMediaSourceProvider
private readonly StreamUrlResolver _streamResolver;
private readonly StreamProxyService _proxyService;
private readonly IServerApplicationHost _appHost;
private readonly Dictionary<string, string> _openTokenToItemId = new();
/// <summary>
/// Initializes a new instance of the <see cref="SRFMediaProvider"/> class.
@@ -184,10 +186,17 @@ public class SRFMediaProvider : IMediaSourceProvider
? config.PublicServerUrl.TrimEnd('/') // Use configured public URL (important for Android/remote clients)
: _appHost.GetSmartApiUrl(string.Empty); // Fall back to Jellyfin's smart URL resolution
// Create proxy URL as absolute HTTP URL (required for ffmpeg)
// Use the actual server URL so remote clients can access it
// Include item ID as query parameter to preserve it during transcoding
var proxyUrl = $"{serverUrl}/Plugins/SRFPlay/Proxy/{itemIdStr}/master.m3u8?itemId={itemIdStr}";
// Detect if this is a live stream
var isLiveStream = chapter.Type == "SCHEDULED_LIVESTREAM" || urn.Contains("livestream", StringComparison.OrdinalIgnoreCase);
// Generate an open token for this media source (used to track transcoding sessions)
var openToken = Guid.NewGuid().ToString("N");
_openTokenToItemId[openToken] = itemIdStr;
_logger.LogDebug("Created open token {OpenToken} for item {ItemId}", openToken, itemIdStr);
// Create proxy URL using token instead of item ID in path
// This prevents Jellyfin from rewriting the URL during transcoding
var proxyUrl = $"{serverUrl}/Plugins/SRFPlay/Proxy/{itemIdStr}/master.m3u8?token={openToken}";
_logger.LogInformation(
"Using proxy URL for item {ItemId}: {ProxyUrl} (PublicServerUrl configured: {IsPublicConfigured})",
@@ -195,9 +204,6 @@ public class SRFMediaProvider : IMediaSourceProvider
proxyUrl,
!string.IsNullOrWhiteSpace(config.PublicServerUrl));
// Detect if this is a live stream
var isLiveStream = chapter.Type == "SCHEDULED_LIVESTREAM" || urn.Contains("livestream", StringComparison.OrdinalIgnoreCase);
// Create media source using proxy URL - enables DirectPlay!
var mediaSource = new MediaSourceInfo
{
@@ -214,10 +220,11 @@ public class SRFMediaProvider : IMediaSourceProvider
RunTimeTicks = chapter.Duration > 0 ? TimeSpan.FromMilliseconds(chapter.Duration).Ticks : null,
VideoType = VideoType.VideoFile,
IsInfiniteStream = isLiveStream, // True for live streams!
RequiresOpening = false,
RequiresOpening = true, // Enable to handle transcoding sessions
RequiresClosing = false,
SupportsProbing = false, // Disable probing for proxy URLs
ReadAtNativeFramerate = isLiveStream, // Read at native framerate for live streams
OpenToken = openToken, // Token to identify this media source
MediaStreams = new List<MediaBrowser.Model.Entities.MediaStream>
{
new MediaBrowser.Model.Entities.MediaStream
@@ -281,10 +288,27 @@ public class SRFMediaProvider : IMediaSourceProvider
}
/// <inheritdoc />
public Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
_logger.LogWarning("OpenMediaSource called with openToken: {OpenToken} - This should not be called for HTTP streams!", openToken);
// Not needed for static HTTP streams
throw new NotImplementedException();
_logger.LogInformation("OpenMediaSource called with openToken: {OpenToken}", openToken);
// Look up the original item ID from the open token
if (!_openTokenToItemId.TryGetValue(openToken, out var originalItemId))
{
_logger.LogError("Open token {OpenToken} not found in registry", openToken);
throw new InvalidOperationException($"Open token {openToken} not found");
}
_logger.LogInformation("Open token {OpenToken} maps to original item ID: {ItemId}", openToken, originalItemId);
// Create a live stream wrapper
var liveStream = new SRFLiveStream(
_logger,
_proxyService,
originalItemId,
openToken,
_loggerFactory);
return await Task.FromResult<ILiveStream>(liveStream).ConfigureAwait(false);
}
}