@ -1,6 +1,8 @@
@@ -1,6 +1,8 @@
using System.IO.Abstractions ;
using CliWrap ;
using ErsatzTV.Application.Playouts ;
using ErsatzTV.Core ;
using ErsatzTV.Core.Domain ;
using ErsatzTV.Core.FFmpeg ;
using ErsatzTV.Core.Interfaces.FFmpeg ;
using ErsatzTV.Core.Interfaces.Metadata ;
@ -19,6 +21,8 @@ public class NextSessionWorker(
@@ -19,6 +21,8 @@ public class NextSessionWorker(
ILogger < NextSessionWorker > logger )
: IHlsSessionWorker
{
private readonly TimeSpan _ checkpointThreshold = TimeSpan . FromMinutes ( 5 ) ;
private readonly SemaphoreSlim _ slim = new ( 1 , 1 ) ;
private CancellationTokenSource _ cancellationTokenSource ;
private IServiceScope _ serviceScope = serviceScopeFactory . CreateScope ( ) ;
@ -26,6 +30,11 @@ public class NextSessionWorker(
@@ -26,6 +30,11 @@ public class NextSessionWorker(
private string _ channelNumber ;
private string _ workingDirectory ;
private string _ heartbeatFileName ;
private DateTimeOffset _l astTouch ;
private DateTimeOffset _l astCheckpoint ;
private ChannelPlayoutMode _ channelPlayoutMode = ChannelPlayoutMode . Continuous ;
private IMediator Mediator = > _ serviceScope . ServiceProvider . GetRequiredService < IMediator > ( ) ;
void IDisposable . Dispose ( )
{
@ -64,6 +73,8 @@ public class NextSessionWorker(
@@ -64,6 +73,8 @@ public class NextSessionWorker(
public void Touch ( Option < string > fileName )
{
_l astTouch = DateTimeOffset . Now ;
if ( ! fileSystem . File . Exists ( _ heartbeatFileName ) )
{
fileSystem . File . WriteAllBytes ( _ heartbeatFileName , [ ] ) ;
@ -92,6 +103,12 @@ public class NextSessionWorker(
@@ -92,6 +103,12 @@ public class NextSessionWorker(
CancellationToken incomingCancellationToken )
{
_ cancellationTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( incomingCancellationToken ) ;
using var checkpointCts = CancellationTokenSource . CreateLinkedTokenSource ( _ cancellationTokenSource . Token ) ;
Task checkpointLoop = Task . CompletedTask ;
DateTimeOffset sessionStart = DateTimeOffset . Now ;
_l astTouch = sessionStart ;
_l astCheckpoint = _l astTouch ;
try
{
@ -99,6 +116,30 @@ public class NextSessionWorker(
@@ -99,6 +116,30 @@ public class NextSessionWorker(
_ workingDirectory = fileSystem . Path . Combine ( FileSystemLayout . TranscodeFolder , _ channelNumber ) ;
_ heartbeatFileName = fileSystem . Path . Combine ( _ workingDirectory , ".heartbeat" ) ;
Option < PlayoutModeViewModel > maybePlayout = await Mediator . Send (
new GetPlayoutModeByChannelNumber ( _ channelNumber ) ,
_ cancellationTokenSource . Token ) ;
foreach ( PlayoutModeViewModel playout in maybePlayout )
{
_ channelPlayoutMode = playout . PlayoutMode ;
if ( _ channelPlayoutMode is ChannelPlayoutMode . OnDemand )
{
checkpointLoop = CheckpointLoop ( checkpointCts . Token ) ;
await Mediator . Send (
new TimeShiftOnDemandPlayout ( playout . PlayoutId , sessionStart , true ) ,
_ cancellationTokenSource . Token ) ;
// next reads serialized playout files rather than the database, so ensure it
// sees the time-shifted items before starting the channel process
await Mediator . Send (
new SyncNextPlayout ( _ channelNumber ) ,
_ cancellationTokenSource . Token ) ;
}
}
List < string > arguments = [ "run" , "--output-folder" , _ workingDirectory , "--number" , channelNumber , "-" ] ;
string defaultOverlayFile = fileSystem . Path . Combine (
@ -150,6 +191,25 @@ public class NextSessionWorker(
@@ -150,6 +191,25 @@ public class NextSessionWorker(
}
finally
{
await checkpointCts . CancelAsync ( ) ;
try
{
await checkpointLoop ;
}
catch ( OperationCanceledException )
{
// do nothing
}
try
{
await UpdateOnDemandCheckpoint ( CancellationToken . None ) ;
}
catch
{
// do nothing
}
try
{
localFileSystem . EmptyFolder ( _ workingDirectory ) ;
@ -171,4 +231,35 @@ public class NextSessionWorker(
@@ -171,4 +231,35 @@ public class NextSessionWorker(
await Task . Delay ( TimeSpan . FromMilliseconds ( 1 0 0 ) , cancellationToken ) ;
}
}
private async Task CheckpointLoop ( CancellationToken cancellationToken )
{
using var timer = new PeriodicTimer ( _ checkpointThreshold ) ;
while ( await timer . WaitForNextTickAsync ( cancellationToken ) )
{
try
{
if ( _l astTouch > _l astCheckpoint )
{
await UpdateOnDemandCheckpoint ( cancellationToken ) ;
}
}
catch ( Exception ex )
{
logger . LogWarning ( ex , "Failed to update on demand checkpoint for channel {Channel}" , _ channelNumber ) ;
}
}
}
private async Task UpdateOnDemandCheckpoint ( CancellationToken cancellationToken )
{
if ( _ channelPlayoutMode is ChannelPlayoutMode . OnDemand )
{
await Mediator . Send (
new UpdateOnDemandCheckpoint ( _ channelNumber , _l astTouch ) ,
cancellationToken ) ;
}
_l astCheckpoint = _l astTouch ;
}
}