|
|
|
@ -21,7 +21,9 @@ using ErsatzTV.FFmpeg.Filter.Vaapi; |
|
|
|
using ErsatzTV.FFmpeg.Format; |
|
|
|
using ErsatzTV.FFmpeg.Format; |
|
|
|
using ErsatzTV.FFmpeg.Pipeline; |
|
|
|
using ErsatzTV.FFmpeg.Pipeline; |
|
|
|
using ErsatzTV.FFmpeg.State; |
|
|
|
using ErsatzTV.FFmpeg.State; |
|
|
|
|
|
|
|
using ErsatzTV.Infrastructure.Images; |
|
|
|
using ErsatzTV.Infrastructure.Runtime; |
|
|
|
using ErsatzTV.Infrastructure.Runtime; |
|
|
|
|
|
|
|
using ErsatzTV.Scanner.Core.Interfaces.Metadata; |
|
|
|
using ErsatzTV.Scanner.Core.Metadata; |
|
|
|
using ErsatzTV.Scanner.Core.Metadata; |
|
|
|
using FluentAssertions; |
|
|
|
using FluentAssertions; |
|
|
|
using Microsoft.Extensions.Caching.Memory; |
|
|
|
using Microsoft.Extensions.Caching.Memory; |
|
|
|
@ -50,6 +52,11 @@ public class TranscodingTests |
|
|
|
LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger); |
|
|
|
LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger); |
|
|
|
|
|
|
|
|
|
|
|
MemoryCache = new MemoryCache(new MemoryCacheOptions()); |
|
|
|
MemoryCache = new MemoryCache(new MemoryCacheOptions()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!Directory.Exists(FileSystemLayout.TempFilePoolFolder)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
Directory.CreateDirectory(FileSystemLayout.TempFilePoolFolder); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
[Test] |
|
|
|
[Test] |
|
|
|
@ -193,6 +200,178 @@ public class TranscodingTests |
|
|
|
public static string[] FilesToTest => new[] { string.Empty }; |
|
|
|
public static string[] FilesToTest => new[] { string.Empty }; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[Test] |
|
|
|
|
|
|
|
[Combinatorial] |
|
|
|
|
|
|
|
public async Task TranscodeSong( |
|
|
|
|
|
|
|
[ValueSource(typeof(TestData), nameof(TestData.Watermarks))] |
|
|
|
|
|
|
|
Watermark watermark, |
|
|
|
|
|
|
|
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))] |
|
|
|
|
|
|
|
Resolution profileResolution, |
|
|
|
|
|
|
|
[ValueSource(typeof(TestData), nameof(TestData.BitDepths))] |
|
|
|
|
|
|
|
FFmpegProfileBitDepth profileBitDepth, |
|
|
|
|
|
|
|
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))] |
|
|
|
|
|
|
|
FFmpegProfileVideoFormat profileVideoFormat, |
|
|
|
|
|
|
|
[ValueSource(typeof(TestData), nameof(TestData.TestAccelerations))] |
|
|
|
|
|
|
|
HardwareAccelerationKind profileAcceleration) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var localFileSystem = new LocalFileSystem(new Mock<IClient>().Object, LoggerFactory.CreateLogger<LocalFileSystem>()); |
|
|
|
|
|
|
|
var tempFilePool = new TempFilePool(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var mockImageCache = new Mock<ImageCache>(localFileSystem, tempFilePool); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// always return the static watermark resource
|
|
|
|
|
|
|
|
mockImageCache.Setup( |
|
|
|
|
|
|
|
ic => ic.GetPathForImage( |
|
|
|
|
|
|
|
It.IsAny<string>(), |
|
|
|
|
|
|
|
It.Is<ArtworkKind>(x => x == ArtworkKind.Watermark), |
|
|
|
|
|
|
|
It.IsAny<Option<int>>())) |
|
|
|
|
|
|
|
.Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "ErsatzTV.png")); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var oldService = new FFmpegProcessService( |
|
|
|
|
|
|
|
new FFmpegPlaybackSettingsCalculator(), |
|
|
|
|
|
|
|
new FakeStreamSelector(), |
|
|
|
|
|
|
|
mockImageCache.Object, |
|
|
|
|
|
|
|
tempFilePool, |
|
|
|
|
|
|
|
new Mock<IClient>().Object, |
|
|
|
|
|
|
|
MemoryCache, |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<FFmpegProcessService>()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var service = new FFmpegLibraryProcessService( |
|
|
|
|
|
|
|
oldService, |
|
|
|
|
|
|
|
new FFmpegPlaybackSettingsCalculator(), |
|
|
|
|
|
|
|
new FakeStreamSelector(), |
|
|
|
|
|
|
|
tempFilePool, |
|
|
|
|
|
|
|
new PipelineBuilderFactory( |
|
|
|
|
|
|
|
new RuntimeInfo(), |
|
|
|
|
|
|
|
//new FakeNvidiaCapabilitiesFactory(),
|
|
|
|
|
|
|
|
new HardwareCapabilitiesFactory( |
|
|
|
|
|
|
|
MemoryCache, |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()), |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<PipelineBuilderFactory>()), |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache.Object, service); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var channel = new Channel(Guid.NewGuid()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
Number = "1", |
|
|
|
|
|
|
|
FFmpegProfile = FFmpegProfile.New("test", profileResolution) with |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
HardwareAcceleration = profileAcceleration, |
|
|
|
|
|
|
|
VideoFormat = profileVideoFormat, |
|
|
|
|
|
|
|
AudioFormat = FFmpegProfileAudioFormat.Aac, |
|
|
|
|
|
|
|
DeinterlaceVideo = true, |
|
|
|
|
|
|
|
BitDepth = profileBitDepth |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
StreamingMode = StreamingMode.TransportStream, |
|
|
|
|
|
|
|
SubtitleMode = ChannelSubtitleMode.None |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, Path.Combine("Resources", "song.mp3")); |
|
|
|
|
|
|
|
var songVersion = new MediaVersion |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
MediaFiles = new List<MediaFile> |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
new() { Path = file } |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Streams = new List<MediaStream>() |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var song = new Song |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
SongMetadata = new List<SongMetadata> |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
new() |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
Title = "Song Title", |
|
|
|
|
|
|
|
Artist = "Song Artist", |
|
|
|
|
|
|
|
Artwork = new List<Artwork>() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
MediaVersions = new List<MediaVersion> { songVersion } |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(string videoPath, MediaVersion videoVersion) = await songVideoGenerator.GenerateSongVideo( |
|
|
|
|
|
|
|
song, |
|
|
|
|
|
|
|
channel, |
|
|
|
|
|
|
|
None, // playout item watermark
|
|
|
|
|
|
|
|
None, // global watermark
|
|
|
|
|
|
|
|
ExecutableName("ffmpeg"), |
|
|
|
|
|
|
|
ExecutableName("ffprobe"), |
|
|
|
|
|
|
|
CancellationToken.None); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var metadataRepository = new Mock<IMetadataRepository>(); |
|
|
|
|
|
|
|
metadataRepository |
|
|
|
|
|
|
|
.Setup(r => r.UpdateStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>())) |
|
|
|
|
|
|
|
.Callback<MediaItem, MediaVersion, bool>( |
|
|
|
|
|
|
|
(_, version, _) => |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (version.Streams.Any(s => s.MediaStreamKind == MediaStreamKind.Video && s.AttachedPic == false)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
version.MediaFiles = videoVersion.MediaFiles; |
|
|
|
|
|
|
|
videoVersion = version; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
version.MediaFiles = songVersion.MediaFiles; |
|
|
|
|
|
|
|
songVersion = version; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var localStatisticsProvider = new LocalStatisticsProvider( |
|
|
|
|
|
|
|
metadataRepository.Object, |
|
|
|
|
|
|
|
new LocalFileSystem(new Mock<IClient>().Object, LoggerFactory.CreateLogger<LocalFileSystem>()), |
|
|
|
|
|
|
|
new Mock<IClient>().Object, |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<LocalStatisticsProvider>()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await localStatisticsProvider.RefreshStatistics(ExecutableName("ffmpeg"), ExecutableName("ffprobe"), song); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DateTimeOffset now = DateTimeOffset.Now; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Command process = await service.ForPlayoutItem( |
|
|
|
|
|
|
|
ExecutableName("ffmpeg"), |
|
|
|
|
|
|
|
ExecutableName("ffprobe"), |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
channel, |
|
|
|
|
|
|
|
videoVersion, |
|
|
|
|
|
|
|
new MediaItemAudioVersion(song, songVersion), |
|
|
|
|
|
|
|
videoPath, |
|
|
|
|
|
|
|
file, |
|
|
|
|
|
|
|
_ => Task.FromResult(new List<ErsatzTV.Core.Domain.Subtitle>()), |
|
|
|
|
|
|
|
string.Empty, |
|
|
|
|
|
|
|
string.Empty, |
|
|
|
|
|
|
|
string.Empty, |
|
|
|
|
|
|
|
ChannelSubtitleMode.None, |
|
|
|
|
|
|
|
now, |
|
|
|
|
|
|
|
now + TimeSpan.FromSeconds(3), |
|
|
|
|
|
|
|
now, |
|
|
|
|
|
|
|
Option<ChannelWatermark>.None, |
|
|
|
|
|
|
|
GetWatermark(watermark), |
|
|
|
|
|
|
|
VaapiDriver.Default, |
|
|
|
|
|
|
|
"/dev/dri/renderD128", |
|
|
|
|
|
|
|
Option<int>.None, |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
FillerKind.None, |
|
|
|
|
|
|
|
TimeSpan.Zero, |
|
|
|
|
|
|
|
TimeSpan.FromSeconds(3), |
|
|
|
|
|
|
|
0, |
|
|
|
|
|
|
|
None, |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
_ => { }); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Console.WriteLine($"ffmpeg arguments {process.Arguments}");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await TranscodeAndVerify( |
|
|
|
|
|
|
|
process, |
|
|
|
|
|
|
|
profileResolution, |
|
|
|
|
|
|
|
profileBitDepth, |
|
|
|
|
|
|
|
profileVideoFormat, |
|
|
|
|
|
|
|
profileAcceleration, |
|
|
|
|
|
|
|
localStatisticsProvider, |
|
|
|
|
|
|
|
() => videoVersion); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
[Test] |
|
|
|
[Test] |
|
|
|
[Combinatorial] |
|
|
|
[Combinatorial] |
|
|
|
public async Task Transcode( |
|
|
|
public async Task Transcode( |
|
|
|
@ -239,39 +418,6 @@ public class TranscodingTests |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var imageCache = new Mock<IImageCache>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// always return the static watermark resource
|
|
|
|
|
|
|
|
imageCache.Setup( |
|
|
|
|
|
|
|
ic => ic.GetPathForImage( |
|
|
|
|
|
|
|
It.IsAny<string>(), |
|
|
|
|
|
|
|
It.Is<ArtworkKind>(x => x == ArtworkKind.Watermark), |
|
|
|
|
|
|
|
It.IsAny<Option<int>>())) |
|
|
|
|
|
|
|
.Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "ErsatzTV.png")); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var oldService = new FFmpegProcessService( |
|
|
|
|
|
|
|
new FFmpegPlaybackSettingsCalculator(), |
|
|
|
|
|
|
|
new FakeStreamSelector(), |
|
|
|
|
|
|
|
imageCache.Object, |
|
|
|
|
|
|
|
new Mock<ITempFilePool>().Object, |
|
|
|
|
|
|
|
new Mock<IClient>().Object, |
|
|
|
|
|
|
|
MemoryCache, |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<FFmpegProcessService>()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var service = new FFmpegLibraryProcessService( |
|
|
|
|
|
|
|
oldService, |
|
|
|
|
|
|
|
new FFmpegPlaybackSettingsCalculator(), |
|
|
|
|
|
|
|
new FakeStreamSelector(), |
|
|
|
|
|
|
|
new Mock<ITempFilePool>().Object, |
|
|
|
|
|
|
|
new PipelineBuilderFactory( |
|
|
|
|
|
|
|
new RuntimeInfo(), |
|
|
|
|
|
|
|
//new FakeNvidiaCapabilitiesFactory(),
|
|
|
|
|
|
|
|
new HardwareCapabilitiesFactory( |
|
|
|
|
|
|
|
MemoryCache, |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()), |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<PipelineBuilderFactory>()), |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var v = new MediaVersion |
|
|
|
var v = new MediaVersion |
|
|
|
{ |
|
|
|
{ |
|
|
|
MediaFiles = new List<MediaFile> |
|
|
|
MediaFiles = new List<MediaFile> |
|
|
|
@ -349,72 +495,7 @@ public class TranscodingTests |
|
|
|
|
|
|
|
|
|
|
|
DateTimeOffset now = DateTimeOffset.Now; |
|
|
|
DateTimeOffset now = DateTimeOffset.Now; |
|
|
|
|
|
|
|
|
|
|
|
Option<ChannelWatermark> channelWatermark = Option<ChannelWatermark>.None; |
|
|
|
Option<ChannelWatermark> channelWatermark = GetWatermark(watermark); |
|
|
|
switch (watermark) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
case Watermark.None: |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case Watermark.IntermittentOpaque: |
|
|
|
|
|
|
|
channelWatermark = new ChannelWatermark |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Intermittent, |
|
|
|
|
|
|
|
// TODO: how do we make sure this actually appears
|
|
|
|
|
|
|
|
FrequencyMinutes = 1, |
|
|
|
|
|
|
|
DurationSeconds = 2, |
|
|
|
|
|
|
|
Opacity = 100 |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case Watermark.IntermittentTransparent: |
|
|
|
|
|
|
|
channelWatermark = new ChannelWatermark |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Intermittent, |
|
|
|
|
|
|
|
// TODO: how do we make sure this actually appears
|
|
|
|
|
|
|
|
FrequencyMinutes = 1, |
|
|
|
|
|
|
|
DurationSeconds = 2, |
|
|
|
|
|
|
|
Opacity = 80 |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case Watermark.PermanentOpaqueScaled: |
|
|
|
|
|
|
|
channelWatermark = new ChannelWatermark |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Permanent, |
|
|
|
|
|
|
|
Opacity = 100, |
|
|
|
|
|
|
|
Size = WatermarkSize.Scaled, |
|
|
|
|
|
|
|
WidthPercent = 15 |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case Watermark.PermanentOpaqueActualSize: |
|
|
|
|
|
|
|
channelWatermark = new ChannelWatermark |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Permanent, |
|
|
|
|
|
|
|
Opacity = 100, |
|
|
|
|
|
|
|
Size = WatermarkSize.ActualSize |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case Watermark.PermanentTransparentScaled: |
|
|
|
|
|
|
|
channelWatermark = new ChannelWatermark |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Permanent, |
|
|
|
|
|
|
|
Opacity = 80, |
|
|
|
|
|
|
|
Size = WatermarkSize.Scaled, |
|
|
|
|
|
|
|
WidthPercent = 15 |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case Watermark.PermanentTransparentActualSize: |
|
|
|
|
|
|
|
channelWatermark = new ChannelWatermark |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Permanent, |
|
|
|
|
|
|
|
Opacity = 80, |
|
|
|
|
|
|
|
Size = WatermarkSize.ActualSize |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ChannelSubtitleMode subtitleMode = subtitle switch |
|
|
|
ChannelSubtitleMode subtitleMode = subtitle switch |
|
|
|
{ |
|
|
|
{ |
|
|
|
@ -495,6 +576,8 @@ public class TranscodingTests |
|
|
|
hasWatermarkFilters.Should().Be(watermark != Watermark.None); |
|
|
|
hasWatermarkFilters.Should().Be(watermark != Watermark.None); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FFmpegLibraryProcessService service = GetService(); |
|
|
|
|
|
|
|
|
|
|
|
Command process = await service.ForPlayoutItem( |
|
|
|
Command process = await service.ForPlayoutItem( |
|
|
|
ExecutableName("ffmpeg"), |
|
|
|
ExecutableName("ffmpeg"), |
|
|
|
ExecutableName("ffprobe"), |
|
|
|
ExecutableName("ffprobe"), |
|
|
|
@ -541,127 +624,79 @@ public class TranscodingTests |
|
|
|
|
|
|
|
|
|
|
|
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
|
|
|
|
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
|
|
|
|
|
|
|
|
|
|
|
|
string[] unsupportedMessages = |
|
|
|
await TranscodeAndVerify( |
|
|
|
{ |
|
|
|
process, |
|
|
|
"No support for codec", |
|
|
|
profileResolution, |
|
|
|
"No usable", |
|
|
|
profileBitDepth, |
|
|
|
"Provided device doesn't support", |
|
|
|
profileVideoFormat, |
|
|
|
"Current pixel format is unsupported" |
|
|
|
profileAcceleration, |
|
|
|
}; |
|
|
|
localStatisticsProvider, |
|
|
|
|
|
|
|
() => v); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var sb = new StringBuilder(); |
|
|
|
private Option<ChannelWatermark> GetWatermark(Watermark watermark) |
|
|
|
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30)); |
|
|
|
{ |
|
|
|
string tempFile = Path.GetTempFileName(); |
|
|
|
switch (watermark) |
|
|
|
try |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
CommandResult result; |
|
|
|
case Watermark.None: |
|
|
|
|
|
|
|
break; |
|
|
|
try |
|
|
|
case Watermark.IntermittentOpaque: |
|
|
|
{ |
|
|
|
return new ChannelWatermark |
|
|
|
result = await process |
|
|
|
|
|
|
|
.WithStandardOutputPipe(PipeTarget.ToFile(tempFile)) |
|
|
|
|
|
|
|
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb)) |
|
|
|
|
|
|
|
.ExecuteAsync(timeoutSignal.Token); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// var arguments = string.Join(
|
|
|
|
|
|
|
|
// ' ',
|
|
|
|
|
|
|
|
// process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a));
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// Log.Logger.Debug(arguments);
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
catch (OperationCanceledException) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var arguments = string.Join( |
|
|
|
|
|
|
|
' ', |
|
|
|
|
|
|
|
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Fail($"Transcode failure (timeout): ffmpeg {arguments}"); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var error = sb.ToString(); |
|
|
|
|
|
|
|
bool isUnsupported = unsupportedMessages.Any(error.Contains); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.ExitCode.Should().Be(1, $"Error message with successful exit code? {process.Arguments}"); |
|
|
|
|
|
|
|
Assert.Warn($"Unsupported on this hardware: ffmpeg {process.Arguments}"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else if (error.Contains("Impossible to convert between")) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var arguments = string.Join( |
|
|
|
|
|
|
|
' ', |
|
|
|
|
|
|
|
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Fail($"Transcode failure: ffmpeg {arguments}"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var arguments = string.Join( |
|
|
|
|
|
|
|
' ', |
|
|
|
|
|
|
|
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result.ExitCode.Should().Be(0, error + Environment.NewLine + arguments); |
|
|
|
|
|
|
|
if (result.ExitCode == 0) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
Console.WriteLine(process.Arguments); |
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
} |
|
|
|
Mode = ChannelWatermarkMode.Intermittent, |
|
|
|
} |
|
|
|
// TODO: how do we make sure this actually appears
|
|
|
|
|
|
|
|
FrequencyMinutes = 1, |
|
|
|
// additional checks on resulting file
|
|
|
|
DurationSeconds = 2, |
|
|
|
await localStatisticsProvider.RefreshStatistics( |
|
|
|
Opacity = 100 |
|
|
|
ExecutableName("ffmpeg"), |
|
|
|
}; |
|
|
|
ExecutableName("ffprobe"), |
|
|
|
case Watermark.IntermittentTransparent: |
|
|
|
new Movie |
|
|
|
return new ChannelWatermark |
|
|
|
{ |
|
|
|
{ |
|
|
|
MediaVersions = new List<MediaVersion> |
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
{ |
|
|
|
Mode = ChannelWatermarkMode.Intermittent, |
|
|
|
new() |
|
|
|
// TODO: how do we make sure this actually appears
|
|
|
|
{ |
|
|
|
FrequencyMinutes = 1, |
|
|
|
MediaFiles = new List<MediaFile> |
|
|
|
DurationSeconds = 2, |
|
|
|
{ |
|
|
|
Opacity = 80 |
|
|
|
new() { Path = tempFile } |
|
|
|
}; |
|
|
|
} |
|
|
|
case Watermark.PermanentOpaqueScaled: |
|
|
|
} |
|
|
|
return new ChannelWatermark |
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// verify de-interlace
|
|
|
|
|
|
|
|
v.VideoScanKind.Should().NotBe(VideoScanKind.Interlaced); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// verify resolution
|
|
|
|
|
|
|
|
v.Height.Should().Be(profileResolution.Height); |
|
|
|
|
|
|
|
v.Width.Should().Be(profileResolution.Width); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (MediaStream videoStream in v.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Video)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
// verify pixel format
|
|
|
|
|
|
|
|
videoStream.PixelFormat.Should().Be( |
|
|
|
|
|
|
|
profileBitDepth == FFmpegProfileBitDepth.TenBit ? PixelFormat.YUV420P10LE : PixelFormat.YUV420P); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// verify colors
|
|
|
|
|
|
|
|
var colorParams = new ColorParams( |
|
|
|
|
|
|
|
videoStream.ColorRange, |
|
|
|
|
|
|
|
videoStream.ColorSpace, |
|
|
|
|
|
|
|
videoStream.ColorTransfer, |
|
|
|
|
|
|
|
videoStream.ColorPrimaries); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// AMF doesn't seem to set this metadata properly
|
|
|
|
|
|
|
|
// MPEG2Video doesn't always seem to set this properly
|
|
|
|
|
|
|
|
if (profileAcceleration != HardwareAccelerationKind.Amf && |
|
|
|
|
|
|
|
profileVideoFormat != FFmpegProfileVideoFormat.Mpeg2Video) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
colorParams.IsBt709.Should().BeTrue($"{colorParams}"); |
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
} |
|
|
|
Mode = ChannelWatermarkMode.Permanent, |
|
|
|
} |
|
|
|
Opacity = 100, |
|
|
|
} |
|
|
|
Size = WatermarkSize.Scaled, |
|
|
|
finally |
|
|
|
WidthPercent = 15 |
|
|
|
{ |
|
|
|
}; |
|
|
|
if (File.Exists(tempFile)) |
|
|
|
case Watermark.PermanentOpaqueActualSize: |
|
|
|
{ |
|
|
|
return new ChannelWatermark |
|
|
|
File.Delete(tempFile); |
|
|
|
{ |
|
|
|
} |
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Permanent, |
|
|
|
|
|
|
|
Opacity = 100, |
|
|
|
|
|
|
|
Size = WatermarkSize.ActualSize |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
case Watermark.PermanentTransparentScaled: |
|
|
|
|
|
|
|
return new ChannelWatermark |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Permanent, |
|
|
|
|
|
|
|
Opacity = 80, |
|
|
|
|
|
|
|
Size = WatermarkSize.Scaled, |
|
|
|
|
|
|
|
WidthPercent = 15 |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
case Watermark.PermanentTransparentActualSize: |
|
|
|
|
|
|
|
return new ChannelWatermark |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ImageSource = ChannelWatermarkImageSource.Custom, |
|
|
|
|
|
|
|
Mode = ChannelWatermarkMode.Permanent, |
|
|
|
|
|
|
|
Opacity = 80, |
|
|
|
|
|
|
|
Size = WatermarkSize.ActualSize |
|
|
|
|
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return Option<ChannelWatermark>.None; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static async Task GenerateTestFile( |
|
|
|
private static async Task GenerateTestFile( |
|
|
|
@ -795,6 +830,178 @@ public class TranscodingTests |
|
|
|
return BitConverter.ToString(hash).Replace("-", string.Empty); |
|
|
|
return BitConverter.ToString(hash).Replace("-", string.Empty); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static FFmpegLibraryProcessService GetService() |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var imageCache = new Mock<IImageCache>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// always return the static watermark resource
|
|
|
|
|
|
|
|
imageCache.Setup( |
|
|
|
|
|
|
|
ic => ic.GetPathForImage( |
|
|
|
|
|
|
|
It.IsAny<string>(), |
|
|
|
|
|
|
|
It.Is<ArtworkKind>(x => x == ArtworkKind.Watermark), |
|
|
|
|
|
|
|
It.IsAny<Option<int>>())) |
|
|
|
|
|
|
|
.Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "ErsatzTV.png")); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var oldService = new FFmpegProcessService( |
|
|
|
|
|
|
|
new FFmpegPlaybackSettingsCalculator(), |
|
|
|
|
|
|
|
new FakeStreamSelector(), |
|
|
|
|
|
|
|
imageCache.Object, |
|
|
|
|
|
|
|
new Mock<ITempFilePool>().Object, |
|
|
|
|
|
|
|
new Mock<IClient>().Object, |
|
|
|
|
|
|
|
MemoryCache, |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<FFmpegProcessService>()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var service = new FFmpegLibraryProcessService( |
|
|
|
|
|
|
|
oldService, |
|
|
|
|
|
|
|
new FFmpegPlaybackSettingsCalculator(), |
|
|
|
|
|
|
|
new FakeStreamSelector(), |
|
|
|
|
|
|
|
new Mock<ITempFilePool>().Object, |
|
|
|
|
|
|
|
new PipelineBuilderFactory( |
|
|
|
|
|
|
|
new RuntimeInfo(), |
|
|
|
|
|
|
|
//new FakeNvidiaCapabilitiesFactory(),
|
|
|
|
|
|
|
|
new HardwareCapabilitiesFactory( |
|
|
|
|
|
|
|
MemoryCache, |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()), |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<PipelineBuilderFactory>()), |
|
|
|
|
|
|
|
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return service; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async Task TranscodeAndVerify( |
|
|
|
|
|
|
|
Command process, |
|
|
|
|
|
|
|
Resolution profileResolution, |
|
|
|
|
|
|
|
FFmpegProfileBitDepth profileBitDepth, |
|
|
|
|
|
|
|
FFmpegProfileVideoFormat profileVideoFormat, |
|
|
|
|
|
|
|
HardwareAccelerationKind profileAcceleration, |
|
|
|
|
|
|
|
ILocalStatisticsProvider localStatisticsProvider, |
|
|
|
|
|
|
|
Func<MediaVersion> getFinalMediaVersion) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
string[] unsupportedMessages = |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
"No support for codec", |
|
|
|
|
|
|
|
"No usable", |
|
|
|
|
|
|
|
"Provided device doesn't support", |
|
|
|
|
|
|
|
"Current pixel format is unsupported" |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var sb = new StringBuilder(); |
|
|
|
|
|
|
|
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30)); |
|
|
|
|
|
|
|
string tempFile = Path.GetTempFileName(); |
|
|
|
|
|
|
|
try |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
CommandResult result; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result = await process |
|
|
|
|
|
|
|
.WithStandardOutputPipe(PipeTarget.ToFile(tempFile)) |
|
|
|
|
|
|
|
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb)) |
|
|
|
|
|
|
|
.ExecuteAsync(timeoutSignal.Token); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// var arguments = string.Join(
|
|
|
|
|
|
|
|
// ' ',
|
|
|
|
|
|
|
|
// process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a));
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// Log.Logger.Debug(arguments);
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
catch (OperationCanceledException) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var arguments = string.Join( |
|
|
|
|
|
|
|
' ', |
|
|
|
|
|
|
|
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Fail($"Transcode failure (timeout): ffmpeg {arguments}"); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var error = sb.ToString(); |
|
|
|
|
|
|
|
bool isUnsupported = unsupportedMessages.Any(error.Contains); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
result.ExitCode.Should().Be(1, $"Error message with successful exit code? {process.Arguments}"); |
|
|
|
|
|
|
|
Assert.Warn($"Unsupported on this hardware: ffmpeg {process.Arguments}"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else if (error.Contains("Impossible to convert between")) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var arguments = string.Join( |
|
|
|
|
|
|
|
' ', |
|
|
|
|
|
|
|
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Fail($"Transcode failure: ffmpeg {arguments}"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
var arguments = string.Join( |
|
|
|
|
|
|
|
' ', |
|
|
|
|
|
|
|
process.Arguments.Split(" ").Map(a => a.Contains('[') ? $"\"{a}\"" : a)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result.ExitCode.Should().Be(0, error + Environment.NewLine + arguments); |
|
|
|
|
|
|
|
if (result.ExitCode == 0) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
Console.WriteLine(process.Arguments); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// additional checks on resulting file
|
|
|
|
|
|
|
|
await localStatisticsProvider.RefreshStatistics( |
|
|
|
|
|
|
|
ExecutableName("ffmpeg"), |
|
|
|
|
|
|
|
ExecutableName("ffprobe"), |
|
|
|
|
|
|
|
new Movie |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
MediaVersions = new List<MediaVersion> |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
new() |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
MediaFiles = new List<MediaFile> |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
new() { Path = tempFile } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MediaVersion v = getFinalMediaVersion(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// verify de-interlace
|
|
|
|
|
|
|
|
v.VideoScanKind.Should().NotBe(VideoScanKind.Interlaced); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// verify resolution
|
|
|
|
|
|
|
|
v.Height.Should().Be(profileResolution.Height); |
|
|
|
|
|
|
|
v.Width.Should().Be(profileResolution.Width); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (MediaStream videoStream in v.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Video)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
// verify pixel format
|
|
|
|
|
|
|
|
videoStream.PixelFormat.Should().Be( |
|
|
|
|
|
|
|
profileBitDepth == FFmpegProfileBitDepth.TenBit ? PixelFormat.YUV420P10LE : PixelFormat.YUV420P); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// verify colors
|
|
|
|
|
|
|
|
var colorParams = new ColorParams( |
|
|
|
|
|
|
|
videoStream.ColorRange, |
|
|
|
|
|
|
|
videoStream.ColorSpace, |
|
|
|
|
|
|
|
videoStream.ColorTransfer, |
|
|
|
|
|
|
|
videoStream.ColorPrimaries); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// AMF doesn't seem to set this metadata properly
|
|
|
|
|
|
|
|
// MPEG2Video doesn't always seem to set this properly
|
|
|
|
|
|
|
|
if (profileAcceleration != HardwareAccelerationKind.Amf && |
|
|
|
|
|
|
|
profileVideoFormat != FFmpegProfileVideoFormat.Mpeg2Video) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
colorParams.IsBt709.Should().BeTrue($"{colorParams}"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
finally |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
if (File.Exists(tempFile)) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
File.Delete(tempFile); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private class FakeStreamSelector : IFFmpegStreamSelector |
|
|
|
private class FakeStreamSelector : IFFmpegStreamSelector |
|
|
|
{ |
|
|
|
{ |
|
|
|
public Task<MediaStream> SelectVideoStream(MediaVersion version) => |
|
|
|
public Task<MediaStream> SelectVideoStream(MediaVersion version) => |
|
|
|
@ -806,7 +1013,8 @@ public class TranscodingTests |
|
|
|
Channel channel, |
|
|
|
Channel channel, |
|
|
|
string preferredAudioLanguage, |
|
|
|
string preferredAudioLanguage, |
|
|
|
string preferredAudioTitle) => |
|
|
|
string preferredAudioTitle) => |
|
|
|
Optional(version.MediaVersion.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask(); |
|
|
|
Optional(version.MediaVersion.Streams.FirstOrDefault(s => s.MediaStreamKind == MediaStreamKind.Audio)) |
|
|
|
|
|
|
|
.AsTask(); |
|
|
|
|
|
|
|
|
|
|
|
public Task<Option<ErsatzTV.Core.Domain.Subtitle>> SelectSubtitleStream( |
|
|
|
public Task<Option<ErsatzTV.Core.Domain.Subtitle>> SelectSubtitleStream( |
|
|
|
List<ErsatzTV.Core.Domain.Subtitle> subtitles, |
|
|
|
List<ErsatzTV.Core.Domain.Subtitle> subtitles, |
|
|
|
@ -815,7 +1023,7 @@ public class TranscodingTests |
|
|
|
ChannelSubtitleMode subtitleMode) => |
|
|
|
ChannelSubtitleMode subtitleMode) => |
|
|
|
subtitles.HeadOrNone().AsTask(); |
|
|
|
subtitles.HeadOrNone().AsTask(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static string ExecutableName(string baseName) => |
|
|
|
private static string ExecutableName(string baseName) => |
|
|
|
OperatingSystem.IsWindows() ? $"{baseName}.exe" : baseName; |
|
|
|
OperatingSystem.IsWindows() ? $"{baseName}.exe" : baseName; |
|
|
|
} |
|
|
|
} |
|
|
|
|