Browse Source

optimize transcoding speed (#1398)

* fix "empty trash" button blinking when loading trash page

* clean channel guide cache on startup

* only work-ahead in hls session for 2 minutes
pull/1399/head
Jason Dove 2 years ago committed by GitHub
parent
commit
8f241f49fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 8
      ErsatzTV.Application/Streaming/HlsSessionWorkAheadState.cs
  3. 31
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  4. 27
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  5. 4
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  6. 19
      ErsatzTV/Pages/Trash.razor
  7. 22
      ErsatzTV/Services/RunOnce/CacheCleanerService.cs

5
CHANGELOG.md

@ -22,6 +22,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

8
ErsatzTV.Application/Streaming/HlsSessionWorkAheadState.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Application.Streaming;
public enum HlsSessionWorkAheadState
{
MaxSpeed,
SeekAndRealtime,
RealtimeFromZero
}

31
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -35,6 +35,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -35,6 +35,7 @@ public class HlsSessionWorker : IHlsSessionWorker
private Option<int> _targetFramerate;
private Timer _timer;
private DateTimeOffset _transcodedUntil;
private HlsSessionWorkAheadState _workAheadState;
public HlsSessionWorker(
IHlsPlaylistFilter hlsPlaylistFilter,
@ -105,6 +106,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -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 @@ -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 @@ -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 @@ -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 @@ -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;
}

27
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -172,6 +172,17 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -172,6 +172,17 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.GetValue<bool>(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< @@ -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< @@ -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<BaseError, PlayoutItemProcessModel>(result);
}

4
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -21,6 +21,7 @@ @@ -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 @@ -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)
{

19
ErsatzTV/Pages/Trash.razor

@ -70,21 +70,21 @@ @@ -70,21 +70,21 @@
<MudLink Class="ml-4" Href="@(_navigationManager.Uri.Split("#").Head() + "#songs")" Style="margin-bottom: auto; margin-top: auto">@_songs.Count Songs</MudLink>
}
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)
{
<MudText>Nothing to see here...</MudText>
}
else
if (IsNotEmpty)
{
<div style="margin-left: auto">
<MudButton Variant="Variant.Filled"
Color="Color.Error"
<MudButton Variant="@Variant.Filled"
Color="@Color.Error"
StartIcon="@Icons.Material.Filled.DeleteForever"
OnClick="@(_ => EmptyTrash())">
Empty Trash
</MudButton>
</div>
}
else
{
<MudText>Nothing to see here...</MudText>
}
}
</div>
</MudPaper>
@ -331,7 +331,7 @@ @@ -331,7 +331,7 @@
private ArtistCardResultsViewModel _artists;
protected override Task OnInitializedAsync() => RefreshData();
protected override async Task RefreshData()
{
_query = "state:FileNotFound";
@ -348,6 +348,9 @@ @@ -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<MediaCardViewModel> GetSortedItems()

22
ErsatzTV/Services/RunOnce/CacheCleanerService.cs

@ -41,6 +41,7 @@ public class CacheCleanerService : BackgroundService @@ -41,6 +41,7 @@ public class CacheCleanerService : BackgroundService
_logger.LogInformation("Migrating channel logos from legacy image cache folder");
List<string> 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 @@ -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<string> 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);
}
}
}
}

Loading…
Cancel
Save