diff --git a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs index d17552de0..f0cd1b8c4 100644 --- a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs +++ b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs @@ -11,7 +11,6 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Streaming; -using ErsatzTV.FFmpeg.OutputFormat; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -123,7 +122,6 @@ public class StartFFmpegSessionHandler : IRequestHandler new HlsSessionWorker( _serviceScopeFactory, - OutputFormatKind.HlsMp4, _graphicsEngine, _client, _hlsPlaylistFilter, diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 7fe193374..1d45e2cb0 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -27,8 +27,8 @@ public class HlsSessionWorker : IHlsSessionWorker private static int _workAheadCount; private readonly IClient _client; private readonly IHlsInitSegmentCache _hlsInitSegmentCache; + private readonly Dictionary _discontinuityMap = []; private readonly IConfigElementRepository _configElementRepository; - private readonly OutputFormatKind _outputFormat; private readonly IGraphicsEngine _graphicsEngine; private readonly IHlsPlaylistFilter _hlsPlaylistFilter; private readonly ILocalFileSystem _localFileSystem; @@ -40,8 +40,8 @@ public class HlsSessionWorker : IHlsSessionWorker private CancellationTokenSource _cancellationTokenSource; private string _channelNumber; private DateTimeOffset _channelStart; + private int _discontinuitySequence; private bool _disposedValue; - private bool _hasWrittenSegments; private DateTimeOffset _lastAccess; private DateTimeOffset _lastDelete = DateTimeOffset.MinValue; private IServiceScope _serviceScope; @@ -52,7 +52,6 @@ public class HlsSessionWorker : IHlsSessionWorker public HlsSessionWorker( IServiceScopeFactory serviceScopeFactory, - OutputFormatKind outputFormat, IGraphicsEngine graphicsEngine, IClient client, IHlsPlaylistFilter hlsPlaylistFilter, @@ -64,7 +63,6 @@ public class HlsSessionWorker : IHlsSessionWorker { _serviceScope = serviceScopeFactory.CreateScope(); _mediator = _serviceScope.ServiceProvider.GetRequiredService(); - _outputFormat = outputFormat; _graphicsEngine = graphicsEngine; _client = client; _hlsInitSegmentCache = hlsInitSegmentCache; @@ -121,7 +119,8 @@ public class HlsSessionWorker : IHlsSessionWorker await RefreshInits(); TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylist( - _outputFormat, + _discontinuityMap, + OutputFormatKind.HlsMp4, PlaylistStart, filterBefore, _hlsInitSegmentCache, @@ -191,10 +190,7 @@ public class HlsSessionWorker : IHlsSessionWorker CancellationToken cancellationToken = _cancellationTokenSource.Token; - _logger.LogInformation( - "Starting HLS session for channel {Channel} with output format {OutputFormat}", - channelNumber, - _outputFormat); + _logger.LogInformation("Starting HLS session for channel {Channel}", channelNumber); if (_localFileSystem.ListFiles(_workingDirectory).Any()) { @@ -269,7 +265,7 @@ public class HlsSessionWorker : IHlsSessionWorker try { - //_localFileSystem.EmptyFolder(_workingDirectory); + _localFileSystem.EmptyFolder(_workingDirectory); } catch { @@ -447,10 +443,7 @@ public class HlsSessionWorker : IHlsSessionWorker } // fmp4 doesn't require pts offsets because each new item gets a discontinuity AND a new init segment - long ptsOffset = _outputFormat is OutputFormatKind.HlsMp4 - ? 0 - : await GetPtsOffset(_channelNumber, cancellationToken); - // _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset); + const long PTS_OFFSET = 0; _logger.LogDebug("HLS session state: {State}", _state); @@ -464,7 +457,7 @@ public class HlsSessionWorker : IHlsSessionWorker startAtZero, realtime, _channelStart, - ptsOffset, + PTS_OFFSET, _targetFramerate); // _logger.LogInformation("Request {@Request}", request); @@ -487,6 +480,14 @@ public class HlsSessionWorker : IHlsSessionWorker { await TrimAndDelete(cancellationToken); + // increment discontinuity sequence and store with segment key (generated at) + foreach (long segmentKey in processModel.SegmentKey) + { + _discontinuitySequence++; + _discontinuityMap.TryAdd(segmentKey, _discontinuitySequence); + //_logger.LogDebug("DISCONTINUITY MAP {Map}", _discontinuityMap); + } + Option maybePipe = Option.None; var stdErrBuffer = new StringBuilder(); @@ -527,7 +528,6 @@ public class HlsSessionWorker : IHlsSessionWorker processModel.Until.Subtract(DateTimeOffset.Now).TotalSeconds); _transcodedUntil = processModel.Until; _state = NextState(_state, processModel); - _hasWrittenSegments = true; return true; } else @@ -552,7 +552,7 @@ public class HlsSessionWorker : IHlsSessionWorker _channelNumber, StreamingMode.HttpLiveStreamingSegmenter, realtime, - ptsOffset, + PTS_OFFSET, processModel.MaybeDuration, processModel.Until, errorMessage), @@ -577,8 +577,6 @@ public class HlsSessionWorker : IHlsSessionWorker _transcodedUntil = processModel.Until; _state = NextState(_state, null); - _hasWrittenSegments = true; - return true; } } @@ -649,7 +647,8 @@ public class HlsSessionWorker : IHlsSessionWorker // trim playlist and insert discontinuity before appending with new ffmpeg process TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity( - _outputFormat, + _discontinuityMap, + OutputFormatKind.HlsMp4, PlaylistStart, DateTimeOffset.Now.AddMinutes(-1), _hlsInitSegmentCache, @@ -725,6 +724,7 @@ public class HlsSessionWorker : IHlsSessionWorker toDelete.Add(init); _hlsInitSegmentCache.DeleteSegment(fileName); + _discontinuityMap.Remove(init.GeneratedAt); } foreach (Segment segment in toDelete) @@ -744,11 +744,6 @@ public class HlsSessionWorker : IHlsSessionWorker private async Task RefreshInits() { - if (_outputFormat is not OutputFormatKind.HlsMp4) - { - return; - } - var allSegments = Directory.GetFiles(_workingDirectory, "live*.m4s") .Map(Path.GetFileName) .Map(s => s.Split("_")[1]) @@ -764,41 +759,6 @@ public class HlsSessionWorker : IHlsSessionWorker } } - private async Task GetPtsOffset(string channelNumber, CancellationToken cancellationToken) - { - await _slim.WaitAsync(cancellationToken); - try - { - long result = 0; - - // if we haven't yet written any segments, start at zero - if (!_hasWrittenSegments) - { - return result; - } - - Either queryResult = await _mediator.Send( - new GetLastPtsTime(channelNumber), - cancellationToken); - - foreach (BaseError error in queryResult.LeftToSeq()) - { - _logger.LogWarning("Unable to determine last pts offset - {Error}", error.ToString()); - } - - foreach (PtsTime pts in queryResult.RightToSeq()) - { - result = pts.Value; - } - - return result; - } - finally - { - _slim.Release(); - } - } - private async Task GetWorkAheadLimit(CancellationToken cancellationToken) => await _configElementRepository.GetValue(ConfigElementKey.FFmpegWorkAheadSegmenters, cancellationToken) .Map(maybeCount => maybeCount.Match(identity, () => 1)); diff --git a/ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs b/ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs index 089a571d2..c55c0944d 100644 --- a/ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs +++ b/ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs @@ -8,4 +8,5 @@ public record PlayoutItemProcessModel( Option GraphicsEngineContext, Option MaybeDuration, DateTimeOffset Until, - bool IsComplete); + bool IsComplete, + Option SegmentKey); diff --git a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs index b32522c44..a2b7d0f83 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs @@ -43,6 +43,7 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler.None, Option.None, DateTimeOffset.MaxValue, - true); + true, + Option.None); } } diff --git a/ErsatzTV.Application/Streaming/Queries/GetErrorProcessHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetErrorProcessHandler.cs index 33b878506..262b87fd7 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetErrorProcessHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetErrorProcessHandler.cs @@ -8,16 +8,11 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Streaming; -public class GetErrorProcessHandler : FFmpegProcessHandler +public class GetErrorProcessHandler( + IDbContextFactory dbContextFactory, + IFFmpegProcessService ffmpegProcessService) + : FFmpegProcessHandler(dbContextFactory) { - private readonly IFFmpegProcessService _ffmpegProcessService; - - public GetErrorProcessHandler( - IDbContextFactory dbContextFactory, - IFFmpegProcessService ffmpegProcessService) - : base(dbContextFactory) => - _ffmpegProcessService = ffmpegProcessService; - protected override async Task> GetProcess( TvContext dbContext, GetErrorProcess request, @@ -26,9 +21,12 @@ public class GetErrorProcessHandler : FFmpegProcessHandler string ffprobePath, CancellationToken cancellationToken) { - Command process = await _ffmpegProcessService.ForError( + DateTimeOffset now = DateTimeOffset.Now; + + Command process = await ffmpegProcessService.ForError( ffmpegPath, channel, + now, request.MaybeDuration, request.ErrorMessage, request.HlsRealtime, @@ -43,6 +41,7 @@ public class GetErrorProcessHandler : FFmpegProcessHandler Option.None, request.MaybeDuration, request.Until, - true); + true, + now.ToUnixTimeSeconds()); } } diff --git a/ErsatzTV.Application/Streaming/Queries/GetLastPtsTime.cs b/ErsatzTV.Application/Streaming/Queries/GetLastPtsTime.cs deleted file mode 100644 index 0b77cd5cf..000000000 --- a/ErsatzTV.Application/Streaming/Queries/GetLastPtsTime.cs +++ /dev/null @@ -1,5 +0,0 @@ -using ErsatzTV.Core; - -namespace ErsatzTV.Application.Streaming; - -public record GetLastPtsTime(string ChannelNumber) : IRequest>; diff --git a/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs deleted file mode 100644 index 04d119bb0..000000000 --- a/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Text; -using Bugsnag; -using CliWrap; -using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.FFmpeg; -using ErsatzTV.Core.Interfaces.FFmpeg; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -namespace ErsatzTV.Application.Streaming; - -public class GetLastPtsTimeHandler : IRequestHandler> -{ - private readonly IClient _client; - private readonly IConfigElementRepository _configElementRepository; - private readonly ILocalFileSystem _localFileSystem; - private readonly ILogger _logger; - private readonly ITempFilePool _tempFilePool; - - public GetLastPtsTimeHandler( - IClient client, - ILocalFileSystem localFileSystem, - ITempFilePool tempFilePool, - IConfigElementRepository configElementRepository, - ILogger logger) - { - _client = client; - _localFileSystem = localFileSystem; - _tempFilePool = tempFilePool; - _configElementRepository = configElementRepository; - _logger = logger; - } - - public async Task> Handle( - GetLastPtsTime request, - CancellationToken cancellationToken) - { - Validation validation = await Validate(request, cancellationToken); - return await validation.Match( - parameters => Handle(parameters, cancellationToken), - error => Task.FromResult>(error.Join())); - } - - private async Task> Validate( - GetLastPtsTime request, - CancellationToken cancellationToken) => - await ValidateFFprobePath(cancellationToken) - .MapT(ffprobePath => new RequestParameters(request.ChannelNumber, ffprobePath)); - - private async Task> Handle( - RequestParameters parameters, - CancellationToken cancellationToken) - { - Option maybeLastSegment = GetLastSegment(parameters.ChannelNumber); - foreach (FileInfo segment in maybeLastSegment) - { - return await GetPts(parameters, segment, cancellationToken).IfNoneAsync(PtsTime.Zero); - } - - return BaseError.New($"Failed to determine last pts duration for channel {parameters.ChannelNumber}"); - } - - private async Task> GetPts( - RequestParameters parameters, - FileInfo segment, - CancellationToken cancellationToken) - { - string[] argumentList = - { - "-v", "0", - "-show_entries", - "packet=pts,duration", - "-of", "compact=p=0:nk=1", - segment.FullName - }; - - string lastLine = string.Empty; - Action replaceLine = s => - { - if (!string.IsNullOrWhiteSpace(s)) - { - lastLine = s.Trim(); - } - }; - - CommandResult probe = await Cli.Wrap(parameters.FFprobePath) - .WithArguments(argumentList) - .WithValidation(CommandResultValidation.None) - .WithStandardOutputPipe(PipeTarget.ToDelegate(replaceLine)) - .ExecuteAsync(cancellationToken); - - if (probe.ExitCode != 0) - { - return Option.None; - } - - try - { - return PtsTime.From(lastLine); - } - catch (Exception ex) - { - _client.Notify(ex); - await SaveTroubleshootingData(parameters.ChannelNumber, lastLine); - } - - return Option.None; - } - - private static Option GetLastSegment(string channelNumber) - { - var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber)); - return Optional(directory.GetFiles("*.ts").OrderByDescending(f => f.Name).FirstOrDefault()); - } - - private Task> ValidateFFprobePath(CancellationToken cancellationToken) => - _configElementRepository.GetValue(ConfigElementKey.FFprobePath, cancellationToken) - .FilterT(File.Exists) - .Map(ffprobePath => ffprobePath.ToValidation("FFprobe path does not exist on the file system")); - - private async Task SaveTroubleshootingData(string channelNumber, string output) - { - try - { - var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber)); - FileInfo[] allFiles = directory.GetFiles(); - - string playlistFileName = Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8"); - string playlistContents = string.Empty; - if (_localFileSystem.FileExists(playlistFileName)) - { - playlistContents = await File.ReadAllTextAsync(playlistFileName); - } - - var data = new TroubleshootingData(allFiles, playlistContents, output); - string serialized = data.Serialize(); - - string file = _tempFilePool.GetNextTempFile(TempFileCategory.BadTranscodeFolder); - await File.WriteAllTextAsync(file, serialized); - - _logger.LogWarning("Transcode folder is in bad state; troubleshooting info saved to {File}", file); - } - catch (Exception ex) - { - _client.Notify(ex); - } - } - - private sealed record RequestParameters(string ChannelNumber, string FFprobePath); - - private sealed record TroubleshootingData(IEnumerable Files, string Playlist, string ProbeOutput) - { - public string Serialize() - { - var data = new InternalData( - Files.Map(f => new FileData(f.FullName, f.Length, f.LastWriteTimeUtc)).ToList(), - Convert.ToBase64String(Encoding.UTF8.GetBytes(Playlist)), - Convert.ToBase64String(Encoding.UTF8.GetBytes(ProbeOutput))); - - return JsonConvert.SerializeObject(data); - } - - private sealed record FileData(string FileName, long Bytes, DateTime LastWriteTimeUtc); - - private sealed record InternalData(List Files, string EncodedPlaylist, string EncodedProbeOutput); - } -} diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index 5f0701562..219571766 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -302,6 +302,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< Command doesNotExistProcess = await _ffmpegProcessService.ForError( ffmpegPath, channel, + now, duration, $"DEBUG_NO_SYNC:\n{Mapper.GetDisplayTitle(playoutItemWithPath.PlayoutItem.MediaItem, Option.None)}\nFrom: {start} To: {finish}", request.HlsRealtime, @@ -316,7 +317,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< Option.None, duration, finish, - true); + true, + now.ToUnixTimeSeconds()); } MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem.GetHeadVersion(); @@ -442,7 +444,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< playoutItemResult.GraphicsEngineContext, duration, finish, - isComplete); + isComplete, + effectiveNow.ToUnixTimeSeconds()); return Right(result); } @@ -473,6 +476,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< Command offlineProcess = await _ffmpegProcessService.ForError( ffmpegPath, channel, + now, maybeDuration, "Channel is Offline", request.HlsRealtime, @@ -487,11 +491,13 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< Option.None, maybeDuration, finish, - true); + true, + now.ToUnixTimeSeconds()); case PlayoutItemDoesNotExistOnDisk: Command doesNotExistProcess = await _ffmpegProcessService.ForError( ffmpegPath, channel, + now, maybeDuration, error.Value, request.HlsRealtime, @@ -506,11 +512,13 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< Option.None, maybeDuration, finish, - true); + true, + now.ToUnixTimeSeconds()); default: Command errorProcess = await _ffmpegProcessService.ForError( ffmpegPath, channel, + now, maybeDuration, "Channel is Offline", request.HlsRealtime, @@ -525,7 +533,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< Option.None, maybeDuration, finish, - true); + true, + now.ToUnixTimeSeconds()); } } diff --git a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs index e644dd669..1ff5d04a3 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs @@ -44,6 +44,7 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler.None, Option.None, DateTimeOffset.MaxValue, - true); + true, + Option.None); } } diff --git a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs index 3b42584c1..882c4fbc2 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs @@ -31,21 +31,26 @@ public class HlsPlaylistFilterTests #EXT-X-MEDIA-SEQUENCE:1137 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:49.320-0500 -live001137.ts +live_1760874038_001137.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:53.320-0500 -live001138.ts +live_1760874038_001138.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 -live001139.ts").Split(Environment.NewLine); +live_1760874038_001139.m4s").Split(Environment.NewLine); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( - OutputFormatKind.Hls, + new Dictionary + { + [1760874038] = 1 + }, + OutputFormatKind.HlsMp4, start, start.AddSeconds(-30), - Substitute.For(), + new FakeHlsInitSegmentCache(), input, maybeMaxSegments: 10); @@ -57,17 +62,18 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 -#EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 -live001137.ts +live_1760874038_001137.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:04.000-0500 -live001138.ts +live_1760874038_001138.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 -live001139.ts +live_1760874038_001139.m4s ")); } @@ -82,21 +88,26 @@ live001139.ts #EXT-X-MEDIA-SEQUENCE:1137 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:49.320-0500 -live001137.ts +live_1760874038_001137.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:53.320-0500 -live001138.ts +live_1760874038_001138.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 -live001139.ts").Split(Environment.NewLine); +live_1760874038_001139.m4s").Split(Environment.NewLine); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( - OutputFormatKind.Hls, + new Dictionary + { + [1760874038] = 1 + }, + OutputFormatKind.HlsMp4, start, start.AddSeconds(-30), - Substitute.For(), + new FakeHlsInitSegmentCache(), input, 2); @@ -108,14 +119,15 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 -#EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 -live001137.ts +live_1760874038_001137.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:04.000-0500 -live001138.ts +live_1760874038_001138.m4s ")); } @@ -130,24 +142,27 @@ live001138.ts #EXT-X-MEDIA-SEQUENCE:1137 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:49.320-0500 -live001137.ts +live_1760874038_001137.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:53.320-0500 -live001138.ts +live_1760874038_001138.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 -live001139.ts").Split(Environment.NewLine); +live_1760874038_001139.m4s").Split(Environment.NewLine); - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( - OutputFormatKind.Hls, + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity( + new Dictionary + { + [1760874038] = 1 + }, + OutputFormatKind.HlsMp4, start, start.AddSeconds(-30), - Substitute.For(), - input, - int.MaxValue, - true); + new FakeHlsInitSegmentCache(), + input); result.PlaylistStart.ShouldBe(start); result.Sequence.ShouldBe(1137); @@ -157,17 +172,18 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 -#EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 -live001137.ts +live_1760874038_001137.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:04.000-0500 -live001138.ts +live_1760874038_001138.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 -live001139.ts +live_1760874038_001139.m4s #EXT-X-DISCONTINUITY ")); } @@ -183,21 +199,26 @@ live001139.ts #EXT-X-MEDIA-SEQUENCE:1137 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:49.320-0500 -live001137.ts +live_1760874038_001137.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:53.320-0500 -live001138.ts +live_1760874038_001138.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 -live001139.ts").Split(Environment.NewLine); +live_1760874038_001139.m4s").Split(Environment.NewLine); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( - OutputFormatKind.Hls, + new Dictionary + { + [1760874038] = 1 + }, + OutputFormatKind.HlsMp4, start, start.AddSeconds(6), - Substitute.For(), + new FakeHlsInitSegmentCache(), input, 1); @@ -209,16 +230,17 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1139 -#EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 -live001139.ts +live_1760874038_001139.m4s ")); } [Test] - public void HlsPlaylistFilter_ShouldFilterOldDiscontinuity() + public void HlsPlaylistFilter_ShouldKeepOldDiscontinuity() { var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); string[] input = NormalizeLineEndings( @@ -228,22 +250,28 @@ live001139.ts #EXT-X-MEDIA-SEQUENCE:1137 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:49.320-0500 -live001137.ts +live_1760874038_001137.m4s #EXT-X-DISCONTINUITY #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:53.320-0500 -live001138.ts +live_1760874042_001138.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 -live001139.ts").Split(Environment.NewLine); +live_1760874042_001139.m4s").Split(Environment.NewLine); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( - OutputFormatKind.Hls, + new Dictionary + { + [1760874038] = 1, + [1760874042] = 2 + }, + OutputFormatKind.HlsMp4, start, start.AddSeconds(6), - Substitute.For(), + new FakeHlsInitSegmentCache(), input, maybeMaxSegments: 10); @@ -255,18 +283,20 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 -#EXT-X-DISCONTINUITY-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:00.000-0500 -live001137.ts +live_1760874038_001137.m4s #EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874042_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:04.000-0500 -live001138.ts +live_1760874042_001138.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 -live001139.ts +live_1760874042_001139.m4s ")); } @@ -279,265 +309,271 @@ live001139.ts #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:0 -#EXT-X-DISCONTINUITY #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:08:55.981-0500 -live000000.ts +live_1760874038_000000.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:08:59.985-0500 -live000001.ts +live_1760874038_000001.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:03.989-0500 -live000002.ts +live_1760874038_000002.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:07.993-0500 -live000003.ts +live_1760874038_000003.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:11.997-0500 -live000004.ts +live_1760874038_000004.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:16.001-0500 -live000005.ts +live_1760874038_000005.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:20.005-0500 -live000006.ts +live_1760874038_000006.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:24.009-0500 -live000007.ts +live_1760874038_000007.m4s #EXTINF:3.970633, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:28.013-0500 -live000008.ts +live_1760874038_000008.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:31.983-0500 -live000009.ts +live_1760874038_000009.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:35.987-0500 -live000010.ts +live_1760874038_000010.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:39.991-0500 -live000011.ts +live_1760874038_000011.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:43.995-0500 -live000012.ts +live_1760874038_000012.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:47.999-0500 -live000013.ts +live_1760874038_000013.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:52.003-0500 -live000014.ts +live_1760874038_000014.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:09:56.007-0500 -live000015.ts +live_1760874038_000015.m4s #EXTINF:3.970633, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:00.011-0500 -live000016.ts +live_1760874038_000016.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:03.982-0500 -live000017.ts +live_1760874038_000017.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:07.986-0500 -live000018.ts +live_1760874038_000018.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:11.990-0500 -live000019.ts +live_1760874038_000019.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:15.994-0500 -live000020.ts +live_1760874038_000020.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:19.998-0500 -live000021.ts +live_1760874038_000021.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:24.002-0500 -live000022.ts +live_1760874038_000022.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:28.006-0500 -live000023.ts +live_1760874038_000023.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:32.010-0500 -live000024.ts +live_1760874038_000024.m4s #EXTINF:3.970633, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:36.014-0500 -live000025.ts +live_1760874038_000025.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:39.985-0500 -live000026.ts +live_1760874038_000026.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:43.989-0500 -live000027.ts +live_1760874038_000027.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:47.993-0500 -live000028.ts +live_1760874038_000028.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:51.997-0500 -live000029.ts +live_1760874038_000029.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:10:56.001-0500 -live000030.ts +live_1760874038_000030.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:00.005-0500 -live000031.ts +live_1760874038_000031.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:04.009-0500 -live000032.ts +live_1760874038_000032.m4s #EXTINF:3.970633, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:08.013-0500 -live000033.ts +live_1760874038_000033.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:11.983-0500 -live000034.ts +live_1760874038_000034.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:15.987-0500 -live000035.ts +live_1760874038_000035.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:19.991-0500 -live000036.ts +live_1760874038_000036.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:23.995-0500 -live000037.ts +live_1760874038_000037.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:27.999-0500 -live000038.ts +live_1760874038_000038.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:32.003-0500 -live000039.ts +live_1760874038_000039.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:36.007-0500 -live000040.ts +live_1760874038_000040.m4s #EXTINF:3.970633, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:40.011-0500 -live000041.ts +live_1760874038_000041.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:43.982-0500 -live000042.ts +live_1760874038_000042.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:47.986-0500 -live000043.ts +live_1760874038_000043.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:51.990-0500 -live000044.ts +live_1760874038_000044.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:55.994-0500 -live000045.ts +live_1760874038_000045.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:59.998-0500 -live000046.ts +live_1760874038_000046.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:04.002-0500 -live000047.ts +live_1760874038_000047.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:08.006-0500 -live000048.ts +live_1760874038_000048.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:12.010-0500 -live000049.ts +live_1760874038_000049.m4s #EXTINF:3.970633, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:16.014-0500 -live000050.ts +live_1760874038_000050.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:19.985-0500 -live000051.ts +live_1760874038_000051.m4s #EXTINF:0.433767, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:23.989-0500 -live000052.ts +live_1760874038_000052.m4s #EXT-X-DISCONTINUITY +#EXT-X-MAP:URI=""1760874138_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:30.007-0500 -live000053.ts +live_1760874138_000053.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:34.007-0500 -live000054.ts +live_1760874138_000054.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:38.007-0500 -live000055.ts +live_1760874138_000055.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:42.007-0500 -live000056.ts +live_1760874138_000056.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:46.007-0500 -live000057.ts +live_1760874138_000057.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:50.007-0500 -live000058.ts +live_1760874138_000058.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:54.007-0500 -live000059.ts +live_1760874138_000059.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:58.007-0500 -live000060.ts +live_1760874138_000060.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:02.007-0500 -live000061.ts +live_1760874138_000061.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:06.007-0500 -live000062.ts +live_1760874138_000062.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:10.007-0500 -live000063.ts +live_1760874138_000063.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:14.007-0500 -live000064.ts +live_1760874138_000064.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:18.007-0500 -live000065.ts +live_1760874138_000065.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:22.007-0500 -live000066.ts +live_1760874138_000066.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:26.007-0500 -live000067.ts +live_1760874138_000067.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:30.007-0500 -live000068.ts +live_1760874138_000068.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:34.007-0500 -live000069.ts +live_1760874138_000069.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:38.007-0500 -live000070.ts +live_1760874138_000070.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:42.007-0500 -live000071.ts +live_1760874138_000071.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:46.007-0500 -live000072.ts +live_1760874138_000072.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:50.007-0500 -live000073.ts +live_1760874138_000073.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:54.007-0500 -live000074.ts +live_1760874138_000074.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:58.007-0500 -live000075.ts +live_1760874138_000075.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:02.007-0500 -live000076.ts +live_1760874138_000076.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:06.007-0500 -live000077.ts +live_1760874138_000077.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:10.007-0500 -live000078.ts +live_1760874138_000078.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:14.007-0500 -live000079.ts +live_1760874138_000079.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:18.007-0500 -live000080.ts +live_1760874138_000080.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:22.007-0500 -live000081.ts +live_1760874138_000081.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:13:26.007-0500 -live000082.ts").Split(Environment.NewLine); +live_1760874138_000082.m4s").Split(Environment.NewLine); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( - OutputFormatKind.Hls, + new Dictionary + { + [1760874038] = 1, + [1760874138] = 2 + }, + OutputFormatKind.HlsMp4, start, start.AddSeconds(220), - Substitute.For(), + new FakeHlsInitSegmentCache(), input, maybeMaxSegments: 10); @@ -551,36 +587,37 @@ live000082.ts").Split(Environment.NewLine); #EXT-X-MEDIA-SEQUENCE:56 #EXT-X-DISCONTINUITY-SEQUENCE:2 #EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI=""1760874138_init.mp4"" #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:40.441-0500 -live000056.ts +live_1760874138_000056.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:44.441-0500 -live000057.ts +live_1760874138_000057.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:48.441-0500 -live000058.ts +live_1760874138_000058.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:52.441-0500 -live000059.ts +live_1760874138_000059.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:11:56.441-0500 -live000060.ts +live_1760874138_000060.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:00.441-0500 -live000061.ts +live_1760874138_000061.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:04.441-0500 -live000062.ts +live_1760874138_000062.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:08.441-0500 -live000063.ts +live_1760874138_000063.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:12.441-0500 -live000064.ts +live_1760874138_000064.m4s #EXTINF:4.000000, #EXT-X-PROGRAM-DATE-TIME:2022-05-25T20:12:16.441-0500 -live000065.ts +live_1760874138_000065.m4s ")); } @@ -595,26 +632,32 @@ live000065.ts #EXT-X-MEDIA-SEQUENCE:46 #EXT-X-DISCONTINUITY-SEQUENCE:2 #EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI=""1760874038_init.mp4"" #EXTINF:1.734000, #EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:05.031-0500 -live000046.ts +live_1760874038_000046.m4s #EXT-X-DISCONTINUITY #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:06.765-0500 -live000047.ts +live_1760874040_000047.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:10.769-0500 -live000048.ts +live_1760874040_000048.m4s ").Split(Environment.NewLine); - // filter 'live000046.ts' + // filter 'live000046.m4s' var filterBefore = start.AddSeconds(2); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( - OutputFormatKind.Hls, + new Dictionary + { + [1760874038] = 2, + [1760874040] = 3 + }, + OutputFormatKind.HlsMp4, start, filterBefore, - Substitute.For(), + new FakeHlsInitSegmentCache(), input, 2); @@ -627,12 +670,327 @@ live000048.ts #EXT-X-MEDIA-SEQUENCE:47 #EXT-X-DISCONTINUITY-SEQUENCE:3 #EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI=""1760874040_init.mp4"" #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:06.765-0500 -live000047.ts +live_1760874040_000047.m4s #EXTINF:4.004000, #EXT-X-PROGRAM-DATE-TIME:2025-09-17T10:11:10.769-0500 -live000048.ts +live_1760874040_000048.m4s +"); + + result.Playlist.ShouldBe(expectedPlaylist); + } + + [Test] + public void HlsPlaylistFilter_ShouldNotPullDiscontinuityForward() + { + // #EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:59.132-0500 + var start = new DateTimeOffset(2025, 10, 19, 6, 42, 27, 132, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings( + @"##EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:5 +#EXT-X-MEDIA-SEQUENCE:38 +#EXT-X-DISCONTINUITY-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI=""1760873994_init.mp4"" +#EXTINF:3.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:27.132-0500 +live_1760874038_000038.m4s +#EXTINF:5.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:30.132-0500 +live_1760874038_000039.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:35.132-0500 +live_1760874038_000040.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:39.132-0500 +live_1760874038_000041.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:43.132-0500 +live_1760874038_000042.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:47.132-0500 +live_1760874038_000043.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:51.132-0500 +live_1760874038_000044.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:55.132-0500 +live_1760874038_000045.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:59.132-0500 +live_1760874038_000046.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:03.132-0500 +live_1760874038_000047.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:07.132-0500 +live_1760874038_000048.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:11.132-0500 +live_1760874038_000049.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:15.132-0500 +live_1760874038_000050.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:19.132-0500 +live_1760874038_000051.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:23.132-0500 +live_1760874038_000052.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:27.132-0500 +live_1760874038_000053.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:31.132-0500 +live_1760874038_000054.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:35.132-0500 +live_1760874038_000055.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:39.132-0500 +live_1760874038_000056.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:43.132-0500 +live_1760874038_000057.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:47.132-0500 +live_1760874038_000058.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:51.132-0500 +live_1760874038_000059.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:55.132-0500 +live_1760874038_000060.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:59.132-0500 +live_1760874038_000061.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:44:03.132-0500 +live_1760874038_000062.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:44:07.132-0500 +live_1760874038_000063.m4s +#EXTINF:1.720000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:44:11.132-0500 +live_1760874038_000064.m4s +#EXT-X-DISCONTINUITY +").Split(Environment.NewLine); + + // filter 'live000046.m4s' + var filterBefore = start.AddSeconds(32); + + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( + new Dictionary + { + [1760874038] = 2 + }, + OutputFormatKind.HlsMp4, + start, + filterBefore, + new FakeHlsInitSegmentCache(), + input, + 10); + + result.Sequence.ShouldBe(46); + + string expectedPlaylist = NormalizeLineEndings( + @"#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:5 +#EXT-X-MEDIA-SEQUENCE:46 +#EXT-X-DISCONTINUITY-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI=""1760874038_init.mp4"" +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:59.132-0500 +live_1760874038_000046.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:03.132-0500 +live_1760874038_000047.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:07.132-0500 +live_1760874038_000048.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:11.132-0500 +live_1760874038_000049.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:15.132-0500 +live_1760874038_000050.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:19.132-0500 +live_1760874038_000051.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:23.132-0500 +live_1760874038_000052.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:27.132-0500 +live_1760874038_000053.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:31.132-0500 +live_1760874038_000054.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:35.132-0500 +live_1760874038_000055.m4s +"); + + result.Playlist.ShouldBe(expectedPlaylist); + } + + [Test] + public void HlsPlaylistFilter_ShouldNotPullDiscontinuityForward_PartTwo() + { + // #EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:03.132-0500 + var start = new DateTimeOffset(2025, 10, 19, 6, 42, 27, 132, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings( + @"##EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:5 +#EXT-X-MEDIA-SEQUENCE:38 +#EXT-X-DISCONTINUITY-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI=""1760873994_init.mp4"" +#EXTINF:3.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:27.132-0500 +live_1760874038_000038.m4s +#EXTINF:5.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:30.132-0500 +live_1760874038_000039.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:35.132-0500 +live_1760874038_000040.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:39.132-0500 +live_1760874038_000041.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:43.132-0500 +live_1760874038_000042.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:47.132-0500 +live_1760874038_000043.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:51.132-0500 +live_1760874038_000044.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:55.132-0500 +live_1760874038_000045.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:42:59.132-0500 +live_1760874038_000046.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:03.132-0500 +live_1760874038_000047.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:07.132-0500 +live_1760874038_000048.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:11.132-0500 +live_1760874038_000049.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:15.132-0500 +live_1760874038_000050.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:19.132-0500 +live_1760874038_000051.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:23.132-0500 +live_1760874038_000052.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:27.132-0500 +live_1760874038_000053.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:31.132-0500 +live_1760874038_000054.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:35.132-0500 +live_1760874038_000055.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:39.132-0500 +live_1760874038_000056.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:43.132-0500 +live_1760874038_000057.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:47.132-0500 +live_1760874038_000058.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:51.132-0500 +live_1760874038_000059.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:55.132-0500 +live_1760874038_000060.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:59.132-0500 +live_1760874038_000061.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:44:03.132-0500 +live_1760874038_000062.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:44:07.132-0500 +live_1760874038_000063.m4s +#EXTINF:1.720000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:44:11.132-0500 +live_1760874038_000064.m4s +#EXT-X-DISCONTINUITY +").Split(Environment.NewLine); + + // filter 'live000046.m4s' + var filterBefore = start.AddSeconds(36); + + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( + new Dictionary + { + [1760874038] = 2 + }, + OutputFormatKind.HlsMp4, + start, + filterBefore, + new FakeHlsInitSegmentCache(), + input, + 10); + + result.Sequence.ShouldBe(47); + + string expectedPlaylist = NormalizeLineEndings( + @"#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:5 +#EXT-X-MEDIA-SEQUENCE:47 +#EXT-X-DISCONTINUITY-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI=""1760874038_init.mp4"" +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:03.132-0500 +live_1760874038_000047.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:07.132-0500 +live_1760874038_000048.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:11.132-0500 +live_1760874038_000049.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:15.132-0500 +live_1760874038_000050.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:19.132-0500 +live_1760874038_000051.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:23.132-0500 +live_1760874038_000052.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:27.132-0500 +live_1760874038_000053.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:31.132-0500 +live_1760874038_000054.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:35.132-0500 +live_1760874038_000055.m4s +#EXTINF:4.000000, +#EXT-X-PROGRAM-DATE-TIME:2025-10-19T06:43:39.132-0500 +live_1760874038_000056.m4s "); result.Playlist.ShouldBe(expectedPlaylist); @@ -643,4 +1001,15 @@ live000048.ts .Replace("\r\n", "\n") .Replace("\r", "\n") .Replace("\n", Environment.NewLine); + + private class FakeHlsInitSegmentCache : IHlsInitSegmentCache + { + public Task AddSegment(string segment) => throw new NotSupportedException(); + + public string EarliestSegmentByHash(long generatedAt) => $"{generatedAt}_init.mp4"; + + public bool IsEarliestByHash(string fileName) => throw new NotSupportedException(); + + public void DeleteSegment(string segment) => throw new NotSupportedException(); + } } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 26965f5d6..522f5f209 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -550,6 +550,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService public async Task ForError( string ffmpegPath, Channel channel, + DateTimeOffset now, Option duration, string errorMessage, bool hlsRealtime, @@ -626,7 +627,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService ? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8") : Option.None; - long nowSeconds = DateTimeOffset.Now.ToUnixTimeSeconds(); + long nowSeconds = now.ToUnixTimeSeconds(); Option hlsSegmentTemplate = outputFormat switch { diff --git a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs index 8c8494251..263fb8f3e 100644 --- a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs +++ b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.FFmpeg.OutputFormat; @@ -9,6 +9,7 @@ namespace ErsatzTV.Core.FFmpeg; public class HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger logger) : IHlsPlaylistFilter { public TrimPlaylistResult TrimPlaylist( + Dictionary discontinuityMap, OutputFormatKind outputFormat, DateTimeOffset playlistStart, DateTimeOffset filterBefore, @@ -24,15 +25,10 @@ public class HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger= lines.Length || !lines[i + 2].Contains('_') || !long.TryParse( lines[i + 2].Split('_')[1], out segmentNameTimeSeconds)) { @@ -89,19 +85,19 @@ public class HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger 0 && items[^1] is not PlaylistDiscontinuity) { items.Add(new PlaylistDiscontinuity()); } (string playlist, DateTimeOffset nextPlaylistStart, long startSequence, long generatedAt, int segments) = GeneratePlaylist( + discontinuityMap, outputFormat, items, hlsInitSegmentCache, filterBefore, targetDuration, - discontinuitySequence, maybeMaxSegments); return new TrimPlaylistResult(nextPlaylistStart, startSequence, generatedAt, playlist, segments); @@ -128,27 +124,23 @@ public class HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger discontinuityMap, OutputFormatKind outputFormat, DateTimeOffset playlistStart, DateTimeOffset filterBefore, IHlsInitSegmentCache hlsInitSegmentCache, string[] lines) => - TrimPlaylist(outputFormat, playlistStart, filterBefore, hlsInitSegmentCache, lines, Option.None, true); + TrimPlaylist(discontinuityMap, outputFormat, playlistStart, filterBefore, hlsInitSegmentCache, lines, Option.None, true); private static Tuple GeneratePlaylist( + Dictionary discontinuityMap, OutputFormatKind outputFormat, List items, IHlsInitSegmentCache hlsInitSegmentCache, DateTimeOffset filterBefore, int targetDuration, - int discontinuitySequence, Option maybeMaxSegments) { - if (items.Count != 0 && items[0] is PlaylistDiscontinuity) - { - discontinuitySequence++; - } - while (items.Count != 0 && items[0] is PlaylistDiscontinuity) { items.RemoveAt(0); @@ -179,18 +171,25 @@ public class HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger().Count(); - discontinuitySequence += count; + // remove anything after the last segment + if (maybeMaxSegments.IsSome) + { + int index = items.IndexOf(allSegments.Last()); + items.RemoveRange(index + 1, items.Count - index - 1); + } } var output = new StringBuilder(); @@ -198,9 +197,19 @@ public class HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger 1) + { + output.AppendLine(CultureInfo.InvariantCulture, $"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuitySequence}"); + } + output.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS"); + if (discontinuitySequence == 1) + { + output.AppendLine("#EXT-X-DISCONTINUITY"); + } + if (outputFormat is OutputFormatKind.HlsMp4) { output.AppendLine( diff --git a/ErsatzTV.Core/FFmpeg/IHlsPlaylistFilter.cs b/ErsatzTV.Core/FFmpeg/IHlsPlaylistFilter.cs index 9803342c5..fc7b33286 100644 --- a/ErsatzTV.Core/FFmpeg/IHlsPlaylistFilter.cs +++ b/ErsatzTV.Core/FFmpeg/IHlsPlaylistFilter.cs @@ -1,10 +1,11 @@ -using ErsatzTV.FFmpeg.OutputFormat; +using ErsatzTV.FFmpeg.OutputFormat; namespace ErsatzTV.Core.FFmpeg; public interface IHlsPlaylistFilter { TrimPlaylistResult TrimPlaylist( + Dictionary discontinuityMap, OutputFormatKind outputFormat, DateTimeOffset playlistStart, DateTimeOffset filterBefore, @@ -14,6 +15,7 @@ public interface IHlsPlaylistFilter bool endWithDiscontinuity = false); TrimPlaylistResult TrimPlaylistWithDiscontinuity( + Dictionary discontinuityMap, OutputFormatKind outputFormat, DateTimeOffset playlistStart, DateTimeOffset filterBefore, diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs index 9dc0ea5fb..e2973723a 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs @@ -46,6 +46,7 @@ public interface IFFmpegProcessService Task ForError( string ffmpegPath, Channel channel, + DateTimeOffset now, Option duration, string errorMessage, bool hlsRealtime, diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index 444814b2a..4f1476c30 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -14,7 +14,7 @@ - + @@ -38,7 +38,7 @@ - +