diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8c625539ab..aed0c14a29 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -841,16 +841,14 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(ChapterManager); MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder( - LoggerFactory, - JsonSerializer, - StartupOptions.FFmpegPath, + LoggerFactory.CreateLogger(), ServerConfigurationManager, FileSystemManager, - () => SubtitleEncoder, - () => MediaSourceManager, ProcessFactory, - 5000, - LocalizationManager); + LocalizationManager, + () => SubtitleEncoder, + _configuration, + StartupOptions.FFmpegPath); serviceCollection.AddSingleton(MediaEncoder); EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); @@ -867,10 +865,19 @@ namespace Emby.Server.Implementations AuthService = new AuthService(LoggerFactory.CreateLogger(), authContext, ServerConfigurationManager, SessionManager, NetworkManager); serviceCollection.AddSingleton(AuthService); - SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LoggerFactory, ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory); + SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder( + LibraryManager, + LoggerFactory.CreateLogger(), + ApplicationPaths, + FileSystemManager, + MediaEncoder, + HttpClient, + MediaSourceManager, + ProcessFactory); serviceCollection.AddSingleton(SubtitleEncoder); serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager)); + serviceCollection.AddSingleton(); _displayPreferencesRepository.Initialize(); diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 62408ee703..2ea7ff6e91 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -1,13 +1,16 @@ using System.Collections.Generic; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations { public static class ConfigurationOptions { - public static readonly Dictionary Configuration = new Dictionary + public static Dictionary Configuration => new Dictionary { { "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, - { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" } + { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" }, + { FfmpegProbeSizeKey, "1G" }, + { FfmpegAnalyzeDurationKey, "200M" } }; } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 4b6ba1fabb..115e07d576 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,7 +29,6 @@ - diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 74406a0641..9961b09841 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -1,11 +1,11 @@ { "Albums": "Àlbums", - "AppDeviceValues": "App: {0}, Dispositiu: {1}", - "Application": "Application", + "AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}", + "Application": "Aplicació", "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "Books": "Llibres", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "Una nova imatge de càmera ha sigut pujada des de {0}", "Channels": "Canals", "ChapterNameValue": "Episodi {0}", "Collections": "Col·leccions", @@ -15,8 +15,8 @@ "Favorites": "Preferits", "Folders": "Directoris", "Genres": "Gèneres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", + "HeaderAlbumArtists": "Artistes dels Àlbums", + "HeaderCameraUploads": "Pujades de Càmera", "HeaderContinueWatching": "Continua Veient", "HeaderFavoriteAlbums": "Àlbums Preferits", "HeaderFavoriteArtists": "Artistes Preferits", @@ -27,71 +27,71 @@ "HeaderNextUp": "A continuació", "HeaderRecordingGroups": "Grups d'Enregistrament", "HomeVideos": "Vídeos domèstics", - "Inherit": "Heretat", - "ItemAddedWithName": "{0} afegit a la biblioteca", - "ItemRemovedWithName": "{0} eliminat de la biblioteca", + "Inherit": "Hereta", + "ItemAddedWithName": "{0} ha estat afegit a la biblioteca", + "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca", "LabelIpAddressValue": "Adreça IP: {0}", - "LabelRunningTimeValue": "Temps en marxa: {0}", + "LabelRunningTimeValue": "Temps en funcionament: {0}", "Latest": "Darreres", - "MessageApplicationUpdated": "El Servidor d'Jellyfin ha estat actualitzat", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "La secció de configuració {0} ha estat actualitzada", + "MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat", + "MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada", "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor", "MixedContent": "Contingut mesclat", "Movies": "Pel·lícules", "Music": "Música", "MusicVideos": "Vídeos musicals", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "Instalació de {0} fallida", "NameSeasonNumber": "Temporada {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NameSeasonUnknown": "Temporada Desconeguda", + "NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.", "NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible", "NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", - "NotificationOptionNewLibraryContent": "New content added", - "NotificationOptionPluginError": "Un component ha fallat", - "NotificationOptionPluginInstalled": "Complement instal·lat", - "NotificationOptionPluginUninstalled": "Complement desinstal·lat", - "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada", - "NotificationOptionServerRestartRequired": "Server restart required", - "NotificationOptionTaskFailed": "Scheduled task failure", - "NotificationOptionUserLockedOut": "User locked out", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", + "NotificationOptionAudioPlayback": "Reproducció d'audio iniciada", + "NotificationOptionAudioPlaybackStopped": "Reproducció d'audio aturada", + "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada", + "NotificationOptionInstallationFailed": "Instalació fallida", + "NotificationOptionNewLibraryContent": "Nou contingut afegit", + "NotificationOptionPluginError": "Un connector ha fallat", + "NotificationOptionPluginInstalled": "Connector instal·lat", + "NotificationOptionPluginUninstalled": "Connector desinstal·lat", + "NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada", + "NotificationOptionServerRestartRequired": "Reinici del servidor requerit", + "NotificationOptionTaskFailed": "Tasca programada fallida", + "NotificationOptionUserLockedOut": "Usuari tancat", + "NotificationOptionVideoPlayback": "Reproducció de video iniciada", + "NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada", "Photos": "Fotos", "Playlists": "Llistes de reproducció", - "Plugin": "Plugin", + "Plugin": "Connector", "PluginInstalledWithName": "{0} ha estat instal·lat", "PluginUninstalledWithName": "{0} ha estat desinstal·lat", "PluginUpdatedWithName": "{0} ha estat actualitzat", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", "ScheduledTaskStartedWithName": "{0} iniciat", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Espectacles", + "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat", + "Shows": "Programes", "Songs": "Cançons", "StartupEmbyServerIsLoading": "El Servidor d'Jellyfin està carregant. Si et plau, prova de nou en breus.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}", "SubtitlesDownloadedForItem": "Subtítols descarregats per a {0}", - "Sync": "Sync", + "Sync": "Sincronitzar", "System": "System", "TvShows": "Espectacles de TV", "User": "User", "UserCreatedWithName": "S'ha creat l'usuari {0}", "UserDeletedWithName": "L'usuari {0} ha estat eliminat", "UserDownloadingItemWithValues": "{0} està descarregant {1}", - "UserLockedOutWithName": "User {0} has been locked out", + "UserLockedOutWithName": "L'usuari {0} ha sigut tancat", "UserOfflineFromDevice": "{0} s'ha desconnectat de {1}", "UserOnlineFromDevice": "{0} està connectat des de {1}", "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", + "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per {0}", "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", + "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva llibreria", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versió {0}" } diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 988ac364ae..febb1adabc 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -4,6 +4,7 @@ netstandard2.1 false true + true @@ -22,4 +23,16 @@ + + + + + + + + + + ../jellyfin.ruleset + + diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs index c72f295fdd..f2df066ec8 100644 --- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs +++ b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs @@ -4,10 +4,19 @@ using SkiaSharp; namespace Jellyfin.Drawing.Skia { + /// + /// Static helper class used to draw percentage-played indicators on images. + /// public static class PercentPlayedDrawer { private const int IndicatorHeight = 8; + /// + /// Draw a percentage played indicator on a canvas. + /// + /// The canvas to draw the indicator on. + /// The size of the image being drawn on. + /// The percentage played to display with the indicator. public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent) { using (var paint = new SKPaint()) diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs index 7f3c18bb24..5084fd211c 100644 --- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs +++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs @@ -3,10 +3,21 @@ using SkiaSharp; namespace Jellyfin.Drawing.Skia { + /// + /// Static helper class for drawing 'played' indicators. + /// public static class PlayedIndicatorDrawer { private const int OffsetFromTopRightCorner = 38; + /// + /// Draw a 'played' indicator in the top right corner of a canvas. + /// + /// The canvas to draw the indicator on. + /// + /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the + /// indicator. + /// public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize) { var x = imageSize.Width - OffsetFromTopRightCorner; @@ -26,10 +37,10 @@ namespace Jellyfin.Drawing.Skia paint.TextSize = 30; paint.IsAntialias = true; + // or: + // var emojiChar = 0x1F680; var text = "✔️"; var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32); - // or: - //var emojiChar = 0x1F680; // ask the font manager for a font with that character var fontManager = SKFontManager.Default; diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs index f848636bcb..8158b846dd 100644 --- a/Jellyfin.Drawing.Skia/SkiaCodecException.cs +++ b/Jellyfin.Drawing.Skia/SkiaCodecException.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Globalization; using SkiaSharp; @@ -8,16 +9,10 @@ namespace Jellyfin.Drawing.Skia /// public class SkiaCodecException : SkiaException { - /// - /// Returns the non-successfull codec result returned by Skia. - /// - /// The non-successfull codec result returned by Skia. - public SKCodecResult CodecResult { get; } - /// /// Initializes a new instance of the class. /// - /// The non-successfull codec result returned by Skia. + /// The non-successful codec result returned by Skia. public SkiaCodecException(SKCodecResult result) : base() { CodecResult = result; @@ -27,7 +22,7 @@ namespace Jellyfin.Drawing.Skia /// Initializes a new instance of the class /// with a specified error message. /// - /// The non-successfull codec result returned by Skia. + /// The non-successful codec result returned by Skia. /// The message that describes the error. public SkiaCodecException(SKCodecResult result, string message) : base(message) @@ -35,6 +30,11 @@ namespace Jellyfin.Drawing.Skia CodecResult = result; } + /// + /// Gets the non-successful codec result returned by Skia. + /// + public SKCodecResult CodecResult { get; } + /// public override string ToString() => string.Format( diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 66b814f6eb..b080b3e6a5 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -13,6 +13,9 @@ using static Jellyfin.Drawing.Skia.SkiaHelper; namespace Jellyfin.Drawing.Skia { + /// + /// Image encoder that uses to manipulate images. + /// public class SkiaEncoder : IImageEncoder { private readonly ILogger _logger; @@ -22,6 +25,12 @@ namespace Jellyfin.Drawing.Skia private static readonly HashSet _transparentImageTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; + /// + /// Initializes a new instance of the class. + /// + /// The application logger. + /// The application paths. + /// The application localization manager. public SkiaEncoder( ILogger logger, IApplicationPaths appPaths, @@ -32,12 +41,16 @@ namespace Jellyfin.Drawing.Skia _localizationManager = localizationManager; } + /// public string Name => "Skia"; + /// public bool SupportsImageCollageCreation => true; + /// public bool SupportsImageEncoding => true; + /// public IReadOnlyCollection SupportedInputFormats => new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -65,11 +78,12 @@ namespace Jellyfin.Drawing.Skia "arw" }; + /// public IReadOnlyCollection SupportedOutputFormats => new HashSet() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; /// - /// Test to determine if the native lib is available + /// Test to determine if the native lib is available. /// public static void TestSkia() { @@ -80,6 +94,11 @@ namespace Jellyfin.Drawing.Skia private static bool IsTransparent(SKColor color) => (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0; + /// + /// Convert a to a . + /// + /// The format to convert. + /// The converted format. public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat) { switch (selectedFormat) @@ -186,6 +205,9 @@ namespace Jellyfin.Drawing.Skia } /// + /// The path is null. + /// The path is not valid. + /// The file at the specified path could not be used to generate a codec. public ImageDimensions GetImageSize(string path) { if (path == null) @@ -269,6 +291,14 @@ namespace Jellyfin.Drawing.Skia } } + /// + /// Decode an image. + /// + /// The filepath of the image to decode. + /// Whether to force clean the bitmap. + /// The orientation of the image. + /// The detected origin of the image. + /// The resulting bitmap of the image. internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) { if (!File.Exists(path)) @@ -358,16 +388,6 @@ namespace Jellyfin.Drawing.Skia private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) { - //var transformations = { - // 2: { rotate: 0, flip: true}, - // 3: { rotate: 180, flip: false}, - // 4: { rotate: 180, flip: true}, - // 5: { rotate: 90, flip: true}, - // 6: { rotate: 90, flip: false}, - // 7: { rotate: 270, flip: true}, - // 8: { rotate: 270, flip: false}, - //} - switch (origin) { case SKEncodedOrigin.TopRight: @@ -497,6 +517,7 @@ namespace Jellyfin.Drawing.Skia } } + /// public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { if (string.IsNullOrWhiteSpace(inputPath)) @@ -520,7 +541,7 @@ namespace Jellyfin.Drawing.Skia { if (bitmap == null) { - throw new ArgumentOutOfRangeException(string.Format("Skia unable to read image {0}", inputPath)); + throw new ArgumentOutOfRangeException($"Skia unable to read image {inputPath}"); } var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); @@ -556,7 +577,7 @@ namespace Jellyfin.Drawing.Skia } // create bitmap to use for canvas drawing used to draw into bitmap - using (var saveBitmap = new SKBitmap(width, height))//, bitmap.ColorType, bitmap.AlphaType)) + using (var saveBitmap = new SKBitmap(width, height)) // , bitmap.ColorType, bitmap.AlphaType)) using (var canvas = new SKCanvas(saveBitmap)) { // set background color if present @@ -609,9 +630,11 @@ namespace Jellyfin.Drawing.Skia } } } + return outputPath; } + /// public void CreateImageCollage(ImageCollageOptions options) { double ratio = (double)options.Width / options.Height; diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/Jellyfin.Drawing.Skia/SkiaException.cs index 7aeaf083e2..968d3a2448 100644 --- a/Jellyfin.Drawing.Skia/SkiaException.cs +++ b/Jellyfin.Drawing.Skia/SkiaException.cs @@ -7,17 +7,30 @@ namespace Jellyfin.Drawing.Skia /// public class SkiaException : Exception { - /// + /// + /// Initializes a new instance of the class. + /// public SkiaException() : base() { } - /// + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. public SkiaException(string message) : base(message) { } - /// + /// + /// Initializes a new instance of the class with a specified error message and a + /// reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if + /// no inner exception is specified. + /// public SkiaException(string message, Exception innerException) : base(message, innerException) { diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 1f2a6e81a4..0735ef194a 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -5,15 +5,27 @@ using SkiaSharp; namespace Jellyfin.Drawing.Skia { + /// + /// Used to build collages of multiple images arranged in vertical strips. + /// public class StripCollageBuilder { private readonly SkiaEncoder _skiaEncoder; + /// + /// Initializes a new instance of the class. + /// + /// The encoder to use for building collages. public StripCollageBuilder(SkiaEncoder skiaEncoder) { _skiaEncoder = skiaEncoder; } + /// + /// Check which format an image has been encoded with using its filename extension. + /// + /// The path to the image to get the format for. + /// The image format. public static SKEncodedImageFormat GetEncodedFormat(string outputPath) { if (outputPath == null) @@ -48,6 +60,13 @@ namespace Jellyfin.Drawing.Skia return SKEncodedImageFormat.Png; } + /// + /// Create a square collage. + /// + /// The paths of the images to use in the collage. + /// The path at which to place the resulting collage image. + /// The desired width of the collage. + /// The desired height of the collage. public void BuildSquareCollage(string[] paths, string outputPath, int width, int height) { using (var bitmap = BuildSquareCollageBitmap(paths, width, height)) @@ -58,6 +77,13 @@ namespace Jellyfin.Drawing.Skia } } + /// + /// Create a thumb collage. + /// + /// The paths of the images to use in the collage. + /// The path at which to place the resulting image. + /// The desired width of the collage. + /// The desired height of the collage. public void BuildThumbCollage(string[] paths, string outputPath, int width, int height) { using (var bitmap = BuildThumbCollageBitmap(paths, width, height)) @@ -98,6 +124,7 @@ namespace Jellyfin.Drawing.Skia using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType)) { currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High); + // crop image int ix = (int)Math.Abs((iWidth - iSlice) / 2); using (var image = SKImage.FromBitmap(resizeBitmap)) diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs index dbf935f4e7..a10fff9dfe 100644 --- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs +++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs @@ -4,10 +4,25 @@ using SkiaSharp; namespace Jellyfin.Drawing.Skia { + /// + /// Static helper class for drawing unplayed count indicators. + /// public static class UnplayedCountIndicator { + /// + /// The x-offset used when drawing an unplayed count indicator. + /// private const int OffsetFromTopRightCorner = 38; + /// + /// Draw an unplayed count indicator in the top right corner of a canvas. + /// + /// The canvas to draw the indicator on. + /// + /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the + /// indicator. + /// + /// The number to draw in the indicator. public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count) { var x = imageSize.Width - OffsetFromTopRightCorner; @@ -19,6 +34,7 @@ namespace Jellyfin.Drawing.Skia paint.Style = SKPaintStyle.Fill; canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); } + using (var paint = new SKPaint()) { paint.Color = new SKColor(255, 255, 255, 255); @@ -33,6 +49,7 @@ namespace Jellyfin.Drawing.Skia { x -= 7; } + if (text.Length == 2) { x -= 13; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 5ac005b40b..712990a1ee 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Security; using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -145,6 +146,10 @@ namespace Jellyfin.Server ApplicationHost.LogEnvironmentInfo(_logger, appPaths); + // Make sure we have all the code pages we can get + // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + // Increase the max http request limit // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others. ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); @@ -456,9 +461,9 @@ namespace Jellyfin.Server return new ConfigurationBuilder() .SetBasePath(appPaths.ConfigurationDirectoryPath) + .AddInMemoryCollection(ConfigurationOptions.Configuration) .AddJsonFile("logging.json", false, true) .AddEnvironmentVariables("JELLYFIN_") - .AddInMemoryCollection(ConfigurationOptions.Configuration) .Build(); } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 1e9cd33136..5881e22a7c 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -63,8 +63,6 @@ namespace MediaBrowser.Api.Playback protected IDeviceManager DeviceManager { get; private set; } - protected ISubtitleEncoder SubtitleEncoder { get; private set; } - protected IMediaSourceManager MediaSourceManager { get; private set; } protected IJsonSerializer JsonSerializer { get; private set; } @@ -92,11 +90,11 @@ namespace MediaBrowser.Api.Playback IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) : base(logger, serverConfigurationManager, httpResultFactory) { UserManager = userManager; @@ -105,13 +103,12 @@ namespace MediaBrowser.Api.Playback MediaEncoder = mediaEncoder; FileSystem = fileSystem; DlnaManager = dlnaManager; - SubtitleEncoder = subtitleEncoder; DeviceManager = deviceManager; MediaSourceManager = mediaSourceManager; JsonSerializer = jsonSerializer; AuthorizationContext = authorizationContext; - EncodingHelper = new EncodingHelper(MediaEncoder, FileSystem, SubtitleEncoder); + EncodingHelper = encodingHelper; } /// @@ -148,8 +145,6 @@ namespace MediaBrowser.Api.Playback return Path.Combine(folder, filename + ext); } - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - protected virtual string GetDefaultEncoderPreset() { return "superfast"; @@ -764,13 +759,13 @@ namespace MediaBrowser.Api.Playback if (mediaSource == null) { - var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false)).ToList(); + var mediaSources = await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false); mediaSource = string.IsNullOrEmpty(request.MediaSourceId) ? mediaSources[0] : mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId)); - if (mediaSource == null && request.MediaSourceId.Equals(request.Id)) + if (mediaSource == null && Guid.Parse(request.MediaSourceId) == request.Id) { mediaSource = mediaSources[0]; } diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 8fdc6fa497..5d0dc98ddc 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -34,26 +34,26 @@ namespace MediaBrowser.Api.Playback.Hls IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) - : base( - logger, - serverConfigurationManager, - httpResultFactory, - userManager, - libraryManager, - isoManager, - mediaEncoder, - fileSystem, - dlnaManager, - subtitleEncoder, - deviceManager, - mediaSourceManager, - jsonSerializer, - authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + isoManager, + mediaEncoder, + fileSystem, + dlnaManager, + deviceManager, + mediaSourceManager, + jsonSerializer, + authorizationContext, + encodingHelper) { } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 56758db963..0178f53af6 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -104,12 +104,12 @@ namespace MediaBrowser.Api.Playback.Hls IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, - INetworkManager networkManager) + INetworkManager networkManager, + EncodingHelper encodingHelper) : base( logger, serverConfigurationManager, @@ -120,11 +120,11 @@ namespace MediaBrowser.Api.Playback.Hls mediaEncoder, fileSystem, dlnaManager, - subtitleEncoder, deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext) + authorizationContext, + encodingHelper) { NetworkManager = networkManager; } diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 6d12a1ccd6..d1c53c1c11 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -27,6 +27,39 @@ namespace MediaBrowser.Api.Playback.Hls [Authenticated] public class VideoHlsService : BaseHlsService { + public VideoHlsService( + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IHttpResultFactory httpResultFactory, + IUserManager userManager, + ILibraryManager libraryManager, + IIsoManager isoManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + IDlnaManager dlnaManager, + IDeviceManager deviceManager, + IMediaSourceManager mediaSourceManager, + IJsonSerializer jsonSerializer, + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) + : base( + logger, + serverConfigurationManager, + httpResultFactory, + userManager, + libraryManager, + isoManager, + mediaEncoder, + fileSystem, + dlnaManager, + deviceManager, + mediaSourceManager, + jsonSerializer, + authorizationContext, + encodingHelper) + { + } + public Task Get(GetLiveHlsStream request) { return ProcessRequestAsync(request, true); @@ -136,38 +169,5 @@ namespace MediaBrowser.Api.Playback.Hls return args; } - - public VideoHlsService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ILibraryManager libraryManager, - IIsoManager isoManager, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, - IDeviceManager deviceManager, - IMediaSourceManager mediaSourceManager, - IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) - : base( - logger, - serverConfigurationManager, - httpResultFactory, - userManager, - libraryManager, - isoManager, - mediaEncoder, - fileSystem, - dlnaManager, - subtitleEncoder, - deviceManager, - mediaSourceManager, - jsonSerializer, - authorizationContext) - { - } } } diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 11527007b5..8d1e3a3f23 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -43,11 +43,11 @@ namespace MediaBrowser.Api.Playback.Progressive IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) : base( logger, serverConfigurationManager, @@ -59,11 +59,11 @@ namespace MediaBrowser.Api.Playback.Progressive mediaEncoder, fileSystem, dlnaManager, - subtitleEncoder, deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext) + authorizationContext, + encodingHelper) { } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 4ada90d095..ed30dbba61 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -38,11 +38,11 @@ namespace MediaBrowser.Api.Playback.Progressive IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) : base( logger, serverConfigurationManager, @@ -53,11 +53,11 @@ namespace MediaBrowser.Api.Playback.Progressive mediaEncoder, fileSystem, dlnaManager, - subtitleEncoder, deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext) + authorizationContext, + encodingHelper) { HttpClient = httpClient; } diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 56ec86cd47..4de81655ce 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -80,11 +80,11 @@ namespace MediaBrowser.Api.Playback.Progressive IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, - ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, - IAuthorizationContext authorizationContext) + IAuthorizationContext authorizationContext, + EncodingHelper encodingHelper) : base( logger, serverConfigurationManager, @@ -96,11 +96,11 @@ namespace MediaBrowser.Api.Playback.Progressive mediaEncoder, fileSystem, dlnaManager, - subtitleEncoder, deviceManager, mediaSourceManager, jsonSerializer, - authorizationContext) + authorizationContext, + encodingHelper) { } diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs index b1450e2cc8..9cba9df139 100644 --- a/MediaBrowser.Api/Playback/UniversalAudioService.cs +++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs @@ -9,7 +9,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; @@ -75,6 +74,9 @@ namespace MediaBrowser.Api.Playback [Authenticated] public class UniversalAudioService : BaseApiService { + private readonly ILoggerFactory _loggerFactory; + private readonly EncodingHelper _encodingHelper; + public UniversalAudioService( ILogger logger, IServerConfigurationManager serverConfigurationManager, @@ -87,11 +89,11 @@ namespace MediaBrowser.Api.Playback IFileSystem fileSystem, IDlnaManager dlnaManager, IDeviceManager deviceManager, - ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, - INetworkManager networkManager) + INetworkManager networkManager, + EncodingHelper encodingHelper) : base(logger, serverConfigurationManager, httpResultFactory) { HttpClient = httpClient; @@ -102,11 +104,11 @@ namespace MediaBrowser.Api.Playback FileSystem = fileSystem; DlnaManager = dlnaManager; DeviceManager = deviceManager; - SubtitleEncoder = subtitleEncoder; MediaSourceManager = mediaSourceManager; JsonSerializer = jsonSerializer; AuthorizationContext = authorizationContext; NetworkManager = networkManager; + _encodingHelper = encodingHelper; } protected IHttpClient HttpClient { get; private set; } @@ -117,7 +119,6 @@ namespace MediaBrowser.Api.Playback protected IFileSystem FileSystem { get; private set; } protected IDlnaManager DlnaManager { get; private set; } protected IDeviceManager DeviceManager { get; private set; } - protected ISubtitleEncoder SubtitleEncoder { get; private set; } protected IMediaSourceManager MediaSourceManager { get; private set; } protected IJsonSerializer JsonSerializer { get; private set; } protected IAuthorizationContext AuthorizationContext { get; private set; } @@ -287,12 +288,12 @@ namespace MediaBrowser.Api.Playback MediaEncoder, FileSystem, DlnaManager, - SubtitleEncoder, DeviceManager, MediaSourceManager, JsonSerializer, AuthorizationContext, - NetworkManager) + NetworkManager, + _encodingHelper) { Request = Request }; @@ -337,11 +338,11 @@ namespace MediaBrowser.Api.Playback MediaEncoder, FileSystem, DlnaManager, - SubtitleEncoder, DeviceManager, MediaSourceManager, JsonSerializer, - AuthorizationContext) + AuthorizationContext, + _encodingHelper) { Request = Request }; diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index a0f9ae46e4..88e67b6486 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -11,6 +11,7 @@ namespace MediaBrowser.Controller.Drawing /// /// The supported input formats. IReadOnlyCollection SupportedInputFormats { get; } + /// /// Gets the supported output formats. /// @@ -18,9 +19,9 @@ namespace MediaBrowser.Controller.Drawing IReadOnlyCollection SupportedOutputFormats { get; } /// - /// Gets the name. + /// Gets the display name for the encoder. /// - /// The name. + /// The display name. string Name { get; } /// @@ -35,17 +36,22 @@ namespace MediaBrowser.Controller.Drawing /// true if [supports image encoding]; otherwise, false. bool SupportsImageEncoding { get; } + /// + /// Get the dimensions of an image from the filesystem. + /// + /// The filepath of the image. + /// The image dimensions. ImageDimensions GetImageSize(string path); /// - /// Encodes the image. + /// Encode an image. /// string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); /// - /// Creates the image collage. + /// Create an image collage. /// - /// The options. + /// The options to use when creating the collage. void CreateImageCollage(ImageCollageOptions options); } } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 60906bdb08..af4d227bc8 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -137,7 +137,7 @@ namespace MediaBrowser.Controller.Entities /// The video3 D format. public Video3DFormat? Video3DFormat { get; set; } - public string[] GetPlayableStreamFileNames(IMediaEncoder mediaEncoder) + public string[] GetPlayableStreamFileNames() { var videoType = VideoType; @@ -153,7 +153,8 @@ namespace MediaBrowser.Controller.Entities { return Array.Empty(); } - return mediaEncoder.GetPlayableStreamFileNames(Path, videoType); + + throw new NotImplementedException(); } /// diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000000..76c9b4b26c --- /dev/null +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Configuration; + +namespace MediaBrowser.Controller.Extensions +{ + /// + /// Configuration extensions for MediaBrowser.Controller. + /// + public static class ConfigurationExtensions + { + /// + /// The key for the FFmpeg probe size option. + /// + public const string FfmpegProbeSizeKey = "FFmpeg:probesize"; + + /// + /// The key for the FFmpeg analyse duration option. + /// + public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration"; + + /// + /// Retrieves the FFmpeg probe size from the . + /// + /// This configuration. + /// The FFmpeg probe size option. + public static string GetFFmpegProbeSize(this IConfiguration configuration) + => configuration[FfmpegProbeSizeKey]; + + /// + /// Retrieves the FFmpeg analyse duration from the . + /// + /// This configuration. + /// The FFmpeg analyse duration option. + public static string GetFFmpegAnalyzeDuration(this IConfiguration configuration) + => configuration[FfmpegAnalyzeDurationKey]; + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 276eb71bcf..60c76ef7db 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -7,6 +7,10 @@ https://github.com/jellyfin/jellyfin + + + + diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 897e22018c..020f0553ed 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6,12 +6,14 @@ using System.Linq; using System.Text; using System.Threading; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Configuration; namespace MediaBrowser.Controller.MediaEncoding { @@ -22,6 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfiguration _configuration; private static readonly string[] _videoProfiles = new[] { @@ -34,11 +37,16 @@ namespace MediaBrowser.Controller.MediaEncoding "ConstrainedHigh" }; - public EncodingHelper(IMediaEncoder mediaEncoder, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder) + public EncodingHelper( + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration) { _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; _subtitleEncoder = subtitleEncoder; + _configuration = configuration; } public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) @@ -172,7 +180,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public string GetInputFormat(string container) + public static string GetInputFormat(string container) { if (string.IsNullOrEmpty(container)) { @@ -662,7 +670,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(state.SubtitleStream.Language)) { - var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).Result; + var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet( + subtitlePath, + state.SubtitleStream.Language, + state.MediaSource.Protocol, + CancellationToken.None).GetAwaiter().GetResult(); if (!string.IsNullOrEmpty(charenc)) { @@ -1948,7 +1960,7 @@ namespace MediaBrowser.Controller.MediaEncoding // If transcoding from 10 bit, transform colour spaces too if (!string.IsNullOrEmpty(videoStream.PixelFormat) && videoStream.PixelFormat.IndexOf("p10", StringComparison.OrdinalIgnoreCase) != -1 - && string.Equals(outputVideoCodec,"libx264", StringComparison.OrdinalIgnoreCase)) + && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) { filters.Add("format=p010le"); filters.Add("format=nv12"); @@ -2011,7 +2023,9 @@ namespace MediaBrowser.Controller.MediaEncoding var output = string.Empty; - if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + if (state.SubtitleStream != null + && state.SubtitleStream.IsTextSubtitleStream + && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { var subParam = GetTextSubtitleParam(state); @@ -2100,11 +2114,11 @@ namespace MediaBrowser.Controller.MediaEncoding } } - public static string GetProbeSizeArgument(int numInputFiles) - => numInputFiles > 1 ? "-probesize 1G" : ""; + public string GetProbeSizeArgument(int numInputFiles) + => numInputFiles > 1 ? "-probesize " + _configuration.GetFFmpegProbeSize() : string.Empty; - public static string GetAnalyzeDurationArgument(int numInputFiles) - => numInputFiles > 1 ? "-analyzeduration 200M" : ""; + public string GetAnalyzeDurationArgument(int numInputFiles) + => numInputFiles > 1 ? "-analyzeduration " + _configuration.GetFFmpegAnalyzeDuration() : string.Empty; public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions) { diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index d032a849e7..37f0b11a74 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -15,6 +15,9 @@ namespace MediaBrowser.Controller.MediaEncoding /// public interface IMediaEncoder : ITranscoderSupport { + /// + /// The location of the discovered FFmpeg tool. + /// FFmpegLocation EncoderLocation { get; } /// @@ -97,7 +100,6 @@ namespace MediaBrowser.Controller.MediaEncoding void UpdateEncoderPath(string path, string pathType); bool SupportsEncoder(string encoder); - string[] GetPlayableStreamFileNames(string path, VideoType videoType); IEnumerable GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 04ff66991d..6bcd6cd46a 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; @@ -19,9 +19,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; namespace MediaBrowser.MediaEncoding.Encoder { @@ -31,55 +31,60 @@ namespace MediaBrowser.MediaEncoding.Encoder public class MediaEncoder : IMediaEncoder, IDisposable { /// - /// Gets the encoder path. + /// The default image extraction timeout in milliseconds. /// - /// The encoder path. - public string EncoderPath => FFmpegPath; - - /// - /// The location of the discovered FFmpeg tool. - /// - public FFmpegLocation EncoderLocation { get; private set; } + internal const int DefaultImageExtractionTimeout = 5000; private readonly ILogger _logger; - private readonly IJsonSerializer _jsonSerializer; - private string FFmpegPath; - private string FFprobePath; - protected readonly IServerConfigurationManager ConfigurationManager; - protected readonly IFileSystem FileSystem; - protected readonly Func SubtitleEncoder; - protected readonly Func MediaSourceManager; + private readonly IServerConfigurationManager _configurationManager; + private readonly IFileSystem _fileSystem; private readonly IProcessFactory _processFactory; - private readonly int DefaultImageExtractionTimeoutMs; - private readonly string StartupOptionFFmpegPath; + private readonly ILocalizationManager _localization; + private readonly Func _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly string _startupOptionFFmpegPath; private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); + + private readonly object _runningProcessesLock = new object(); private readonly List _runningProcesses = new List(); - private readonly ILocalizationManager _localization; + + private EncodingHelper _encodingHelper; + + private string _ffmpegPath; + private string _ffprobePath; public MediaEncoder( - ILoggerFactory loggerFactory, - IJsonSerializer jsonSerializer, - string startupOptionsFFmpegPath, + ILogger logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, - Func subtitleEncoder, - Func mediaSourceManager, IProcessFactory processFactory, - int defaultImageExtractionTimeoutMs, - ILocalizationManager localization) + ILocalizationManager localization, + Func subtitleEncoder, + IConfiguration configuration, + string startupOptionsFFmpegPath) { - _logger = loggerFactory.CreateLogger(nameof(MediaEncoder)); - _jsonSerializer = jsonSerializer; - StartupOptionFFmpegPath = startupOptionsFFmpegPath; - ConfigurationManager = configurationManager; - FileSystem = fileSystem; - SubtitleEncoder = subtitleEncoder; + _logger = logger; + _configurationManager = configurationManager; + _fileSystem = fileSystem; _processFactory = processFactory; - DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs; _localization = localization; + _startupOptionFFmpegPath = startupOptionsFFmpegPath; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; } + private EncodingHelper EncodingHelper + => LazyInitializer.EnsureInitialized( + ref _encodingHelper, + () => new EncodingHelper(this, _fileSystem, _subtitleEncoder(), _configuration)); + + /// + public string EncoderPath => _ffmpegPath; + + /// + public FFmpegLocation EncoderLocation { get; private set; } + /// /// Run at startup or if the user removes a Custom path from transcode page. /// Sets global variables FFmpegPath. @@ -88,39 +93,39 @@ namespace MediaBrowser.MediaEncoding.Encoder public void SetFFmpegPath() { // 1) Custom path stored in config/encoding xml file under tag takes precedence - if (!ValidatePath(ConfigurationManager.GetConfiguration("encoding").EncoderAppPath, FFmpegLocation.Custom)) + if (!ValidatePath(_configurationManager.GetConfiguration("encoding").EncoderAppPath, FFmpegLocation.Custom)) { // 2) Check if the --ffmpeg CLI switch has been given - if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument)) + if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument)) { // 3) Search system $PATH environment variable for valid FFmpeg if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System)) { EncoderLocation = FFmpegLocation.NotFound; - FFmpegPath = null; + _ffmpegPath = null; } } } // Write the FFmpeg path to the config/encoding.xml file as so it appears in UI - var config = ConfigurationManager.GetConfiguration("encoding"); - config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty; - ConfigurationManager.SaveConfiguration("encoding", config); + var config = _configurationManager.GetConfiguration("encoding"); + config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty; + _configurationManager.SaveConfiguration("encoding", config); // Only if mpeg path is set, try and set path to probe - if (FFmpegPath != null) + if (_ffmpegPath != null) { // Determine a probe path from the mpeg path - FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); + _ffprobePath = Regex.Replace(_ffmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); // Interrogate to understand what coders are supported - var validator = new EncoderValidator(_logger, FFmpegPath); + var validator = new EncoderValidator(_logger, _ffmpegPath); SetAvailableDecoders(validator.GetDecoders()); SetAvailableEncoders(validator.GetEncoders()); } - _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, FFmpegPath ?? string.Empty); + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, _ffmpegPath ?? string.Empty); } /// @@ -160,9 +165,9 @@ namespace MediaBrowser.MediaEncoding.Encoder // Write the new ffmpeg path to the xml as // This ensures its not lost on next startup - var config = ConfigurationManager.GetConfiguration("encoding"); + var config = _configurationManager.GetConfiguration("encoding"); config.EncoderAppPath = newPath; - ConfigurationManager.SaveConfiguration("encoding", config); + _configurationManager.SaveConfiguration("encoding", config); // Trigger SetFFmpegPath so we validate the new path and setup probe path SetFFmpegPath(); @@ -193,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // ToDo - Enable the ffmpeg validator. At the moment any version can be used. rc = true; - FFmpegPath = path; + _ffmpegPath = path; EncoderLocation = location; } else @@ -209,7 +214,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { try { - var files = FileSystem.GetFilePaths(path); + var files = _fileSystem.GetFilePaths(path); var excludeExtensions = new[] { ".c" }; @@ -304,7 +309,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - var inputFiles = MediaEncoderHelpers.GetInputArgument(FileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); + var inputFiles = MediaEncoderHelpers.GetInputArgument(_fileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); var probeSize = EncodingHelper.GetProbeSizeArgument(inputFiles.Length); string analyzeDuration; @@ -365,7 +370,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Must consume both or ffmpeg may hang due to deadlocks. See comments below. RedirectStandardOutput = true, - FileName = FFprobePath, + FileName = _ffprobePath, Arguments = args, @@ -383,7 +388,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); } - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { _logger.LogDebug("Starting ffprobe with args {Args}", args); StartProcess(processWrapper); @@ -391,7 +396,7 @@ namespace MediaBrowser.MediaEncoding.Encoder InternalMediaInfoResult result; try { - result = await _jsonSerializer.DeserializeFromStreamAsync( + result = await JsonSerializer.DeserializeAsync( process.StandardOutput.BaseStream).ConfigureAwait(false); } catch @@ -423,7 +428,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return new ProbeResultNormalizer(_logger, FileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); + return new ProbeResultNormalizer(_logger, _fileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); } } @@ -486,7 +491,7 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new ArgumentNullException(nameof(inputPath)); } - var tempExtractPath = Path.Combine(ConfigurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); + var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600. @@ -545,7 +550,6 @@ namespace MediaBrowser.MediaEncoding.Encoder args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args; } - var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder()); if (videoStream != null) { /* fix @@ -559,7 +563,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(container)) { - var inputFormat = encodinghelper.GetInputFormat(container); + var inputFormat = EncodingHelper.GetInputFormat(container); if (!string.IsNullOrWhiteSpace(inputFormat)) { args = "-f " + inputFormat + " " + args; @@ -570,7 +574,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFmpegPath, + FileName = _ffmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -579,7 +583,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { bool ranToCompletion; @@ -588,10 +592,10 @@ namespace MediaBrowser.MediaEncoding.Encoder { StartProcess(processWrapper); - var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs; + var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs; if (timeoutMs <= 0) { - timeoutMs = DefaultImageExtractionTimeoutMs; + timeoutMs = DefaultImageExtractionTimeout; } ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false); @@ -607,7 +611,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; - var file = FileSystem.GetFileInfo(tempExtractPath); + var file = _fileSystem.GetFileInfo(tempExtractPath); if (exitCode == -1 || !file.Exists || file.Length == 0) { @@ -675,7 +679,6 @@ namespace MediaBrowser.MediaEncoding.Encoder args = analyzeDurationArgument + " " + args; } - var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder()); if (videoStream != null) { /* fix @@ -689,7 +692,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!string.IsNullOrWhiteSpace(container)) { - var inputFormat = encodinghelper.GetInputFormat(container); + var inputFormat = EncodingHelper.GetInputFormat(container); if (!string.IsNullOrWhiteSpace(inputFormat)) { args = "-f " + inputFormat + " " + args; @@ -700,7 +703,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { CreateNoWindow = true, UseShellExecute = false, - FileName = FFmpegPath, + FileName = _ffmpegPath, Arguments = args, IsHidden = true, ErrorDialog = false, @@ -713,7 +716,7 @@ namespace MediaBrowser.MediaEncoding.Encoder bool ranToCompletion = false; - using (var processWrapper = new ProcessWrapper(process, this, _logger)) + using (var processWrapper = new ProcessWrapper(process, this)) { try { @@ -736,10 +739,10 @@ namespace MediaBrowser.MediaEncoding.Encoder cancellationToken.ThrowIfCancellationRequested(); - var jpegCount = FileSystem.GetFilePaths(targetDirectory) + var jpegCount = _fileSystem.GetFilePaths(targetDirectory) .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); - isResponsive = (jpegCount > lastCount); + isResponsive = jpegCount > lastCount; lastCount = jpegCount; } @@ -770,7 +773,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { process.Process.Start(); - lock (_runningProcesses) + lock (_runningProcessesLock) { _runningProcesses.Add(process); } @@ -804,7 +807,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private void StopProcesses() { List proceses; - lock (_runningProcesses) + lock (_runningProcessesLock) { proceses = _runningProcesses.ToList(); _runningProcesses.Clear(); @@ -827,12 +830,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return path.Replace('\\', '/').Replace(":", "\\:").Replace("'", "'\\\\\\''"); } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// + /// public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// @@ -852,11 +854,6 @@ namespace MediaBrowser.MediaEncoding.Encoder throw new NotImplementedException(); } - public string[] GetPlayableStreamFileNames(string path, VideoType videoType) - { - throw new NotImplementedException(); - } - public IEnumerable GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber) { throw new NotImplementedException(); @@ -870,21 +867,24 @@ namespace MediaBrowser.MediaEncoding.Encoder private class ProcessWrapper : IDisposable { - public readonly IProcess Process; - public bool HasExited; - public int? ExitCode; private readonly MediaEncoder _mediaEncoder; - private readonly ILogger _logger; - public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder, ILogger logger) + private bool _disposed = false; + + public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder) { Process = process; _mediaEncoder = mediaEncoder; - _logger = logger; - Process.Exited += Process_Exited; + Process.Exited += OnProcessExited; } - void Process_Exited(object sender, EventArgs e) + public IProcess Process { get; } + + public bool HasExited { get; private set; } + + public int? ExitCode { get; private set; } + + void OnProcessExited(object sender, EventArgs e) { var process = (IProcess)sender; @@ -903,7 +903,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private void DisposeProcess(IProcess process) { - lock (_mediaEncoder._runningProcesses) + lock (_mediaEncoder._runningProcessesLock) { _mediaEncoder._runningProcesses.Remove(this); } @@ -917,23 +917,18 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - private bool _disposed; - private readonly object _syncLock = new object(); public void Dispose() { - lock (_syncLock) + if (!_disposed) { - if (!_disposed) + if (Process != null) { - if (Process != null) - { - Process.Exited -= Process_Exited; - DisposeProcess(Process); - } + Process.Exited -= OnProcessExited; + DisposeProcess(Process); } - - _disposed = true; } + + _disposed = true; } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs index 3401c2d670..dec714121d 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles { /// - /// Interface ISubtitleWriter + /// Interface ISubtitleWriter. /// public interface ISubtitleWriter { diff --git a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs index 8995fcfe1f..241ebc6df5 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs @@ -1,27 +1,39 @@ using System.IO; -using System.Text; +using System.Text.Json; using System.Threading; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.MediaEncoding.Subtitles { + /// + /// JSON subtitle writer. + /// public class JsonWriter : ISubtitleWriter { - private readonly IJsonSerializer _json; - - public JsonWriter(IJsonSerializer json) - { - _json = json; - } - + /// public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { - using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + using (var writer = new Utf8JsonWriter(stream)) { - var json = _json.SerializeToString(info); + var trackevents = info.TrackEvents; + writer.WriteStartArray("TrackEvents"); - writer.Write(json); + for (int i = 0; i < trackevents.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var current = trackevents[i]; + writer.WriteStartObject(); + + writer.WriteString("Id", current.Id); + writer.WriteString("Text", current.Text); + writer.WriteNumber("StartPositionTicks", current.StartPositionTicks); + writer.WriteNumber("EndPositionTicks", current.EndPositionTicks); + + writer.WriteEndObject(); + } + + writer.WriteEndObject(); } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs index 6f96a641e9..45b317b2ed 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs @@ -14,14 +14,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { - var index = 1; + var trackEvents = info.TrackEvents; - foreach (var trackEvent in info.TrackEvents) + for (int i = 0; i < trackEvents.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); - writer.WriteLine(index.ToString(CultureInfo.InvariantCulture)); - writer.WriteLine(@"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", TimeSpan.FromTicks(trackEvent.StartPositionTicks), TimeSpan.FromTicks(trackEvent.EndPositionTicks)); + var trackEvent = trackEvents[i]; + + writer.WriteLine((i + 1).ToString(CultureInfo.InvariantCulture)); + writer.WriteLine( + @"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", + TimeSpan.FromTicks(trackEvent.StartPositionTicks), + TimeSpan.FromTicks(trackEvent.EndPositionTicks)); var text = trackEvent.Text; @@ -29,9 +34,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); - writer.WriteLine(string.Empty); - - index++; + writer.WriteLine(); } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index d5fa76c3ab..183d7566d4 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; using UtfUnknown; @@ -30,28 +29,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; - private readonly IJsonSerializer _json; private readonly IHttpClient _httpClient; private readonly IMediaSourceManager _mediaSourceManager; private readonly IProcessFactory _processFactory; public SubtitleEncoder( ILibraryManager libraryManager, - ILoggerFactory loggerFactory, + ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, - IJsonSerializer json, IHttpClient httpClient, IMediaSourceManager mediaSourceManager, IProcessFactory processFactory) { _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(SubtitleEncoder)); + _logger = logger; _appPaths = appPaths; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; - _json = json; _httpClient = httpClient; _mediaSourceManager = mediaSourceManager; _processFactory = processFactory; @@ -59,7 +55,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); - private Stream ConvertSubtitles(Stream stream, + private Stream ConvertSubtitles( + Stream stream, string inputFormat, string outputFormat, long startTimeTicks, @@ -170,7 +167,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles && (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd)) { var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSource.Id)); - inputFiles = mediaSourceItem.GetPlayableStreamFileNames(_mediaEncoder); + inputFiles = mediaSourceItem.GetPlayableStreamFileNames(); } else { @@ -179,32 +176,27 @@ namespace MediaBrowser.MediaEncoding.Subtitles var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false); - var stream = await GetSubtitleStream(fileInfo.Path, subtitleStream.Language, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); + var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); return (stream, fileInfo.Format); } - private async Task GetSubtitleStream(string path, string language, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) + private async Task GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) { if (requiresCharset) { - var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false); - - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; - _logger.LogDebug("charset {CharSet} detected for {Path}", charset ?? "null", path); - - if (!string.IsNullOrEmpty(charset)) + using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) { - // Make sure we have all the code pages we can get - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - using (var inputStream = new MemoryStream(bytes)) - using (var reader = new StreamReader(inputStream, Encoding.GetEncoding(charset))) + var result = CharsetDetector.DetectFromStream(stream).Detected; + + if (result != null) { + _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path); + + using var reader = new StreamReader(stream, result.Encoding); var text = await reader.ReadToEndAsync().ConfigureAwait(false); - bytes = Encoding.UTF8.GetBytes(text); - - return new MemoryStream(bytes); + return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } } @@ -323,7 +315,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) { - return new JsonWriter(_json); + return new JsonWriter(); } if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) { @@ -544,7 +536,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!File.Exists(outputPath)) { - await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), subtitleStreamIndex, outputCodec, outputPath, cancellationToken).ConfigureAwait(false); + await ExtractTextSubtitleInternal( + _mediaEncoder.GetInputArgument(inputFiles, protocol), + subtitleStreamIndex, + outputCodec, + outputPath, + cancellationToken).ConfigureAwait(false); } } finally @@ -572,8 +569,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath, - subtitleStreamIndex, outputCodec, outputPath); + var processArgs = string.Format( + CultureInfo.InvariantCulture, + "-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", + inputPath, + subtitleStreamIndex, + outputCodec, + outputPath); var process = _processFactory.Create(new ProcessOptions { @@ -721,41 +723,38 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } + /// public async Task GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken) { - var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false); + using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) + { + var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName; - var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName; + _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); - _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); - - return charset; + return charset; + } } - private async Task GetBytes(string path, MediaProtocol protocol, CancellationToken cancellationToken) + private Task GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) { - if (protocol == MediaProtocol.Http) + switch (protocol) { - var opts = new HttpRequestOptions() - { - Url = path, - CancellationToken = cancellationToken - }; - using (var file = await _httpClient.Get(opts).ConfigureAwait(false)) - using (var memoryStream = new MemoryStream()) - { - await file.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; + case MediaProtocol.Http: + var opts = new HttpRequestOptions() + { + Url = path, + CancellationToken = cancellationToken, + BufferContent = true + }; - return memoryStream.ToArray(); - } - } - if (protocol == MediaProtocol.File) - { - return File.ReadAllBytes(path); - } + return _httpClient.Get(opts); - throw new ArgumentOutOfRangeException(nameof(protocol)); + case MediaProtocol.File: + return Task.FromResult(File.OpenRead(path)); + default: + throw new ArgumentOutOfRangeException(nameof(protocol)); + } } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs index cdaf949641..4f15bac496 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -49,12 +49,5 @@ namespace MediaBrowser.MediaEncoding.Subtitles writer.WriteLine(""); } } - - private string FormatTime(long ticks) - { - var time = TimeSpan.FromTicks(ticks); - - return string.Format(@"{0:hh\:mm\:ss\,fff}", time); - } } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 9de6fef619..cf6d9c2f6c 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -233,7 +233,6 @@ namespace MediaBrowser.Model.Configuration LocalNetworkSubnets = Array.Empty(); LocalNetworkAddresses = Array.Empty(); CodecsUsed = Array.Empty(); - ImageExtractionTimeoutMs = 0; PathSubstitutions = Array.Empty(); IgnoreVirtualInterfaces = false; EnableSimpleArtistDetection = true; diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs index 962f4d2fe3..c382b20c9a 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs @@ -1,12 +1,15 @@ +using System; +using System.Collections.Generic; + namespace MediaBrowser.Model.MediaInfo { public class SubtitleTrackInfo { - public SubtitleTrackEvent[] TrackEvents { get; set; } + public IReadOnlyList TrackEvents { get; set; } public SubtitleTrackInfo() { - TrackEvents = new SubtitleTrackEvent[] { }; + TrackEvents = Array.Empty(); } } } diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index e0b23108f0..95b915b3d8 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -62,7 +62,11 @@ namespace MediaBrowser.Providers.MediaInfo { var protocol = item.PathProtocol ?? MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, item.Path, null, item.GetPlayableStreamFileNames(_mediaEncoder)); + var inputPath = MediaEncoderHelpers.GetInputArgument( + _fileSystem, + item.Path, + null, + item.GetPlayableStreamFileNames()); var mediaStreams = item.GetMediaStreams(); diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 75b5573b67..27d8a7cd92 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -31,6 +31,8 @@ + +