175 lines
6.2 KiB
C#
175 lines
6.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Factory for creating MediaSourceInfo objects with consistent configuration.
|
|
/// Consolidates duplicated logic from SRFMediaProvider and SRFPlayChannel.
|
|
/// </summary>
|
|
public class MediaSourceFactory : IMediaSourceFactory
|
|
{
|
|
private readonly ILogger<MediaSourceFactory> _logger;
|
|
private readonly IStreamUrlResolver _streamResolver;
|
|
private readonly IStreamProxyService _proxyService;
|
|
private readonly IServerApplicationHost _appHost;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MediaSourceFactory"/> class.
|
|
/// </summary>
|
|
/// <param name="logger">The logger.</param>
|
|
/// <param name="streamResolver">The stream URL resolver.</param>
|
|
/// <param name="proxyService">The stream proxy service.</param>
|
|
/// <param name="appHost">The server application host.</param>
|
|
public MediaSourceFactory(
|
|
ILogger<MediaSourceFactory> logger,
|
|
IStreamUrlResolver streamResolver,
|
|
IStreamProxyService proxyService,
|
|
IServerApplicationHost appHost)
|
|
{
|
|
_logger = logger;
|
|
_streamResolver = streamResolver;
|
|
_proxyService = proxyService;
|
|
_appHost = appHost;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<MediaSourceInfo?> 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<MediaSourceInfo?>(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 codec info so clients know they can direct play
|
|
// Provide MediaStreams with H.264+AAC so Android TV/ExoPlayer doesn't trigger transcoding
|
|
var mediaStreams = CreateMediaStreams(qualityPreference);
|
|
|
|
var mediaSource = new MediaSourceInfo
|
|
{
|
|
Id = itemId,
|
|
Name = chapter.Title,
|
|
Path = proxyUrl,
|
|
Protocol = MediaProtocol.Http,
|
|
// Use "hls" to trigger hls.js player in web client
|
|
Container = "hls",
|
|
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,
|
|
// Disable probing - we provide stream info directly
|
|
SupportsProbing = false,
|
|
ReadAtNativeFramerate = isLiveStream,
|
|
// Provide codec info so clients know they can direct play H.264+AAC
|
|
MediaStreams = mediaStreams.ToList(),
|
|
AnalyzeDurationMs = isLiveStream ? 1000 : 3000,
|
|
IgnoreDts = isLiveStream,
|
|
IgnoreIndex = isLiveStream,
|
|
};
|
|
|
|
return Task.FromResult<MediaSourceInfo?>(mediaSource);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string BuildProxyUrl(string itemId)
|
|
{
|
|
return $"{GetServerBaseUrl()}{ApiEndpoints.ProxyMasterManifestPath.Replace("{0}", itemId, StringComparison.Ordinal)}";
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string GetServerBaseUrl()
|
|
{
|
|
var config = Plugin.Instance?.Configuration;
|
|
return config != null && !string.IsNullOrWhiteSpace(config.PublicServerUrl)
|
|
? config.PublicServerUrl.TrimEnd('/')
|
|
: _appHost.GetSmartApiUrl(string.Empty);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<MediaStream> 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<MediaStream>
|
|
{
|
|
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
|
|
}
|
|
};
|
|
}
|
|
}
|