Преглед изворни кода

Move SkiaSharp related code to Jellyfin.Drawing and IImageEncoder

Nick пре 2 година
родитељ
комит
0e2c362078

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

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Drawing;
 
 namespace MediaBrowser.Controller.Drawing
@@ -81,5 +82,15 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="posters">The list of poster paths.</param>
         /// <param name="backdrops">The list of backdrop paths.</param>
         void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops);
+
+        /// <summary>
+        /// Creates a new jpeg trickplay grid image.
+        /// </summary>
+        /// <param name="options">The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels.</param>
+        /// <param name="quality">The image encode quality.</param>
+        /// <param name="imgWidth">The width of a single trickplay image.</param>
+        /// <param name="imgHeight">Optional height of a single trickplay image, if it is known.</param>
+        /// <returns>Height of single decoded trickplay image.</returns>
+        int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
     }
 }

+ 1 - 2
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
   <PropertyGroup>
@@ -22,7 +22,6 @@
     <PackageReference Include="Microsoft.Extensions.Http" />
     <PackageReference Include="Newtonsoft.Json" />
     <PackageReference Include="PlaylistsNET" />
-    <PackageReference Include="SkiaSharp" />
     <PackageReference Include="TagLibSharp" />
     <PackageReference Include="TMDbLib" />
   </ItemGroup>

+ 32 - 61
MediaBrowser.Providers/Trickplay/TrickplayManager.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
@@ -15,7 +16,6 @@ using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
-using SkiaSharp;
 
 namespace MediaBrowser.Providers.Trickplay;
 
@@ -31,6 +31,7 @@ public class TrickplayManager : ITrickplayManager
     private readonly EncodingHelper _encodingHelper;
     private readonly ILibraryManager _libraryManager;
     private readonly IServerConfigurationManager _config;
+    private readonly IImageEncoder _imageEncoder;
 
     private static readonly SemaphoreSlim _resourcePool = new(1, 1);
     private static readonly string[] _trickplayImgExtensions = { ".jpg" };
@@ -45,6 +46,7 @@ public class TrickplayManager : ITrickplayManager
     /// <param name="encodingHelper">The encoding helper.</param>
     /// <param name="libraryManager">The library manager.</param>
     /// <param name="config">The server configuration manager.</param>
+    /// <param name="imageEncoder">The image encoder.</param>
     public TrickplayManager(
         ILogger<TrickplayManager> logger,
         IItemRepository itemRepo,
@@ -52,7 +54,8 @@ public class TrickplayManager : ITrickplayManager
         IFileSystem fileSystem,
         EncodingHelper encodingHelper,
         ILibraryManager libraryManager,
-        IServerConfigurationManager config)
+        IServerConfigurationManager config,
+        IImageEncoder imageEncoder)
     {
         _logger = logger;
         _itemRepo = itemRepo;
@@ -61,6 +64,7 @@ public class TrickplayManager : ITrickplayManager
         _encodingHelper = encodingHelper;
         _libraryManager = libraryManager;
         _config = config;
+        _imageEncoder = imageEncoder;
     }
 
     /// <inheritdoc />
@@ -141,7 +145,8 @@ public class TrickplayManager : ITrickplayManager
             }
 
             var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
-                .OrderBy(i => i.FullName)
+                .Select(i => i.FullName)
+                .OrderBy(i => i)
                 .ToList();
 
             // Create tiles
@@ -185,11 +190,11 @@ public class TrickplayManager : ITrickplayManager
         }
     }
 
-    private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, TrickplayOptions options, string workDir, string outputDir)
+    private TrickplayTilesInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string workDir, string outputDir)
     {
         if (images.Count == 0)
         {
-            throw new InvalidOperationException("Can't create trickplay from 0 images.");
+            throw new ArgumentException("Can't create trickplay from 0 images.");
         }
 
         Directory.CreateDirectory(workDir);
@@ -200,76 +205,42 @@ public class TrickplayManager : ITrickplayManager
             Interval = options.Interval,
             TileWidth = options.TileWidth,
             TileHeight = options.TileHeight,
-            TileCount = 0,
+            TileCount = images.Count,
+            // Set during image generation
+            Height = 0,
             Bandwidth = 0
         };
 
-        var firstImg = SKBitmap.Decode(images[0].FullName);
-        if (firstImg == null)
+        /*
+         * Generate trickplay tile grids from sets of images
+         */
+        var imageOptions = new ImageCollageOptions
         {
-            throw new InvalidDataException("Could not decode image data.");
-        }
+            Width = tilesInfo.TileWidth,
+            Height = tilesInfo.TileHeight
+        };
 
-        tilesInfo.Height = firstImg.Height;
-        if (tilesInfo.Width != firstImg.Width)
-        {
-            throw new InvalidOperationException("Image width does not match config width.");
-        }
+        var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
+        var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid);
 
-        /*
-         * Generate grids of trickplay image tiles
-         */
-        var imgNo = 0;
-        var i = 0;
-        while (i < images.Count)
+        for (int i = 0; i < requiredTileGrids; i++)
         {
-            var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight);
+            // Set output/input paths
+            var tileGridPath = Path.Combine(workDir, $"{i}.jpg");
 
-            using (var canvas = new SKCanvas(tileGrid))
-            {
-                for (var y = 0; y < tilesInfo.TileHeight; y++)
-                {
-                    for (var x = 0; x < tilesInfo.TileWidth; x++)
-                    {
-                        if (i >= images.Count)
-                        {
-                            break;
-                        }
-
-                        var img = SKBitmap.Decode(images[i].FullName);
-                        if (img == null)
-                        {
-                            throw new InvalidDataException("Could not decode image data.");
-                        }
-
-                        if (tilesInfo.Width != img.Width)
-                        {
-                            throw new InvalidOperationException("Image width does not match config width.");
-                        }
-
-                        if (tilesInfo.Height != img.Height)
-                        {
-                            throw new InvalidOperationException("Image height does not match first image height.");
-                        }
-
-                        canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height);
-                        tilesInfo.TileCount++;
-                        i++;
-                    }
-                }
-            }
+            imageOptions.OutputPath = tileGridPath;
+            imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).ToList();
 
-            // Output each tile grid to singular file
-            var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg");
-            using (var stream = File.OpenWrite(tileGridPath))
+            // Generate image and use returned height for tiles info
+            var height = _imageEncoder.CreateTrickplayGrid(imageOptions, options.JpegQuality, tilesInfo.Width, tilesInfo.Height != 0 ? tilesInfo.Height : null);
+            if (tilesInfo.Height == 0)
             {
-                tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality);
+                tilesInfo.Height = height;
             }
 
+            // Update bitrate
             var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000));
             tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate);
-
-            imgNo++;
         }
 
         /*

+ 79 - 0
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -2,14 +2,18 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
+using System.Linq;
+using System.Security.Cryptography.Xml;
 using BlurHashSharp.SkiaSharp;
 using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Drawing;
 using Microsoft.Extensions.Logging;
 using SkiaSharp;
+using static System.Net.Mime.MediaTypeNames;
 using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
 
 namespace Jellyfin.Drawing.Skia;
@@ -526,6 +530,81 @@ public class SkiaEncoder : IImageEncoder
         splashBuilder.GenerateSplash(posters, backdrops, outputPath);
     }
 
+    /// <inheritdoc />
+    public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+    {
+        var paths = options.InputPaths;
+        var tileWidth = options.Width;
+        var tileHeight = options.Height;
+
+        if (paths.Count < 1)
+        {
+            throw new ArgumentException("InputPaths cannot be empty.");
+        }
+        else if (paths.Count > tileWidth * tileHeight)
+        {
+            throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} grid.");
+        }
+
+        // If no height provided, use height of first image.
+        if (!imgHeight.HasValue)
+        {
+            using var firstImg = Decode(paths[0], false, null, out _);
+
+            if (firstImg is null)
+            {
+                throw new InvalidDataException("Could not decode image data.");
+            }
+
+            if (firstImg.Width != imgWidth)
+            {
+                throw new InvalidOperationException("Image width does not match provided width.");
+            }
+
+            imgHeight = firstImg.Height;
+        }
+
+        // Make horizontal strips using every provided image.
+        using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
+        using var canvas = new SKCanvas(tileGrid);
+
+        var imgIndex = 0;
+        for (var y = 0; y < tileHeight; y++)
+        {
+            for (var x = 0; x < tileWidth; x++)
+            {
+                if (imgIndex >= paths.Count)
+                {
+                    break;
+                }
+
+                using var img = Decode(paths[imgIndex++], false, null, out _);
+
+                if (img is null)
+                {
+                    throw new InvalidDataException("Could not decode image data.");
+                }
+
+                if (img.Width != imgWidth)
+                {
+                    throw new InvalidOperationException("Image width does not match provided width.");
+                }
+
+                if (img.Height != imgHeight)
+                {
+                    throw new InvalidOperationException("Image height does not match first image height.");
+                }
+
+                canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
+            }
+        }
+
+        using var outputStream = new SKFileWStream(options.OutputPath);
+        tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
+
+        return imgHeight.Value;
+    }
+
     private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
     {
         try

+ 6 - 0
src/Jellyfin.Drawing/NullImageEncoder.cs

@@ -49,6 +49,12 @@ public class NullImageEncoder : IImageEncoder
         throw new NotImplementedException();
     }
 
+    /// <inheritdoc />
+    public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+    {
+        throw new NotImplementedException();
+    }
+
     /// <inheritdoc />
     public string GetImageBlurHash(int xComp, int yComp, string path)
     {