@ -6,6 +6,7 @@ using ErsatzTV.Core;
@@ -6,6 +6,7 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain ;
using ErsatzTV.Core.Graphics ;
using ErsatzTV.Core.Interfaces.Metadata ;
using ErsatzTV.Core.Interfaces.Streaming ;
using Microsoft.Extensions.Logging ;
using SkiaSharp ;
@ -25,8 +26,9 @@ public class MotionElement(
@@ -25,8 +26,9 @@ public class MotionElement(
private SKPointI _ point ;
private SKBitmap _ canvasBitmap ;
private SKBitmap _ motionFrameBitmap ;
private bool _ isFinished ;
private TimeSpan _ startTime ;
private TimeSpan _ endTime ;
private MotionElementState _ state ;
public void Dispose ( )
{
@ -52,21 +54,24 @@ public class MotionElement(
@@ -52,21 +54,24 @@ public class MotionElement(
_ motionFrameBitmap ? . Dispose ( ) ;
}
public override async Task InitializeAsync (
Resolution squarePixelFrameSize ,
Resolution frameSize ,
int frameRate ,
TimeSpan seek ,
CancellationToken cancellationToken )
public override async Task InitializeAsync ( GraphicsEngineContext context , CancellationToken cancellationToken )
{
try
{
_ startTime = TimeSpan . FromSeconds ( motionElement . StartSeconds ? ? 0 ) ;
ProbeResult probeResult = await ProbeMotionElement ( frameSize ) ;
var overlayDuration = probeResult . Duration ;
var holdDuration = TimeSpan . FromSeconds ( motionElement . HoldSeconds ? ? 0 ) ;
ProbeResult probeResult = await ProbeMotionElement ( context . FrameSize ) ;
var overlayDuration = motionElement . EndBehavior switch
{
MotionEndBehavior . Loop = > context . Duration ,
MotionEndBehavior . Hold = > probeResult . Duration + holdDuration ,
_ = > probeResult . Duration
} ;
_ endTime = _ startTime + overlayDuration ;
// already past the time when this is supposed to play; don't do any more work
if ( _ startTime + overlayDuration < seek )
if ( _ startTime + overlayDuration < context . S eek)
{
IsFinished = true ;
return ;
@ -76,9 +81,9 @@ public class MotionElement(
@@ -76,9 +81,9 @@ public class MotionElement(
_ pipeReader = pipe . Reader ;
var overlaySeekTime = TimeSpan . Zero ;
if ( _ startTime < s eek)
if ( _ startTime < context . S eek)
{
overlaySeekTime = s eek - _ startTime ;
overlaySeekTime = context . S eek - _ startTime ;
}
Resolution sourceSize = probeResult . Size ;
@ -88,7 +93,8 @@ public class MotionElement(
@@ -88,7 +93,8 @@ public class MotionElement(
if ( motionElement . Scale )
{
scaledWidth = ( int ) Math . Round ( ( motionElement . ScaleWidthPercent ? ? 1 0 0 ) / 1 0 0.0 * frameSize . Width ) ;
scaledWidth = ( int ) Math . Round (
( motionElement . ScaleWidthPercent ? ? 1 0 0 ) / 1 0 0.0 * context . FrameSize . Width ) ;
double aspectRatio = ( double ) sourceSize . Height / sourceSize . Width ;
scaledHeight = ( int ) Math . Round ( scaledWidth * aspectRatio ) ;
}
@ -108,7 +114,11 @@ public class MotionElement(
@@ -108,7 +114,11 @@ public class MotionElement(
_f rameSize = targetSize . Width * targetSize . Height * 4 ;
_ canvasBitmap = new SKBitmap ( frameSize . Width , frameSize . Height , SKColorType . Bgra8888 , SKAlphaType . Unpremul ) ;
_ canvasBitmap = new SKBitmap (
context . FrameSize . Width ,
context . FrameSize . Height ,
SKColorType . Bgra8888 ,
SKAlphaType . Unpremul ) ;
_ motionFrameBitmap = new SKBitmap (
targetSize . Width ,
@ -119,14 +129,14 @@ public class MotionElement(
@@ -119,14 +129,14 @@ public class MotionElement(
_ point = SKPointI . Empty ;
( int horizontalMargin , int verticalMargin ) = NormalMargins (
f rameSize,
context . F rameSize,
motionElement . HorizontalMarginPercent ? ? 0 ,
motionElement . VerticalMarginPercent ? ? 0 ) ;
_ point = CalculatePosition (
motionElement . Location ,
f rameSize. Width ,
f rameSize. Height ,
context . F rameSize. Width ,
context . F rameSize. Height ,
targetSize . Width ,
targetSize . Height ,
horizontalMargin ,
@ -134,6 +144,11 @@ public class MotionElement(
@@ -134,6 +144,11 @@ public class MotionElement(
List < string > arguments = [ "-nostdin" , "-hide_banner" , "-nostats" , "-loglevel" , "error" ] ;
if ( motionElement . EndBehavior is MotionEndBehavior . Loop )
{
arguments . AddRange ( [ "-stream_loop" , "-1" ] ) ;
}
foreach ( string decoder in probeResult . Decoder )
{
arguments . AddRange ( [ "-c:v" , decoder ] ) ;
@ -149,26 +164,38 @@ public class MotionElement(
@@ -149,26 +164,38 @@ public class MotionElement(
"-i" , motionElement . VideoPath ,
] ) ;
var videoFilter = $"fps={f rameRate}" ;
var videoFilter = $"fps={context.F rameRate}" ;
if ( motionElement . Scale )
{
videoFilter + = $",scale={targetSize.Width}:{targetSize.Height}" ;
}
arguments . AddRange ( [ "-vf" , videoFilter ] ) ;
if ( motionElement . EndBehavior is MotionEndBehavior . Loop )
{
arguments . AddRange (
"-t" ,
$"{(int)context.Duration.TotalHours:00}:{context.Duration:mm}:{context.Duration:ss\\.fffffff}" ) ;
}
arguments . AddRange (
[
"-vf" , videoFilter ,
"-f" , "image2pipe" ,
"-pix_fmt" , "bgra" ,
"-vcodec" , "rawvideo" ,
"-"
] ) ;
_ state = MotionElementState . PlayingIn ;
Command command = Cli . Wrap ( "ffmpeg" )
. WithArguments ( arguments )
. WithWorkingDirectory ( FileSystemLayout . TempFilePoolFolder )
. WithStandardOutputPipe ( PipeTarget . ToStream ( pipe . Writer . AsStream ( ) ) ) ;
//logger.LogDebug("ffmpeg motion element arguments {FFmpegArguments}", command.Arguments);
_ cancellationTokenSource = new CancellationTokenSource ( ) ;
var linkedToken = CancellationTokenSource . CreateLinkedTokenSource (
cancellationToken ,
@ -192,60 +219,87 @@ public class MotionElement(
@@ -192,60 +219,87 @@ public class MotionElement(
TimeSpan channelTime ,
CancellationToken cancellationToken )
{
if ( _ isFinished | | contentTime < _ startTime )
{
return Option < PreparedElementImage > . None ;
}
while ( true )
try
{
ReadResult readResult = await _ pipeReader . ReadAsync ( cancellationToken ) ;
ReadOnlySequence < byte > buffer = readResult . Buffer ;
SequencePosition consumed = buffer . Start ;
SequencePosition examined = buffer . End ;
if ( _ state is MotionElementState . Finished | | contentTime < _ startTime )
{
return Option < PreparedElementImage > . None ;
}
try
if ( _ state is MotionElementState . Holding )
{
if ( buffer . Length > = _f rameSiz e)
if ( contentTime < = _ endTim e)
{
ReadOnlySequence < byte > sequence = buffer . Slice ( 0 , _f rameSize ) ;
return new PreparedElementImage ( _ canvasBitmap , SKPointI . Empty , 1.0f , false ) ;
}
using ( SKPixmap pixmap = _ motionFrameBitmap . PeekPixels ( ) )
{
sequence . CopyTo ( pixmap . GetPixelSpan ( ) ) ;
}
_ state = MotionElementState . Finished ;
return Option < PreparedElementImage > . None ;
}
_ canvasBitmap . Erase ( SKColors . Transparent ) ;
while ( true )
{
ReadResult readResult = await _ pipeReader . ReadAsync ( cancellationToken ) ;
ReadOnlySequence < byte > buffer = readResult . Buffer ;
SequencePosition consumed = buffer . Start ;
SequencePosition examined = buffer . End ;
using ( var canvas = new SKCanvas ( _ canvasBitmap ) )
try
{
if ( buffer . Length > = _f rameSize )
{
canvas . DrawBitmap ( _ motionFrameBitmap , _ point ) ;
}
ReadOnlySequence < byte > sequence = buffer . Slice ( 0 , _f rameSize ) ;
// mark this frame as consumed
consumed = sequence . End ;
using ( SKPixmap pixmap = _ motionFrameBitmap . PeekPixels ( ) )
{
sequence . CopyTo ( pixmap . GetPixelSpan ( ) ) ;
}
// we are done, return the frame
return new PreparedElementImage ( _ canvasBitmap , SKPointI . Empty , 1.0f , false ) ;
}
_ canvasBitmap . Erase ( SKColors . Transparent ) ;
if ( readResult . IsCompleted )
{
_ isFinished = true ;
using ( var canvas = new SKCanvas ( _ canvasBitmap ) )
{
canvas . DrawBitmap ( _ motionFrameBitmap , _ point ) ;
}
// mark this frame as consumed
consumed = sequence . End ;
await _ pipeReader . CompleteAsync ( ) ;
return Option < PreparedElementImage > . None ;
// we are done, return the frame
return new PreparedElementImage ( _ canvasBitmap , SKPointI . Empty , 1.0f , false ) ;
}
if ( readResult . IsCompleted )
{
await _ pipeReader . CompleteAsync ( ) ;
if ( motionElement . EndBehavior is MotionEndBehavior . Hold )
{
_ state = MotionElementState . Holding ;
return new PreparedElementImage ( _ canvasBitmap , SKPointI . Empty , 1.0f , false ) ;
}
else
{
_ state = MotionElementState . Finished ;
}
return Option < PreparedElementImage > . None ;
}
}
}
finally
{
if ( ! _ isFinished )
finally
{
// advance the reader, consuming the processed frame and examining the entire buffer
_ pipeReader . AdvanceTo ( consumed , examined ) ;
if ( _ state is not ( MotionElementState . Finished or MotionElementState . Holding ) )
{
// advance the reader, consuming the processed frame and examining the entire buffer
_ pipeReader . AdvanceTo ( consumed , examined ) ;
}
}
}
}
catch ( TaskCanceledException )
{
return Option < PreparedElementImage > . None ;
}
}
private async Task < ProbeResult > ProbeMotionElement ( Resolution frameSize )