working livestreams!
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user