@ -21,7 +21,9 @@ using ErsatzTV.FFmpeg.Filter.Vaapi;
@@ -21,7 +21,9 @@ using ErsatzTV.FFmpeg.Filter.Vaapi;
using ErsatzTV.FFmpeg.Format ;
using ErsatzTV.FFmpeg.Pipeline ;
using ErsatzTV.FFmpeg.State ;
using ErsatzTV.Infrastructure.Images ;
using ErsatzTV.Infrastructure.Runtime ;
using ErsatzTV.Scanner.Core.Interfaces.Metadata ;
using ErsatzTV.Scanner.Core.Metadata ;
using FluentAssertions ;
using Microsoft.Extensions.Caching.Memory ;
@ -50,6 +52,11 @@ public class TranscodingTests
@@ -50,6 +52,11 @@ public class TranscodingTests
LoggerFactory = new LoggerFactory ( ) . AddSerilog ( Log . Logger ) ;
MemoryCache = new MemoryCache ( new MemoryCacheOptions ( ) ) ;
if ( ! Directory . Exists ( FileSystemLayout . TempFilePoolFolder ) )
{
Directory . CreateDirectory ( FileSystemLayout . TempFilePoolFolder ) ;
}
}
[Test]
@ -193,6 +200,178 @@ public class TranscodingTests
@@ -193,6 +200,178 @@ public class TranscodingTests
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]
[Combinatorial]
public async Task Transcode (
@ -239,39 +418,6 @@ public class TranscodingTests
@@ -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
{
MediaFiles = new List < MediaFile >
@ -349,72 +495,7 @@ public class TranscodingTests
@@ -349,72 +495,7 @@ public class TranscodingTests
DateTimeOffset now = DateTimeOffset . Now ;
Option < ChannelWatermark > channelWatermark = Option < ChannelWatermark > . None ;
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 = 1 0 0
} ;
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 = 8 0
} ;
break ;
case Watermark . PermanentOpaqueScaled :
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Permanent ,
Opacity = 1 0 0 ,
Size = WatermarkSize . Scaled ,
WidthPercent = 1 5
} ;
break ;
case Watermark . PermanentOpaqueActualSize :
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Permanent ,
Opacity = 1 0 0 ,
Size = WatermarkSize . ActualSize
} ;
break ;
case Watermark . PermanentTransparentScaled :
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Permanent ,
Opacity = 8 0 ,
Size = WatermarkSize . Scaled ,
WidthPercent = 1 5
} ;
break ;
case Watermark . PermanentTransparentActualSize :
channelWatermark = new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Permanent ,
Opacity = 8 0 ,
Size = WatermarkSize . ActualSize
} ;
break ;
}
Option < ChannelWatermark > channelWatermark = GetWatermark ( watermark ) ;
ChannelSubtitleMode subtitleMode = subtitle switch
{
@ -495,6 +576,8 @@ public class TranscodingTests
@@ -495,6 +576,8 @@ public class TranscodingTests
hasWatermarkFilters . Should ( ) . Be ( watermark ! = Watermark . None ) ;
}
FFmpegLibraryProcessService service = GetService ( ) ;
Command process = await service . ForPlayoutItem (
ExecutableName ( "ffmpeg" ) ,
ExecutableName ( "ffprobe" ) ,
@ -541,127 +624,79 @@ public class TranscodingTests
@@ -541,127 +624,79 @@ public class TranscodingTests
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
string [ ] unsupportedMessages =
{
"No support for codec" ,
"No usable" ,
"Provided device doesn't support" ,
"Current pixel format is unsupported"
} ;
await TranscodeAndVerify (
process ,
profileResolution ,
profileBitDepth ,
profileVideoFormat ,
profileAcceleration ,
localStatisticsProvider ,
( ) = > v ) ;
}
var sb = new StringBuilder ( ) ;
var timeoutSignal = new CancellationTokenSource ( TimeSpan . FromSeconds ( 3 0 ) ) ;
string tempFile = Path . GetTempFileName ( ) ;
try
private Option < ChannelWatermark > GetWatermark ( Watermark watermark )
{
switch ( watermark )
{
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 )
case Watermark . None :
break ;
case Watermark . IntermittentOpaque :
return new ChannelWatermark
{
Console . WriteLine ( process . Arguments ) ;
}
}
// additional checks on resulting file
await localStatisticsProvider . RefreshStatistics (
ExecutableName ( "ffmpeg" ) ,
ExecutableName ( "ffprobe" ) ,
new Movie
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Intermittent ,
// TODO: how do we make sure this actually appears
FrequencyMinutes = 1 ,
DurationSeconds = 2 ,
Opacity = 1 0 0
} ;
case Watermark . IntermittentTransparent :
return new ChannelWatermark
{
MediaVersions = new List < MediaVersion >
{
new ( )
{
MediaFiles = new List < MediaFile >
{
new ( ) { Path = tempFile }
}
}
}
} ) ;
// 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 )
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Intermittent ,
// TODO: how do we make sure this actually appears
FrequencyMinutes = 1 ,
DurationSeconds = 2 ,
Opacity = 8 0
} ;
case Watermark . PermanentOpaqueScaled :
return new ChannelWatermark
{
colorParams . IsBt709 . Should ( ) . BeTrue ( $"{colorParams}" ) ;
}
}
}
finally
{
if ( File . Exists ( tempFile ) )
{
File . Delete ( tempFile ) ;
}
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Permanent ,
Opacity = 1 0 0 ,
Size = WatermarkSize . Scaled ,
WidthPercent = 1 5
} ;
case Watermark . PermanentOpaqueActualSize :
return new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Permanent ,
Opacity = 1 0 0 ,
Size = WatermarkSize . ActualSize
} ;
case Watermark . PermanentTransparentScaled :
return new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Permanent ,
Opacity = 8 0 ,
Size = WatermarkSize . Scaled ,
WidthPercent = 1 5
} ;
case Watermark . PermanentTransparentActualSize :
return new ChannelWatermark
{
ImageSource = ChannelWatermarkImageSource . Custom ,
Mode = ChannelWatermarkMode . Permanent ,
Opacity = 8 0 ,
Size = WatermarkSize . ActualSize
} ;
}
return Option < ChannelWatermark > . None ;
}
private static async Task GenerateTestFile (
@ -795,6 +830,178 @@ public class TranscodingTests
@@ -795,6 +830,178 @@ public class TranscodingTests
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 ( 3 0 ) ) ;
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
{
public Task < MediaStream > SelectVideoStream ( MediaVersion version ) = >
@ -806,7 +1013,8 @@ public class TranscodingTests
@@ -806,7 +1013,8 @@ public class TranscodingTests
Channel channel ,
string preferredAudioLanguage ,
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 (
List < ErsatzTV . Core . Domain . Subtitle > subtitles ,
@ -815,7 +1023,7 @@ public class TranscodingTests
@@ -815,7 +1023,7 @@ public class TranscodingTests
ChannelSubtitleMode subtitleMode ) = >
subtitles . HeadOrNone ( ) . AsTask ( ) ;
}
private static string ExecutableName ( string baseName ) = >
OperatingSystem . IsWindows ( ) ? $"{baseName}.exe" : baseName ;
}