Finaly working version of livestreams
🏗️ Build Plugin / build (push) Successful in 2m30s
🧪 Test Plugin / test (push) Successful in 1m11s
🚀 Release Plugin / build-and-release (push) Successful in 2m31s

This commit is contained in:
2025-11-15 22:34:21 +01:00
parent 8e86db100a
commit cfe510e15c
5 changed files with 608 additions and 40 deletions
@@ -25,6 +25,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
private readonly ILoggerFactory _loggerFactory;
private readonly ContentRefreshService _contentRefreshService;
private readonly StreamUrlResolver _streamResolver;
private readonly StreamProxyService _proxyService;
private readonly CategoryService? _categoryService;
/// <summary>
@@ -33,17 +34,20 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="contentRefreshService">The content refresh service.</param>
/// <param name="streamResolver">The stream resolver.</param>
/// <param name="proxyService">The stream proxy service.</param>
/// <param name="categoryService">The category service (optional).</param>
public SRFPlayChannel(
ILoggerFactory loggerFactory,
ContentRefreshService contentRefreshService,
StreamUrlResolver streamResolver,
StreamProxyService proxyService,
CategoryService? categoryService = null)
{
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger<SRFPlayChannel>();
_contentRefreshService = contentRefreshService;
_streamResolver = streamResolver;
_proxyService = proxyService;
_categoryService = categoryService;
if (_categoryService == null)
@@ -61,7 +65,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
public string Description => "Swiss Radio and Television video-on-demand content";
/// <inheritdoc />
public string DataVersion => "1.1";
public string DataVersion => "2.0"; // Back to authenticating at channel refresh with auto-refresh for fresh tokens
/// <inheritdoc />
public string HomePageUrl => "https://www.srf.ch/play";
@@ -433,7 +437,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
// Generate deterministic GUID from URN
var itemId = UrnToGuid(urn);
// Get stream URL (unauthenticated - token will be added at playback time by SRFMediaProvider)
// Get stream URL and authenticate it
var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference);
// Skip scheduled livestreams that haven't started yet (no stream URL available)
@@ -447,6 +451,12 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
continue;
}
// Authenticate the stream URL with fresh token
if (!string.IsNullOrEmpty(streamUrl))
{
streamUrl = await _streamResolver.GetAuthenticatedStreamUrlAsync(streamUrl, cancellationToken).ConfigureAwait(false);
}
// Skip items without a valid stream URL
if (string.IsNullOrEmpty(streamUrl))
{
@@ -458,6 +468,13 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
continue;
}
// Register stream with proxy service
_proxyService.RegisterStream(itemId, streamUrl);
// Create proxy URL as absolute HTTP URL (required for ffmpeg)
// Use localhost as Jellyfin should be able to access its own endpoints
var proxyUrl = $"http://localhost:8096/Plugins/SRFPlay/Proxy/{itemId}/master.m3u8";
// Build overview
var overview = chapter.Description ?? chapter.Lead;
@@ -477,7 +494,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
// Use ValidFrom for premiere date if this is a scheduled livestream, otherwise use Date
var premiereDate = chapter.Type == "SCHEDULED_LIVESTREAM" ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime();
// Store unauthenticated URL as placeholder - SRFMediaProvider will provide authenticated version at playback
// Store authenticated URL - tokens refresh automatically via scheduled channel scans
var item = new ChannelItemInfo
{
Id = itemId,
@@ -501,15 +518,38 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
{
Id = itemId,
Name = chapter.Title,
Path = streamUrl, // Unauthenticated URL - placeholder only
Path = proxyUrl, // Proxy URL instead of direct Akamai URL
Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http,
Container = "m3u8",
SupportsDirectPlay = false, // Disable direct play - requires auth from provider
SupportsDirectStream = false, // Disable direct stream - requires auth from provider
SupportsTranscoding = false, // Force use of IMediaSourceProvider
IsRemote = true,
Type = MediaBrowser.Model.Dto.MediaSourceType.Placeholder, // Mark as placeholder
VideoType = VideoType.VideoFile
Container = "hls",
SupportsDirectStream = true,
SupportsDirectPlay = true, // ✅ Enabled! Proxy handles auth
SupportsTranscoding = true,
IsRemote = false, // False because it's a local proxy endpoint
Type = MediaBrowser.Model.Dto.MediaSourceType.Default,
VideoType = VideoType.VideoFile,
RequiresOpening = false,
RequiresClosing = false,
SupportsProbing = false, // Disable probing for proxy URLs
ReadAtNativeFramerate = false,
MediaStreams = new List<MediaBrowser.Model.Entities.MediaStream>
{
new MediaBrowser.Model.Entities.MediaStream
{
Type = MediaBrowser.Model.Entities.MediaStreamType.Video,
Codec = "h264",
Profile = "high",
IsInterlaced = false,
IsDefault = true,
Index = 0
},
new MediaBrowser.Model.Entities.MediaStream
{
Type = MediaBrowser.Model.Entities.MediaStreamType.Audio,
Codec = "aac",
IsDefault = true,
Index = 1
}
}
}
}
};
@@ -523,6 +563,13 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
items.Add(item);
successCount++;
_logger.LogInformation("URN {Urn}: Successfully converted to channel item - {Title}", urn, chapter.Title);
_logger.LogInformation(
"URN {Urn}: MediaSource configured - DirectStream={DirectStream}, DirectPlay={DirectPlay}, Transcoding={Transcoding}, Container={Container}",
urn,
true,
true,
true,
"hls");
}
catch (Exception ex)
{