diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs b/Emby.Drawing/Common/ImageHeader.cs similarity index 99% rename from MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs rename to Emby.Drawing/Common/ImageHeader.cs index 7117482c8a..b66bd71ea5 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs +++ b/Emby.Drawing/Common/ImageHeader.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -namespace MediaBrowser.Server.Implementations.Drawing +namespace Emby.Drawing.Common { /// /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349 diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj new file mode 100644 index 0000000000..dbb976f036 --- /dev/null +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -0,0 +1,98 @@ + + + + + Debug + AnyCPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716} + Library + Properties + Emby.Drawing + Emby.Drawing + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\ImageMagickSharp.1.0.0.14\lib\net45\ImageMagickSharp.dll + + + + + + + + + + + + + Properties\SharedVersion.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + \ No newline at end of file diff --git a/Emby.Drawing/GDI/DynamicImageHelpers.cs b/Emby.Drawing/GDI/DynamicImageHelpers.cs new file mode 100644 index 0000000000..c49007c5fd --- /dev/null +++ b/Emby.Drawing/GDI/DynamicImageHelpers.cs @@ -0,0 +1,138 @@ +using Emby.Drawing.ImageMagick; +using MediaBrowser.Common.IO; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; + +namespace Emby.Drawing.GDI +{ + public static class DynamicImageHelpers + { + public static void CreateThumbCollage(List files, + IFileSystem fileSystem, + string file, + int width, + int height) + { + const int numStrips = 4; + files = StripCollageBuilder.ProjectPaths(files, numStrips).ToList(); + + const int rows = 1; + int cols = numStrips; + + int cellWidth = 2 * (width / 3); + int cellHeight = height; + var index = 0; + + using (var img = new Bitmap(width, height, PixelFormat.Format32bppPArgb)) + { + using (var graphics = Graphics.FromImage(img)) + { + graphics.CompositingQuality = CompositingQuality.HighQuality; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingMode = CompositingMode.SourceCopy; + + for (var row = 0; row < rows; row++) + { + for (var col = 0; col < cols; col++) + { + var x = col * (cellWidth / 2); + var y = row * cellHeight; + + if (files.Count > index) + { + using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true)) + { + using (var memoryStream = new MemoryStream()) + { + fileStream.CopyTo(memoryStream); + + memoryStream.Position = 0; + + using (var imgtemp = Image.FromStream(memoryStream, true, false)) + { + graphics.DrawImage(imgtemp, x, y, cellWidth, cellHeight); + } + } + } + } + + index++; + } + } + img.Save(file); + } + } + } + + public static void CreateSquareCollage(List files, + IFileSystem fileSystem, + string file, + int width, + int height) + { + files = StripCollageBuilder.ProjectPaths(files, 4).ToList(); + + const int rows = 2; + const int cols = 2; + + int singleSize = width / 2; + var index = 0; + + using (var img = new Bitmap(width, height, PixelFormat.Format32bppPArgb)) + { + using (var graphics = Graphics.FromImage(img)) + { + graphics.CompositingQuality = CompositingQuality.HighQuality; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingMode = CompositingMode.SourceCopy; + + for (var row = 0; row < rows; row++) + { + for (var col = 0; col < cols; col++) + { + var x = col * singleSize; + var y = row * singleSize; + + using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true)) + { + using (var memoryStream = new MemoryStream()) + { + fileStream.CopyTo(memoryStream); + + memoryStream.Position = 0; + + using (var imgtemp = Image.FromStream(memoryStream, true, false)) + { + graphics.DrawImage(imgtemp, x, y, singleSize, singleSize); + } + } + } + + index++; + } + } + img.Save(file); + } + } + } + + private static Stream GetStream(Image image) + { + var ms = new MemoryStream(); + + image.Save(ms, ImageFormat.Png); + + ms.Position = 0; + + return ms; + } + } +} diff --git a/Emby.Drawing/GDI/GDIImageEncoder.cs b/Emby.Drawing/GDI/GDIImageEncoder.cs new file mode 100644 index 0000000000..d968b8b5fe --- /dev/null +++ b/Emby.Drawing/GDI/GDIImageEncoder.cs @@ -0,0 +1,254 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Logging; +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using ImageFormat = MediaBrowser.Model.Drawing.ImageFormat; + +namespace Emby.Drawing.GDI +{ + public class GDIImageEncoder : IImageEncoder + { + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + + public GDIImageEncoder(IFileSystem fileSystem, ILogger logger) + { + _fileSystem = fileSystem; + _logger = logger; + } + + public string[] SupportedInputFormats + { + get + { + return new[] + { + "png", + "jpeg", + "jpg", + "gif", + "bmp" + }; + } + } + + public ImageFormat[] SupportedOutputFormats + { + get + { + return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png }; + } + } + + public ImageSize GetImageSize(string path) + { + using (var image = Image.FromFile(path)) + { + return new ImageSize + { + Width = image.Width, + Height = image.Height + }; + } + } + + public void CropWhiteSpace(string inputPath, string outputPath) + { + using (var image = (Bitmap)Image.FromFile(inputPath)) + { + using (var croppedImage = image.CropWhitespace()) + { + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + + using (var outputStream = _fileSystem.GetFileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read, false)) + { + croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100); + } + } + } + } + + public void EncodeImage(string inputPath, string cacheFilePath, int width, int height, int quality, ImageProcessingOptions options) + { + var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0; + + using (var originalImage = Image.FromFile(inputPath)) + { + var newWidth = Convert.ToInt32(width); + var newHeight = Convert.ToInt32(height); + + var selectedOutputFormat = options.OutputFormat; + + // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here + // Also, Webp only supports Format32bppArgb and Format32bppRgb + var pixelFormat = selectedOutputFormat == ImageFormat.Webp + ? PixelFormat.Format32bppArgb + : PixelFormat.Format32bppPArgb; + + using (var thumbnail = new Bitmap(newWidth, newHeight, pixelFormat)) + { + // Mono throw an exeception if assign 0 to SetResolution + if (originalImage.HorizontalResolution > 0 && originalImage.VerticalResolution > 0) + { + // Preserve the original resolution + thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution); + } + + using (var thumbnailGraph = Graphics.FromImage(thumbnail)) + { + thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality; + thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality; + thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic; + thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality; + thumbnailGraph.CompositingMode = !hasPostProcessing ? + CompositingMode.SourceCopy : + CompositingMode.SourceOver; + + SetBackgroundColor(thumbnailGraph, options); + + thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight); + + DrawIndicator(thumbnailGraph, newWidth, newHeight, options); + + var outputFormat = GetOutputFormat(originalImage, selectedOutputFormat); + + Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); + + // Save to the cache location + using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, false)) + { + // Save to the memory stream + thumbnail.Save(outputFormat, cacheFileStream, quality); + } + } + } + + } + } + + /// + /// Sets the color of the background. + /// + /// The graphics. + /// The options. + private void SetBackgroundColor(Graphics graphics, ImageProcessingOptions options) + { + var color = options.BackgroundColor; + + if (!string.IsNullOrEmpty(color)) + { + Color drawingColor; + + try + { + drawingColor = ColorTranslator.FromHtml(color); + } + catch + { + drawingColor = ColorTranslator.FromHtml("#" + color); + } + + graphics.Clear(drawingColor); + } + } + + /// + /// Draws the indicator. + /// + /// The graphics. + /// Width of the image. + /// Height of the image. + /// The options. + private void DrawIndicator(Graphics graphics, int imageWidth, int imageHeight, ImageProcessingOptions options) + { + if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0)) + { + return; + } + + try + { + if (options.AddPlayedIndicator) + { + var currentImageSize = new Size(imageWidth, imageHeight); + + new PlayedIndicatorDrawer().DrawPlayedIndicator(graphics, currentImageSize); + } + else if (options.UnplayedCount.HasValue) + { + var currentImageSize = new Size(imageWidth, imageHeight); + + new UnplayedCountIndicator().DrawUnplayedCountIndicator(graphics, currentImageSize, options.UnplayedCount.Value); + } + + if (options.PercentPlayed > 0) + { + var currentImageSize = new Size(imageWidth, imageHeight); + + new PercentPlayedDrawer().Process(graphics, currentImageSize, options.PercentPlayed); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error drawing indicator overlay", ex); + } + } + + /// + /// Gets the output format. + /// + /// The image. + /// The output format. + /// ImageFormat. + private System.Drawing.Imaging.ImageFormat GetOutputFormat(Image image, ImageFormat outputFormat) + { + switch (outputFormat) + { + case ImageFormat.Bmp: + return System.Drawing.Imaging.ImageFormat.Bmp; + case ImageFormat.Gif: + return System.Drawing.Imaging.ImageFormat.Gif; + case ImageFormat.Jpg: + return System.Drawing.Imaging.ImageFormat.Jpeg; + case ImageFormat.Png: + return System.Drawing.Imaging.ImageFormat.Png; + default: + return image.RawFormat; + } + } + + public void CreateImageCollage(ImageCollageOptions options) + { + double ratio = options.Width; + ratio /= options.Height; + + if (ratio >= 1.4) + { + DynamicImageHelpers.CreateThumbCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Height); + } + else if (ratio >= .9) + { + DynamicImageHelpers.CreateSquareCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Height); + } + else + { + DynamicImageHelpers.CreateSquareCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Width); + } + } + + public void Dispose() + { + } + + public string Name + { + get { return "GDI"; } + } + } +} diff --git a/Emby.Drawing/GDI/ImageExtensions.cs b/Emby.Drawing/GDI/ImageExtensions.cs new file mode 100644 index 0000000000..6af5a8688f --- /dev/null +++ b/Emby.Drawing/GDI/ImageExtensions.cs @@ -0,0 +1,217 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; + +namespace Emby.Drawing.GDI +{ + public static class ImageExtensions + { + /// + /// Saves the image. + /// + /// The output format. + /// The image. + /// To stream. + /// The quality. + public static void Save(this Image image, ImageFormat outputFormat, Stream toStream, int quality) + { + // Use special save methods for jpeg and png that will result in a much higher quality image + // All other formats use the generic Image.Save + if (ImageFormat.Jpeg.Equals(outputFormat)) + { + SaveAsJpeg(image, toStream, quality); + } + else if (ImageFormat.Png.Equals(outputFormat)) + { + image.Save(toStream, ImageFormat.Png); + } + else + { + image.Save(toStream, outputFormat); + } + } + + /// + /// Saves the JPEG. + /// + /// The image. + /// The target. + /// The quality. + public static void SaveAsJpeg(this Image image, Stream target, int quality) + { + using (var encoderParameters = new EncoderParameters(1)) + { + encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality); + image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters); + } + } + + private static readonly ImageCodecInfo[] Encoders = ImageCodecInfo.GetImageEncoders(); + + /// + /// Gets the image codec info. + /// + /// Type of the MIME. + /// ImageCodecInfo. + private static ImageCodecInfo GetImageCodecInfo(string mimeType) + { + foreach (var encoder in Encoders) + { + if (string.Equals(encoder.MimeType, mimeType, StringComparison.OrdinalIgnoreCase)) + { + return encoder; + } + } + + return Encoders.Length == 0 ? null : Encoders[0]; + } + + /// + /// Crops an image by removing whitespace and transparency from the edges + /// + /// The BMP. + /// Bitmap. + /// + public static Bitmap CropWhitespace(this Bitmap bmp) + { + var width = bmp.Width; + var height = bmp.Height; + + var topmost = 0; + for (int row = 0; row < height; ++row) + { + if (IsAllWhiteRow(bmp, row, width)) + topmost = row; + else break; + } + + int bottommost = 0; + for (int row = height - 1; row >= 0; --row) + { + if (IsAllWhiteRow(bmp, row, width)) + bottommost = row; + else break; + } + + int leftmost = 0, rightmost = 0; + for (int col = 0; col < width; ++col) + { + if (IsAllWhiteColumn(bmp, col, height)) + leftmost = col; + else + break; + } + + for (int col = width - 1; col >= 0; --col) + { + if (IsAllWhiteColumn(bmp, col, height)) + rightmost = col; + else + break; + } + + if (rightmost == 0) rightmost = width; // As reached left + if (bottommost == 0) bottommost = height; // As reached top. + + var croppedWidth = rightmost - leftmost; + var croppedHeight = bottommost - topmost; + + if (croppedWidth == 0) // No border on left or right + { + leftmost = 0; + croppedWidth = width; + } + + if (croppedHeight == 0) // No border on top or bottom + { + topmost = 0; + croppedHeight = height; + } + + // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here + var thumbnail = new Bitmap(croppedWidth, croppedHeight, PixelFormat.Format32bppPArgb); + + // Preserve the original resolution + TrySetResolution(thumbnail, bmp.HorizontalResolution, bmp.VerticalResolution); + + using (var thumbnailGraph = Graphics.FromImage(thumbnail)) + { + thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality; + thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality; + thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic; + thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality; + thumbnailGraph.CompositingMode = CompositingMode.SourceCopy; + + thumbnailGraph.DrawImage(bmp, + new RectangleF(0, 0, croppedWidth, croppedHeight), + new RectangleF(leftmost, topmost, croppedWidth, croppedHeight), + GraphicsUnit.Pixel); + } + return thumbnail; + } + + /// + /// Tries the set resolution. + /// + /// The BMP. + /// The x. + /// The y. + private static void TrySetResolution(Bitmap bmp, float x, float y) + { + if (x > 0 && y > 0) + { + bmp.SetResolution(x, y); + } + } + + /// + /// Determines whether or not a row of pixels is all whitespace + /// + /// The BMP. + /// The row. + /// The width. + /// true if [is all white row] [the specified BMP]; otherwise, false. + private static bool IsAllWhiteRow(Bitmap bmp, int row, int width) + { + for (var i = 0; i < width; ++i) + { + if (!IsWhiteSpace(bmp.GetPixel(i, row))) + { + return false; + } + } + return true; + } + + /// + /// Determines whether or not a column of pixels is all whitespace + /// + /// The BMP. + /// The col. + /// The height. + /// true if [is all white column] [the specified BMP]; otherwise, false. + private static bool IsAllWhiteColumn(Bitmap bmp, int col, int height) + { + for (var i = 0; i < height; ++i) + { + if (!IsWhiteSpace(bmp.GetPixel(col, i))) + { + return false; + } + } + return true; + } + + /// + /// Determines if a color is whitespace + /// + /// The color. + /// true if [is white space] [the specified color]; otherwise, false. + private static bool IsWhiteSpace(Color color) + { + return (color.R == 255 && color.G == 255 && color.B == 255) || color.A == 0; + } + } +} diff --git a/Emby.Drawing/GDI/PercentPlayedDrawer.cs b/Emby.Drawing/GDI/PercentPlayedDrawer.cs new file mode 100644 index 0000000000..c7afda60e4 --- /dev/null +++ b/Emby.Drawing/GDI/PercentPlayedDrawer.cs @@ -0,0 +1,34 @@ +using System; +using System.Drawing; + +namespace Emby.Drawing.GDI +{ + public class PercentPlayedDrawer + { + private const int IndicatorHeight = 8; + + public void Process(Graphics graphics, Size imageSize, double percent) + { + var y = imageSize.Height - IndicatorHeight; + + using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 0, 0, 0))) + { + const int innerX = 0; + var innerY = y; + var innerWidth = imageSize.Width; + var innerHeight = imageSize.Height; + + graphics.FillRectangle(backdroundBrush, innerX, innerY, innerWidth, innerHeight); + + using (var foregroundBrush = new SolidBrush(Color.FromArgb(82, 181, 75))) + { + double foregroundWidth = innerWidth; + foregroundWidth *= percent; + foregroundWidth /= 100; + + graphics.FillRectangle(foregroundBrush, innerX, innerY, Convert.ToInt32(Math.Round(foregroundWidth)), innerHeight); + } + } + } + } +} diff --git a/Emby.Drawing/GDI/PlayedIndicatorDrawer.cs b/Emby.Drawing/GDI/PlayedIndicatorDrawer.cs new file mode 100644 index 0000000000..4428e4cbac --- /dev/null +++ b/Emby.Drawing/GDI/PlayedIndicatorDrawer.cs @@ -0,0 +1,32 @@ +using System.Drawing; + +namespace Emby.Drawing.GDI +{ + public class PlayedIndicatorDrawer + { + private const int IndicatorHeight = 40; + public const int IndicatorWidth = 40; + private const int FontSize = 40; + private const int OffsetFromTopRightCorner = 10; + + public void DrawPlayedIndicator(Graphics graphics, Size imageSize) + { + var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner; + + using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75))) + { + graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight); + + x = imageSize.Width - 45 - OffsetFromTopRightCorner; + + using (var font = new Font("Webdings", FontSize, FontStyle.Regular, GraphicsUnit.Pixel)) + { + using (var fontBrush = new SolidBrush(Color.White)) + { + graphics.DrawString("a", font, fontBrush, x, OffsetFromTopRightCorner - 2); + } + } + } + } + } +} diff --git a/Emby.Drawing/GDI/UnplayedCountIndicator.cs b/Emby.Drawing/GDI/UnplayedCountIndicator.cs new file mode 100644 index 0000000000..6420abb27f --- /dev/null +++ b/Emby.Drawing/GDI/UnplayedCountIndicator.cs @@ -0,0 +1,50 @@ +using System.Drawing; + +namespace Emby.Drawing.GDI +{ + public class UnplayedCountIndicator + { + private const int IndicatorHeight = 41; + public const int IndicatorWidth = 41; + private const int OffsetFromTopRightCorner = 10; + + public void DrawUnplayedCountIndicator(Graphics graphics, Size imageSize, int count) + { + var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner; + + using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75))) + { + graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight); + + var text = count.ToString(); + + x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner; + var y = OffsetFromTopRightCorner + 6; + var fontSize = 24; + + if (text.Length == 1) + { + x += 10; + } + else if (text.Length == 2) + { + x += 3; + } + else if (text.Length == 3) + { + x += 1; + y += 1; + fontSize = 20; + } + + using (var font = new Font("Sans-Serif", fontSize, FontStyle.Regular, GraphicsUnit.Pixel)) + { + using (var fontBrush = new SolidBrush(Color.White)) + { + graphics.DrawString(text, font, fontBrush, x, y); + } + } + } + } + } +} diff --git a/Emby.Drawing/IImageEncoder.cs b/Emby.Drawing/IImageEncoder.cs new file mode 100644 index 0000000000..29261dbdb3 --- /dev/null +++ b/Emby.Drawing/IImageEncoder.cs @@ -0,0 +1,53 @@ +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Drawing; +using System; + +namespace Emby.Drawing +{ + public interface IImageEncoder : IDisposable + { + /// + /// Gets the supported input formats. + /// + /// The supported input formats. + string[] SupportedInputFormats { get; } + /// + /// Gets the supported output formats. + /// + /// The supported output formats. + ImageFormat[] SupportedOutputFormats { get; } + /// + /// Gets the size of the image. + /// + /// The path. + /// ImageSize. + ImageSize GetImageSize(string path); + /// + /// Crops the white space. + /// + /// The input path. + /// The output path. + void CropWhiteSpace(string inputPath, string outputPath); + /// + /// Encodes the image. + /// + /// The input path. + /// The output path. + /// The width. + /// The height. + /// The quality. + /// The options. + void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options); + + /// + /// Creates the image collage. + /// + /// The options. + void CreateImageCollage(ImageCollageOptions options); + /// + /// Gets the name. + /// + /// The name. + string Name { get; } + } +} diff --git a/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs b/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs new file mode 100644 index 0000000000..3d6cdd03dd --- /dev/null +++ b/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs @@ -0,0 +1,229 @@ +using ImageMagickSharp; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Logging; +using System; +using System.IO; + +namespace Emby.Drawing.ImageMagick +{ + public class ImageMagickEncoder : IImageEncoder + { + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + + public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths) + { + _logger = logger; + _appPaths = appPaths; + + LogImageMagickVersion(); + } + + public string[] SupportedInputFormats + { + get + { + // Some common file name extensions for RAW picture files include: .cr2, .crw, .dng, .nef, .orf, .rw2, .pef, .arw, .sr2, .srf, and .tif. + return new[] + { + "tiff", + "jpeg", + "jpg", + "png", + "aiff", + "cr2", + "crw", + "dng", + "nef", + "orf", + "pef", + "arw", + "webp", + "gif", + "bmp" + }; + } + } + + public ImageFormat[] SupportedOutputFormats + { + get + { + if (_webpAvailable) + { + return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png }; + } + return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png }; + } + } + + private void LogImageMagickVersion() + { + _logger.Info("ImageMagick version: " + Wand.VersionString); + TestWebp(); + } + + private bool _webpAvailable = true; + private void TestWebp() + { + try + { + var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp"); + Directory.CreateDirectory(Path.GetDirectoryName(tmpPath)); + + using (var wand = new MagickWand(1, 1, new PixelWand("none", 1))) + { + wand.SaveImage(tmpPath); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error loading webp: ", ex); + _webpAvailable = false; + } + } + + public void CropWhiteSpace(string inputPath, string outputPath) + { + CheckDisposed(); + + using (var wand = new MagickWand(inputPath)) + { + wand.CurrentImage.TrimImage(10); + wand.SaveImage(outputPath); + } + } + + public ImageSize GetImageSize(string path) + { + CheckDisposed(); + + using (var wand = new MagickWand()) + { + wand.PingImage(path); + var img = wand.CurrentImage; + + return new ImageSize + { + Width = img.Width, + Height = img.Height + }; + } + } + + public void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options) + { + if (string.IsNullOrWhiteSpace(options.BackgroundColor)) + { + using (var originalImage = new MagickWand(inputPath)) + { + originalImage.CurrentImage.ResizeImage(width, height); + + DrawIndicator(originalImage, width, height, options); + + originalImage.CurrentImage.CompressionQuality = quality; + + originalImage.SaveImage(outputPath); + } + } + else + { + using (var wand = new MagickWand(width, height, options.BackgroundColor)) + { + using (var originalImage = new MagickWand(inputPath)) + { + originalImage.CurrentImage.ResizeImage(width, height); + + wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0); + DrawIndicator(wand, width, height, options); + + wand.CurrentImage.CompressionQuality = quality; + + wand.SaveImage(outputPath); + } + } + } + } + + /// + /// Draws the indicator. + /// + /// The wand. + /// Width of the image. + /// Height of the image. + /// The options. + private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options) + { + if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0)) + { + return; + } + + try + { + if (options.AddPlayedIndicator) + { + var currentImageSize = new ImageSize(imageWidth, imageHeight); + + new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize); + } + else if (options.UnplayedCount.HasValue) + { + var currentImageSize = new ImageSize(imageWidth, imageHeight); + + new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value); + } + + if (options.PercentPlayed > 0) + { + new PercentPlayedDrawer().Process(wand, options.PercentPlayed); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error drawing indicator overlay", ex); + } + } + + public void CreateImageCollage(ImageCollageOptions options) + { + double ratio = options.Width; + ratio /= options.Height; + + if (ratio >= 1.4) + { + new StripCollageBuilder(_appPaths).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text); + } + else if (ratio >= .9) + { + new StripCollageBuilder(_appPaths).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text); + } + else + { + new StripCollageBuilder(_appPaths).BuildPosterCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text); + } + } + + public string Name + { + get { return "ImageMagick"; } + } + + private bool _disposed; + public void Dispose() + { + _disposed = true; + Wand.CloseEnvironment(); + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Drawing/PercentPlayedDrawer.cs b/Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs similarity index 95% rename from MediaBrowser.Server.Implementations/Drawing/PercentPlayedDrawer.cs rename to Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs index 20c2ab93be..90f9d56095 100644 --- a/MediaBrowser.Server.Implementations/Drawing/PercentPlayedDrawer.cs +++ b/Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs @@ -1,7 +1,7 @@ using ImageMagickSharp; using System; -namespace MediaBrowser.Server.Implementations.Drawing +namespace Emby.Drawing.ImageMagick { public class PercentPlayedDrawer { diff --git a/MediaBrowser.Server.Implementations/Drawing/PlayedIndicatorDrawer.cs b/Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs similarity index 98% rename from MediaBrowser.Server.Implementations/Drawing/PlayedIndicatorDrawer.cs rename to Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs index 359065cc2e..5eeb157715 100644 --- a/MediaBrowser.Server.Implementations/Drawing/PlayedIndicatorDrawer.cs +++ b/Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Drawing; using System; using System.IO; -namespace MediaBrowser.Server.Implementations.Drawing +namespace Emby.Drawing.ImageMagick { public class PlayedIndicatorDrawer { diff --git a/Emby.Drawing/ImageMagick/StripCollageBuilder.cs b/Emby.Drawing/ImageMagick/StripCollageBuilder.cs new file mode 100644 index 0000000000..7cdd0077de --- /dev/null +++ b/Emby.Drawing/ImageMagick/StripCollageBuilder.cs @@ -0,0 +1,518 @@ +using ImageMagickSharp; +using MediaBrowser.Common.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Drawing.ImageMagick +{ + public class StripCollageBuilder + { + private readonly IApplicationPaths _appPaths; + + public StripCollageBuilder(IApplicationPaths appPaths) + { + _appPaths = appPaths; + } + + public void BuildPosterCollage(IEnumerable paths, string outputPath, int width, int height, string text) + { + if (!string.IsNullOrWhiteSpace(text)) + { + using (var wand = BuildPosterCollageWandWithText(paths, text, width, height)) + { + wand.SaveImage(outputPath); + } + } + else + { + using (var wand = BuildPosterCollageWand(paths, width, height)) + { + wand.SaveImage(outputPath); + } + } + } + + public void BuildSquareCollage(IEnumerable paths, string outputPath, int width, int height, string text) + { + if (!string.IsNullOrWhiteSpace(text)) + { + using (var wand = BuildSquareCollageWandWithText(paths, text, width, height)) + { + wand.SaveImage(outputPath); + } + } + else + { + using (var wand = BuildSquareCollageWand(paths, width, height)) + { + wand.SaveImage(outputPath); + } + } + } + + public void BuildThumbCollage(IEnumerable paths, string outputPath, int width, int height, string text) + { + if (!string.IsNullOrWhiteSpace(text)) + { + using (var wand = BuildThumbCollageWandWithText(paths, text, width, height)) + { + wand.SaveImage(outputPath); + } + } + else + { + using (var wand = BuildThumbCollageWand(paths, width, height)) + { + wand.SaveImage(outputPath); + } + } + } + + internal static string[] ProjectPaths(IEnumerable paths, int count) + { + var clone = paths.ToList(); + var list = new List(); + + while (list.Count < count) + { + foreach (var path in clone) + { + list.Add(path); + + if (list.Count >= count) + { + break; + } + } + } + + return list.Take(count).ToArray(); + } + + private MagickWand BuildThumbCollageWandWithText(IEnumerable paths, string text, int width, int height) + { + var inputPaths = ProjectPaths(paths, 8); + using (var wandImages = new MagickWand(inputPaths)) + { + var wand = new MagickWand(width, height); + wand.OpenImage("gradient:#111111-#111111"); + using (var draw = new DrawingWand()) + { + using (var fcolor = new PixelWand(ColorName.White)) + { + draw.FillColor = fcolor; + draw.Font = MontserratLightFont; + draw.FontSize = 60; + draw.FontWeight = FontWeightType.LightStyle; + draw.TextAntialias = true; + } + + var fontMetrics = wand.QueryFontMetrics(draw, text); + var textContainerY = Convert.ToInt32(height * .165); + wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, text); + + var iSlice = Convert.ToInt32(width * .1166666667); + int iTrans = Convert.ToInt32(height * 0.2); + int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296); + var horizontalImagePadding = Convert.ToInt32(width * 0.0125); + + foreach (var element in wandImages.ImageList) + { + int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); + element.Gravity = GravityType.CenterGravity; + element.BackgroundColor = new PixelWand("none", 1); + element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + int ix = (int)Math.Abs((iWidth - iSlice) / 2); + element.CropImage(iSlice, iHeight, ix, 0); + + element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); + } + + wandImages.SetFirstIterator(); + using (var wandList = wandImages.AppendImages()) + { + wandList.CurrentImage.TrimImage(1); + using (var mwr = wandList.CloneMagickWand()) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + { + mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); + mwr.CurrentImage.FlipImage(); + + mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; + mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); + + using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + { + mwg.OpenImage("gradient:black-none"); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing); + + wandList.AddImage(mwr); + int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; + wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852)); + } + } + } + } + } + } + + return wand; + } + } + + private MagickWand BuildPosterCollageWand(IEnumerable paths, int width, int height) + { + var inputPaths = ProjectPaths(paths, 4); + using (var wandImages = new MagickWand(inputPaths)) + { + var wand = new MagickWand(width, height); + wand.OpenImage("gradient:#111111-#111111"); + using (var draw = new DrawingWand()) + { + var iSlice = Convert.ToInt32(width * 0.225); + int iTrans = Convert.ToInt32(height * .25); + int iHeight = Convert.ToInt32(height * .65); + var horizontalImagePadding = Convert.ToInt32(width * 0.0275); + + foreach (var element in wandImages.ImageList) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); + element.Gravity = GravityType.CenterGravity; + element.BackgroundColor = blackPixelWand; + element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + int ix = (int)Math.Abs((iWidth - iSlice) / 2); + element.CropImage(iSlice, iHeight, ix, 0); + + element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); + } + } + + wandImages.SetFirstIterator(); + using (var wandList = wandImages.AppendImages()) + { + wandList.CurrentImage.TrimImage(1); + using (var mwr = wandList.CloneMagickWand()) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + { + mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); + mwr.CurrentImage.FlipImage(); + + mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; + mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); + + using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + { + mwg.OpenImage("gradient:black-none"); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing); + + wandList.AddImage(mwr); + int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; + wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .05)); + } + } + } + } + } + } + + return wand; + } + } + + private MagickWand BuildPosterCollageWandWithText(IEnumerable paths, string label, int width, int height) + { + var inputPaths = ProjectPaths(paths, 4); + using (var wandImages = new MagickWand(inputPaths)) + { + var wand = new MagickWand(width, height); + wand.OpenImage("gradient:#111111-#111111"); + using (var draw = new DrawingWand()) + { + using (var fcolor = new PixelWand(ColorName.White)) + { + draw.FillColor = fcolor; + draw.Font = MontserratLightFont; + draw.FontSize = 60; + draw.FontWeight = FontWeightType.LightStyle; + draw.TextAntialias = true; + } + + var fontMetrics = wand.QueryFontMetrics(draw, label); + var textContainerY = Convert.ToInt32(height * .165); + wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, label); + + var iSlice = Convert.ToInt32(width * 0.225); + int iTrans = Convert.ToInt32(height * 0.2); + int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296); + var horizontalImagePadding = Convert.ToInt32(width * 0.0275); + + foreach (var element in wandImages.ImageList) + { + int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); + element.Gravity = GravityType.CenterGravity; + element.BackgroundColor = new PixelWand("none", 1); + element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + int ix = (int)Math.Abs((iWidth - iSlice) / 2); + element.CropImage(iSlice, iHeight, ix, 0); + + element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); + } + + wandImages.SetFirstIterator(); + using (var wandList = wandImages.AppendImages()) + { + wandList.CurrentImage.TrimImage(1); + using (var mwr = wandList.CloneMagickWand()) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + { + mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); + mwr.CurrentImage.FlipImage(); + + mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; + mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); + + using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + { + mwg.OpenImage("gradient:black-none"); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing); + + wandList.AddImage(mwr); + int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; + wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852)); + } + } + } + } + } + } + + return wand; + } + } + + private MagickWand BuildThumbCollageWand(IEnumerable paths, int width, int height) + { + var inputPaths = ProjectPaths(paths, 8); + using (var wandImages = new MagickWand(inputPaths)) + { + var wand = new MagickWand(width, height); + wand.OpenImage("gradient:#111111-#111111"); + using (var draw = new DrawingWand()) + { + var iSlice = Convert.ToInt32(width * .1166666667); + int iTrans = Convert.ToInt32(height * .25); + int iHeight = Convert.ToInt32(height * .62); + var horizontalImagePadding = Convert.ToInt32(width * 0.0125); + + foreach (var element in wandImages.ImageList) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); + element.Gravity = GravityType.CenterGravity; + element.BackgroundColor = blackPixelWand; + element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + int ix = (int)Math.Abs((iWidth - iSlice) / 2); + element.CropImage(iSlice, iHeight, ix, 0); + + element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); + } + } + + wandImages.SetFirstIterator(); + using (var wandList = wandImages.AppendImages()) + { + wandList.CurrentImage.TrimImage(1); + using (var mwr = wandList.CloneMagickWand()) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + { + mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); + mwr.CurrentImage.FlipImage(); + + mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; + mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); + + using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + { + mwg.OpenImage("gradient:black-none"); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing); + + wandList.AddImage(mwr); + int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; + wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .085)); + } + } + } + } + } + } + + return wand; + } + } + + private MagickWand BuildSquareCollageWand(IEnumerable paths, int width, int height) + { + var inputPaths = ProjectPaths(paths, 4); + using (var wandImages = new MagickWand(inputPaths)) + { + var wand = new MagickWand(width, height); + wand.OpenImage("gradient:#111111-#111111"); + using (var draw = new DrawingWand()) + { + var iSlice = Convert.ToInt32(width * .225); + int iTrans = Convert.ToInt32(height * .25); + int iHeight = Convert.ToInt32(height * .63); + var horizontalImagePadding = Convert.ToInt32(width * 0.02); + + foreach (var element in wandImages.ImageList) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); + element.Gravity = GravityType.CenterGravity; + element.BackgroundColor = blackPixelWand; + element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + int ix = (int)Math.Abs((iWidth - iSlice) / 2); + element.CropImage(iSlice, iHeight, ix, 0); + + element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); + } + } + + wandImages.SetFirstIterator(); + using (var wandList = wandImages.AppendImages()) + { + wandList.CurrentImage.TrimImage(1); + using (var mwr = wandList.CloneMagickWand()) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + { + mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); + mwr.CurrentImage.FlipImage(); + + mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; + mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); + + using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + { + mwg.OpenImage("gradient:black-none"); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing); + + wandList.AddImage(mwr); + int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; + wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .07)); + } + } + } + } + } + } + + return wand; + } + } + + private MagickWand BuildSquareCollageWandWithText(IEnumerable paths, string label, int width, int height) + { + var inputPaths = ProjectPaths(paths, 4); + using (var wandImages = new MagickWand(inputPaths)) + { + var wand = new MagickWand(width, height); + wand.OpenImage("gradient:#111111-#111111"); + using (var draw = new DrawingWand()) + { + using (var fcolor = new PixelWand(ColorName.White)) + { + draw.FillColor = fcolor; + draw.Font = MontserratLightFont; + draw.FontSize = 60; + draw.FontWeight = FontWeightType.LightStyle; + draw.TextAntialias = true; + } + + var fontMetrics = wand.QueryFontMetrics(draw, label); + var textContainerY = Convert.ToInt32(height * .165); + wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, label); + + var iSlice = Convert.ToInt32(width * .225); + int iTrans = Convert.ToInt32(height * 0.2); + int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296); + var horizontalImagePadding = Convert.ToInt32(width * 0.02); + + foreach (var element in wandImages.ImageList) + { + int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); + element.Gravity = GravityType.CenterGravity; + element.BackgroundColor = new PixelWand("none", 1); + element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + int ix = (int)Math.Abs((iWidth - iSlice) / 2); + element.CropImage(iSlice, iHeight, ix, 0); + + element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); + } + + wandImages.SetFirstIterator(); + using (var wandList = wandImages.AppendImages()) + { + wandList.CurrentImage.TrimImage(1); + using (var mwr = wandList.CloneMagickWand()) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + { + mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); + mwr.CurrentImage.FlipImage(); + + mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; + mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); + + using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + { + mwg.OpenImage("gradient:black-none"); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing); + + wandList.AddImage(mwr); + int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; + wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852)); + } + } + } + } + } + } + + return wand; + } + } + + private string MontserratLightFont + { + get { return PlayedIndicatorDrawer.ExtractFont("MontserratLight.otf", _appPaths); } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Drawing/UnplayedCountIndicator.cs b/Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs similarity index 97% rename from MediaBrowser.Server.Implementations/Drawing/UnplayedCountIndicator.cs rename to Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs index 71cced0416..dd25004d66 100644 --- a/MediaBrowser.Server.Implementations/Drawing/UnplayedCountIndicator.cs +++ b/Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs @@ -3,7 +3,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Drawing; using System.Globalization; -namespace MediaBrowser.Server.Implementations.Drawing +namespace Emby.Drawing.ImageMagick { public class UnplayedCountIndicator { diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs similarity index 83% rename from MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs rename to Emby.Drawing/ImageProcessor.cs index d78d5e8ea1..c7d06559ac 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -1,4 +1,4 @@ -using ImageMagickSharp; +using Emby.Drawing.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Controller; @@ -18,7 +18,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Server.Implementations.Drawing +namespace Emby.Drawing { /// /// Class ImageProcessor @@ -50,12 +50,14 @@ namespace MediaBrowser.Server.Implementations.Drawing private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationPaths _appPaths; + private readonly IImageEncoder _imageEncoder; - public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer) + public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IImageEncoder imageEncoder) { _logger = logger; _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; + _imageEncoder = imageEncoder; _appPaths = appPaths; _saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite); @@ -85,8 +87,14 @@ namespace MediaBrowser.Server.Implementations.Drawing } _cachedImagedSizes = new ConcurrentDictionary(sizeDictionary); + } - LogImageMagickVersionVersion(); + public string[] SupportedInputFormats + { + get + { + return _imageEncoder.SupportedInputFormats; + } } private string ResizedImageCachePath @@ -130,44 +138,7 @@ namespace MediaBrowser.Server.Implementations.Drawing public ImageFormat[] GetSupportedImageOutputFormats() { - if (_webpAvailable) - { - return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png }; - } - return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png }; - } - - private bool _webpAvailable = true; - private void TestWebp() - { - try - { - var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp"); - Directory.CreateDirectory(Path.GetDirectoryName(tmpPath)); - - using (var wand = new MagickWand(1, 1, new PixelWand("none", 1))) - { - wand.SaveImage(tmpPath); - } - } - catch (Exception ex) - { - _logger.ErrorException("Error loading webp: ", ex); - _webpAvailable = false; - } - } - - private void LogImageMagickVersionVersion() - { - try - { - _logger.Info("ImageMagick version: " + Wand.VersionString); - } - catch (Exception ex) - { - _logger.ErrorException("Error loading ImageMagick: ", ex); - } - TestWebp(); + return _imageEncoder.SupportedOutputFormats; } public async Task ProcessImage(ImageProcessingOptions options) @@ -244,36 +215,7 @@ namespace MediaBrowser.Server.Implementations.Drawing Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - if (string.IsNullOrWhiteSpace(options.BackgroundColor)) - { - using (var originalImage = new MagickWand(originalImagePath)) - { - originalImage.CurrentImage.ResizeImage(newWidth, newHeight); - - DrawIndicator(originalImage, newWidth, newHeight, options); - - originalImage.CurrentImage.CompressionQuality = quality; - - originalImage.SaveImage(cacheFilePath); - } - } - else - { - using (var wand = new MagickWand(newWidth, newHeight, options.BackgroundColor)) - { - using (var originalImage = new MagickWand(originalImagePath)) - { - originalImage.CurrentImage.ResizeImage(newWidth, newHeight); - - wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0); - DrawIndicator(wand, newWidth, newHeight, options); - - wand.CurrentImage.CompressionQuality = quality; - - wand.SaveImage(cacheFilePath); - } - } - } + _imageEncoder.EncodeImage(originalImagePath, cacheFilePath, newWidth, newHeight, quality, options); } } finally @@ -286,7 +228,7 @@ namespace MediaBrowser.Server.Implementations.Drawing private ImageFormat GetOutputFormat(ImageFormat requestedFormat) { - if (requestedFormat == ImageFormat.Webp && !_webpAvailable) + if (requestedFormat == ImageFormat.Webp && !_imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp)) { return ImageFormat.Png; } @@ -294,46 +236,6 @@ namespace MediaBrowser.Server.Implementations.Drawing return requestedFormat; } - /// - /// Draws the indicator. - /// - /// The wand. - /// Width of the image. - /// Height of the image. - /// The options. - private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options) - { - if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0)) - { - return; - } - - try - { - if (options.AddPlayedIndicator) - { - var currentImageSize = new ImageSize(imageWidth, imageHeight); - - new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize); - } - else if (options.UnplayedCount.HasValue) - { - var currentImageSize = new ImageSize(imageWidth, imageHeight); - - new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value); - } - - if (options.PercentPlayed > 0) - { - new PercentPlayedDrawer().Process(wand, options.PercentPlayed); - } - } - catch (Exception ex) - { - _logger.ErrorException("Error drawing indicator overlay", ex); - } - } - /// /// Crops whitespace from an image, caches the result, and returns the cached path /// @@ -360,11 +262,7 @@ namespace MediaBrowser.Server.Implementations.Drawing { Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath)); - using (var wand = new MagickWand(originalImagePath)) - { - wand.CurrentImage.TrimImage(10); - wand.SaveImage(croppedImagePath); - } + _imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath); } catch (Exception ex) { @@ -500,17 +398,7 @@ namespace MediaBrowser.Server.Implementations.Drawing CheckDisposed(); - using (var wand = new MagickWand()) - { - wand.PingImage(path); - var img = wand.CurrentImage; - - size = new ImageSize - { - Width = img.Width, - Height = img.Height - }; - } + size = _imageEncoder.GetImageSize(path); } StartSaveImageSizeTimer(); @@ -838,6 +726,11 @@ namespace MediaBrowser.Server.Implementations.Drawing return Path.Combine(path, filename); } + public void CreateImageCollage(ImageCollageOptions options) + { + _imageEncoder.CreateImageCollage(options); + } + public IEnumerable GetSupportedEnhancers(IHasImages item, ImageType imageType) { return ImageEnhancers.Where(i => @@ -860,7 +753,7 @@ namespace MediaBrowser.Server.Implementations.Drawing public void Dispose() { _disposed = true; - Wand.CloseEnvironment(); + _imageEncoder.Dispose(); _saveImageSizeTimer.Dispose(); } diff --git a/Emby.Drawing/Properties/AssemblyInfo.cs b/Emby.Drawing/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..fba168d034 --- /dev/null +++ b/Emby.Drawing/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Emby.Drawing")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Emby.Drawing")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("87b6f14e-16d8-4a58-a553-fd9945e47458")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// \ No newline at end of file diff --git a/Emby.Drawing/packages.config b/Emby.Drawing/packages.config new file mode 100644 index 0000000000..a331f20b3a --- /dev/null +++ b/Emby.Drawing/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 08ac5671d5..e91dd1a9b0 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -151,7 +151,7 @@ namespace MediaBrowser.Api { lock (_activeTranscodingJobs) { - var job = new TranscodingJob + var job = new TranscodingJob(Logger) { Type = type, Path = path, @@ -284,28 +284,72 @@ namespace MediaBrowser.Api { job.ActiveRequestCount++; - job.DisposeKillTimer(); + if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive) + { + job.StopKillTimer(); + } } - + public void OnTranscodeEndRequest(TranscodingJob job) { job.ActiveRequestCount--; + Logger.Debug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount); + if (job.ActiveRequestCount <= 0) + { + PingTimer(job, false); + } + } + internal void PingTranscodingJob(string playSessionId) + { + if (string.IsNullOrEmpty(playSessionId)) + { + throw new ArgumentNullException("playSessionId"); + } + + Logger.Debug("PingTranscodingJob PlaySessionId={0}", playSessionId); + + var jobs = new List(); - if (job.ActiveRequestCount == 0) + lock (_activeTranscodingJobs) { - // TODO: Lower this hls timeout - var timerDuration = job.Type == TranscodingJobType.Progressive ? - 1000 : - 7200000; + // This is really only needed for HLS. + // Progressive streams can stop on their own reliably + jobs = jobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList(); + } - if (job.KillTimer == null) - { - job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite); - } - else - { - job.KillTimer.Change(timerDuration, Timeout.Infinite); - } + foreach (var job in jobs) + { + PingTimer(job, true); + } + } + + private void PingTimer(TranscodingJob job, bool isProgressCheckIn) + { + if (job.HasExited) + { + job.StopKillTimer(); + return; + } + + // TODO: Lower this hls timeout + var timerDuration = job.Type == TranscodingJobType.Progressive ? + 1000 : + 1800000; + + // We can really reduce the timeout for apps that are using the newer api + if (!string.IsNullOrWhiteSpace(job.PlaySessionId) && job.Type != TranscodingJobType.Progressive) + { + timerDuration = 20000; + } + + // Don't start the timer for playback checkins with progressive streaming + if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn) + { + job.StartKillTimer(timerDuration, OnTranscodeKillTimerStopped); + } + else + { + job.ChangeKillTimerIfStarted(timerDuration); } } @@ -317,6 +361,8 @@ namespace MediaBrowser.Api { var job = (TranscodingJob)state; + Logger.Debug("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId); + KillTranscodingJob(job, path => true); } @@ -329,19 +375,14 @@ namespace MediaBrowser.Api /// Task. internal void KillTranscodingJobs(string deviceId, string playSessionId, Func deleteFiles) { - if (string.IsNullOrEmpty(deviceId)) - { - throw new ArgumentNullException("deviceId"); - } - KillTranscodingJobs(j => { - if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(playSessionId)) { - return string.IsNullOrWhiteSpace(playSessionId) || string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase); + return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase); } - return false; + return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase); }, deleteFiles); } @@ -381,6 +422,10 @@ namespace MediaBrowser.Api /// The delete. private void KillTranscodingJob(TranscodingJob job, Func delete) { + job.DisposeKillTimer(); + + Logger.Debug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId); + lock (_activeTranscodingJobs) { _activeTranscodingJobs.Remove(job); @@ -389,34 +434,23 @@ namespace MediaBrowser.Api { job.CancellationTokenSource.Cancel(); } - - job.DisposeKillTimer(); } lock (job.ProcessLock) { - var process = job.Process; - - var hasExited = true; - - try + if (job.TranscodingThrottler != null) { - hasExited = process.HasExited; - } - catch (Exception ex) - { - Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path); + job.TranscodingThrottler.Stop(); } + var process = job.Process; + + var hasExited = job.HasExited; + if (!hasExited) { try { - if (job.TranscodingThrottler != null) - { - job.TranscodingThrottler.Stop(); - } - Logger.Info("Killing ffmpeg process for {0}", job.Path); //process.Kill(); @@ -558,6 +592,7 @@ namespace MediaBrowser.Api /// /// The process. public Process Process { get; set; } + public ILogger Logger { get; private set; } /// /// Gets or sets the active request count. /// @@ -567,7 +602,7 @@ namespace MediaBrowser.Api /// Gets or sets the kill timer. /// /// The kill timer. - public Timer KillTimer { get; set; } + private Timer KillTimer { get; set; } public string DeviceId { get; set; } @@ -590,12 +625,74 @@ namespace MediaBrowser.Api public TranscodingThrottler TranscodingThrottler { get; set; } + private readonly object _timerLock = new object(); + + public TranscodingJob(ILogger logger) + { + Logger = logger; + } + + public void StopKillTimer() + { + lock (_timerLock) + { + if (KillTimer != null) + { + KillTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + } + } + public void DisposeKillTimer() { - if (KillTimer != null) + lock (_timerLock) + { + if (KillTimer != null) + { + KillTimer.Dispose(); + KillTimer = null; + } + } + } + + public void StartKillTimer(int intervalMs, TimerCallback callback) + { + CheckHasExited(); + + lock (_timerLock) + { + if (KillTimer == null) + { + Logger.Debug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + KillTimer = new Timer(callback, this, intervalMs, Timeout.Infinite); + } + else + { + Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + KillTimer.Change(intervalMs, Timeout.Infinite); + } + } + } + + public void ChangeKillTimerIfStarted(int intervalMs) + { + CheckHasExited(); + + lock (_timerLock) + { + if (KillTimer != null) + { + Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + KillTimer.Change(intervalMs, Timeout.Infinite); + } + } + } + + private void CheckHasExited() + { + if (HasExited) { - KillTimer.Dispose(); - KillTimer = null; + throw new ObjectDisposedException("Job"); } } } diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 4465be97a2..66b2a314e4 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -259,7 +259,7 @@ namespace MediaBrowser.Api .GetRecursiveChildren(i => i is IHasArtist) .Cast() .SelectMany(i => i.AllArtists) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .FirstOrDefault(i => { i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); @@ -281,7 +281,7 @@ namespace MediaBrowser.Api return libraryManager.RootFolder.GetRecursiveChildren() .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .FirstOrDefault(i => { i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); @@ -301,7 +301,7 @@ namespace MediaBrowser.Api return libraryManager.RootFolder .GetRecursiveChildren(i => i is Game) .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .FirstOrDefault(i => { i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); @@ -324,7 +324,7 @@ namespace MediaBrowser.Api return libraryManager.RootFolder .GetRecursiveChildren() .SelectMany(i => i.Studios) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .FirstOrDefault(i => { i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); @@ -348,7 +348,7 @@ namespace MediaBrowser.Api .GetRecursiveChildren() .SelectMany(i => i.People) .Select(i => i.Name) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .FirstOrDefault(i => { i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs index d0abd18c28..9f6c07dd21 100644 --- a/MediaBrowser.Api/ConfigurationService.cs +++ b/MediaBrowser.Api/ConfigurationService.cs @@ -123,7 +123,7 @@ namespace MediaBrowser.Api public void Post(AutoSetMetadataOptions request) { - _configurationManager.DisableMetadataService("Media Browser Xml"); + _configurationManager.DisableMetadataService("Emby Xml"); _configurationManager.SaveConfiguration(); } diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs index fb51b2bdd4..6d1c5d868d 100644 --- a/MediaBrowser.Api/FilterService.cs +++ b/MediaBrowser.Api/FilterService.cs @@ -76,7 +76,7 @@ namespace MediaBrowser.Api .ToArray(); result.Genres = items.SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .OrderBy(i => i) .ToArray(); diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 24c91e172f..b8b74369ce 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dto; @@ -186,6 +187,9 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] public bool? IsMovie { get; set; } + [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] + public bool? IsSports { get; set; } + [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? StartIndex { get; set; } @@ -218,6 +222,9 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool? HasAired { get; set; } + [ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] + public bool? IsSports { get; set; } + [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool? IsMovie { get; set; } } @@ -422,11 +429,12 @@ namespace MediaBrowser.Api.LiveTv query.SortBy = (request.SortBy ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); query.SortOrder = request.SortOrder; query.IsMovie = request.IsMovie; + query.IsSports = request.IsSports; query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false); - return ToOptimizedSerializedResultUsingCache(result); + return ToOptimizedResult(result); } public async Task Get(GetRecommendedPrograms request) @@ -437,12 +445,13 @@ namespace MediaBrowser.Api.LiveTv IsAiring = request.IsAiring, Limit = request.Limit, HasAired = request.HasAired, - IsMovie = request.IsMovie + IsMovie = request.IsMovie, + IsSports = request.IsSports }; var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false); - return ToOptimizedSerializedResultUsingCache(result); + return ToOptimizedResult(result); } public object Post(GetPrograms request) @@ -452,6 +461,9 @@ namespace MediaBrowser.Api.LiveTv public async Task Get(GetRecordings request) { + var options = new DtoOptions(); + options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId; + var result = await _liveTvManager.GetRecordings(new RecordingQuery { ChannelId = request.ChannelId, @@ -463,16 +475,19 @@ namespace MediaBrowser.Api.LiveTv SeriesTimerId = request.SeriesTimerId, IsInProgress = request.IsInProgress - }, CancellationToken.None).ConfigureAwait(false); + }, options, CancellationToken.None).ConfigureAwait(false); - return ToOptimizedSerializedResultUsingCache(result); + return ToOptimizedResult(result); } public async Task Get(GetRecording request) { var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId); - var result = await _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).ConfigureAwait(false); + var options = new DtoOptions(); + options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId; + + var result = await _liveTvManager.GetRecording(request.Id, options, CancellationToken.None, user).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); } diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 07f5942c62..1a7f6d8f49 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -410,7 +410,7 @@ namespace MediaBrowser.Api.Movies return items .SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2)) .Select(i => i.Name) - .Distinct(StringComparer.OrdinalIgnoreCase); + .DistinctNames(); } private IEnumerable GetDirectors(IEnumerable items) @@ -419,7 +419,7 @@ namespace MediaBrowser.Api.Movies .Select(i => i.People.FirstOrDefault(p => string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase))) .Where(i => i != null) .Select(i => i.Name) - .Distinct(StringComparer.OrdinalIgnoreCase); + .DistinctNames(); } } } diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs index a1c98addbe..37f79bf208 100644 --- a/MediaBrowser.Api/Music/AlbumsService.cs +++ b/MediaBrowser.Api/Music/AlbumsService.cs @@ -79,12 +79,12 @@ namespace MediaBrowser.Api.Music var artists1 = album1 .AllArtists - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .ToList(); var artists2 = album2 .AllArtists - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); return points + artists1.Where(artists2.ContainsKey).Sum(i => 5); diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 75321f872c..827aed4f2d 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -1026,7 +1026,7 @@ namespace MediaBrowser.Api.Playback // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); - var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine); + var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); @@ -1514,6 +1514,10 @@ namespace MediaBrowser.Api.Playback request.PlaySessionId = val; } else if (i == 22) + { + // api_key + } + else if (i == 23) { request.LiveStreamId = val; } @@ -1624,14 +1628,19 @@ namespace MediaBrowser.Api.Playback var archivable = item as IArchivable; state.IsInputArchive = archivable != null && archivable.IsArchive; - MediaSourceInfo mediaSource = null; + MediaSourceInfo mediaSource; if (string.IsNullOrWhiteSpace(request.LiveStreamId)) { - var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false); + var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(request.Id, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false)).ToList(); mediaSource = string.IsNullOrEmpty(request.MediaSourceId) ? mediaSources.First() - : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId)); + : mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId)); + + if (mediaSource == null && string.Equals(request.Id, request.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + { + mediaSource = mediaSources.First(); + } } else { @@ -1700,6 +1709,102 @@ namespace MediaBrowser.Api.Playback { state.OutputAudioCodec = "copy"; } + + if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && TranscodingJobType == TranscodingJobType.Hls) + { + var segmentLength = GetSegmentLength(state); + if (segmentLength.HasValue) + { + state.SegmentLength = segmentLength.Value; + } + } + } + + private int? GetSegmentLength(StreamState state) + { + var stream = state.VideoStream; + + if (stream == null) + { + return null; + } + + var frames = stream.KeyFrames; + + if (frames == null || frames.Count < 2) + { + return null; + } + + Logger.Debug("Found keyframes at {0}", string.Join(",", frames.ToArray())); + + var intervals = new List(); + for (var i = 1; i < frames.Count; i++) + { + var start = frames[i - 1]; + var end = frames[i]; + intervals.Add(end - start); + } + + Logger.Debug("Found keyframes intervals {0}", string.Join(",", intervals.ToArray())); + + var results = new List>(); + + for (var i = 1; i <= 10; i++) + { + var idealMs = i*1000; + + if (intervals.Max() < idealMs - 1000) + { + break; + } + + var segments = PredictStreamCopySegments(intervals, idealMs); + var variance = segments.Select(s => Math.Abs(idealMs - s)).Sum(); + + results.Add(new Tuple(i, variance)); + } + + if (results.Count == 0) + { + return null; + } + + return results.OrderBy(i => i.Item2).ThenBy(i => i.Item1).Select(i => i.Item1).First(); + } + + private List PredictStreamCopySegments(List intervals, int idealMs) + { + var segments = new List(); + var currentLength = 0; + + foreach (var interval in intervals) + { + if (currentLength == 0 || (currentLength + interval) <= idealMs) + { + currentLength += interval; + } + + else + { + // The segment will either be above or below the ideal. + // Need to figure out which is preferable + var offset1 = Math.Abs(idealMs - currentLength); + var offset2 = Math.Abs(idealMs - (currentLength + interval)); + + if (offset1 <= offset2) + { + segments.Add(currentLength); + currentLength = interval; + } + else + { + currentLength += interval; + } + } + } + Logger.Debug("Predicted actual segment lengths for length {0}: {1}", idealMs, string.Join(",", segments.ToArray())); + return segments; } private void AttachMediaSourceInfo(StreamState state, diff --git a/MediaBrowser.Api/Playback/Dash/MpegDashService.cs b/MediaBrowser.Api/Playback/Dash/MpegDashService.cs index ba3f172579..0692c4863e 100644 --- a/MediaBrowser.Api/Playback/Dash/MpegDashService.cs +++ b/MediaBrowser.Api/Playback/Dash/MpegDashService.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.IO; @@ -518,25 +517,14 @@ namespace MediaBrowser.Api.Playback.Dash private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken) { - var tmpPath = playlist + ".tmp"; - var segmentFilename = Path.GetFileName(segment); Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist); while (true) { - FileStream fileStream; - try - { - fileStream = FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true); - } - catch (IOException) - { - fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true); - } // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written - using (fileStream) + using (var fileStream = GetPlaylistFileStream(playlist)) { using (var reader = new StreamReader(fileStream)) { diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 207bc2f679..919fe07733 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; @@ -86,6 +85,7 @@ namespace MediaBrowser.Api.Playback.Hls state.Request.StartTimeTicks = null; } + TranscodingJob job = null; var playlist = state.OutputFilePath; if (!File.Exists(playlist)) @@ -98,7 +98,7 @@ namespace MediaBrowser.Api.Playback.Hls // If the playlist doesn't already exist, startup ffmpeg try { - await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false); + job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false); } catch { @@ -117,6 +117,12 @@ namespace MediaBrowser.Api.Playback.Hls if (isLive) { + job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); + + if (job != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(job); + } return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); } @@ -135,6 +141,13 @@ namespace MediaBrowser.Api.Playback.Hls var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate); + job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); + + if (job != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(job); + } + return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); } @@ -186,7 +199,7 @@ namespace MediaBrowser.Api.Playback.Hls while (true) { // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written - using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + using (var fileStream = GetPlaylistFileStream(playlist)) { using (var reader = new StreamReader(fileStream)) { @@ -212,6 +225,20 @@ namespace MediaBrowser.Api.Playback.Hls } } + protected Stream GetPlaylistFileStream(string path) + { + var tmpPath = path + ".tmp"; + + try + { + return FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true); + } + catch (IOException) + { + return FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true); + } + } + protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) { var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream; diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index cbfadb886d..1b11f1f337 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; @@ -128,9 +127,27 @@ namespace MediaBrowser.Api.Playback.Hls } else { + var startTranscoding = false; + var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); - var segmentGapRequiringTranscodingChange = 24/state.SegmentLength; - if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange) + var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; + + if (currentTranscodingIndex == null) + { + Logger.Debug("Starting transcoding because currentTranscodingIndex=null"); + startTranscoding = true; + } + else if (requestedIndex < currentTranscodingIndex.Value) + { + Logger.Debug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex); + startTranscoding = true; + } + else if ((requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange) + { + Logger.Debug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", (requestedIndex - currentTranscodingIndex.Value), segmentGapRequiringTranscodingChange, requestedIndex); + startTranscoding = true; + } + if (startTranscoding) { // If the playlist doesn't already exist, startup ffmpeg try @@ -145,7 +162,6 @@ namespace MediaBrowser.Api.Playback.Hls request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex); job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false); - ApiEntryPoint.Instance.OnTranscodeBeginRequest(job); } catch { @@ -153,7 +169,15 @@ namespace MediaBrowser.Api.Playback.Hls throw; } - await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false); + //await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false); + } + else + { + job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + if (job.TranscodingThrottler != null) + { + job.TranscodingThrottler.UnpauseTranscoding(); + } } } } @@ -300,7 +324,7 @@ namespace MediaBrowser.Api.Playback.Hls var segmentFilename = Path.GetFileName(segmentPath); - using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + using (var fileStream = GetPlaylistFileStream(playlistPath)) { using (var reader = new StreamReader(fileStream)) { @@ -712,7 +736,7 @@ namespace MediaBrowser.Api.Playback.Hls ).Trim(); } - return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"", + return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -copyts -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"", inputModifier, GetInputArgument(state), threads, diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index b1964f4aef..8e2854c5e8 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -3,12 +3,10 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.IO; using ServiceStack; using System; -using System.IO; namespace MediaBrowser.Api.Playback.Hls { diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 08c5d56db2..c71048b0da 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Controller.Devices; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -59,23 +61,27 @@ namespace MediaBrowser.Api.Playback private readonly IMediaSourceManager _mediaSourceManager; private readonly IDeviceManager _deviceManager; private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; + private readonly INetworkManager _networkManager; - public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager) + public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager) { _mediaSourceManager = mediaSourceManager; _deviceManager = deviceManager; _libraryManager = libraryManager; + _config = config; + _networkManager = networkManager; } public async Task Get(GetPlaybackInfo request) { - var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false); + var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false); return ToOptimizedResult(result); } public async Task Get(GetLiveMediaInfo request) { - var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false); + var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false); return ToOptimizedResult(result); } @@ -122,29 +128,32 @@ namespace MediaBrowser.Api.Playback public async Task Post(GetPostedPlaybackInfo request) { - var info = await GetPlaybackInfo(request.Id, request.UserId, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false); var authInfo = AuthorizationContext.GetAuthorizationInfo(Request); var profile = request.DeviceProfile; - if (profile == null) + + var caps = _deviceManager.GetCapabilities(authInfo.DeviceId); + if (caps != null) { - var caps = _deviceManager.GetCapabilities(authInfo.DeviceId); - if (caps != null) + if (profile == null) { profile = caps.DeviceProfile; } } + var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false); + if (profile != null) { var mediaSourceId = request.MediaSourceId; + SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex); } return ToOptimizedResult(info); } - private async Task GetPlaybackInfo(string id, string userId, string mediaSourceId = null, string liveStreamId = null) + private async Task GetPlaybackInfo(string id, string userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null) { var result = new PlaybackInfoResponse(); @@ -153,7 +162,7 @@ namespace MediaBrowser.Api.Playback IEnumerable mediaSources; try { - mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, CancellationToken.None).ConfigureAwait(false); + mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, supportedLiveMediaTypes, CancellationToken.None).ConfigureAwait(false); } catch (PlaybackException ex) { @@ -223,7 +232,7 @@ namespace MediaBrowser.Api.Playback int? subtitleStreamIndex, string playSessionId) { - var streamBuilder = new StreamBuilder(); + var streamBuilder = new StreamBuilder(Logger); var options = new VideoOptions { @@ -231,8 +240,7 @@ namespace MediaBrowser.Api.Playback Context = EncodingContext.Streaming, DeviceId = auth.DeviceId, ItemId = item.Id.ToString("N"), - Profile = profile, - MaxBitrate = maxBitrate + Profile = profile }; if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) @@ -248,6 +256,7 @@ namespace MediaBrowser.Api.Playback // Dummy this up to fool StreamBuilder mediaSource.SupportsDirectStream = true; + options.MaxBitrate = maxBitrate; // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? @@ -270,6 +279,8 @@ namespace MediaBrowser.Api.Playback if (mediaSource.SupportsDirectStream) { + options.MaxBitrate = GetMaxBitrate(maxBitrate); + // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? streamBuilder.BuildAudioItem(options) : @@ -288,6 +299,8 @@ namespace MediaBrowser.Api.Playback if (mediaSource.SupportsTranscoding) { + options.MaxBitrate = GetMaxBitrate(maxBitrate); + // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? streamBuilder.BuildAudioItem(options) : @@ -309,6 +322,18 @@ namespace MediaBrowser.Api.Playback } } + private int? GetMaxBitrate(int? clientMaxBitrate) + { + var maxBitrate = clientMaxBitrate; + + if (_config.Configuration.RemoteClientBitrateLimit > 0 && !_networkManager.IsInLocalNetwork(Request.RemoteIp)) + { + maxBitrate = Math.Min(maxBitrate ?? _config.Configuration.RemoteClientBitrateLimit, _config.Configuration.RemoteClientBitrateLimit); + } + + return maxBitrate; + } + private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) { var profiles = info.GetSubtitleProfiles(false, "-", accessToken); diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 4a51f86444..6a3443f359 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -63,6 +63,13 @@ namespace MediaBrowser.Api.Playback.Progressive new ProgressiveFileCopier(_fileSystem, _job) .StreamFile(Path, responseStream); } + catch (IOException) + { + // These error are always the same so don't dump the whole stack trace + Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed."); + + throw; + } catch (Exception ex) { Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex); diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 540c39a0c7..0ded108b1d 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.IO; using ServiceStack; diff --git a/MediaBrowser.Api/Playback/TranscodingThrottler.cs b/MediaBrowser.Api/Playback/TranscodingThrottler.cs index 58cfa086e3..ece4550095 100644 --- a/MediaBrowser.Api/Playback/TranscodingThrottler.cs +++ b/MediaBrowser.Api/Playback/TranscodingThrottler.cs @@ -70,7 +70,7 @@ namespace MediaBrowser.Api.Playback } } - private void UnpauseTranscoding() + public void UnpauseTranscoding() { if (_isPaused) { diff --git a/MediaBrowser.Api/Session/SessionsService.cs b/MediaBrowser.Api/Session/SessionsService.cs index 52ecb95ec6..d4ea6a0eb0 100644 --- a/MediaBrowser.Api/Session/SessionsService.cs +++ b/MediaBrowser.Api/Session/SessionsService.cs @@ -383,12 +383,12 @@ namespace MediaBrowser.Api.Session if (!user.Policy.EnableRemoteControlOfOtherUsers) { - result = result.Where(i => i.ContainsUser(request.ControllableByUserId.Value)); + result = result.Where(i => !i.UserId.HasValue || i.ContainsUser(request.ControllableByUserId.Value)); } if (!user.Policy.EnableSharedDeviceControl) { - result = result.Where(i => !i.UserId.HasValue); + result = result.Where(i => i.UserId.HasValue); } result = result.Where(i => diff --git a/MediaBrowser.Api/SimilarItemsHelper.cs b/MediaBrowser.Api/SimilarItemsHelper.cs index e061c391a2..fb04dd0301 100644 --- a/MediaBrowser.Api/SimilarItemsHelper.cs +++ b/MediaBrowser.Api/SimilarItemsHelper.cs @@ -170,7 +170,7 @@ namespace MediaBrowser.Api points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3); var item2PeopleNames = item2.People.Select(i => i.Name) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); points += item1.People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i => diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 73589d6777..a70118d3ce 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -136,11 +136,11 @@ namespace MediaBrowser.Api.Subtitles _providerManager = providerManager; } - public object Get(GetSubtitlePlaylist request) + public async Task Get(GetSubtitlePlaylist request) { var item = (Video)_libraryManager.GetItemById(new Guid(request.Id)); - var mediaSource = _mediaSourceManager.GetStaticMediaSource(item, request.MediaSourceId, false); + var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, false).ConfigureAwait(false); var builder = new StringBuilder(); diff --git a/MediaBrowser.Api/Sync/SyncService.cs b/MediaBrowser.Api/Sync/SyncService.cs index b9dbf59469..d5f88e6a43 100644 --- a/MediaBrowser.Api/Sync/SyncService.cs +++ b/MediaBrowser.Api/Sync/SyncService.cs @@ -248,6 +248,9 @@ namespace MediaBrowser.Api.Sync result.Targets = _syncManager.GetSyncTargets(request.UserId) .ToList(); + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + var authenticatedUser = _userManager.GetUserById(auth.UserId); + if (!string.IsNullOrWhiteSpace(request.TargetId)) { result.Targets = result.Targets @@ -255,11 +258,11 @@ namespace MediaBrowser.Api.Sync .ToList(); result.QualityOptions = _syncManager - .GetQualityOptions(request.TargetId) + .GetQualityOptions(request.TargetId, authenticatedUser) .ToList(); result.ProfileOptions = _syncManager - .GetProfileOptions(request.TargetId) + .GetProfileOptions(request.TargetId, authenticatedUser) .ToList(); } @@ -277,10 +280,6 @@ namespace MediaBrowser.Api.Sync } }; - var auth = AuthorizationContext.GetAuthorizationInfo(Request); - - var authenticatedUser = _userManager.GetUserById(auth.UserId); - var items = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(_libraryManager.GetItemById) .Where(i => i != null); diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs index dd9825debd..9f3f174657 100644 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -132,7 +132,7 @@ namespace MediaBrowser.Api.UserLibrary .Where(i => !i.IsFolder) .OfType() .SelectMany(i => i.AlbumArtists) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(name => { try @@ -152,7 +152,7 @@ namespace MediaBrowser.Api.UserLibrary .Where(i => !i.IsFolder) .OfType() .SelectMany(i => i.AllArtists) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(name => { try diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index 609c1048f2..b2364ce3c6 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -142,7 +142,7 @@ namespace MediaBrowser.Api.UserLibrary } IEnumerable>> tuples; - if (dtoOptions.Fields.Contains(ItemFields.ItemCounts) || true) + if (dtoOptions.Fields.Contains(ItemFields.ItemCounts)) { tuples = ibnItems.Select(i => new Tuple>(i, i.GetTaggedItems(libraryItems).ToList())); } @@ -177,7 +177,6 @@ namespace MediaBrowser.Api.UserLibrary return true; } - return true; return options.Fields.Contains(ItemFields.ItemCounts); } diff --git a/MediaBrowser.Api/UserLibrary/GameGenresService.cs b/MediaBrowser.Api/UserLibrary/GameGenresService.cs index 3063e19c72..2f7430d333 100644 --- a/MediaBrowser.Api/UserLibrary/GameGenresService.cs +++ b/MediaBrowser.Api/UserLibrary/GameGenresService.cs @@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary return itemsList .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(name => LibraryManager.GetGameGenre(name)); } } diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs index c659852de8..63c0575bff 100644 --- a/MediaBrowser.Api/UserLibrary/GenresService.cs +++ b/MediaBrowser.Api/UserLibrary/GenresService.cs @@ -108,7 +108,7 @@ namespace MediaBrowser.Api.UserLibrary { return items .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(name => { try diff --git a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs b/MediaBrowser.Api/UserLibrary/MusicGenresService.cs index 3733128f04..1fe9dfaaa3 100644 --- a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs +++ b/MediaBrowser.Api/UserLibrary/MusicGenresService.cs @@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary return itemsList .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(name => LibraryManager.GetMusicGenre(name)); } } diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs index e9b3fa402f..08ee6e462e 100644 --- a/MediaBrowser.Api/UserLibrary/PersonsService.cs +++ b/MediaBrowser.Api/UserLibrary/PersonsService.cs @@ -127,7 +127,7 @@ namespace MediaBrowser.Api.UserLibrary return allPeople .Select(i => i.Name) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(name => { diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs index 55e1681e0d..4661abf4cc 100644 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -114,6 +114,15 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] public int? SubtitleStreamIndex { get; set; } + + [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public PlayMethod PlayMethod { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } } /// @@ -160,6 +169,15 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] public int? VolumeLevel { get; set; } + + [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public PlayMethod PlayMethod { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } } /// @@ -191,6 +209,12 @@ namespace MediaBrowser.Api.UserLibrary /// The position ticks. [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")] public long? PositionTicks { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } } [Authenticated] @@ -260,7 +284,10 @@ namespace MediaBrowser.Api.UserLibrary QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(), MediaSourceId = request.MediaSourceId, AudioStreamIndex = request.AudioStreamIndex, - SubtitleStreamIndex = request.SubtitleStreamIndex + SubtitleStreamIndex = request.SubtitleStreamIndex, + PlayMethod = request.PlayMethod, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId }); } @@ -288,12 +315,20 @@ namespace MediaBrowser.Api.UserLibrary MediaSourceId = request.MediaSourceId, AudioStreamIndex = request.AudioStreamIndex, SubtitleStreamIndex = request.SubtitleStreamIndex, - VolumeLevel = request.VolumeLevel + VolumeLevel = request.VolumeLevel, + PlayMethod = request.PlayMethod, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId }); } public void Post(ReportPlaybackProgress request) { + if (!string.IsNullOrWhiteSpace(request.PlaySessionId)) + { + ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId); + } + request.SessionId = GetSession().Result.Id; var task = _sessionManager.OnPlaybackProgress(request); @@ -311,12 +346,19 @@ namespace MediaBrowser.Api.UserLibrary { ItemId = request.Id, PositionTicks = request.PositionTicks, - MediaSourceId = request.MediaSourceId + MediaSourceId = request.MediaSourceId, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId }); } public void Post(ReportPlaybackStopped request) { + if (!string.IsNullOrWhiteSpace(request.PlaySessionId)) + { + ApiEntryPoint.Instance.KillTranscodingJobs(AuthorizationContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true); + } + request.SessionId = GetSession().Result.Id; var task = _sessionManager.OnPlaybackStopped(request); diff --git a/MediaBrowser.Api/UserLibrary/StudiosService.cs b/MediaBrowser.Api/UserLibrary/StudiosService.cs index a4ebef6846..ae1da03468 100644 --- a/MediaBrowser.Api/UserLibrary/StudiosService.cs +++ b/MediaBrowser.Api/UserLibrary/StudiosService.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Api.UserLibrary return itemsList .SelectMany(i => i.Studios) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(name => LibraryManager.GetStudio(name)); } } diff --git a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs index bc1b0e7855..70ed5c3191 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs +++ b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs @@ -101,12 +101,6 @@ namespace MediaBrowser.Common.Implementations /// The failed assemblies. public List FailedAssemblies { get; protected set; } - /// - /// Gets all types within all running assemblies - /// - /// All types. - public Type[] AllTypes { get; protected set; } - /// /// Gets all concrete types. /// @@ -438,9 +432,10 @@ namespace MediaBrowser.Common.Implementations Logger.Info("Loading {0}", assembly.FullName); } - AllTypes = assemblies.SelectMany(GetTypes).ToArray(); - - AllConcreteTypes = AllTypes.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType).ToArray(); + AllConcreteTypes = assemblies + .SelectMany(GetTypes) + .Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType) + .ToArray(); } /// diff --git a/MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs b/MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs index 1762ed5754..0fd4e27874 100644 --- a/MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs +++ b/MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs @@ -172,11 +172,11 @@ namespace MediaBrowser.Common.Implementations.Networking Uri uri; if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out uri)) { - var host = uri.DnsSafeHost; - Logger.Debug("Resolving host {0}", host); - try { + var host = uri.DnsSafeHost; + Logger.Debug("Resolving host {0}", host); + address = GetIpAddresses(host).FirstOrDefault(); if (address != null) @@ -186,9 +186,13 @@ namespace MediaBrowser.Common.Implementations.Networking return IsInLocalNetworkInternal(address.ToString(), false); } } + catch (InvalidOperationException) + { + // Can happen with reverse proxy or IIS url rewriting + } catch (Exception ex) { - Logger.ErrorException("Error resovling hostname {0}", ex, host); + Logger.ErrorException("Error resovling hostname", ex); } } } diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 78dcea493e..c2551731fb 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -121,12 +121,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks { if (_lastExecutionResult == null) { + var path = GetHistoryFilePath(); + lock (_lastExecutionResultSyncLock) { if (_lastExecutionResult == null) { - var path = GetHistoryFilePath(); - try { return JsonSerializer.DeserializeFromFile(path); @@ -152,6 +152,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks private set { _lastExecutionResult = value; + + var path = GetHistoryFilePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_lastExecutionResultSyncLock) + { + JsonSerializer.SerializeToFile(value, path); + } } } @@ -582,11 +590,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks result.LongErrorMessage = ex.StackTrace; } - var path = GetHistoryFilePath(); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - JsonSerializer.SerializeToFile(result, path); - LastExecutionResult = result; ((TaskManager)TaskManager).OnTaskCompleted(this, result); diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index b6514ca0a6..f746d87fff 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -5,7 +5,6 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Channels { @@ -15,19 +14,9 @@ namespace MediaBrowser.Controller.Channels public override bool IsVisible(User user) { - if (user.Policy.BlockedChannels != null) + if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) { - if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) - { - return false; - } - } - else - { - if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } return base.IsVisible(user); diff --git a/MediaBrowser.Controller/Channels/ChannelAudioItem.cs b/MediaBrowser.Controller/Channels/ChannelAudioItem.cs index 8d90246765..82fe66c7ba 100644 --- a/MediaBrowser.Controller/Channels/ChannelAudioItem.cs +++ b/MediaBrowser.Controller/Channels/ChannelAudioItem.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; @@ -100,5 +101,10 @@ namespace MediaBrowser.Controller.Channels { return false; } + + public override bool IsVisibleStandalone(User user) + { + return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user); + } } } diff --git a/MediaBrowser.Controller/Channels/ChannelFolderItem.cs b/MediaBrowser.Controller/Channels/ChannelFolderItem.cs index 7ba73d126c..641d37161a 100644 --- a/MediaBrowser.Controller/Channels/ChannelFolderItem.cs +++ b/MediaBrowser.Controller/Channels/ChannelFolderItem.cs @@ -80,5 +80,10 @@ namespace MediaBrowser.Controller.Channels { return false; } + + public override bool IsVisibleStandalone(User user) + { + return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user); + } } } diff --git a/MediaBrowser.Controller/Channels/ChannelVideoItem.cs b/MediaBrowser.Controller/Channels/ChannelVideoItem.cs index 8eec2021b5..ef3cc7cbab 100644 --- a/MediaBrowser.Controller/Channels/ChannelVideoItem.cs +++ b/MediaBrowser.Controller/Channels/ChannelVideoItem.cs @@ -130,5 +130,17 @@ namespace MediaBrowser.Controller.Channels { return false; } + + public override bool IsVisibleStandalone(User user) + { + return IsVisibleStandaloneInternal(user, false) && IsChannelVisible(this, user); + } + + internal static bool IsChannelVisible(IChannelItem item, User user) + { + var channel = ChannelManager.GetChannel(item.ChannelId); + + return channel.IsVisible(user); + } } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 6fafc2b464..685d2706da 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -13,6 +13,12 @@ namespace MediaBrowser.Controller.Drawing /// public interface IImageProcessor { + /// + /// Gets the supported input formats. + /// + /// The supported input formats. + string[] SupportedInputFormats { get; } + /// /// Gets the image enhancers. /// @@ -93,5 +99,11 @@ namespace MediaBrowser.Controller.Drawing /// /// ImageOutputFormat[]. ImageFormat[] GetSupportedImageOutputFormats(); + + /// + /// Creates the image collage. + /// + /// The options. + void CreateImageCollage(ImageCollageOptions options); } } diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs new file mode 100644 index 0000000000..edc4f85586 --- /dev/null +++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs @@ -0,0 +1,32 @@ + +namespace MediaBrowser.Controller.Drawing +{ + public class ImageCollageOptions + { + /// + /// Gets or sets the input paths. + /// + /// The input paths. + public string[] InputPaths { get; set; } + /// + /// Gets or sets the output path. + /// + /// The output path. + public string OutputPath { get; set; } + /// + /// Gets or sets the width. + /// + /// The width. + public int Width { get; set; } + /// + /// Gets or sets the height. + /// + /// The height. + public int Height { get; set; } + /// + /// Gets or sets the text. + /// + /// The text. + public string Text { get; set; } + } +} diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index ea311d9937..5ec8f274be 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -35,6 +35,14 @@ namespace MediaBrowser.Controller.Dto /// Task{BaseItemDto}. BaseItemDto GetBaseItemDto(BaseItem item, List fields, User user = null, BaseItem owner = null); + /// + /// Fills the synchronize information. + /// + /// The dtos. + /// The options. + /// The user. + void FillSyncInfo(IEnumerable dtos, DtoOptions options, User user); + /// /// Gets the base item dto. /// diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index 56921409ae..254f90376d 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -1,6 +1,5 @@ -using System; +using MediaBrowser.Controller.Library; using System.Collections.Generic; -using System.Linq; namespace MediaBrowser.Controller.Entities.Audio { @@ -20,11 +19,11 @@ namespace MediaBrowser.Controller.Entities.Audio { public static bool HasArtist(this IHasArtist hasArtist, string artist) { - return hasArtist.Artists.Contains(artist, StringComparer.OrdinalIgnoreCase); + return NameExtensions.EqualsAny(hasArtist.Artists, artist); } public static bool HasAnyArtist(this IHasArtist hasArtist, string artist) { - return hasArtist.AllArtists.Contains(artist, StringComparer.OrdinalIgnoreCase); + return NameExtensions.EqualsAny(hasArtist.AllArtists, artist); } } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index cdb52ec668..b7322494d6 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Extensions; +using System.Globalization; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; @@ -44,7 +45,7 @@ namespace MediaBrowser.Controller.Entities /// /// The supported image extensions /// - public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn" }; + public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" }; public static readonly List SupportedImageExtensionsList = SupportedImageExtensions.ToList(); @@ -1143,6 +1144,11 @@ namespace MediaBrowser.Controller.Entities } public virtual bool IsVisibleStandalone(User user) + { + return IsVisibleStandaloneInternal(user, true); + } + + protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) { if (!IsVisible(user)) { @@ -1154,7 +1160,23 @@ namespace MediaBrowser.Controller.Entities return false; } - // TODO: Need some work here, e.g. is in user library, for channels, can user access channel, etc. + if (checkFolders) + { + var topParent = Parents.LastOrDefault() ?? this; + + if (string.IsNullOrWhiteSpace(topParent.Path)) + { + return true; + } + + var userCollectionFolders = user.RootFolder.GetChildren(user, true).Select(i => i.Id).ToList(); + var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id); + + if (!itemCollectionFolders.Any(userCollectionFolders.Contains)) + { + return false; + } + } return true; } @@ -1219,18 +1241,6 @@ namespace MediaBrowser.Controller.Entities private BaseItem FindLinkedChild(LinkedChild info) { - if (!string.IsNullOrWhiteSpace(info.ItemName)) - { - if (string.Equals(info.ItemType, "musicgenre", StringComparison.OrdinalIgnoreCase)) - { - return LibraryManager.GetMusicGenre(info.ItemName); - } - if (string.Equals(info.ItemType, "musicartist", StringComparison.OrdinalIgnoreCase)) - { - return LibraryManager.GetArtist(info.ItemName); - } - } - if (!string.IsNullOrEmpty(info.Path)) { var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path); @@ -1243,23 +1253,6 @@ namespace MediaBrowser.Controller.Entities return itemByPath; } - if (!string.IsNullOrWhiteSpace(info.ItemName) && !string.IsNullOrWhiteSpace(info.ItemType)) - { - return LibraryManager.RootFolder.GetRecursiveChildren(i => - { - if (string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(i.GetType().Name, info.ItemType, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - - }).FirstOrDefault(); - } - return null; } @@ -1540,7 +1533,7 @@ namespace MediaBrowser.Controller.Entities } // Remove it from the item - ImageInfos.Remove(info); + RemoveImage(info); // Delete the source file var currentFile = new FileInfo(info.Path); @@ -1559,6 +1552,11 @@ namespace MediaBrowser.Controller.Entities return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); } + public void RemoveImage(ItemImageInfo image) + { + ImageInfos.Remove(image); + } + public virtual Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) { return LibraryManager.UpdateItem(this, updateReason, cancellationToken); @@ -1651,7 +1649,7 @@ namespace MediaBrowser.Controller.Entities public bool AddImages(ImageType imageType, IEnumerable images) { - return AddImages(imageType, images.Cast()); + return AddImages(imageType, images.Cast().ToList()); } /// @@ -1661,7 +1659,7 @@ namespace MediaBrowser.Controller.Entities /// The images. /// true if XXXX, false otherwise. /// Cannot call AddImages with chapter images - public bool AddImages(ImageType imageType, IEnumerable images) + public bool AddImages(ImageType imageType, List images) { if (imageType == ImageType.Chapter) { @@ -1672,6 +1670,7 @@ namespace MediaBrowser.Controller.Entities .ToList(); var newImageList = new List(); + var imageAdded = false; foreach (var newImage in images) { @@ -1686,14 +1685,26 @@ namespace MediaBrowser.Controller.Entities if (existing == null) { newImageList.Add(newImage); + imageAdded = true; } else { existing.DateModified = FileSystem.GetLastWriteTimeUtc(newImage); - existing.Length = ((FileInfo) newImage).Length; + existing.Length = ((FileInfo)newImage).Length; } } + if (imageAdded || images.Count != existingImages.Count) + { + var newImagePaths = images.Select(i => i.FullName).ToList(); + + var deleted = existingImages + .Where(i => !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path)) + .ToList(); + + ImageInfos = ImageInfos.Except(deleted).ToList(); + } + ImageInfos.AddRange(newImageList.Select(i => GetImageInfo(i, imageType))); return newImageList.Count > 0; @@ -1882,5 +1893,18 @@ namespace MediaBrowser.Controller.Entities return video.RefreshMetadata(newOptions, cancellationToken); } + + public string GetEtag() + { + return string.Join("|", GetEtagValues().ToArray()).GetMD5().ToString("N"); + } + + protected virtual List GetEtagValues() + { + return new List + { + DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture) + }; + } } } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index cffc0989a5..61e5acdb3f 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -334,22 +334,9 @@ namespace MediaBrowser.Controller.Entities { if (this is ICollectionFolder && !(this is BasePluginFolder)) { - if (user.Policy.BlockedMediaFolders != null) + if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) { - if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) || - - // Backwards compatibility - user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - } - else - { - if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } } @@ -1004,8 +991,9 @@ namespace MediaBrowser.Controller.Entities } var locations = user.RootFolder - .GetChildren(user, true) + .Children .OfType() + .Where(i => i.IsVisible(user)) .SelectMany(i => i.PhysicalLocations) .ToList(); diff --git a/MediaBrowser.Controller/Entities/IHasImages.cs b/MediaBrowser.Controller/Entities/IHasImages.cs index 00a42271b5..1871d7b68a 100644 --- a/MediaBrowser.Controller/Entities/IHasImages.cs +++ b/MediaBrowser.Controller/Entities/IHasImages.cs @@ -141,7 +141,7 @@ namespace MediaBrowser.Controller.Entities /// Type of the image. /// The images. /// true if XXXX, false otherwise. - bool AddImages(ImageType imageType, IEnumerable images); + bool AddImages(ImageType imageType, List images); /// /// Determines whether [is save local metadata enabled]. @@ -190,6 +190,12 @@ namespace MediaBrowser.Controller.Entities /// /// true if [is internet metadata enabled]; otherwise, false. bool IsInternetMetadataEnabled(); + + /// + /// Removes the image. + /// + /// The image. + void RemoveImage(ItemImageInfo image); } public static class HasImagesExtensions diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index 949c9741b1..ac13657b94 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -9,9 +9,6 @@ namespace MediaBrowser.Controller.Entities public string Path { get; set; } public LinkedChildType Type { get; set; } - public string ItemName { get; set; } - public string ItemType { get; set; } - [IgnoreDataMember] public string Id { get; set; } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 0778643da5..02e9d4cf9e 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -175,19 +175,19 @@ namespace MediaBrowser.Controller.Entities.Movies public override bool IsVisible(User user) { - if (base.IsVisible(user)) - { - var userId = user.Id.ToString("N"); - - // Need to check Count > 0 for boxsets created prior to the introduction of Shares - if (Shares.Count > 0 && !Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase))) - { - //return false; - } + var userId = user.Id.ToString("N"); + // Need to check Count > 0 for boxsets created prior to the introduction of Shares + if (Shares.Count > 0 && Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase))) + { return true; } + if (base.IsVisible(user)) + { + return GetChildren(user, true).Any(); + } + return false; } } diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index 24ebf88153..5b48a70e9c 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -1,11 +1,15 @@ -using MediaBrowser.Model.Configuration; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Users; +using System; using System.Linq; using System.Runtime.Serialization; -using MediaBrowser.Model.Users; +using System.Threading; +using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities { - public class PhotoAlbum : Folder + public class PhotoAlbum : Folder, IMetadataContainer { public override bool SupportsLocalMetadata { @@ -28,5 +32,31 @@ namespace MediaBrowser.Controller.Entities { return config.BlockUnratedItems.Contains(UnratedItem.Other); } + + public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress progress, CancellationToken cancellationToken) + { + var items = GetRecursiveChildren().ToList(); + + var totalItems = items.Count; + var numComplete = 0; + + // Refresh songs + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= totalItems; + progress.Report(percent * 100); + } + + // Refresh current item + await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + + progress.Report(100); + } } } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 0e602dabe2..63ce223afe 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -50,6 +50,16 @@ namespace MediaBrowser.Controller.Entities { var user = query.User; + if (query.IncludeItemTypes != null && + query.IncludeItemTypes.Length == 1 && + string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase)) + { + if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + { + return await FindPlaylists(queryParent, user, query).ConfigureAwait(false); + } + } + switch (viewType) { case CollectionType.Channels: @@ -107,9 +117,7 @@ namespace MediaBrowser.Controller.Entities case CollectionType.LiveTv: { - var result = await GetLiveTvFolders(user).ConfigureAwait(false); - - return GetResult(result, queryParent, query); + return await GetLiveTvView(queryParent, user, query).ConfigureAwait(false); } case CollectionType.Books: @@ -205,6 +213,9 @@ namespace MediaBrowser.Controller.Entities case SpecialFolder.MusicLatest: return GetMusicLatest(queryParent, user, query); + case SpecialFolder.MusicPlaylists: + return await GetMusicPlaylists(queryParent, user, query).ConfigureAwait(false); + case SpecialFolder.MusicAlbums: return GetMusicAlbums(queryParent, user, query); @@ -240,6 +251,16 @@ namespace MediaBrowser.Controller.Entities } } + private async Task> FindPlaylists(Folder parent, User user, InternalItemsQuery query) + { + var collectionFolders = user.RootFolder.GetChildren(user, true).Select(i => i.Id).ToList(); + + var list = _playlistManager.GetPlaylists(user.Id.ToString("N")) + .Where(i => i.GetChildren(user, true).Any(media => _libraryManager.GetCollectionFolders(media).Select(c => c.Id).Any(collectionFolders.Contains))); + + return GetResult(list, parent, query); + } + private int GetSpecialItemsLimit() { return 50; @@ -257,12 +278,13 @@ namespace MediaBrowser.Controller.Entities var list = new List(); list.Add(await GetUserView(SpecialFolder.MusicLatest, user, "0", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "1", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "2", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "3", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "4", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "5", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "6", parent).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.MusicPlaylists, user, "1", parent).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "2", parent).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "3", parent).ConfigureAwait(false)); + //list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "4", parent).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "5", parent).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "6", parent).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "7", parent).ConfigureAwait(false)); return GetResult(list, parent, query); } @@ -283,7 +305,7 @@ namespace MediaBrowser.Controller.Entities var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos }) .Where(i => !i.IsFolder) .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(i => { try @@ -313,7 +335,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => i.Genres.Contains(displayParent.Name, StringComparer.OrdinalIgnoreCase)) .OfType() .SelectMany(i => i.AlbumArtists) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(i => { try @@ -337,7 +359,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => !i.IsFolder) .OfType() .SelectMany(i => i.AlbumArtists) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(i => { try @@ -361,7 +383,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => !i.IsFolder) .OfType() .SelectMany(i => i.Artists) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(i => { try @@ -385,7 +407,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => !i.IsFolder) .OfType() .SelectMany(i => i.AlbumArtists) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(i => { try @@ -403,6 +425,14 @@ namespace MediaBrowser.Controller.Entities return GetResult(artists, parent, query); } + private Task> GetMusicPlaylists(Folder parent, User user, InternalItemsQuery query) + { + query.IncludeItemTypes = new[] { "Playlist" }; + query.Recursive = true; + + return parent.GetItems(query); + } + private QueryResult GetMusicAlbums(Folder parent, User user, InternalItemsQuery query) { var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos }, i => (i is MusicAlbum) && FilterItem(i, query)); @@ -552,7 +582,7 @@ namespace MediaBrowser.Controller.Entities var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Movies, CollectionType.BoxSets, string.Empty }) .Where(i => i is Movie) .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(i => { try @@ -724,7 +754,7 @@ namespace MediaBrowser.Controller.Entities var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.TvShows, string.Empty }) .OfType() .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(i => { try @@ -776,7 +806,7 @@ namespace MediaBrowser.Controller.Entities var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Games }) .OfType() .SelectMany(i => i.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase) + .DistinctNames() .Select(i => { try @@ -1749,17 +1779,26 @@ namespace MediaBrowser.Controller.Entities return parent.GetRecursiveChildren(user, filter); } - private async Task> GetLiveTvFolders(User user) + private async Task> GetLiveTvView(Folder queryParent, User user, InternalItemsQuery query) { - var list = new List(); + if (query.Recursive) + { + return await _liveTvManager.GetInternalRecordings(new RecordingQuery + { + IsInProgress = false, + Status = RecordingStatus.Completed, + UserId = user.Id.ToString("N") - var parent = user.RootFolder; + }, CancellationToken.None).ConfigureAwait(false); + } + + var list = new List(); //list.Add(await GetUserSubView(SpecialFolder.LiveTvNowPlaying, user, "0", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, parent).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, user.RootFolder).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, user.RootFolder).ConfigureAwait(false)); - return list; + return GetResult(list, queryParent, query); } private async Task GetUserView(string name, string type, User user, string sortName, BaseItem parent) diff --git a/MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs b/MediaBrowser.Controller/IO/ThrottledStream.cs similarity index 97% rename from MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs rename to MediaBrowser.Controller/IO/ThrottledStream.cs index 4bde30dac3..1df00b45a2 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/ThrottledStream.cs +++ b/MediaBrowser.Controller/IO/ThrottledStream.cs @@ -3,7 +3,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace MediaBrowser.Controller.IO { /// /// Class for streaming data with throttling support. @@ -15,8 +15,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// public const long Infinite = 0; - public Func ThrottleCallback { get; set; } - #region Private members /// /// The base stream. @@ -293,16 +291,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer return false; } - if (ThrottleCallback != null) - { - var val = ThrottleCallback(_maximumBytesPerSecond, _bytesWritten); - - if (val == 0) - { - return false; - } - } - return true; } diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 9cbbabc8db..a77d88049b 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -43,18 +43,10 @@ namespace MediaBrowser.Controller.Library /// The identifier. /// The user identifier. /// if set to true [enable path substitution]. + /// The supported live media types. /// The cancellation token. /// IEnumerable<MediaSourceInfo>. - Task> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken); - - /// - /// Gets the playack media sources. - /// - /// The identifier. - /// if set to true [enable path substitution]. - /// The cancellation token. - /// Task<IEnumerable<MediaSourceInfo>>. - Task> GetPlayackMediaSources(string id, bool enablePathSubstitution, CancellationToken cancellationToken); + Task> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken); /// /// Gets the static media sources. @@ -63,16 +55,8 @@ namespace MediaBrowser.Controller.Library /// if set to true [enable path substitution]. /// The user. /// IEnumerable<MediaSourceInfo>. - IEnumerable GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user); + IEnumerable GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null); - /// - /// Gets the static media sources. - /// - /// The item. - /// if set to true [enable path substitution]. - /// IEnumerable<MediaSourceInfo>. - IEnumerable GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution); - /// /// Gets the static media source. /// @@ -80,7 +64,7 @@ namespace MediaBrowser.Controller.Library /// The media source identifier. /// if set to true [enable path substitution]. /// MediaSourceInfo. - MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution); + Task GetMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution); /// /// Opens the media source. diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs new file mode 100644 index 0000000000..b2acdc7ea5 --- /dev/null +++ b/MediaBrowser.Controller/Library/NameExtensions.cs @@ -0,0 +1,41 @@ +using MediaBrowser.Common.Extensions; +using MoreLinq; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Controller.Library +{ + public static class NameExtensions + { + public static bool AreEqual(string name1, string name2) + { + name1 = NormalizeForComparison(name1); + name2 = NormalizeForComparison(name2); + + return string.Equals(name1, name2, StringComparison.OrdinalIgnoreCase); + } + + public static bool EqualsAny(IEnumerable names, string name) + { + name = NormalizeForComparison(name); + + return names.Any(i => string.Equals(NormalizeForComparison(i), name, StringComparison.OrdinalIgnoreCase)); + } + + private static string NormalizeForComparison(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + + return name.RemoveDiacritics(); + } + + public static IEnumerable DistinctNames(this IEnumerable names) + { + return names.DistinctBy(NormalizeForComparison, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index d5b5d92a6e..4ee0565f9b 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.LiveTv; @@ -74,10 +75,11 @@ namespace MediaBrowser.Controller.LiveTv /// Gets the recording. /// /// The identifier. - /// The user. + /// The options. /// The cancellation token. + /// The user. /// Task{RecordingInfoDto}. - Task GetRecording(string id, CancellationToken cancellationToken, User user = null); + Task GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null); /// /// Gets the channel. @@ -103,14 +105,15 @@ namespace MediaBrowser.Controller.LiveTv /// The cancellation token. /// Task{TimerInfoDto}. Task GetSeriesTimer(string id, CancellationToken cancellationToken); - + /// /// Gets the recordings. /// /// The query. + /// The options. /// The cancellation token. /// QueryResult{RecordingInfoDto}. - Task> GetRecordings(RecordingQuery query, CancellationToken cancellationToken); + Task> GetRecordings(RecordingQuery query, DtoOptions options, CancellationToken cancellationToken); /// /// Gets the timers. diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index d7e3df4e23..4ef4847a34 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -1,10 +1,9 @@ -using System; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Dto; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.LiveTv { diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 2fa62f79f2..b9a161a9f2 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -52,6 +52,10 @@ False ..\packages\morelinq.1.1.0\lib\net35\MoreLinq.dll + + False + ..\packages\Patterns.IO.1.0.0.3\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.IO.dll + @@ -115,6 +119,7 @@ + @@ -171,6 +176,7 @@ + @@ -184,6 +190,7 @@ + @@ -211,8 +218,8 @@ - + @@ -394,6 +401,7 @@ + diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index fe0fb32955..bb8841222a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -41,6 +41,8 @@ namespace MediaBrowser.Controller.MediaEncoding public int? SubtitleStreamIndex { get; set; } public int? MaxRefFrames { get; set; } public int? MaxVideoBitDepth { get; set; } + public int? CpuCoreLimit { get; set; } + public bool ReadInputAtNativeFramerate { get; set; } public SubtitleDeliveryMethod SubtitleMethod { get; set; } /// diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 47544f972b..5bec7980aa 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -63,16 +63,14 @@ namespace MediaBrowser.Controller.MediaEncoding string filenamePrefix, int? maxWidth, CancellationToken cancellationToken); - + /// /// Gets the media info. /// - /// The input files. - /// The protocol. - /// if set to true [is audio]. + /// The request. /// The cancellation token. /// Task. - Task GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio, CancellationToken cancellationToken); + Task GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken); /// /// Gets the probe size argument. diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index 530c127da7..da9dd4dfd2 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -1,9 +1,7 @@ -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; +using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; @@ -46,291 +44,5 @@ namespace MediaBrowser.Controller.MediaEncoding .Where(f => !string.IsNullOrEmpty(f)) .ToList(); } - - public static MediaInfo GetMediaInfo(InternalMediaInfoResult data) - { - var internalStreams = data.streams ?? new MediaStreamInfo[] { }; - - var info = new MediaInfo - { - MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format)) - .Where(i => i != null) - .ToList() - }; - - if (data.format != null) - { - info.Format = data.format.format_name; - - if (!string.IsNullOrEmpty(data.format.bit_rate)) - { - info.TotalBitrate = int.Parse(data.format.bit_rate, UsCulture); - } - } - - return info; - } - - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - /// - /// Converts ffprobe stream info to our MediaStream class - /// - /// The stream info. - /// The format info. - /// MediaStream. - private static MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo) - { - var stream = new MediaStream - { - Codec = streamInfo.codec_name, - Profile = streamInfo.profile, - Level = streamInfo.level, - Index = streamInfo.index, - PixelFormat = streamInfo.pix_fmt - }; - - if (streamInfo.tags != null) - { - stream.Language = GetDictionaryValue(streamInfo.tags, "language"); - } - - if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase)) - { - stream.Type = MediaStreamType.Audio; - - stream.Channels = streamInfo.channels; - - if (!string.IsNullOrEmpty(streamInfo.sample_rate)) - { - stream.SampleRate = int.Parse(streamInfo.sample_rate, UsCulture); - } - - stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout); - } - else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase)) - { - stream.Type = MediaStreamType.Subtitle; - } - else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase)) - { - stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1 - ? MediaStreamType.EmbeddedImage - : MediaStreamType.Video; - - stream.Width = streamInfo.width; - stream.Height = streamInfo.height; - stream.AspectRatio = GetAspectRatio(streamInfo); - - stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate); - stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate); - - stream.BitDepth = GetBitDepth(stream.PixelFormat); - - //stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) || - // string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) || - // string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase); - - stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase); - } - else - { - return null; - } - - // Get stream bitrate - var bitrate = 0; - - if (!string.IsNullOrEmpty(streamInfo.bit_rate)) - { - bitrate = int.Parse(streamInfo.bit_rate, UsCulture); - } - else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video) - { - // If the stream info doesn't have a bitrate get the value from the media format info - bitrate = int.Parse(formatInfo.bit_rate, UsCulture); - } - - if (bitrate > 0) - { - stream.BitRate = bitrate; - } - - if (streamInfo.disposition != null) - { - var isDefault = GetDictionaryValue(streamInfo.disposition, "default"); - var isForced = GetDictionaryValue(streamInfo.disposition, "forced"); - - stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase); - - stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase); - } - - return stream; - } - - private static int? GetBitDepth(string pixelFormat) - { - var eightBit = new List - { - "yuv420p", - "yuv411p", - "yuvj420p", - "uyyvyy411", - "nv12", - "nv21", - "rgb444le", - "rgb444be", - "bgr444le", - "bgr444be", - "yuvj411p" - }; - - if (!string.IsNullOrEmpty(pixelFormat)) - { - if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase)) - { - return 8; - } - } - - return null; - } - - /// - /// Gets a string from an FFProbeResult tags dictionary - /// - /// The tags. - /// The key. - /// System.String. - private static string GetDictionaryValue(Dictionary tags, string key) - { - if (tags == null) - { - return null; - } - - string val; - - tags.TryGetValue(key, out val); - return val; - } - - private static string ParseChannelLayout(string input) - { - if (string.IsNullOrEmpty(input)) - { - return input; - } - - return input.Split('(').FirstOrDefault(); - } - - private static string GetAspectRatio(MediaStreamInfo info) - { - var original = info.display_aspect_ratio; - - int height; - int width; - - var parts = (original ?? string.Empty).Split(':'); - if (!(parts.Length == 2 && - int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width) && - int.TryParse(parts[1], NumberStyles.Any, UsCulture, out height) && - width > 0 && - height > 0)) - { - width = info.width; - height = info.height; - } - - if (width > 0 && height > 0) - { - double ratio = width; - ratio /= height; - - if (IsClose(ratio, 1.777777778, .03)) - { - return "16:9"; - } - - if (IsClose(ratio, 1.3333333333, .05)) - { - return "4:3"; - } - - if (IsClose(ratio, 1.41)) - { - return "1.41:1"; - } - - if (IsClose(ratio, 1.5)) - { - return "1.5:1"; - } - - if (IsClose(ratio, 1.6)) - { - return "1.6:1"; - } - - if (IsClose(ratio, 1.66666666667)) - { - return "5:3"; - } - - if (IsClose(ratio, 1.85, .02)) - { - return "1.85:1"; - } - - if (IsClose(ratio, 2.35, .025)) - { - return "2.35:1"; - } - - if (IsClose(ratio, 2.4, .025)) - { - return "2.40:1"; - } - } - - return original; - } - - private static bool IsClose(double d1, double d2, double variance = .005) - { - return Math.Abs(d1 - d2) <= variance; - } - - /// - /// Gets a frame rate from a string value in ffprobe output - /// This could be a number or in the format of 2997/125. - /// - /// The value. - /// System.Nullable{System.Single}. - private static float? GetFrameRate(string value) - { - if (!string.IsNullOrEmpty(value)) - { - var parts = value.Split('/'); - - float result; - - if (parts.Length == 2) - { - result = float.Parse(parts[0], UsCulture) / float.Parse(parts[1], UsCulture); - } - else - { - result = float.Parse(parts[0], UsCulture); - } - - return float.IsNaN(result) ? (float?)null : result; - } - - return null; - } - } } diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs new file mode 100644 index 0000000000..24df7b8854 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class MediaInfoRequest + { + public string InputPath { get; set; } + public MediaProtocol Protocol { get; set; } + public bool ExtractChapters { get; set; } + public DlnaProfileType MediaType { get; set; } + public IIsoMount MountedIso { get; set; } + public VideoType VideoType { get; set; } + public List PlayableStreamFileNames { get; set; } + public bool ExtractKeyFrameInterval { get; set; } + + public MediaInfoRequest() + { + PlayableStreamFileNames = new List(); + } + } +} diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs index 6facc1074a..13f83c0fc9 100644 --- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs +++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs @@ -1404,24 +1404,12 @@ namespace MediaBrowser.Controller.Providers { switch (reader.Name) { - case "Name": - { - linkedItem.ItemName = reader.ReadElementContentAsString(); - break; - } - case "Path": { linkedItem.Path = reader.ReadElementContentAsString(); break; } - case "Type": - { - linkedItem.ItemType = reader.ReadElementContentAsString(); - break; - } - default: reader.Skip(); break; @@ -1435,7 +1423,7 @@ namespace MediaBrowser.Controller.Providers return linkedItem; } - return string.IsNullOrWhiteSpace(linkedItem.ItemName) || string.IsNullOrWhiteSpace(linkedItem.ItemType) ? null : linkedItem; + return null; } diff --git a/MediaBrowser.Controller/Providers/IImageEnhancer.cs b/MediaBrowser.Controller/Providers/IImageEnhancer.cs index e5a51a56e0..a43941607e 100644 --- a/MediaBrowser.Controller/Providers/IImageEnhancer.cs +++ b/MediaBrowser.Controller/Providers/IImageEnhancer.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index d40fa835fc..d6fc39c5f5 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -78,6 +78,19 @@ namespace MediaBrowser.Controller.Providers /// The cancellation token. /// Task. Task SaveImage(IHasImages item, Stream source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken); + + /// + /// Saves the image. + /// + /// The item. + /// The source. + /// Type of the MIME. + /// The type. + /// Index of the image. + /// The internal cache key. + /// The cancellation token. + /// Task. + Task SaveImage(IHasImages item, string source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken); /// /// Adds the metadata providers. diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs index f907de7290..cf868a3812 100644 --- a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs +++ b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs @@ -9,10 +9,10 @@ namespace MediaBrowser.Controller.Sync /// /// Gets the synced file information. /// - /// The remote path. + /// The identifier. /// The target. /// The cancellation token. /// Task<SyncedFileInfo>. - Task GetSyncedFileInfo(string remotePath, SyncTarget target, CancellationToken cancellationToken); + Task GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs new file mode 100644 index 0000000000..aeb7a3bff3 --- /dev/null +++ b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs @@ -0,0 +1,10 @@ + +namespace MediaBrowser.Controller.Sync +{ + /// + /// A marker interface + /// + public interface IRemoteSyncProvider + { + } +} diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs index 46bbbd3299..6b694d26d9 100644 --- a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs +++ b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs @@ -1,6 +1,7 @@ -using MediaBrowser.Model.Sync; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Sync; +using Patterns.IO; using System; -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -13,46 +14,39 @@ namespace MediaBrowser.Controller.Sync /// Transfers the file. /// /// The stream. - /// The remote path. + /// The path parts. /// The target. /// The progress. /// The cancellation token. /// Task. - Task SendFile(Stream stream, string remotePath, SyncTarget target, IProgress progress, CancellationToken cancellationToken); + Task SendFile(Stream stream, string[] pathParts, SyncTarget target, IProgress progress, CancellationToken cancellationToken); /// /// Deletes the file. /// - /// The path. + /// The identifier. /// The target. /// The cancellation token. /// Task. - Task DeleteFile(string path, SyncTarget target, CancellationToken cancellationToken); + Task DeleteFile(string id, SyncTarget target, CancellationToken cancellationToken); /// /// Gets the file. /// - /// The path. + /// The identifier. /// The target. /// The progress. /// The cancellation token. /// Task<Stream>. - Task GetFile(string path, SyncTarget target, IProgress progress, CancellationToken cancellationToken); + Task GetFile(string id, SyncTarget target, IProgress progress, CancellationToken cancellationToken); /// - /// Gets the full path. + /// Gets the files. /// - /// The path. + /// The query. /// The target. - /// System.String. - string GetFullPath(IEnumerable path, SyncTarget target); - - /// - /// Gets the parent directory path. - /// - /// The path. - /// The target. - /// System.String. - string GetParentDirectoryPath(string path, SyncTarget target); + /// The cancellation token. + /// Task<QueryResult<FileMetadata>>. + Task> GetFiles(FileQuery query, SyncTarget target, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Sync/ISyncDataProvider.cs b/MediaBrowser.Controller/Sync/ISyncDataProvider.cs index dc3edc4e40..ebff50cb02 100644 --- a/MediaBrowser.Controller/Sync/ISyncDataProvider.cs +++ b/MediaBrowser.Controller/Sync/ISyncDataProvider.cs @@ -7,20 +7,12 @@ namespace MediaBrowser.Controller.Sync public interface ISyncDataProvider { /// - /// Gets the server item ids. + /// Gets the local items. /// /// The target. /// The server identifier. - /// Task<List<System.String>>. - Task> GetServerItemIds(SyncTarget target, string serverId); - - /// - /// Gets the synchronize job item ids. - /// - /// The target. - /// The server identifier. - /// Task<List<System.String>>. - Task> GetSyncJobItemIds(SyncTarget target, string serverId); + /// Task<List<LocalItem>>. + Task> GetLocalItems(SyncTarget target, string serverId); /// /// Adds the or update. diff --git a/MediaBrowser.Controller/Sync/ISyncManager.cs b/MediaBrowser.Controller/Sync/ISyncManager.cs index 3b6e20edc9..97591551c1 100644 --- a/MediaBrowser.Controller/Sync/ISyncManager.cs +++ b/MediaBrowser.Controller/Sync/ISyncManager.cs @@ -174,6 +174,13 @@ namespace MediaBrowser.Controller.Sync /// The target identifier. /// IEnumerable<SyncQualityOption>. IEnumerable GetQualityOptions(string targetId); + /// + /// Gets the quality options. + /// + /// The target identifier. + /// The user. + /// IEnumerable<SyncQualityOption>. + IEnumerable GetQualityOptions(string targetId, User user); /// /// Gets the profile options. @@ -181,5 +188,12 @@ namespace MediaBrowser.Controller.Sync /// The target identifier. /// IEnumerable<SyncQualityOption>. IEnumerable GetProfileOptions(string targetId); + /// + /// Gets the profile options. + /// + /// The target identifier. + /// The user. + /// IEnumerable<SyncProfileOption>. + IEnumerable GetProfileOptions(string targetId, User user); } } diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs index 550af2d554..844e7d890d 100644 --- a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs +++ b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs @@ -20,6 +20,11 @@ namespace MediaBrowser.Controller.Sync /// /// The required HTTP headers. public Dictionary RequiredHttpHeaders { get; set; } + /// + /// Gets or sets the identifier. + /// + /// The identifier. + public string Id { get; set; } public SyncedFileInfo() { diff --git a/MediaBrowser.Controller/packages.config b/MediaBrowser.Controller/packages.config index 6df1662040..8d10de2f1b 100644 --- a/MediaBrowser.Controller/packages.config +++ b/MediaBrowser.Controller/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs index 5ccea52bad..abd649ad7b 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs +++ b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs @@ -223,7 +223,7 @@ namespace MediaBrowser.Dlna.ContentDirectory if (string.Equals(flag, "BrowseMetadata")) { totalCount = 1; - + if (item.IsFolder || serverItem.StubType.HasValue) { var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false)); @@ -350,7 +350,7 @@ namespace MediaBrowser.Dlna.ContentDirectory }; } - private async Task> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) + private Task> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) { var folder = (Folder)item; @@ -389,7 +389,7 @@ namespace MediaBrowser.Dlna.ContentDirectory isFolder = true; } - return await folder.GetItems(new InternalItemsQuery + return folder.GetItems(new InternalItemsQuery { Limit = limit, StartIndex = startIndex, @@ -401,7 +401,7 @@ namespace MediaBrowser.Dlna.ContentDirectory IsFolder = isFolder, MediaTypes = mediaTypes.ToArray() - }).ConfigureAwait(false); + }); } private async Task> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit) diff --git a/MediaBrowser.Dlna/Didl/DidlBuilder.cs b/MediaBrowser.Dlna/Didl/DidlBuilder.cs index 3b1cdb5428..629b95f67e 100644 --- a/MediaBrowser.Dlna/Didl/DidlBuilder.cs +++ b/MediaBrowser.Dlna/Didl/DidlBuilder.cs @@ -12,6 +12,7 @@ using MediaBrowser.Dlna.ContentDirectory; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using System; using System.Globalization; @@ -124,9 +125,9 @@ namespace MediaBrowser.Dlna.Didl { if (streamInfo == null) { - var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(video, true).ToList() : _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList(); + var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList(); - streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions + streamInfo = new StreamBuilder(new NullLogger()).BuildVideoItem(new VideoOptions { ItemId = GetClientId(video), MediaSources = sources, @@ -351,9 +352,9 @@ namespace MediaBrowser.Dlna.Didl if (streamInfo == null) { - var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(audio, true).ToList() : _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList(); + var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList(); - streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions + streamInfo = new StreamBuilder(new NullLogger()).BuildAudioItem(new AudioOptions { ItemId = GetClientId(audio), MediaSources = sources, diff --git a/MediaBrowser.Dlna/PlayTo/PlayToController.cs b/MediaBrowser.Dlna/PlayTo/PlayToController.cs index 38c0f71cc7..9df69b1157 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToController.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToController.cs @@ -470,7 +470,7 @@ namespace MediaBrowser.Dlna.PlayTo var hasMediaSources = item as IHasMediaSources; var mediaSources = hasMediaSources != null - ? (user == null ? _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true) : _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList() + ? (_mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList() : new List(); var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex); @@ -542,7 +542,7 @@ namespace MediaBrowser.Dlna.PlayTo { return new PlaylistItem { - StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions + StreamInfo = new StreamBuilder(_logger).BuildVideoItem(new VideoOptions { ItemId = item.Id.ToString("N"), MediaSources = mediaSources, @@ -562,7 +562,7 @@ namespace MediaBrowser.Dlna.PlayTo { return new PlaylistItem { - StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions + StreamInfo = new StreamBuilder(_logger).BuildAudioItem(new AudioOptions { ItemId = item.Id.ToString("N"), MediaSources = mediaSources, @@ -892,7 +892,7 @@ namespace MediaBrowser.Dlna.PlayTo request.MediaSource = hasMediaSources == null ? null : - mediaSourceManager.GetStaticMediaSource(hasMediaSources, request.MediaSourceId, false); + mediaSourceManager.GetMediaSource(hasMediaSources, request.MediaSourceId, false).Result; diff --git a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs index 8ca16832da..73bc4984c8 100644 --- a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs +++ b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs @@ -62,16 +62,22 @@ namespace MediaBrowser.Dlna.Ssdp { if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase)) { - TimeSpan delay = GetSearchDelay(args.Headers); + var headers = args.Headers; + + TimeSpan delay = GetSearchDelay(headers); if (_config.GetDlnaConfiguration().EnableDebugLogging) { _logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds); } - await Task.Delay(delay).ConfigureAwait(false); + await Task.Delay(delay).ConfigureAwait(false); - RespondToSearch(args.EndPoint, args.Headers["st"]); + string st; + if (headers.TryGetValue("st", out st)) + { + RespondToSearch(args.EndPoint, st); + } } EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger); diff --git a/MediaBrowser.LocalMetadata/BaseXmlProvider.cs b/MediaBrowser.LocalMetadata/BaseXmlProvider.cs index afe4b5799b..154d026008 100644 --- a/MediaBrowser.LocalMetadata/BaseXmlProvider.cs +++ b/MediaBrowser.LocalMetadata/BaseXmlProvider.cs @@ -92,7 +92,7 @@ namespace MediaBrowser.LocalMetadata { get { - return "Media Browser Xml"; + return "Emby Xml"; } } diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs index 31329e6d7d..0089a71d52 100644 --- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs @@ -10,7 +10,7 @@ using System.Linq; namespace MediaBrowser.LocalMetadata.Images { - public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider + public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider, IHasOrder { private readonly IFileSystem _fileSystem; @@ -24,6 +24,11 @@ namespace MediaBrowser.LocalMetadata.Images get { return "Local Images"; } } + public int Order + { + get { return 0; } + } + public bool Supports(IHasImages item) { return item is Episode && item.SupportsLocalMetadata; diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs index 60533797ab..f118c2763f 100644 --- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -26,6 +26,11 @@ namespace MediaBrowser.LocalMetadata.Images public bool Supports(IHasImages item) { + if (item is Photo) + { + return false; + } + if (!item.IsSaveLocalMetadataEnabled()) { return true; diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 6083c88dea..ba87b3c43b 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -12,7 +12,7 @@ using System.Linq; namespace MediaBrowser.LocalMetadata.Images { - public class LocalImageProvider : ILocalImageFileProvider + public class LocalImageProvider : ILocalImageFileProvider, IHasOrder { private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs index c59d574bf4..1b98e75bef 100644 --- a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs +++ b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs @@ -756,11 +756,6 @@ namespace MediaBrowser.LocalMetadata.Savers { builder.Append("<" + singularNodeName + ">"); - if (!string.IsNullOrWhiteSpace(link.ItemType)) - { - builder.Append("" + SecurityElement.Escape(link.ItemType) + ""); - } - if (!string.IsNullOrWhiteSpace(link.Path)) { builder.Append("" + SecurityElement.Escape((link.Path)) + ""); diff --git a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs index 78ac92f25b..c30ceb62db 100644 --- a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs @@ -70,10 +70,7 @@ namespace MediaBrowser.MediaEncoding.Encoder encodingJob.OutputFilePath = GetOutputFilePath(encodingJob); Directory.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath)); - if (options.Context == EncodingContext.Static && encodingJob.IsInputVideo) - { - encodingJob.ReadInputAtNativeFramerate = true; - } + encodingJob.ReadInputAtNativeFramerate = options.ReadInputAtNativeFramerate; await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false); @@ -305,19 +302,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// System.Int32. protected int GetNumberOfThreads(EncodingJob job, bool isWebm) { - // Only need one thread for sync - if (job.Options.Context == EncodingContext.Static) - { - return 1; - } - - if (isWebm) - { - // Recommended per docs - return Math.Max(Environment.ProcessorCount - 1, 2); - } - - return 0; + return job.Options.CpuCoreLimit ?? 0; } protected EncodingQuality GetQualitySetting() diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs index 8d82010749..ea3cc6d898 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); - var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, false, cancellationToken).ConfigureAwait(false); + var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false); var mediaSource = string.IsNullOrEmpty(request.MediaSourceId) ? mediaSources.First() @@ -124,10 +124,14 @@ namespace MediaBrowser.MediaEncoding.Encoder state.InputContainer = mediaSource.Container; state.InputFileSize = mediaSource.Size; state.InputBitrate = mediaSource.Bitrate; - state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; state.RunTimeTicks = mediaSource.RunTimeTicks; state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; + if (mediaSource.ReadAtNativeFramerate) + { + state.ReadInputAtNativeFramerate = true; + } + if (mediaSource.VideoType.HasValue) { state.VideoType = mediaSource.VideoType.Value; @@ -148,7 +152,6 @@ namespace MediaBrowser.MediaEncoding.Encoder state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; state.InputBitrate = mediaSource.Bitrate; state.InputFileSize = mediaSource.Size; - state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; if (state.ReadInputAtNativeFramerate || mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4258898073..df7351ad1e 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -5,12 +5,15 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Session; +using MediaBrowser.MediaEncoding.Probing; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; @@ -72,6 +75,8 @@ namespace MediaBrowser.MediaEncoding.Encoder protected readonly Func SubtitleEncoder; protected readonly Func MediaSourceManager; + private readonly List _runningProcesses = new List(); + public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func subtitleEncoder, Func mediaSourceManager) { _logger = logger; @@ -102,16 +107,19 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// Gets the media info. /// - /// The input files. - /// The protocol. - /// if set to true [is audio]. + /// The request. /// The cancellation token. /// Task. - public Task GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio, - CancellationToken cancellationToken) + public Task GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { - return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), !isAudio, - GetProbeSizeArgument(inputFiles, protocol), cancellationToken); + var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; + + var inputFiles = MediaEncoderHelpers.GetInputArgument(request.InputPath, request.Protocol, request.MountedIso, request.PlayableStreamFileNames); + + var extractKeyFrameInterval = request.ExtractKeyFrameInterval && request.Protocol == MediaProtocol.File && request.VideoType == VideoType.VideoFile; + + return GetMediaInfoInternal(GetInputArgument(inputFiles, request.Protocol), request.InputPath, request.Protocol, extractChapters, extractKeyFrameInterval, + GetProbeSizeArgument(inputFiles, request.Protocol), request.MediaType == DlnaProfileType.Audio, cancellationToken); } /// @@ -141,13 +149,22 @@ namespace MediaBrowser.MediaEncoding.Encoder /// Gets the media info internal. /// /// The input path. + /// The primary path. + /// The protocol. /// if set to true [extract chapters]. + /// if set to true [extract key frame interval]. /// The probe size argument. + /// if set to true [is audio]. /// The cancellation token. /// Task{MediaInfoResult}. /// - private async Task GetMediaInfoInternal(string inputPath, bool extractChapters, + private async Task GetMediaInfoInternal(string inputPath, + string primaryPath, + MediaProtocol protocol, + bool extractChapters, + bool extractKeyFrameInterval, string probeSizeArgument, + bool isAudio, CancellationToken cancellationToken) { var args = extractChapters @@ -164,6 +181,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Must consume both or ffmpeg may hang due to deadlocks. See comments below. RedirectStandardOutput = true, RedirectStandardError = true, + RedirectStandardInput = true, FileName = FFProbePath, Arguments = string.Format(args, probeSizeArgument, inputPath).Trim(), @@ -177,15 +195,13 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - process.Exited += ProcessExited; - await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - InternalMediaInfoResult result; + var processWrapper = new ProcessWrapper(process, this); try { - process.Start(); + StartProcess(processWrapper); } catch (Exception ex) { @@ -200,19 +216,57 @@ namespace MediaBrowser.MediaEncoding.Encoder { process.BeginErrorReadLine(); - result = _jsonSerializer.DeserializeFromStream(process.StandardOutput.BaseStream); + var result = _jsonSerializer.DeserializeFromStream(process.StandardOutput.BaseStream); + + if (result != null) + { + if (result.streams != null) + { + // Normalize aspect ratio if invalid + foreach (var stream in result.streams) + { + if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + { + stream.display_aspect_ratio = string.Empty; + } + if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + { + stream.sample_aspect_ratio = string.Empty; + } + } + } + + var mediaInfo = new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, isAudio, primaryPath, protocol); + + if (extractKeyFrameInterval && mediaInfo.RunTimeTicks.HasValue) + { + foreach (var stream in mediaInfo.MediaStreams) + { + if (stream.Type == MediaStreamType.Video && string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase)) + { + try + { + //stream.KeyFrames = await GetKeyFrames(inputPath, stream.Index, cancellationToken) + // .ConfigureAwait(false); + } + catch (OperationCanceledException) + { + + } + catch (Exception ex) + { + _logger.ErrorException("Error getting key frame interval", ex); + } + } + } + } + + return mediaInfo; + } } catch { - // Hate having to do this - try - { - process.Kill(); - } - catch (Exception ex1) - { - _logger.ErrorException("Error killing ffprobe", ex1); - } + StopProcess(processWrapper, 100, true); throw; } @@ -221,30 +275,102 @@ namespace MediaBrowser.MediaEncoding.Encoder _ffProbeResourcePool.Release(); } - if (result == null) + throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath)); + } + + private async Task> GetKeyFrames(string inputPath, int videoStreamIndex, CancellationToken cancellationToken) + { + const string args = "-i {0} -select_streams v:{1} -show_frames -show_entries frame=pkt_dts,key_frame -print_format compact"; + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + + // Must consume both or ffmpeg may hang due to deadlocks. See comments below. + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + FileName = FFProbePath, + Arguments = string.Format(args, inputPath, videoStreamIndex.ToString(CultureInfo.InvariantCulture)).Trim(), + + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + + EnableRaisingEvents = true + }; + + _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + + var processWrapper = new ProcessWrapper(process, this); + + StartProcess(processWrapper); + + var lines = new List(); + + try { - throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath)); + process.BeginErrorReadLine(); + + await StartReadingOutput(process.StandardOutput.BaseStream, lines, 120000, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + if (cancellationToken.IsCancellationRequested) + { + throw; + } + } + finally + { + StopProcess(processWrapper, 100, true); } - cancellationToken.ThrowIfCancellationRequested(); + return lines; + } - if (result.streams != null) + private async Task StartReadingOutput(Stream source, List lines, int timeoutMs, CancellationToken cancellationToken) + { + try { - // Normalize aspect ratio if invalid - foreach (var stream in result.streams) + using (var reader = new StreamReader(source)) { - if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) - { - stream.display_aspect_ratio = string.Empty; - } - if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase)) + while (!reader.EndOfStream) { - stream.sample_aspect_ratio = string.Empty; + cancellationToken.ThrowIfCancellationRequested(); + + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + var values = (line ?? string.Empty).Split('|') + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(i => i.Split('=')) + .Where(i => i.Length == 2) + .ToDictionary(i => i[0], i => i[1]); + + string pktDts; + int frameMs; + if (values.TryGetValue("pkt_dts", out pktDts) && int.TryParse(pktDts, NumberStyles.Any, CultureInfo.InvariantCulture, out frameMs)) + { + string keyFrame; + if (values.TryGetValue("key_frame", out keyFrame) && string.Equals(keyFrame, "1", StringComparison.OrdinalIgnoreCase)) + { + lines.Add(frameMs); + } + } } } } - - return result; + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error reading ffprobe output", ex); + } } /// @@ -252,16 +378,6 @@ namespace MediaBrowser.MediaEncoding.Encoder /// protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// - /// Processes the exited. - /// - /// The sender. - /// The instance containing the event data. - private void ProcessExited(object sender, EventArgs e) - { - ((Process)sender).Dispose(); - } - public Task ExtractAudioImage(string path, CancellationToken cancellationToken) { return ExtractImage(new[] { path }, MediaProtocol.File, true, null, null, cancellationToken); @@ -286,6 +402,10 @@ namespace MediaBrowser.MediaEncoding.Encoder { return await ExtractImageInternal(inputArgument, protocol, threedFormat, offset, true, resourcePool, cancellationToken).ConfigureAwait(false); } + catch (ArgumentException) + { + throw; + } catch { _logger.Error("I-frame image extraction failed, will attempt standard way. Input: {0}", inputArgument); @@ -368,7 +488,9 @@ namespace MediaBrowser.MediaEncoding.Encoder await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - process.Start(); + var processWrapper = new ProcessWrapper(process, this); + + StartProcess(processWrapper); var memoryStream = new MemoryStream(); @@ -384,23 +506,12 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!ranToCompletion) { - try - { - _logger.Info("Killing ffmpeg process"); - - process.StandardInput.WriteLine("q"); - - process.WaitForExit(1000); - } - catch (Exception ex) - { - _logger.ErrorException("Error killing process", ex); - } + StopProcess(processWrapper, 1000, false); } resourcePool.Release(); - var exitCode = ranToCompletion ? process.ExitCode : -1; + var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; process.Dispose(); @@ -419,31 +530,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return memoryStream; } - public Task EncodeImage(ImageEncodingOptions options, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Dispose(true); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - _videoImageResourcePool.Dispose(); - } - } - public string GetTimeParameter(long ticks) { var time = TimeSpan.FromTicks(ticks); @@ -510,9 +596,11 @@ namespace MediaBrowser.MediaEncoding.Encoder bool ranToCompletion; + var processWrapper = new ProcessWrapper(process, this); + try { - process.Start(); + StartProcess(processWrapper); // Need to give ffmpeg enough time to make all the thumbnails, which could be a while, // but we still need to detect if the process hangs. @@ -536,18 +624,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!ranToCompletion) { - try - { - _logger.Info("Killing ffmpeg process"); - - process.StandardInput.WriteLine("q"); - - process.WaitForExit(1000); - } - catch (Exception ex) - { - _logger.ErrorException("Error killing process", ex); - } + StopProcess(processWrapper, 1000, false); } } finally @@ -555,7 +632,7 @@ namespace MediaBrowser.MediaEncoding.Encoder resourcePool.Release(); } - var exitCode = ranToCompletion ? process.ExitCode : -1; + var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; process.Dispose(); @@ -608,5 +685,122 @@ namespace MediaBrowser.MediaEncoding.Encoder return job.OutputFilePath; } + + private void StartProcess(ProcessWrapper process) + { + process.Process.Start(); + + lock (_runningProcesses) + { + _runningProcesses.Add(process); + } + } + private void StopProcess(ProcessWrapper process, int waitTimeMs, bool enableForceKill) + { + try + { + _logger.Info("Killing ffmpeg process"); + + try + { + process.Process.StandardInput.WriteLine("q"); + } + catch (Exception) + { + _logger.Error("Error sending q command to process"); + } + + try + { + if (process.Process.WaitForExit(waitTimeMs)) + { + return; + } + } + catch (Exception ex) + { + _logger.Error("Error in WaitForExit", ex); + } + + if (enableForceKill) + { + process.Process.Kill(); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error killing process", ex); + } + } + + private void StopProcesses() + { + List proceses; + lock (_runningProcesses) + { + proceses = _runningProcesses.ToList(); + } + _runningProcesses.Clear(); + + foreach (var process in proceses) + { + if (!process.HasExited) + { + StopProcess(process, 500, true); + } + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + _videoImageResourcePool.Dispose(); + StopProcesses(); + } + } + + private class ProcessWrapper + { + public readonly Process Process; + public bool HasExited; + public int? ExitCode; + private readonly MediaEncoder _mediaEncoder; + + public ProcessWrapper(Process process, MediaEncoder mediaEncoder) + { + Process = process; + this._mediaEncoder = mediaEncoder; + Process.Exited += Process_Exited; + } + + void Process_Exited(object sender, EventArgs e) + { + var process = (Process)sender; + + HasExited = true; + + ExitCode = process.ExitCode; + + lock (_mediaEncoder._runningProcesses) + { + _mediaEncoder._runningProcesses.Remove(this); + } + + process.Dispose(); + } + } } } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 72dc0feac5..ad561c4849 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -68,6 +68,9 @@ + + + @@ -91,6 +94,10 @@ {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} MediaBrowser.Controller + + {6e4145e4-c6d4-4e4d-94f2-87188db6e239} + MediaBrowser.MediaInfo + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} MediaBrowser.Model @@ -99,7 +106,9 @@ - + + +