diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2eef0ab..bc212f3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fix subtitle scaling when using QSV hardware acceleration - Fix log viewer crash when log file contains invalid data +- Clean channel guide cache on startup (delete channels that no longer exist) + +### Changed +- Optimize transcoding session to only work ahead (at max speed) for 2 minutes before throttling to realtime + - This should *greatly* reduce cpu/gpu use when joining a channel, particularly with long content ## [0.8.1-beta] - 2023-08-07 ### Added diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorkAheadState.cs b/ErsatzTV.Application/Streaming/HlsSessionWorkAheadState.cs new file mode 100644 index 000000000..7a0ed49c8 --- /dev/null +++ b/ErsatzTV.Application/Streaming/HlsSessionWorkAheadState.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Application.Streaming; + +public enum HlsSessionWorkAheadState +{ + MaxSpeed, + SeekAndRealtime, + RealtimeFromZero +} diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index a82a35ef1..263b01b2f 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -35,6 +35,7 @@ public class HlsSessionWorker : IHlsSessionWorker private Option _targetFramerate; private Timer _timer; private DateTimeOffset _transcodedUntil; + private HlsSessionWorkAheadState _workAheadState; public HlsSessionWorker( IHlsPlaylistFilter hlsPlaylistFilter, @@ -105,6 +106,7 @@ public class HlsSessionWorker : IHlsSessionWorker { _firstProcess = true; _seekNextItem = true; + _workAheadState = HlsSessionWorkAheadState.SeekAndRealtime; } public async Task Run(string channelNumber, TimeSpan idleTimeout, CancellationToken incomingCancellationToken) @@ -149,6 +151,10 @@ public class HlsSessionWorker : IHlsSessionWorker _firstProcess = true; bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); + _workAheadState = initialWorkAhead + ? HlsSessionWorkAheadState.MaxSpeed + : HlsSessionWorkAheadState.SeekAndRealtime; + if (!await Transcode(!initialWorkAhead, cancellationToken)) { return; @@ -225,11 +231,24 @@ public class HlsSessionWorker : IHlsSessionWorker long ptsOffset = await GetPtsOffset(mediator, _channelNumber, cancellationToken); // _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset); + // this shouldn't happen, but respect realtime + if (realtime && _workAheadState is HlsSessionWorkAheadState.MaxSpeed) + { + _workAheadState = HlsSessionWorkAheadState.SeekAndRealtime; + } + + _logger.LogInformation("Work ahead state: {State}", _workAheadState); + + DateTimeOffset now = (_firstProcess || _workAheadState is HlsSessionWorkAheadState.MaxSpeed) + ? DateTimeOffset.Now + : _transcodedUntil.AddSeconds(_workAheadState is HlsSessionWorkAheadState.SeekAndRealtime ? 0 : 1); + bool startAtZero = _workAheadState is HlsSessionWorkAheadState.RealtimeFromZero && !_firstProcess; + var request = new GetPlayoutItemProcessByChannelNumber( _channelNumber, "segmenter", - _firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1), - !_firstProcess, + now, + startAtZero, realtime, ptsOffset, _targetFramerate); @@ -267,6 +286,7 @@ public class HlsSessionWorker : IHlsSessionWorker if (commandResult.ExitCode == 0) { _logger.LogInformation("HLS process has completed for channel {Channel}", _channelNumber); + _logger.LogDebug("Transcoded until: {Until}", processModel.Until); _transcodedUntil = processModel.Until; _firstProcess = false; if (_seekNextItem) @@ -275,6 +295,13 @@ public class HlsSessionWorker : IHlsSessionWorker _seekNextItem = false; } + _workAheadState = _workAheadState switch + { + HlsSessionWorkAheadState.MaxSpeed => HlsSessionWorkAheadState.SeekAndRealtime, + HlsSessionWorkAheadState.SeekAndRealtime => HlsSessionWorkAheadState.RealtimeFromZero, + _ => _workAheadState + }; + _hasWrittenSegments = true; return true; } diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index 0da765e95..86214a121 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -172,6 +172,17 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< .GetValue(ConfigElementKey.FFmpegSaveReports) .Map(result => result.IfNone(false)); + DateTimeOffset start = playoutItemWithPath.PlayoutItem.StartOffset; + DateTimeOffset finish = playoutItemWithPath.PlayoutItem.FinishOffset; + TimeSpan inPoint = playoutItemWithPath.PlayoutItem.InPoint; + TimeSpan outPoint = playoutItemWithPath.PlayoutItem.OutPoint; + DateTimeOffset effectiveNow = request.StartAtZero ? start : now; + if (!request.HlsRealtime && (outPoint - inPoint) > TimeSpan.FromMinutes(2)) + { + finish = effectiveNow + TimeSpan.FromMinutes(2); + outPoint = finish - start + TimeSpan.FromMinutes(2); + } + Command process = await _ffmpegProcessService.ForPlayoutItem( ffmpegPath, ffprobePath, @@ -186,9 +197,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< playoutItemWithPath.PlayoutItem.PreferredAudioTitle ?? channel.PreferredAudioTitle, playoutItemWithPath.PlayoutItem.PreferredSubtitleLanguageCode ?? channel.PreferredSubtitleLanguageCode, playoutItemWithPath.PlayoutItem.SubtitleMode ?? channel.SubtitleMode, - playoutItemWithPath.PlayoutItem.StartOffset, - playoutItemWithPath.PlayoutItem.FinishOffset, - request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now, + start, + finish, + request.StartAtZero ? start : now, Optional(playoutItemWithPath.PlayoutItem.Watermark), maybeGlobalWatermark, channel.FFmpegProfile.VaapiDriver, @@ -196,18 +207,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< Optional(channel.FFmpegProfile.QsvExtraHardwareFrames), request.HlsRealtime, playoutItemWithPath.PlayoutItem.FillerKind, - playoutItemWithPath.PlayoutItem.InPoint, - playoutItemWithPath.PlayoutItem.OutPoint, + inPoint, + outPoint, request.PtsOffset, request.TargetFramerate, playoutItemWithPath.PlayoutItem.DisableWatermarks, _ => { }); - var result = new PlayoutItemProcessModel( - process, - playoutItemWithPath.PlayoutItem.FinishOffset - - (request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now), - playoutItemWithPath.PlayoutItem.FinishOffset); + var result = new PlayoutItemProcessModel(process, finish - effectiveNow, finish); return Right(result); } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs index 80b531d7d..43da9faae 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs @@ -21,6 +21,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.FFmpeg.Format; +using Serilog; namespace ErsatzTV.Core.FFmpeg; @@ -66,8 +67,7 @@ public class FFmpegPlaybackSettingsCalculator } }; - // always use one thread with realtime output - result.ThreadCount = result.RealtimeOutput ? 1 : ffmpegProfile.ThreadCount; + result.ThreadCount = ffmpegProfile.ThreadCount; if (now != start || inPoint != TimeSpan.Zero) { diff --git a/ErsatzTV/Pages/Trash.razor b/ErsatzTV/Pages/Trash.razor index 3b855b5fb..2f60db368 100644 --- a/ErsatzTV/Pages/Trash.razor +++ b/ErsatzTV/Pages/Trash.razor @@ -70,21 +70,21 @@ @_songs.Count Songs } - if (_movies?.Count == 0 && _shows?.Count == 0 && _seasons?.Count == 0 && _episodes?.Count == 0 && _artists?.Count == 0 && _musicVideos?.Count == 0 && _otherVideos?.Count == 0 && _songs?.Count == 0) - { - Nothing to see here... - } - else + if (IsNotEmpty) {
- Empty Trash
} + else + { + Nothing to see here... + } } @@ -331,7 +331,7 @@ private ArtistCardResultsViewModel _artists; protected override Task OnInitializedAsync() => RefreshData(); - + protected override async Task RefreshData() { _query = "state:FileNotFound"; @@ -348,6 +348,9 @@ } } + private bool IsNotEmpty => + _movies?.Count > 0 || _shows?.Count > 0 || _seasons?.Count > 0 || _episodes?.Count > 0 || _musicVideos?.Count > 0 || _otherVideos?.Count > 0 || _songs?.Count > 0 || _artists?.Count > 0; + private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { List GetSortedItems() diff --git a/ErsatzTV/Services/RunOnce/CacheCleanerService.cs b/ErsatzTV/Services/RunOnce/CacheCleanerService.cs index d338b51c4..fe53dabc6 100644 --- a/ErsatzTV/Services/RunOnce/CacheCleanerService.cs +++ b/ErsatzTV/Services/RunOnce/CacheCleanerService.cs @@ -41,6 +41,7 @@ public class CacheCleanerService : BackgroundService _logger.LogInformation("Migrating channel logos from legacy image cache folder"); List logos = await dbContext.Channels + .AsNoTracking() .SelectMany(c => c.Artwork) .Where(a => a.ArtworkKind == ArtworkKind.Logo) .Map(a => a.Path) @@ -67,5 +68,26 @@ public class CacheCleanerService : BackgroundService localFileSystem.EmptyFolder(FileSystemLayout.TranscodeFolder); _logger.LogInformation("Done emptying transcode cache folder"); } + + if (localFileSystem.FolderExists(FileSystemLayout.ChannelGuideCacheFolder)) + { + _logger.LogInformation("Cleaning channel cache"); + + List channelFiles = await dbContext.Channels + .AsNoTracking() + .Select(c => c.Number) + .Map(num => Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{num}.xml")) + .ToListAsync(cancellationToken); + + foreach (string fileName in localFileSystem.ListFiles(FileSystemLayout.ChannelGuideCacheFolder)) + { + if (fileName.Contains("channels") || channelFiles.Contains(fileName)) + { + continue; + } + + File.Delete(fileName); + } + } } }