using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.SRFPlay.Api.Models; using Jellyfin.Plugin.SRFPlay.Configuration; using Jellyfin.Plugin.SRFPlay.Constants; using Jellyfin.Plugin.SRFPlay.Services.Interfaces; using MediaBrowser.Controller; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.SRFPlay.Services; /// /// Factory for creating MediaSourceInfo objects with consistent configuration. /// Consolidates duplicated logic from SRFMediaProvider and SRFPlayChannel. /// public class MediaSourceFactory : IMediaSourceFactory { private readonly ILogger _logger; private readonly IStreamUrlResolver _streamResolver; private readonly IStreamProxyService _proxyService; private readonly IServerApplicationHost _appHost; /// /// Initializes a new instance of the class. /// /// The logger. /// The stream URL resolver. /// The stream proxy service. /// The server application host. public MediaSourceFactory( ILogger logger, IStreamUrlResolver streamResolver, IStreamProxyService proxyService, IServerApplicationHost appHost) { _logger = logger; _streamResolver = streamResolver; _proxyService = proxyService; _appHost = appHost; } /// public Task CreateMediaSourceAsync( Chapter chapter, string itemId, string urn, QualityPreference qualityPreference, CancellationToken cancellationToken = default) { // Get stream URL based on quality preference (unauthenticated) var streamUrl = _streamResolver.GetStreamUrl(chapter, qualityPreference); if (string.IsNullOrEmpty(streamUrl)) { _logger.LogWarning("Could not resolve stream URL for chapter: {ChapterId}", chapter.Id); return Task.FromResult(null); } // Detect if this is a live stream var isLiveStream = chapter.Type == "SCHEDULED_LIVESTREAM" || urn.Contains("livestream", StringComparison.OrdinalIgnoreCase); // Register stream with UNAUTHENTICATED URL - proxy will authenticate on-demand // This avoids wasting 30-second tokens during category browsing _proxyService.RegisterStreamDeferred(itemId, streamUrl, urn, isLiveStream); // Build proxy URL var proxyUrl = BuildProxyUrl(itemId); _logger.LogDebug( "Created media source for {Title} - ItemId: {ItemId}, IsLiveStream: {IsLiveStream}", chapter.Title, itemId, isLiveStream); // Create MediaSourceInfo with minimal settings to let clients determine playback // Don't specify Container or MediaStreams - let the .m3u8 path trigger HLS detection var mediaSource = new MediaSourceInfo { Id = itemId, Name = chapter.Title, Path = proxyUrl, Protocol = MediaProtocol.Http, // Empty container - let clients detect HLS from .m3u8 extension Container = string.Empty, SupportsDirectStream = true, SupportsDirectPlay = true, SupportsTranscoding = false, IsRemote = true, Type = MediaSourceType.Default, RunTimeTicks = chapter.Duration > 0 ? TimeSpan.FromMilliseconds(chapter.Duration).Ticks : null, VideoType = VideoType.VideoFile, IsInfiniteStream = isLiveStream, RequiresOpening = false, RequiresClosing = false, SupportsProbing = false, ReadAtNativeFramerate = isLiveStream, // Don't specify MediaStreams - let client determine codec compatibility MediaStreams = new List(), // Reduce analyze duration for faster startup (3000ms is Jellyfin default, 1000ms for live) AnalyzeDurationMs = isLiveStream ? 1000 : 3000, // Ignore DTS timestamps for live streams to avoid sync issues IgnoreDts = isLiveStream, // Ignore index for live streams IgnoreIndex = isLiveStream, }; return Task.FromResult(mediaSource); } /// public string BuildProxyUrl(string itemId) { return $"{GetServerBaseUrl()}{ApiEndpoints.ProxyMasterManifestPath.Replace("{0}", itemId, StringComparison.Ordinal)}"; } /// public string GetServerBaseUrl() { var config = Plugin.Instance?.Configuration; return config != null && !string.IsNullOrWhiteSpace(config.PublicServerUrl) ? config.PublicServerUrl.TrimEnd('/') : _appHost.GetSmartApiUrl(string.Empty); } /// public IReadOnlyList CreateMediaStreams(QualityPreference quality) { // Set resolution/bitrate based on quality preference var (width, height, videoBitrate) = quality switch { QualityPreference.SD => (1280, 720, 2500000), QualityPreference.HD => (1920, 1080, 5000000), _ => (1280, 720, 3000000) }; return new List { new MediaStream { Type = MediaStreamType.Video, Codec = "h264", Profile = "high", Level = 40, Width = width, Height = height, BitRate = videoBitrate, BitDepth = 8, IsInterlaced = false, IsDefault = true, Index = 0, IsAVC = true, PixelFormat = "yuv420p" }, new MediaStream { Type = MediaStreamType.Audio, Codec = "aac", Profile = "LC", Channels = 2, SampleRate = 48000, BitRate = 128000, IsDefault = true, Index = 1 } }; } }