Merge remote-tracking branch 'upstream/master' into epg-fixes

This commit is contained in:
Shadowghost
2026-04-11 17:48:00 +02:00
119 changed files with 2160 additions and 1049 deletions

View File

@@ -24,61 +24,29 @@ public class SkiaEncoder : IImageEncoder
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths;
private static readonly SKImageFilter _imageFilter;
private static readonly SKTypeface[] _typefaces;
private static readonly SKTypeface?[] _typefaces = InitializeTypefaces();
private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution(
new SKSizeI(3, 3),
[
0, -.1f, 0,
-.1f, 1.4f, -.1f,
0, -.1f, 0
],
1f,
0f,
new SKPointI(1, 1),
SKShaderTileMode.Clamp,
true);
/// <summary>
/// The default sampling options, equivalent to old high quality filter settings when upscaling.
/// </summary>
public static readonly SKSamplingOptions UpscaleSamplingOptions;
public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
/// <summary>
/// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
/// </summary>
public static readonly SKSamplingOptions DefaultSamplingOptions;
#pragma warning disable CA1810
static SkiaEncoder()
#pragma warning restore CA1810
{
var kernel = new[]
{
0, -.1f, 0,
-.1f, 1.4f, -.1f,
0, -.1f, 0,
};
var kernelSize = new SKSizeI(3, 3);
var kernelOffset = new SKPointI(1, 1);
_imageFilter = SKImageFilter.CreateMatrixConvolution(
kernelSize,
kernel,
1f,
0f,
kernelOffset,
SKShaderTileMode.Clamp,
true);
// Initialize the list of typefaces
// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F)
_typefaces =
[
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ''), // CJK Japanese
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
];
// use cubic for upscaling
UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
// use bilinear for everything else
DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
}
public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
/// <summary>
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
@@ -132,7 +100,7 @@ public class SkiaEncoder : IImageEncoder
/// <summary>
/// Gets the default typeface to use.
/// </summary>
public static SKTypeface DefaultTypeFace => _typefaces.Last();
public static SKTypeface? DefaultTypeFace => _typefaces.Last();
/// <summary>
/// Check if the native lib is available.
@@ -152,6 +120,40 @@ public class SkiaEncoder : IImageEncoder
}
}
/// <summary>
/// Initialize the list of typefaces
/// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
/// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F).
/// </summary>
/// <returns>The list of typefaces.</returns>
private static SKTypeface?[] InitializeTypefaces()
{
int[] chars = [
'鸡', // CJK Simplified Chinese
'雞', // CJK Traditional Chinese
'', // CJK Japanese
'각', // CJK Korean
128169, // Emojis, 128169 is the Pile of Poo (💩) emoji
'ז', // Hebrew
'ي' // Arabic
];
var fonts = new List<SKTypeface>(chars.Length + 1);
foreach (var ch in chars)
{
var font = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ch);
if (font is not null)
{
fonts.Add(font);
}
}
// Default font
fonts.Add(SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)
?? SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'a'));
return fonts.ToArray();
}
/// <summary>
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
/// </summary>
@@ -809,7 +811,7 @@ public class SkiaEncoder : IImageEncoder
{
foreach (var typeface in _typefaces)
{
if (typeface.ContainsGlyphs(c))
if (typeface is not null && typeface.ContainsGlyphs(c))
{
return typeface;
}

View File

@@ -85,7 +85,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
"jpeg",
"jpg",
"png",
"aiff",
"cr2",
"crw",
"nef",

View File

@@ -156,6 +156,13 @@ namespace Jellyfin.LiveTv.IO
if (mediaSource.ReadAtNativeFramerate)
{
inputModifier += " -re";
// Set a larger catchup value to revert to the old behavior,
// otherwise, remuxing might stall due to this new option
if (_mediaEncoder.EncoderVersion >= new Version(8, 0))
{
inputModifier += " -readrate_catchup 100";
}
}
if (mediaSource.RequiresLooping)

View File

@@ -93,6 +93,13 @@ namespace Jellyfin.LiveTv.TunerHosts
}
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
if (!IsValidChannelUrl(trimmedLine))
{
_logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine);
extInf = string.Empty;
continue;
}
var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine);
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
@@ -247,6 +254,16 @@ namespace Jellyfin.LiveTv.TunerHosts
return numberString;
}
private static bool IsValidChannelUrl(string url)
{
return Uri.TryCreate(url, UriKind.Absolute, out var uri)
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase));
}
private static bool IsValidChannelNumber(string numberString)
{
if (string.IsNullOrWhiteSpace(numberString)

View File

@@ -747,12 +747,13 @@ public class NetworkManager : INetworkManager, IDisposable
/// <inheritdoc/>
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
{
return NetworkManager.GetAllBindInterfaces(individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled);
return NetworkManager.GetAllBindInterfaces(_logger, individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled);
}
/// <summary>
/// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound.
/// </summary>
/// <param name="logger">Logger to use for messages.</param>
/// <param name="individualInterfaces">Defines that only known interfaces should be used.</param>
/// <param name="configurationManager">The ConfigurationManager.</param>
/// <param name="knownInterfaces">The known interfaces that gets returned if possible or instructed.</param>
@@ -760,6 +761,7 @@ public class NetworkManager : INetworkManager, IDisposable
/// <param name="readIpv6">Include IPV6 type interfaces.</param>
/// <returns>A list of ip address of which jellyfin should bind to.</returns>
public static IReadOnlyList<IPData> GetAllBindInterfaces(
ILogger<NetworkManager> logger,
bool individualInterfaces,
IConfigurationManager configurationManager,
IReadOnlyList<IPData> knownInterfaces,
@@ -773,6 +775,13 @@ public class NetworkManager : INetworkManager, IDisposable
return knownInterfaces;
}
// TODO: remove when upgrade to dotnet 11 is done
if (readIpv6 && !Socket.OSSupportsIPv6)
{
logger.LogWarning("IPv6 Unsupported by OS, not listening on IPv6");
readIpv6 = false;
}
// No bind address and no exclusions, so listen on all interfaces.
var result = new List<IPData>();
if (readIpv4 && readIpv6)
@@ -869,7 +878,20 @@ public class NetworkManager : INetworkManager, IDisposable
if (availableInterfaces.Count == 0)
{
// There isn't any others, so we'll use the loopback.
result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1";
// Prefer loopback address matching the source's address family
if (source is not null && source.AddressFamily == AddressFamily.InterNetwork && IsIPv4Enabled)
{
result = "127.0.0.1";
}
else if (source is not null && source.AddressFamily == AddressFamily.InterNetworkV6 && IsIPv6Enabled)
{
result = "::1";
}
else
{
result = IsIPv4Enabled ? "127.0.0.1" : "::1";
}
_logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
return result;
}
@@ -894,9 +916,19 @@ public class NetworkManager : INetworkManager, IDisposable
}
}
// Fallback to first available interface
// Fallback to an interface matching the source's address family, or first available
var preferredInterface = availableInterfaces
.FirstOrDefault(x => x.Address.AddressFamily == source.AddressFamily);
if (preferredInterface is not null)
{
result = NetworkUtils.FormatIPString(preferredInterface.Address);
_logger.LogDebug("{Source}: No matching subnet found, using interface with matching address family: {Result}", source, result);
return result;
}
result = NetworkUtils.FormatIPString(availableInterfaces[0].Address);
_logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result);
_logger.LogDebug("{Source}: No matching interfaces found, using first available interface as bind address: {Result}", source, result);
return result;
}