diff --git a/CHANGELOG.md b/CHANGELOG.md index 216a442cc..516238176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Add log warnings when actual transcoding speed is potentially insufficient to support smooth playback + - Log messages will include media item id, channel number and transcoding speed ## [26.2.0] - 2026-02-02 ### Added diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 370ce367d..bb41a8307 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -528,9 +528,12 @@ public class HlsSessionWorker : IHlsSessionWorker linkedCts.Token); } + var progressParser = new FFmpegProgress(); + CommandResult commandResult = await processWithPipe .WithWorkingDirectory(_workingDirectory) .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer)) + .WithStandardOutputPipe(PipeTarget.ToDelegate(progressParser.ParseLine)) .WithValidation(CommandResultValidation.None) .ExecuteAsync(linkedCts.Token); @@ -538,12 +541,20 @@ public class HlsSessionWorker : IHlsSessionWorker { _logger.LogDebug("HLS process has completed for channel {Channel}", _channelNumber); _logger.LogDebug( - "Transcoded until: {Until} - Buffer: {Buffer} seconds", + "Transcoded until: {Until} - Buffer: {Buffer} seconds - Speed {Speed}", processModel.Until, - processModel.Until.Subtract(DateTimeOffset.Now).TotalSeconds); + processModel.Until.Subtract(DateTimeOffset.Now).TotalSeconds, + progressParser.Speed); _transcodedUntil = processModel.Until; _state = NextState(_state, processModel); _hasWrittenSegments = true; + + progressParser.LogSpeed( + processModel.MediaItemId, + processModel.IsWorkingAhead, + _channelNumber, + _logger); + return true; } else diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index b19130eb9..ff89d948b 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -321,7 +321,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< if (request.IsTroubleshooting) { - channel.Number = ".troubleshooting"; + channel.Number = FileSystemLayout.TranscodeTroubleshootingChannel; } if (_isDebugNoSync) @@ -508,7 +508,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< if (request.IsTroubleshooting) { - channel.Number = ".troubleshooting"; + channel.Number = FileSystemLayout.TranscodeTroubleshootingChannel; maybeDuration = TimeSpan.FromSeconds(30); finish = now + TimeSpan.FromSeconds(30); diff --git a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs index cb1ba5bec..09c470884 100644 --- a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs @@ -185,7 +185,7 @@ public class PrepareTroubleshootingPlaybackHandler( { Artwork = [], Name = "ETV", - Number = ".troubleshooting", + Number = FileSystemLayout.TranscodeTroubleshootingChannel, FFmpegProfile = ffmpegProfile, StreamingMode = request.StreamingMode, StreamSelectorMode = ChannelStreamSelectorMode.Troubleshooting, diff --git a/ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs b/ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs index 6a5529023..2b1608623 100644 --- a/ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs @@ -138,7 +138,7 @@ public class StartTroubleshootingPlaybackHandler( linkedCts.Token); } - var progressParser = new FFmpegProgressParser(); + var progressParser = new FFmpegProgress(); CommandResult commandResult = await processWithPipe .WithWorkingDirectory(FileSystemLayout.TranscodeTroubleshootingFolder) @@ -149,6 +149,12 @@ public class StartTroubleshootingPlaybackHandler( logger.LogDebug("Troubleshooting playback completed with exit code {ExitCode}", commandResult.ExitCode); + progressParser.LogSpeed( + request.MediaItemInfo.Map(i => i.Id), + true, + FileSystemLayout.TranscodeTroubleshootingChannel, + logger); + try { IEnumerable logs = logService.Sink.GetLogs(request.SessionId); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 5bd884af8..cbb8d1193 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -576,7 +576,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService videoVersion.MediaVersion is BackgroundImageMediaVersion { IsSongWithProgress: true }, false, GetTonemapAlgorithm(playbackSettings), - channel.Number == ".troubleshooting"); + channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel); _logger.LogDebug("FFmpeg desired state {FrameState}", desiredState); @@ -803,7 +803,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService _logger.LogDebug("HW accel mode: {HwAccel}", hwAccel); var ffmpegState = new FFmpegState( - channel.Number == ".troubleshooting", + channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel, HardwareAccelerationMode.None, // no hw accel decode since errors loop hwAccel, VaapiDriverName(hwAccel, vaapiDriver), @@ -827,7 +827,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService false, false, GetTonemapAlgorithm(playbackSettings), - channel.Number == ".troubleshooting"); + channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel); var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video); @@ -851,7 +851,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService VaapiDisplayName(hwAccel, vaapiDisplay), VaapiDriverName(hwAccel, vaapiDriver), VaapiDeviceName(hwAccel, vaapiDevice), - channel.Number == ".troubleshooting" + channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel ? FileSystemLayout.TranscodeTroubleshootingFolder : FileSystemLayout.FFmpegReportsFolder, FileSystemLayout.FontsCacheFolder, diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProgress.cs b/ErsatzTV.Core/FFmpeg/FFmpegProgress.cs new file mode 100644 index 000000000..3b4665820 --- /dev/null +++ b/ErsatzTV.Core/FFmpeg/FFmpegProgress.cs @@ -0,0 +1,55 @@ +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.FFmpeg; + +public partial class FFmpegProgress +{ + public Option Speed { get; private set; } = Option.None; + + public void ParseLine(string line) + { + Match match = FFmpegSpeed().Match(line); + if (match.Success && double.TryParse(match.Groups[1].Value, out double speed)) + { + Speed = speed; + } + } + + public void LogSpeed(Option mediaItemId, bool isWorkingAhead, string channelNumber, ILogger logger) + { + foreach (double speed in Speed) + { + if (isWorkingAhead) + { + if (speed < 1.0) + { + logger.LogCritical( + "Media item {MediaItemId} on channel {Channel} transcoded at {Speed}x (NOT throttled) which is NOT fast enough to support playback", + mediaItemId, + channelNumber, + speed); + } + else if (speed <= 1.5) + { + logger.LogWarning( + "Media item {MediaItemId} on channel {Channel} transcoded at {Speed}x (NOT throttled) which may not be fast enough to support playback", + mediaItemId, + channelNumber, + speed); + } + } + else if (speed < 1.0) + { + logger.LogWarning( + "Media item {MediaItemId} on channel {Channel} transcoded at {Speed}x (throttled) which may not be fast enough to support playback", + mediaItemId, + channelNumber, + speed); + } + } + } + + [GeneratedRegex(@"speed=\s*([\d\.]+)x", RegexOptions.IgnoreCase)] + private static partial Regex FFmpegSpeed(); +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProgressParser.cs b/ErsatzTV.Core/FFmpeg/FFmpegProgressParser.cs deleted file mode 100644 index 7b331e4f5..000000000 --- a/ErsatzTV.Core/FFmpeg/FFmpegProgressParser.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.RegularExpressions; - -namespace ErsatzTV.Core.FFmpeg; - -public partial class FFmpegProgressParser -{ - public Option Speed { get; private set; } = Option.None; - - public void ParseLine(string line) - { - Match match = FFmpegSpeed().Match(line); - if (match.Success && double.TryParse(match.Groups[1].Value, out double speed)) - { - Speed = speed; - } - } - - [GeneratedRegex(@"speed=\s*([\d\.]+)x", RegexOptions.IgnoreCase)] - private static partial Regex FFmpegSpeed(); -} diff --git a/ErsatzTV.Core/FileSystemLayout.cs b/ErsatzTV.Core/FileSystemLayout.cs index 37cb5e31c..13e9f42ee 100644 --- a/ErsatzTV.Core/FileSystemLayout.cs +++ b/ErsatzTV.Core/FileSystemLayout.cs @@ -8,6 +8,7 @@ public static class FileSystemLayout public static readonly string AppDataFolder; public static readonly string TranscodeFolder; + public static readonly string TranscodeTroubleshootingChannel; public static readonly string TranscodeTroubleshootingFolder; public static readonly string DataProtectionFolder; @@ -132,7 +133,8 @@ public static class FileSystemLayout } TranscodeFolder = useCustomTranscodeFolder ? customTranscodeFolder : defaultTranscodeFolder; - TranscodeTroubleshootingFolder = Path.Combine(TranscodeFolder, ".troubleshooting"); + TranscodeTroubleshootingChannel = ".troubleshooting"; + TranscodeTroubleshootingFolder = Path.Combine(TranscodeFolder, TranscodeTroubleshootingChannel); DataProtectionFolder = Path.Combine(AppDataFolder, "data-protection"); LogsFolder = Path.Combine(AppDataFolder, "logs"); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs index fe5883ee5..ae4c72017 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs @@ -34,7 +34,9 @@ public class TemplateDataRepository(IFileSystem fileSystem, IDbContextFactory