Merge pull request #6689 from 1337joe/expand-image-extraction

pull/6735/head
Claus Vium 3 years ago committed by GitHub
commit 768ec60e11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -95,9 +95,10 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="mediaSource">Media source information.</param>
/// <param name="imageStream">Media stream information.</param>
/// <param name="imageStreamIndex">Index of the stream to extract from.</param>
/// <param name="outputExtension">The extension of the file to write, including the '.'.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
/// <returns>Location of video image.</returns>
Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken);
Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken);
/// <summary>
/// Extracts the video images on interval.

@ -469,17 +469,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
Protocol = MediaProtocol.File
};
return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken);
return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ".jpg", cancellationToken);
}
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
{
return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken);
return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ".jpg", cancellationToken);
}
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken)
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, string outputExtension, CancellationToken cancellationToken)
{
return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken);
return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, outputExtension, cancellationToken);
}
private async Task<string> ExtractImage(
@ -491,6 +491,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool isAudio,
Video3DFormat? threedFormat,
TimeSpan? offset,
string outputExtension,
CancellationToken cancellationToken)
{
var inputArgument = GetInputArgument(inputFile, mediaSource);
@ -500,7 +501,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter.
try
{
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, cancellationToken).ConfigureAwait(false);
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, outputExtension, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@ -513,7 +514,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, cancellationToken).ConfigureAwait(false);
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, outputExtension, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@ -526,7 +527,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, cancellationToken).ConfigureAwait(false);
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, outputExtension, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@ -538,17 +539,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, cancellationToken).ConfigureAwait(false);
return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, outputExtension, cancellationToken).ConfigureAwait(false);
}
private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, CancellationToken cancellationToken)
private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, string outputExtension, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException(nameof(inputPath));
}
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
if (string.IsNullOrEmpty(outputExtension))
{
outputExtension = ".jpg";
}
else if (outputExtension[0] != '.')
{
outputExtension = "." + outputExtension;
}
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar.

@ -582,7 +582,8 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <returns>MediaAttachments.</returns>
private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
{
if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase))
if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)
&& streamInfo.Disposition?.GetValueOrDefault("attached_pic") != 1)
{
return null;
}

@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
/// Uses <see cref="IMediaEncoder"/> to extract embedded images.
/// </summary>
public class EmbeddedImageProvider : IDynamicImageProvider, IHasOrder
{
private static readonly string[] _primaryImageFileNames =
{
"poster",
"folder",
"cover",
"default"
};
private static readonly string[] _backdropImageFileNames =
{
"backdrop",
"fanart",
"background",
"art"
};
private static readonly string[] _logoImageFileNames =
{
"logo",
};
private readonly IMediaEncoder _mediaEncoder;
/// <summary>
/// Initializes a new instance of the <see cref="EmbeddedImageProvider"/> class.
/// </summary>
/// <param name="mediaEncoder">The media encoder for extracting attached/embedded images.</param>
public EmbeddedImageProvider(IMediaEncoder mediaEncoder)
{
_mediaEncoder = mediaEncoder;
}
/// <inheritdoc />
public string Name => "Embedded Image Extractor";
/// <inheritdoc />
// Default to after internet image providers but before Screen Grabber
public int Order => 99;
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
if (item is Video)
{
if (item is Episode)
{
return new[]
{
ImageType.Primary,
};
}
return new[]
{
ImageType.Primary,
ImageType.Backdrop,
ImageType.Logo,
};
}
return Array.Empty<ImageType>();
}
/// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
var video = (Video)item;
// No support for these
if (video.IsPlaceHolder || video.VideoType == VideoType.Dvd)
{
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
return GetEmbeddedImage(video, type, cancellationToken);
}
private async Task<DynamicImageResponse> GetEmbeddedImage(Video item, ImageType type, CancellationToken cancellationToken)
{
MediaSourceInfo mediaSource = new MediaSourceInfo
{
VideoType = item.VideoType,
IsoType = item.IsoType,
Protocol = item.PathProtocol ?? MediaProtocol.File,
};
string[] imageFileNames = type switch
{
ImageType.Primary => _primaryImageFileNames,
ImageType.Backdrop => _backdropImageFileNames,
ImageType.Logo => _logoImageFileNames,
_ => _primaryImageFileNames
};
// Try attachments first
var attachmentStream = item.GetMediaSources(false)
.SelectMany(source => source.MediaAttachments)
.FirstOrDefault(attachment => !string.IsNullOrEmpty(attachment.FileName)
&& imageFileNames.Any(name => attachment.FileName.Contains(name, StringComparison.OrdinalIgnoreCase)));
if (attachmentStream != null)
{
return await ExtractAttachment(item, cancellationToken, attachmentStream, mediaSource);
}
// Fall back to EmbeddedImage streams
var imageStreams = item.GetMediaStreams().FindAll(i => i.Type == MediaStreamType.EmbeddedImage);
if (imageStreams.Count == 0)
{
// Can't extract if we don't have any EmbeddedImage streams
return new DynamicImageResponse { HasImage = false };
}
// Extract first stream containing an element of imageFileNames
var imageStream = imageStreams
.FirstOrDefault(stream => !string.IsNullOrEmpty(stream.Comment)
&& imageFileNames.Any(name => stream.Comment.Contains(name, StringComparison.OrdinalIgnoreCase)));
// Primary type only: default to first image if none found by label
if (imageStream == null)
{
if (type == ImageType.Primary)
{
imageStream = imageStreams[0];
}
else
{
// No streams matched, abort
return new DynamicImageResponse { HasImage = false };
}
}
string extractedImagePath =
await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, ".jpg", cancellationToken)
.ConfigureAwait(false);
return new DynamicImageResponse
{
Format = ImageFormat.Jpg,
HasImage = true,
Path = extractedImagePath,
Protocol = MediaProtocol.File
};
}
private async Task<DynamicImageResponse> ExtractAttachment(Video item, CancellationToken cancellationToken, MediaAttachment attachmentStream, MediaSourceInfo mediaSource)
{
var extension = string.IsNullOrEmpty(attachmentStream.MimeType)
? Path.GetExtension(attachmentStream.FileName)
: MimeTypes.ToExtension(attachmentStream.MimeType);
if (string.IsNullOrEmpty(extension))
{
extension = ".jpg";
}
string extractedAttachmentPath =
await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken)
.ConfigureAwait(false);
ImageFormat format = extension switch
{
".bmp" => ImageFormat.Bmp,
".gif" => ImageFormat.Gif,
".jpg" => ImageFormat.Jpg,
".png" => ImageFormat.Png,
".webp" => ImageFormat.Webp,
_ => ImageFormat.Jpg
};
return new DynamicImageResponse
{
Format = format,
HasImage = true,
Path = extractedAttachmentPath,
Protocol = MediaProtocol.File
};
}
/// <inheritdoc />
public bool Supports(BaseItem item)
{
if (item.IsShortcut)
{
return false;
}
if (!item.IsFileProtocol)
{
return false;
}
return item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia;
}
}
}

@ -1,6 +1,3 @@
#nullable enable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
@ -17,11 +14,19 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
/// Uses <see cref="IMediaEncoder"/> to create still images from the main video.
/// </summary>
public class VideoImageProvider : IDynamicImageProvider, IHasOrder
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger<VideoImageProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="VideoImageProvider"/> class.
/// </summary>
/// <param name="mediaEncoder">The media encoder for capturing images.</param>
/// <param name="logger">The logger.</param>
public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger)
{
_mediaEncoder = mediaEncoder;
@ -71,36 +76,22 @@ namespace MediaBrowser.Providers.MediaInfo
Protocol = item.PathProtocol ?? MediaProtocol.File,
};
var mediaStreams =
item.GetMediaStreams();
var imageStreams =
mediaStreams
.Where(i => i.Type == MediaStreamType.EmbeddedImage)
.ToList();
// If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
// Always use 10 seconds for dvd because our duration could be out of whack
var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
item.RunTimeTicks.Value > 0
? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10)
: TimeSpan.FromSeconds(10);
string extractedImagePath;
var videoStream = item.GetDefaultVideoStream() ?? item.GetMediaStreams().FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (imageStreams.Count == 0)
if (videoStream == null)
{
// If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
// Always use 10 seconds for dvd because our duration could be out of whack
var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
item.RunTimeTicks.Value > 0
? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10)
: TimeSpan.FromSeconds(10);
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Skipping image extraction: no video stream found for {Path}.", item.Path ?? string.Empty);
return new DynamicImageResponse { HasImage = false };
}
else
{
var imageStream = imageStreams.Find(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase))
?? imageStreams.Find(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase))
?? imageStreams[0];
extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, cancellationToken).ConfigureAwait(false);
}
string extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
return new DynamicImageResponse
{

@ -0,0 +1,216 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Providers.MediaInfo;
using Moq;
using Xunit;
namespace Jellyfin.Providers.Tests.MediaInfo
{
public class EmbeddedImageProviderTests
{
private static TheoryData<BaseItem> GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData()
{
return new ()
{
new AudioBook(),
new BoxSet(),
new Series(),
new Season(),
};
}
[Theory]
[MemberData(nameof(GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData))]
public void GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty(BaseItem item)
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
Assert.Empty(embeddedImageProvider.GetSupportedImages(item));
}
private static TheoryData<BaseItem, IEnumerable<ImageType>> GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData()
{
return new TheoryData<BaseItem, IEnumerable<ImageType>>
{
{ new Episode(), new List<ImageType> { ImageType.Primary } },
{ new Movie(), new List<ImageType> { ImageType.Logo, ImageType.Backdrop, ImageType.Primary } },
};
}
[Theory]
[MemberData(nameof(GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData))]
public void GetSupportedImages_SupportedBaseItems_ReturnsPopulated(BaseItem item, IEnumerable<ImageType> expected)
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
var actual = embeddedImageProvider.GetSupportedImages(item);
Assert.Equal(expected.OrderBy(i => i.ToString()), actual.OrderBy(i => i.ToString()));
}
[Fact]
public async void GetImage_InputWithNoStreams_ReturnsNoImage()
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
var input = GetMovie(new List<MediaAttachment>(), new List<MediaStream>());
var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_InputWithUnlabeledAttachments_ReturnsNoImage()
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
// add an attachment without a filename - has a list to look through but finds nothing
var input = GetMovie(
new List<MediaAttachment> { new () },
new List<MediaStream>());
var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_InputWithLabeledAttachments_ReturnsCorrectSelection()
{
// first tests file extension detection, second uses mimetype, third defaults to jpg
MediaAttachment sampleAttachment1 = new () { FileName = "clearlogo.png", Index = 1 };
MediaAttachment sampleAttachment2 = new () { FileName = "backdrop", MimeType = "image/bmp", Index = 2 };
MediaAttachment sampleAttachment3 = new () { FileName = "poster", Index = 3 };
string targetPath1 = "path1.png";
string targetPath2 = "path2.bmp";
string targetPath3 = "path2.jpg";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 1, ".png", CancellationToken.None))
.Returns(Task.FromResult(targetPath1));
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 2, ".bmp", CancellationToken.None))
.Returns(Task.FromResult(targetPath2));
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 3, ".jpg", CancellationToken.None))
.Returns(Task.FromResult(targetPath3));
var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
var input = GetMovie(
new List<MediaAttachment> { sampleAttachment1, sampleAttachment2, sampleAttachment3 },
new List<MediaStream>());
var actualLogo = await embeddedImageProvider.GetImage(input, ImageType.Logo, CancellationToken.None);
Assert.NotNull(actualLogo);
Assert.True(actualLogo.HasImage);
Assert.Equal(targetPath1, actualLogo.Path);
Assert.Equal(ImageFormat.Png, actualLogo.Format);
var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
Assert.NotNull(actualBackdrop);
Assert.True(actualBackdrop.HasImage);
Assert.Equal(targetPath2, actualBackdrop.Path);
Assert.Equal(ImageFormat.Bmp, actualBackdrop.Format);
var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actualPrimary);
Assert.True(actualPrimary.HasImage);
Assert.Equal(targetPath3, actualPrimary.Path);
Assert.Equal(ImageFormat.Jpg, actualPrimary.Format);
}
[Fact]
public async void GetImage_InputWithUnlabeledEmbeddedImages_BackdropReturnsNoImage()
{
var embeddedImageProvider = GetEmbeddedImageProvider(null);
var input = GetMovie(
new List<MediaAttachment>(),
new List<MediaStream> { new () { Type = MediaStreamType.EmbeddedImage } });
var actual = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_InputWithUnlabeledEmbeddedImages_PrimaryReturnsImage()
{
MediaStream sampleStream = new () { Type = MediaStreamType.EmbeddedImage, Index = 1 };
string targetPath = "path";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream, 1, ".jpg", CancellationToken.None))
.Returns(Task.FromResult(targetPath));
var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
var input = GetMovie(
new List<MediaAttachment>(),
new List<MediaStream> { sampleStream });
var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.True(actual.HasImage);
Assert.Equal(targetPath, actual.Path);
Assert.Equal(ImageFormat.Jpg, actual.Format);
}
[Fact]
public async void GetImage_InputWithLabeledEmbeddedImages_ReturnsCorrectSelection()
{
// primary is second stream to ensure it's not defaulting, backdrop is first
MediaStream sampleStream1 = new () { Type = MediaStreamType.EmbeddedImage, Index = 1, Comment = "backdrop" };
MediaStream sampleStream2 = new () { Type = MediaStreamType.EmbeddedImage, Index = 2, Comment = "cover" };
string targetPath1 = "path1.jpg";
string targetPath2 = "path2.jpg";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream1, 1, ".jpg", CancellationToken.None))
.Returns(Task.FromResult(targetPath1));
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream2, 2, ".jpg", CancellationToken.None))
.Returns(Task.FromResult(targetPath2));
var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
var input = GetMovie(
new List<MediaAttachment>(),
new List<MediaStream> { sampleStream1, sampleStream2 });
var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actualPrimary);
Assert.True(actualPrimary.HasImage);
Assert.Equal(targetPath2, actualPrimary.Path);
Assert.Equal(ImageFormat.Jpg, actualPrimary.Format);
var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
Assert.NotNull(actualBackdrop);
Assert.True(actualBackdrop.HasImage);
Assert.Equal(targetPath1, actualBackdrop.Path);
Assert.Equal(ImageFormat.Jpg, actualBackdrop.Format);
}
private static EmbeddedImageProvider GetEmbeddedImageProvider(IMediaEncoder? mediaEncoder)
{
return new EmbeddedImageProvider(mediaEncoder);
}
private static Movie GetMovie(List<MediaAttachment> mediaAttachments, List<MediaStream> mediaStreams)
{
// Mocking IMediaSourceManager GetMediaAttachments and GetMediaStreams instead of mocking Movie works, but
// has concurrency problems between this and VideoImageProviderTests due to BaseItem.MediaSourceManager
// being static
var movie = new Mock<Movie>();
movie.Setup(item => item.GetMediaSources(It.IsAny<bool>()))
.Returns(new List<MediaSourceInfo> { new () { MediaAttachments = mediaAttachments } } );
movie.Setup(item => item.GetMediaStreams())
.Returns(mediaStreams);
return movie.Object;
}
}
}

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Providers.Tests.MediaInfo
{
public class VideoImageProviderTests
{
[Fact]
public async void GetImage_InputIsPlaceholder_ReturnsNoImage()
{
var videoImageProvider = GetVideoImageProvider(null);
var input = new Movie
{
IsPlaceHolder = true
};
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_NoDefaultVideoStream_ReturnsNoImage()
{
var videoImageProvider = GetVideoImageProvider(null);
var input = new Movie
{
DefaultVideoStreamIndex = null
};
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_DefaultSetButNoVideoStream_ReturnsNoImage()
{
var videoImageProvider = GetVideoImageProvider(null);
// set a default index but don't put anything there (invalid input, but provider shouldn't break)
var input = GetMovie(0, null, new List<MediaStream>());
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.False(actual.HasImage);
}
[Fact]
public async void GetImage_DefaultSetMultipleVideoStreams_ReturnsDefaultStreamImage()
{
MediaStream firstStream = new () { Type = MediaStreamType.Video, Index = 0 };
MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 1 };
string targetPath = "path.jpg";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), firstStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Returns(Task.FromResult("wrong stream called!"));
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), targetStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Returns(Task.FromResult(targetPath));
var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
var input = GetMovie(1, targetStream, new List<MediaStream> { firstStream, targetStream } );
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.True(actual.HasImage);
Assert.Equal(targetPath, actual.Path);
Assert.Equal(ImageFormat.Jpg, actual.Format);
}
[Fact]
public async void GetImage_InvalidDefaultSingleVideoStream_ReturnsFirstVideoStreamImage()
{
MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
string targetPath = "path.jpg";
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), targetStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Returns(Task.FromResult(targetPath));
var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
// provide query results for default (empty) and all streams (populated)
var input = GetMovie(5, null, new List<MediaStream> { targetStream });
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
Assert.True(actual.HasImage);
Assert.Equal(targetPath, actual.Path);
Assert.Equal(ImageFormat.Jpg, actual.Format);
}
[Fact]
public async void GetImage_NoTimeSpanSet_CallsEncoderWithDefaultTime()
{
MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
// use a callback to catch the actual value
// provides more information on failure than verifying a specific input was called on the mock
TimeSpan? actualTimeSpan = null;
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Callback<string, string, MediaSourceInfo, MediaStream, Video3DFormat?, TimeSpan?, CancellationToken>((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan)
.Returns(Task.FromResult("path"));
var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
var input = GetMovie(0, targetStream, new List<MediaStream> { targetStream });
// not testing return, just verifying what gets requested for time span
await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.Equal(TimeSpan.FromSeconds(10), actualTimeSpan);
}
[Fact]
public async void GetImage_TimeSpanSet_CallsEncoderWithCalculatedTime()
{
MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
TimeSpan? actualTimeSpan = null;
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
.Callback<string, string, MediaSourceInfo, MediaStream, Video3DFormat?, TimeSpan?, CancellationToken>((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan)
.Returns(Task.FromResult("path"));
var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
var input = GetMovie(0, targetStream, new List<MediaStream> { targetStream });
input.RunTimeTicks = 5000;
// not testing return, just verifying what gets requested for time span
await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.Equal(TimeSpan.FromTicks(500), actualTimeSpan);
}
private static VideoImageProvider GetVideoImageProvider(IMediaEncoder? mediaEncoder)
{
// strict to ensure this isn't accidentally used where a prepared mock is intended
mediaEncoder ??= new Mock<IMediaEncoder>(MockBehavior.Strict).Object;
return new VideoImageProvider(mediaEncoder, new NullLogger<VideoImageProvider>());
}
private static Movie GetMovie(int defaultVideoStreamIndex, MediaStream? defaultStream, List<MediaStream> mediaStreams)
{
// Mocking IMediaSourceManager GetMediaStreams instead of mocking Movie works, but has concurrency problems
// between this and EmbeddedImageProviderTests due to BaseItem.MediaSourceManager being static
var movie = new Mock<Movie>
{
Object =
{
DefaultVideoStreamIndex = defaultVideoStreamIndex
}
};
movie.Setup(item => item.GetDefaultVideoStream())
.Returns(defaultStream!);
movie.Setup(item => item.GetMediaStreams())
.Returns(mediaStreams);
return movie.Object;
}
}
}
Loading…
Cancel
Save