@ -15,6 +15,7 @@ namespace ErsatzTV.Application.Streaming;
@@ -15,6 +15,7 @@ namespace ErsatzTV.Application.Streaming;
public class HlsSessionWorker : IHlsSessionWorker
{
private static readonly SemaphoreSlim Slim = new ( 1 , 1 ) ;
private static int _ workAheadCount ;
private readonly IHlsPlaylistFilter _ hlsPlaylistFilter ;
private readonly IServiceScopeFactory _ serviceScopeFactory ;
@ -25,8 +26,12 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -25,8 +26,12 @@ public class HlsSessionWorker : IHlsSessionWorker
private readonly object _ sync = new ( ) ;
private DateTimeOffset _ playlistStart ;
private Option < int > _ targetFramerate ;
private string _ channelNumber ;
public HlsSessionWorker ( IHlsPlaylistFilter hlsPlaylistFilter , IServiceScopeFactory serviceScopeFactory , ILogger < HlsSessionWorker > logger )
public HlsSessionWorker (
IHlsPlaylistFilter hlsPlaylistFilter ,
IServiceScopeFactory serviceScopeFactory ,
ILogger < HlsSessionWorker > logger )
{
_ hlsPlaylistFilter = hlsPlaylistFilter ;
_ serviceScopeFactory = serviceScopeFactory ;
@ -46,13 +51,23 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -46,13 +51,23 @@ public class HlsSessionWorker : IHlsSessionWorker
}
}
public async Task < Option < TrimPlaylistResult > > TrimPlaylist (
DateTimeOffset filterBefore ,
CancellationToken cancellationToken )
{
Option < string [ ] > maybeLines = await ReadPlaylistLines ( cancellationToken ) ;
return maybeLines . Map ( input = > _ hlsPlaylistFilter . TrimPlaylist ( PlaylistStart , filterBefore , input ) ) ;
}
public async Task Run ( string channelNumber , TimeSpan idleTimeout )
{
var cts = new CancellationTokenSource ( ) ;
void Cancel ( object o , ElapsedEventArgs e ) = > cts . Cancel ( ) ;
try
{
_ channelNumber = channelNumber ;
lock ( _ sync )
{
_ timer = new Timer ( idleTimeout . TotalMilliseconds ) { AutoReset = false } ;
@ -75,7 +90,7 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -75,7 +90,7 @@ public class HlsSessionWorker : IHlsSessionWorker
_ playlistStart = _ transcodedUntil ;
bool initialWorkAhead = Volatile . Read ( ref _ workAheadCount ) < await GetWorkAheadLimit ( ) ;
if ( ! await Transcode ( channelNumber , true , ! initialWorkAhead , cancellationToken ) )
if ( ! await Transcode ( true , ! initialWorkAhead , cancellationToken ) )
{
return ;
}
@ -96,14 +111,14 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -96,14 +111,14 @@ public class HlsSessionWorker : IHlsSessionWorker
bool realtime = transcodedBuffer > = TimeSpan . FromSeconds ( 3 0 ) ;
bool subsequentWorkAhead =
! realtime & & Volatile . Read ( ref _ workAheadCount ) < await GetWorkAheadLimit ( ) ;
if ( ! await Transcode ( channelNumber , false , ! subsequentWorkAhead , cancellationToken ) )
if ( ! await Transcode ( false , ! subsequentWorkAhead , cancellationToken ) )
{
return ;
}
}
else
{
await TrimAndDelete ( channelNumber , c ancellationToken ) ;
await TrimAndDelete ( cancellationToken ) ;
await Task . Delay ( TimeSpan . FromSeconds ( 5 ) , cancellationToken ) ;
}
}
@ -118,7 +133,6 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -118,7 +133,6 @@ public class HlsSessionWorker : IHlsSessionWorker
}
private async Task < bool > Transcode (
string channelNumber ,
bool firstProcess ,
bool realtime ,
CancellationToken cancellationToken )
@ -130,22 +144,22 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -130,22 +144,22 @@ public class HlsSessionWorker : IHlsSessionWorker
if ( ! realtime )
{
Interlocked . Increment ( ref _ workAheadCount ) ;
_l ogger . LogInformation ( "HLS segmenter will work ahead for channel {Channel}" , channelNumber ) ;
_l ogger . LogInformation ( "HLS segmenter will work ahead for channel {Channel}" , _ channelNumber ) ;
}
else
{
_l ogger . LogInformation (
"HLS segmenter will NOT work ahead for channel {Channel}" ,
channelNumber ) ;
_ channelNumber ) ;
}
IMediator mediator = scope . ServiceProvider . GetRequiredService < IMediator > ( ) ;
long ptsOffset = await GetPtsOffset ( mediator , channelNumber , cancellationToken ) ;
long ptsOffset = await GetPtsOffset ( mediator , _ channelNumber , cancellationToken ) ;
// _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset);
var request = new GetPlayoutItemProcessByChannelNumber (
channelNumber ,
_ channelNumber ,
"segmenter" ,
firstProcess ? DateTimeOffset . Now : _ transcodedUntil . AddSeconds ( 1 ) ,
! firstProcess ,
@ -163,7 +177,7 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -163,7 +177,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{
_l ogger . LogWarning (
"Failed to create process for HLS session on channel {Channel}: {Error}" ,
channelNumber ,
_ channelNumber ,
error . ToString ( ) ) ;
return false ;
@ -171,7 +185,7 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -171,7 +185,7 @@ public class HlsSessionWorker : IHlsSessionWorker
foreach ( PlayoutItemProcessModel processModel in result . RightAsEnumerable ( ) )
{
await TrimAndDelete ( channelNumber , c ancellationToken ) ;
await TrimAndDelete ( cancellationToken ) ;
Process process = processModel . Process ;
@ -187,21 +201,21 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -187,21 +201,21 @@ public class HlsSessionWorker : IHlsSessionWorker
}
catch ( TaskCanceledException )
{
_l ogger . LogInformation ( "Terminating HLS process for channel {Channel}" , channelNumber ) ;
_l ogger . LogInformation ( "Terminating HLS process for channel {Channel}" , _ channelNumber ) ;
process . Kill ( ) ;
process . WaitForExit ( ) ;
return false ;
}
_l ogger . LogInformation ( "HLS process has completed for channel {Channel}" , channelNumber ) ;
_l ogger . LogInformation ( "HLS process has completed for channel {Channel}" , _ channelNumber ) ;
_ transcodedUntil = processModel . Until ;
}
}
catch ( Exception ex )
{
_l ogger . LogError ( ex , "Error transcoding channel {Channel}" , channelNumber ) ;
_l ogger . LogError ( ex , "Error transcoding channel {Channel}" , _ channelNumber ) ;
try
{
@ -223,26 +237,21 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -223,26 +237,21 @@ public class HlsSessionWorker : IHlsSessionWorker
return true ;
}
private async Task TrimAndDelete ( string channelNumber , CancellationToken cancellationToken )
private async Task TrimAndDelete ( CancellationToken cancellationToken )
{
string playlistFileName = Path . Combine (
FileSystemLayout . TranscodeFolder ,
channelNumber ,
"live.m3u8" ) ;
if ( File . Exists ( playlistFileName ) )
Option < string [ ] > maybeLines = await ReadPlaylistLines ( cancellationToken ) ;
foreach ( string [ ] lines in maybeLines )
{
// trim playlist and insert discontinuity before appending with new ffmpeg process
string [ ] lines = await File . ReadAllLinesAsync ( playlistFileName , cancellationToken ) ;
TrimPlaylistResult trimResult = _ hlsPlaylistFilter . TrimPlaylistWithDiscontinuity (
_ playlistStart ,
DateTimeOffset . Now . AddMinutes ( - 1 ) ,
lines ) ;
await File . WriteAllTextAsync ( playlistFileName , trimResult . Playlist , cancellationToken ) ;
await WritePlaylist ( trimResult . Playlist , cancellationToken ) ;
// delete old segments
var allSegments = Directory . GetFiles (
Path . Combine ( FileSystemLayout . TranscodeFolder , channelNumber ) ,
Path . Combine ( FileSystemLayout . TranscodeFolder , _ channelNumber ) ,
"live*.ts" )
. Map (
file = >
@ -302,5 +311,43 @@ public class HlsSessionWorker : IHlsSessionWorker
@@ -302,5 +311,43 @@ public class HlsSessionWorker : IHlsSessionWorker
. Map ( maybeCount = > maybeCount . Match ( identity , ( ) = > 1 ) ) ;
}
private async Task < Option < string [ ] > > ReadPlaylistLines ( CancellationToken cancellationToken )
{
await Slim . WaitAsync ( cancellationToken ) ;
try
{
string fileName = PlaylistFileName ( ) ;
if ( File . Exists ( fileName ) )
{
return await File . ReadAllLinesAsync ( fileName , cancellationToken ) ;
}
return None ;
}
finally
{
Slim . Release ( ) ;
}
}
private async Task WritePlaylist ( string playlist , CancellationToken cancellationToken )
{
await Slim . WaitAsync ( cancellationToken ) ;
try
{
string fileName = PlaylistFileName ( ) ;
await File . WriteAllTextAsync ( fileName , playlist , cancellationToken ) ;
}
finally
{
Slim . Release ( ) ;
}
}
private string PlaylistFileName ( ) = > Path . Combine (
FileSystemLayout . TranscodeFolder ,
_ channelNumber ,
"live.m3u8" ) ;
private record Segment ( string File , int SequenceNumber ) ;
}