소스 검색

Merge pull request #3 from jellyfin/master

nightly, big one
Artiume 5 년 전
부모
커밋
6a6e02e1ec
37개의 변경된 파일554개의 추가작업 그리고 358개의 파일을 삭제
  1. 15 8
      Emby.Server.Implementations/ApplicationHost.cs
  2. 5 2
      Emby.Server.Implementations/ConfigurationOptions.cs
  3. 0 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  4. 37 37
      Emby.Server.Implementations/Localization/Core/ca.json
  5. 13 0
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  6. 9 0
      Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs
  7. 13 2
      Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs
  8. 8 8
      Jellyfin.Drawing.Skia/SkiaCodecException.cs
  9. 36 13
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  10. 16 3
      Jellyfin.Drawing.Skia/SkiaException.cs
  11. 27 0
      Jellyfin.Drawing.Skia/StripCollageBuilder.cs
  12. 17 0
      Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
  13. 6 1
      Jellyfin.Server/Program.cs
  14. 5 10
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  15. 17 17
      MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
  16. 4 4
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  17. 33 33
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  18. 4 4
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  19. 4 4
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  20. 4 4
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  21. 10 9
      MediaBrowser.Api/Playback/UniversalAudioService.cs
  22. 11 5
      MediaBrowser.Controller/Drawing/IImageEncoder.cs
  23. 3 2
      MediaBrowser.Controller/Entities/Video.cs
  24. 36 0
      MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
  25. 4 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  26. 23 9
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  27. 3 1
      MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
  28. 93 98
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  29. 1 1
      MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs
  30. 24 12
      MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs
  31. 10 7
      MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
  32. 51 52
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  33. 0 7
      MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
  34. 0 1
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  35. 5 2
      MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
  36. 5 1
      MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
  37. 2 0
      jellyfin.ruleset

+ 15 - 8
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<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(),
                 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<AuthService>(), 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<MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(),
+                ApplicationPaths,
+                FileSystemManager,
+                MediaEncoder,
+                HttpClient,
+                MediaSourceManager,
+                ProcessFactory);
             serviceCollection.AddSingleton(SubtitleEncoder);
 
             serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
+            serviceCollection.AddSingleton<EncodingHelper>();
 
             _displayPreferencesRepository.Initialize();
 

+ 5 - 2
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<string, string> Configuration = new Dictionary<string, string>
+        public static Dictionary<string, string> Configuration => new Dictionary<string, string>
         {
             { "HttpListenerHost:DefaultRedirectPath", "web/index.html" },
-            { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" }
+            { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" },
+            { FfmpegProbeSizeKey, "1G" },
+            { FfmpegAnalyzeDurationKey, "200M" }
         };
     }
 }

+ 0 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -29,7 +29,6 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.1" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.1" />
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.0.1" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.1" />

+ 37 - 37
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&agrave; 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}"
 }

+ 13 - 0
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -4,6 +4,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
 
   <ItemGroup>
@@ -22,4 +23,16 @@
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
   </ItemGroup>
 
+  <!-- Code analysers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
 </Project>

+ 9 - 0
Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs

@@ -4,10 +4,19 @@ using SkiaSharp;
 
 namespace Jellyfin.Drawing.Skia
 {
+    /// <summary>
+    /// Static helper class used to draw percentage-played indicators on images.
+    /// </summary>
     public static class PercentPlayedDrawer
     {
         private const int IndicatorHeight = 8;
 
+        /// <summary>
+        /// Draw a percentage played indicator on a canvas.
+        /// </summary>
+        /// <param name="canvas">The canvas to draw the indicator on.</param>
+        /// <param name="imageSize">The size of the image being drawn on.</param>
+        /// <param name="percent">The percentage played to display with the indicator.</param>
         public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
         {
             using (var paint = new SKPaint())

+ 13 - 2
Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs

@@ -3,10 +3,21 @@ using SkiaSharp;
 
 namespace Jellyfin.Drawing.Skia
 {
+    /// <summary>
+    /// Static helper class for drawing 'played' indicators.
+    /// </summary>
     public static class PlayedIndicatorDrawer
     {
         private const int OffsetFromTopRightCorner = 38;
 
+        /// <summary>
+        /// Draw a 'played' indicator in the top right corner of a canvas.
+        /// </summary>
+        /// <param name="canvas">The canvas to draw the indicator on.</param>
+        /// <param name="imageSize">
+        /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
+        /// indicator.
+        /// </param>
         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;

+ 8 - 8
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
     /// </summary>
     public class SkiaCodecException : SkiaException
     {
-        /// <summary>
-        /// Returns the non-successfull codec result returned by Skia.
-        /// </summary>
-        /// <value>The non-successfull codec result returned by Skia.</value>
-        public SKCodecResult CodecResult { get; }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
         /// </summary>
-        /// <param name="result">The non-successfull codec result returned by Skia.</param>
+        /// <param name="result">The non-successful codec result returned by Skia.</param>
         public SkiaCodecException(SKCodecResult result) : base()
         {
             CodecResult = result;
@@ -27,7 +22,7 @@ namespace Jellyfin.Drawing.Skia
         /// Initializes a new instance of the <see cref="SkiaCodecException" /> class
         /// with a specified error message.
         /// </summary>
-        /// <param name="result">The non-successfull codec result returned by Skia.</param>
+        /// <param name="result">The non-successful codec result returned by Skia.</param>
         /// <param name="message">The message that describes the error.</param>
         public SkiaCodecException(SKCodecResult result, string message)
             : base(message)
@@ -35,6 +30,11 @@ namespace Jellyfin.Drawing.Skia
             CodecResult = result;
         }
 
+        /// <summary>
+        /// Gets the non-successful codec result returned by Skia.
+        /// </summary>
+        public SKCodecResult CodecResult { get; }
+
         /// <inheritdoc />
         public override string ToString()
             => string.Format(

+ 36 - 13
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -13,6 +13,9 @@ using static Jellyfin.Drawing.Skia.SkiaHelper;
 
 namespace Jellyfin.Drawing.Skia
 {
+    /// <summary>
+    /// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
+    /// </summary>
     public class SkiaEncoder : IImageEncoder
     {
         private readonly ILogger _logger;
@@ -22,6 +25,12 @@ namespace Jellyfin.Drawing.Skia
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
+        /// </summary>
+        /// <param name="logger">The application logger.</param>
+        /// <param name="appPaths">The application paths.</param>
+        /// <param name="localizationManager">The application localization manager.</param>
         public SkiaEncoder(
             ILogger<SkiaEncoder> logger,
             IApplicationPaths appPaths,
@@ -32,12 +41,16 @@ namespace Jellyfin.Drawing.Skia
             _localizationManager = localizationManager;
         }
 
+        /// <inheritdoc/>
         public string Name => "Skia";
 
+        /// <inheritdoc/>
         public bool SupportsImageCollageCreation => true;
 
+        /// <inheritdoc/>
         public bool SupportsImageEncoding => true;
 
+        /// <inheritdoc/>
         public IReadOnlyCollection<string> SupportedInputFormats =>
             new HashSet<string>(StringComparer.OrdinalIgnoreCase)
             {
@@ -65,11 +78,12 @@ namespace Jellyfin.Drawing.Skia
                 "arw"
             };
 
+        /// <inheritdoc/>
         public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
             => new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
 
         /// <summary>
-        /// Test to determine if the native lib is available
+        /// Test to determine if the native lib is available.
         /// </summary>
         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;
 
+        /// <summary>
+        /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
+        /// </summary>
+        /// <param name="selectedFormat">The format to convert.</param>
+        /// <returns>The converted format.</returns>
         public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
         {
             switch (selectedFormat)
@@ -186,6 +205,9 @@ namespace Jellyfin.Drawing.Skia
         }
 
         /// <inheritdoc />
+        /// <exception cref="ArgumentNullException">The path is null.</exception>
+        /// <exception cref="FileNotFoundException">The path is not valid.</exception>
+        /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
         public ImageDimensions GetImageSize(string path)
         {
             if (path == null)
@@ -269,6 +291,14 @@ namespace Jellyfin.Drawing.Skia
             }
         }
 
+        /// <summary>
+        /// Decode an image.
+        /// </summary>
+        /// <param name="path">The filepath of the image to decode.</param>
+        /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
+        /// <param name="orientation">The orientation of the image.</param>
+        /// <param name="origin">The detected origin of the image.</param>
+        /// <returns>The resulting bitmap of the image.</returns>
         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
             }
         }
 
+        /// <inheritdoc/>
         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;
         }
 
+        /// <inheritdoc/>
         public void CreateImageCollage(ImageCollageOptions options)
         {
             double ratio = (double)options.Width / options.Height;

+ 16 - 3
Jellyfin.Drawing.Skia/SkiaException.cs

@@ -7,17 +7,30 @@ namespace Jellyfin.Drawing.Skia
     /// </summary>
     public class SkiaException : Exception
     {
-        /// <inheritdoc />
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SkiaException"/> class.
+        /// </summary>
         public SkiaException() : base()
         {
         }
 
-        /// <inheritdoc />
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
+        /// </summary>
+        /// <param name="message">The message that describes the error.</param>
         public SkiaException(string message) : base(message)
         {
         }
 
-        /// <inheritdoc />
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
+        /// reference to the inner exception that is the cause of this exception.
+        /// </summary>
+        /// <param name="message">The error message that explains the reason for the exception.</param>
+        /// <param name="innerException">
+        /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
+        /// no inner exception is specified.
+        /// </param>
         public SkiaException(string message, Exception innerException)
             : base(message, innerException)
         {

+ 27 - 0
Jellyfin.Drawing.Skia/StripCollageBuilder.cs

@@ -5,15 +5,27 @@ using SkiaSharp;
 
 namespace Jellyfin.Drawing.Skia
 {
+    /// <summary>
+    /// Used to build collages of multiple images arranged in vertical strips.
+    /// </summary>
     public class StripCollageBuilder
     {
         private readonly SkiaEncoder _skiaEncoder;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
+        /// </summary>
+        /// <param name="skiaEncoder">The encoder to use for building collages.</param>
         public StripCollageBuilder(SkiaEncoder skiaEncoder)
         {
             _skiaEncoder = skiaEncoder;
         }
 
+        /// <summary>
+        /// Check which format an image has been encoded with using its filename extension.
+        /// </summary>
+        /// <param name="outputPath">The path to the image to get the format for.</param>
+        /// <returns>The image format.</returns>
         public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
         {
             if (outputPath == null)
@@ -48,6 +60,13 @@ namespace Jellyfin.Drawing.Skia
             return SKEncodedImageFormat.Png;
         }
 
+        /// <summary>
+        /// Create a square collage.
+        /// </summary>
+        /// <param name="paths">The paths of the images to use in the collage.</param>
+        /// <param name="outputPath">The path at which to place the resulting collage image.</param>
+        /// <param name="width">The desired width of the collage.</param>
+        /// <param name="height">The desired height of the collage.</param>
         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
             }
         }
 
+        /// <summary>
+        /// Create a thumb collage.
+        /// </summary>
+        /// <param name="paths">The paths of the images to use in the collage.</param>
+        /// <param name="outputPath">The path at which to place the resulting image.</param>
+        /// <param name="width">The desired width of the collage.</param>
+        /// <param name="height">The desired height of the collage.</param>
         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))

+ 17 - 0
Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs

@@ -4,10 +4,25 @@ using SkiaSharp;
 
 namespace Jellyfin.Drawing.Skia
 {
+    /// <summary>
+    /// Static helper class for drawing unplayed count indicators.
+    /// </summary>
     public static class UnplayedCountIndicator
     {
+        /// <summary>
+        /// The x-offset used when drawing an unplayed count indicator.
+        /// </summary>
         private const int OffsetFromTopRightCorner = 38;
 
+        /// <summary>
+        /// Draw an unplayed count indicator in the top right corner of a canvas.
+        /// </summary>
+        /// <param name="canvas">The canvas to draw the indicator on.</param>
+        /// <param name="imageSize">
+        /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
+        /// indicator.
+        /// </param>
+        /// <param name="count">The number to draw in the indicator.</param>
         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;

+ 6 - 1
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();
         }
 

+ 5 - 10
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;
         }
 
         /// <summary>
@@ -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];
                     }

+ 17 - 17
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)
         {
         }
 

+ 4 - 4
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;
         }

+ 33 - 33
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -27,6 +27,39 @@ namespace MediaBrowser.Api.Playback.Hls
     [Authenticated]
     public class VideoHlsService : BaseHlsService
     {
+        public VideoHlsService(
+            ILogger<VideoHlsService> 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<object> Get(GetLiveHlsStream request)
         {
             return ProcessRequestAsync(request, true);
@@ -136,38 +169,5 @@ namespace MediaBrowser.Api.Playback.Hls
 
             return args;
         }
-
-        public VideoHlsService(
-            ILogger<VideoHlsService> 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)
-        {
-        }
     }
 }

+ 4 - 4
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)
         {
         }
 

+ 4 - 4
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;
         }

+ 4 - 4
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)
         {
         }
 

+ 10 - 9
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<UniversalAudioService> 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
                 };

+ 11 - 5
MediaBrowser.Controller/Drawing/IImageEncoder.cs

@@ -11,6 +11,7 @@ namespace MediaBrowser.Controller.Drawing
         /// </summary>
         /// <value>The supported input formats.</value>
         IReadOnlyCollection<string> SupportedInputFormats { get; }
+
         /// <summary>
         /// Gets the supported output formats.
         /// </summary>
@@ -18,9 +19,9 @@ namespace MediaBrowser.Controller.Drawing
         IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; }
 
         /// <summary>
-        /// Gets the name.
+        /// Gets the display name for the encoder.
         /// </summary>
-        /// <value>The name.</value>
+        /// <value>The display name.</value>
         string Name { get; }
 
         /// <summary>
@@ -35,17 +36,22 @@ namespace MediaBrowser.Controller.Drawing
         /// <value><c>true</c> if [supports image encoding]; otherwise, <c>false</c>.</value>
         bool SupportsImageEncoding { get; }
 
+        /// <summary>
+        /// Get the dimensions of an image from the filesystem.
+        /// </summary>
+        /// <param name="path">The filepath of the image.</param>
+        /// <returns>The image dimensions.</returns>
         ImageDimensions GetImageSize(string path);
 
         /// <summary>
-        /// Encodes the image.
+        /// Encode an image.
         /// </summary>
         string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
 
         /// <summary>
-        /// Creates the image collage.
+        /// Create an image collage.
         /// </summary>
-        /// <param name="options">The options.</param>
+        /// <param name="options">The options to use when creating the collage.</param>
         void CreateImageCollage(ImageCollageOptions options);
     }
 }

+ 3 - 2
MediaBrowser.Controller/Entities/Video.cs

@@ -137,7 +137,7 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The video3 D format.</value>
         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<string>();
             }
-            return mediaEncoder.GetPlayableStreamFileNames(Path, videoType);
+
+            throw new NotImplementedException();
         }
 
         /// <summary>

+ 36 - 0
MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs

@@ -0,0 +1,36 @@
+using Microsoft.Extensions.Configuration;
+
+namespace MediaBrowser.Controller.Extensions
+{
+    /// <summary>
+    /// Configuration extensions for <c>MediaBrowser.Controller</c>.
+    /// </summary>
+    public static class ConfigurationExtensions
+    {
+        /// <summary>
+        /// The key for the FFmpeg probe size option.
+        /// </summary>
+        public const string FfmpegProbeSizeKey = "FFmpeg:probesize";
+
+        /// <summary>
+        /// The key for the FFmpeg analyse duration option.
+        /// </summary>
+        public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration";
+
+        /// <summary>
+        /// Retrieves the FFmpeg probe size from the <see cref="IConfiguration" />.
+        /// </summary>
+        /// <param name="configuration">This configuration.</param>
+        /// <returns>The FFmpeg probe size option.</returns>
+        public static string GetFFmpegProbeSize(this IConfiguration configuration)
+            => configuration[FfmpegProbeSizeKey];
+
+        /// <summary>
+        /// Retrieves the FFmpeg analyse duration from the <see cref="IConfiguration" />.
+        /// </summary>
+        /// <param name="configuration">This configuration.</param>
+        /// <returns>The FFmpeg analyse duration option.</returns>
+        public static string GetFFmpegAnalyzeDuration(this IConfiguration configuration)
+            => configuration[FfmpegAnalyzeDurationKey];
+    }
+}

+ 4 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -7,6 +7,10 @@
     <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.0" />
+  </ItemGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />

+ 23 - 9
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)
         {

+ 3 - 1
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -15,6 +15,9 @@ namespace MediaBrowser.Controller.MediaEncoding
     /// </summary>
     public interface IMediaEncoder : ITranscoderSupport
     {
+        /// <summary>
+        /// The location of the discovered FFmpeg tool.
+        /// </summary>
         FFmpegLocation EncoderLocation { get; }
 
         /// <summary>
@@ -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<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber);
     }
 }

+ 93 - 98
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
     {
         /// <summary>
-        /// Gets the encoder path.
+        /// The default image extraction timeout in milliseconds.
         /// </summary>
-        /// <value>The encoder path.</value>
-        public string EncoderPath => FFmpegPath;
-
-        /// <summary>
-        /// The location of the discovered FFmpeg tool.
-        /// </summary>
-        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<ISubtitleEncoder> SubtitleEncoder;
-        protected readonly Func<IMediaSourceManager> 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<ISubtitleEncoder> _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<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
-        private readonly ILocalizationManager _localization;
+
+        private EncodingHelper _encodingHelper;
+
+        private string _ffmpegPath;
+        private string _ffprobePath;
 
         public MediaEncoder(
-            ILoggerFactory loggerFactory,
-            IJsonSerializer jsonSerializer,
-            string startupOptionsFFmpegPath,
+            ILogger<MediaEncoder> logger,
             IServerConfigurationManager configurationManager,
             IFileSystem fileSystem,
-            Func<ISubtitleEncoder> subtitleEncoder,
-            Func<IMediaSourceManager> mediaSourceManager,
             IProcessFactory processFactory,
-            int defaultImageExtractionTimeoutMs,
-            ILocalizationManager localization)
-        {
-            _logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
-            _jsonSerializer = jsonSerializer;
-            StartupOptionFFmpegPath = startupOptionsFFmpegPath;
-            ConfigurationManager = configurationManager;
-            FileSystem = fileSystem;
-            SubtitleEncoder = subtitleEncoder;
+            ILocalizationManager localization,
+            Func<ISubtitleEncoder> subtitleEncoder,
+            IConfiguration configuration,
+            string startupOptionsFFmpegPath)
+        {
+            _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));
+
+        /// <inheritdoc />
+        public string EncoderPath => _ffmpegPath;
+
+        /// <inheritdoc />
+        public FFmpegLocation EncoderLocation { get; private set; }
+
         /// <summary>
         /// 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 <EncoderAppPath> takes precedence
-            if (!ValidatePath(ConfigurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
+            if (!ValidatePath(_configurationManager.GetConfiguration<EncodingOptions>("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 <EncoderAppPathDisplay> so it appears in UI
-            var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
-            config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty;
-            ConfigurationManager.SaveConfiguration("encoding", config);
+            var config = _configurationManager.GetConfiguration<EncodingOptions>("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);
         }
 
         /// <summary>
@@ -160,9 +165,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
             // Write the new ffmpeg path to the xml as <EncoderAppPath>
             // This ensures its not lost on next startup
-            var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
+            var config = _configurationManager.GetConfiguration<EncodingOptions>("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<InternalMediaInfoResult>(
+                    result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
                                         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<ProcessWrapper> proceses;
-            lock (_runningProcesses)
+            lock (_runningProcessesLock)
             {
                 proceses = _runningProcesses.ToList();
                 _runningProcesses.Clear();
@@ -827,12 +830,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return path.Replace('\\', '/').Replace(":", "\\:").Replace("'", "'\\\\\\''");
         }
 
-        /// <summary>
-        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
-        /// </summary>
+        /// <inheritdoc />
         public void Dispose()
         {
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
 
         /// <summary>
@@ -852,11 +854,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
             throw new NotImplementedException();
         }
 
-        public string[] GetPlayableStreamFileNames(string path, VideoType videoType)
-        {
-            throw new NotImplementedException();
-        }
-
         public IEnumerable<string> 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;
             }
         }
     }

+ 1 - 1
MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs

@@ -5,7 +5,7 @@ using MediaBrowser.Model.MediaInfo;
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
     /// <summary>
-    /// Interface ISubtitleWriter
+    /// Interface ISubtitleWriter.
     /// </summary>
     public interface ISubtitleWriter
     {

+ 24 - 12
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
 {
+    /// <summary>
+    /// JSON subtitle writer.
+    /// </summary>
     public class JsonWriter : ISubtitleWriter
     {
-        private readonly IJsonSerializer _json;
-
-        public JsonWriter(IJsonSerializer json)
-        {
-            _json = json;
-        }
-
+        /// <inheritdoc />
         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");
+
+                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.Write(json);
+                writer.WriteEndObject();
             }
         }
     }

+ 10 - 7
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();
                 }
             }
         }

+ 51 - 52
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<SubtitleEncoder> 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<Stream> GetSubtitleStream(string path, string language, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
+        private async Task<Stream> 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)
                     {
-                        var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+                        _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path);
 
-                        bytes = Encoding.UTF8.GetBytes(text);
+                        using var reader = new StreamReader(stream, result.Encoding);
+                        var text = await reader.ReadToEndAsync().ConfigureAwait(false);
 
-                        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
             }
         }
 
+        /// <inheritdoc />
         public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken)
         {
-            var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false);
-
-            var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
+            using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
+            {
+                var charset = CharsetDetector.DetectFromStream(stream).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<byte[]> GetBytes(string path, MediaProtocol protocol, CancellationToken cancellationToken)
+        private Task<Stream> 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<Stream>(File.OpenRead(path));
+            default:
+                throw new ArgumentOutOfRangeException(nameof(protocol));
+            }
         }
     }
 }

+ 0 - 7
MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs

@@ -49,12 +49,5 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 writer.WriteLine("</tt>");
             }
         }
-
-        private string FormatTime(long ticks)
-        {
-            var time = TimeSpan.FromTicks(ticks);
-
-            return string.Format(@"{0:hh\:mm\:ss\,fff}", time);
-        }
     }
 }

+ 0 - 1
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -233,7 +233,6 @@ namespace MediaBrowser.Model.Configuration
             LocalNetworkSubnets = Array.Empty<string>();
             LocalNetworkAddresses = Array.Empty<string>();
             CodecsUsed = Array.Empty<string>();
-            ImageExtractionTimeoutMs = 0;
             PathSubstitutions = Array.Empty<PathSubstitution>();
             IgnoreVirtualInterfaces = false;
             EnableSimpleArtistDetection = true;

+ 5 - 2
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<SubtitleTrackEvent> TrackEvents { get; set; }
 
         public SubtitleTrackInfo()
         {
-            TrackEvents = new SubtitleTrackEvent[] { };
+            TrackEvents = Array.Empty<SubtitleTrackEvent>();
         }
     }
 }

+ 5 - 1
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();

+ 2 - 0
jellyfin.ruleset

@@ -31,6 +31,8 @@
   <Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
     <!-- disable warning CA1031: Do not catch general exception types -->
     <Rule Id="CA1031" Action="Info" />
+    <!-- disable warning CA1032: Implement standard exception constructors -->
+    <Rule Id="CA1032" Action="Info" />
     <!-- disable warning CA1062: Validate arguments of public methods -->
     <Rule Id="CA1062" Action="Info" />
     <!-- disable warning CA1720: Identifiers should not contain type names -->