Browse Source

rework fallback filler (#2719)

* fallback fixes

* use hardware encoding for fallback filler

* rework fallback filler

* fixes
pull/2720/head
Jason Dove 3 weeks ago committed by GitHub
parent
commit
99b8c56a31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      CHANGELOG.md
  2. 5
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  3. 6
      ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs
  4. 3
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs
  5. 3
      ErsatzTV.Application/Streaming/Queries/GetErrorProcessHandler.cs
  6. 70
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  7. 3
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs
  8. 14
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

7
CHANGELOG.md

@ -54,10 +54,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix playback of certain BT.2020 content - Fix playback of certain BT.2020 content
- Use playlist item count when using a playlist as filler (instead of a fixed count of 1 for each playlist item) - Use playlist item count when using a playlist as filler (instead of a fixed count of 1 for each playlist item)
- NVIDIA: fix stream failure with certain content that should decode in hardware but falls back to software - NVIDIA: fix stream failure with certain content that should decode in hardware but falls back to software
- Fix stream failure when configured fallback filler collection is empty
- Fix high CPU when errors are displayed; errors will now work ahead before throttling to realtime, similar to primary content
### Changed ### Changed
- No longer round framerate to nearest integer when normalizing framerate - No longer round framerate to nearest integer when normalizing framerate
- Allow playlists to have no items included in EPG - Allow playlists to have no items included in EPG
- Change how fallback filler works
- Items will no longer loop; instead, a sequence of random items will be selected from the collection
- Items may still be cut as needed
- Hardware acceleration will now be used
- Items can "work ahead" (transcode faster than realtime) when less than 3 minutes in duration
## [25.9.0] - 2025-11-29 ## [25.9.0] - 2025-11-29
### Added ### Added

5
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -488,6 +488,11 @@ public class HlsSessionWorker : IHlsSessionWorker
foreach (PlayoutItemProcessModel processModel in result.RightAsEnumerable()) foreach (PlayoutItemProcessModel processModel in result.RightAsEnumerable())
{ {
if (!realtime && !processModel.IsWorkingAhead)
{
_logger.LogDebug("HLS session throttling (NOT working ahead) based on playout item");
}
await TrimAndDelete(cancellationToken); await TrimAndDelete(cancellationToken);
// increment discontinuity sequence and store with segment key (generated at) // increment discontinuity sequence and store with segment key (generated at)

6
ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs

@ -13,7 +13,8 @@ public class PlayoutItemProcessModel
bool isComplete, bool isComplete,
Option<long> segmentKey, Option<long> segmentKey,
Option<int> mediaItemId, Option<int> mediaItemId,
Option<TimeSpan> playoutOffset) Option<TimeSpan> playoutOffset,
bool isWorkingAhead)
{ {
Process = process; Process = process;
GraphicsEngineContext = graphicsEngineContext; GraphicsEngineContext = graphicsEngineContext;
@ -22,6 +23,7 @@ public class PlayoutItemProcessModel
IsComplete = isComplete; IsComplete = isComplete;
SegmentKey = segmentKey; SegmentKey = segmentKey;
MediaItemId = mediaItemId; MediaItemId = mediaItemId;
IsWorkingAhead = isWorkingAhead;
// undo the offset applied in FFmpegProcessHandler // undo the offset applied in FFmpegProcessHandler
// so we don't continually walk backward/forward in time by the offset amount // so we don't continually walk backward/forward in time by the offset amount
@ -49,4 +51,6 @@ public class PlayoutItemProcessModel
public Option<long> SegmentKey { get; init; } public Option<long> SegmentKey { get; init; }
public Option<int> MediaItemId { get; init; } public Option<int> MediaItemId { get; init; }
public bool IsWorkingAhead { get; init; }
} }

3
ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs

@ -46,6 +46,7 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo
true, true,
Option<long>.None, Option<long>.None,
Option<int>.None, Option<int>.None,
Option<TimeSpan>.None); Option<TimeSpan>.None,
false);
} }
} }

3
ErsatzTV.Application/Streaming/Queries/GetErrorProcessHandler.cs

@ -42,6 +42,7 @@ public class GetErrorProcessHandler(
true, true,
request.Now.ToUnixTimeSeconds(), request.Now.ToUnixTimeSeconds(),
Option<int>.None, Option<int>.None,
Optional(channel.PlayoutOffset)); Optional(channel.PlayoutOffset),
!request.HlsRealtime);
} }
} }

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

@ -27,6 +27,8 @@ namespace ErsatzTV.Application.Streaming;
public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber> public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
{ {
private static readonly Random FallbackRandom = new();
private readonly IArtistRepository _artistRepository; private readonly IArtistRepository _artistRepository;
private readonly IEmbyPathReplacementService _embyPathReplacementService; private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IExternalJsonPlayoutItemProvider _externalJsonPlayoutItemProvider; private readonly IExternalJsonPlayoutItemProvider _externalJsonPlayoutItemProvider;
@ -284,9 +286,20 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
bool isComplete = true; bool isComplete = true;
bool effectiveRealtime = request.HlsRealtime;
// only work ahead on fallback filler up to 3 minutes in duration
// since we always transcode a full fallback filler item
if (!effectiveRealtime &&
playoutItemWithPath.PlayoutItem.FillerKind is FillerKind.Fallback &&
duration > TimeSpan.FromMinutes(3))
{
effectiveRealtime = true;
}
TimeSpan limit = TimeSpan.Zero; TimeSpan limit = TimeSpan.Zero;
if (!request.HlsRealtime) if (!effectiveRealtime)
{ {
// if we are working ahead, limit to 44s (multiple of segment size) // if we are working ahead, limit to 44s (multiple of segment size)
limit = TimeSpan.FromSeconds(44); limit = TimeSpan.FromSeconds(44);
@ -319,7 +332,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
now, now,
duration, duration,
$"DEBUG_NO_SYNC:\n{Mapper.GetDisplayTitle(playoutItemWithPath.PlayoutItem.MediaItem, Option<string>.None)}\nFrom: {start} To: {finish}", $"DEBUG_NO_SYNC:\n{Mapper.GetDisplayTitle(playoutItemWithPath.PlayoutItem.MediaItem, Option<string>.None)}\nFrom: {start} To: {finish}",
request.HlsRealtime, effectiveRealtime,
request.PtsOffset, request.PtsOffset,
channel.FFmpegProfile.VaapiDisplay, channel.FFmpegProfile.VaapiDisplay,
channel.FFmpegProfile.VaapiDriver, channel.FFmpegProfile.VaapiDriver,
@ -334,7 +347,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
true, true,
effectiveNow.ToUnixTimeSeconds(), effectiveNow.ToUnixTimeSeconds(),
Option<int>.None, Option<int>.None,
Optional(channel.PlayoutOffset)); Optional(channel.PlayoutOffset),
!effectiveRealtime);
} }
MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem.GetHeadVersion(); MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem.GetHeadVersion();
@ -443,7 +457,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
channel.FFmpegProfile.VaapiDriver, channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice, channel.FFmpegProfile.VaapiDevice,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames), Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
hlsRealtime: request.HlsRealtime, effectiveRealtime,
playoutItemWithPath.PlayoutItem.MediaItem is RemoteStream { IsLive: true } playoutItemWithPath.PlayoutItem.MediaItem is RemoteStream { IsLive: true }
? StreamInputKind.Live ? StreamInputKind.Live
: StreamInputKind.Vod, : StreamInputKind.Vod,
@ -465,7 +479,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
isComplete, isComplete,
effectiveNow.ToUnixTimeSeconds(), effectiveNow.ToUnixTimeSeconds(),
playoutItemResult.MediaItemId, playoutItemResult.MediaItemId,
Optional(channel.PlayoutOffset)); Optional(channel.PlayoutOffset),
!effectiveRealtime);
return Right<BaseError, PlayoutItemProcessModel>(result); return Right<BaseError, PlayoutItemProcessModel>(result);
} }
@ -482,6 +497,13 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
Option<TimeSpan> maybeDuration = maybeNextStart.Map(s => s - now); Option<TimeSpan> maybeDuration = maybeNextStart.Map(s => s - now);
// limit working ahead on errors to 1 minute
if (!request.HlsRealtime && maybeDuration.IfNone(TimeSpan.FromMinutes(2)) > TimeSpan.FromMinutes(1))
{
maybeNextStart = now.AddMinutes(1);
maybeDuration = TimeSpan.FromMinutes(1);
}
DateTimeOffset finish = maybeNextStart.Match(s => s, () => now); DateTimeOffset finish = maybeNextStart.Match(s => s, () => now);
if (request.IsTroubleshooting) if (request.IsTroubleshooting)
@ -522,7 +544,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
true, true,
now.ToUnixTimeSeconds(), now.ToUnixTimeSeconds(),
Option<int>.None, Option<int>.None,
Optional(channel.PlayoutOffset)); Optional(channel.PlayoutOffset),
!request.HlsRealtime);
case PlayoutItemDoesNotExistOnDisk: case PlayoutItemDoesNotExistOnDisk:
Command doesNotExistProcess = await _ffmpegProcessService.ForError( Command doesNotExistProcess = await _ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
@ -545,7 +568,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
true, true,
now.ToUnixTimeSeconds(), now.ToUnixTimeSeconds(),
Option<int>.None, Option<int>.None,
Optional(channel.PlayoutOffset)); Optional(channel.PlayoutOffset),
!request.HlsRealtime);
default: default:
Command errorProcess = await _ffmpegProcessService.ForError( Command errorProcess = await _ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
@ -568,7 +592,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
true, true,
now.ToUnixTimeSeconds(), now.ToUnixTimeSeconds(),
Option<int>.None, Option<int>.None,
Optional(channel.PlayoutOffset)); Optional(channel.PlayoutOffset),
!request.HlsRealtime);
} }
} }
@ -711,8 +736,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
collectionKey, collectionKey,
cancellationToken); cancellationToken);
// TODO: shuffle? does it really matter since we loop anyway // ignore the fallback filler preset if it has no items
MediaItem item = items[new Random().Next(items.Count)]; if (items.Count == 0)
{
break;
}
// get a random item
MediaItem item = items[FallbackRandom.Next(items.Count)];
Option<TimeSpan> maybeDuration = await dbContext.PlayoutItems Option<TimeSpan> maybeDuration = await dbContext.PlayoutItems
.Filter(pi => pi.Playout.ChannelId == (channel.MirrorSourceChannelId ?? channel.Id)) .Filter(pi => pi.Playout.ChannelId == (channel.MirrorSourceChannelId ?? channel.Id))
@ -734,15 +765,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.Filter(ms => ms.MediaVersionId == version.Id) .Filter(ms => ms.MediaVersionId == version.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
DateTimeOffset finish = maybeDuration.Match( // always play min(duration to next item, version.Duration)
// next playout item exists TimeSpan duration = maybeDuration.IfNone(version.Duration);
// loop until it starts if (version.Duration < duration)
now.Add, {
// no next playout item exists duration = version.Duration;
// loop for 5 minutes if less than 30s, otherwise play full item }
() => version.Duration < TimeSpan.FromSeconds(30)
? now.AddMinutes(5) DateTimeOffset finish = now.Add(duration);
: now.Add(version.Duration));
var playoutItem = new PlayoutItem var playoutItem = new PlayoutItem
{ {
@ -752,7 +782,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
Finish = finish.UtcDateTime, Finish = finish.UtcDateTime,
FillerKind = FillerKind.Fallback, FillerKind = FillerKind.Fallback,
InPoint = TimeSpan.Zero, InPoint = TimeSpan.Zero,
OutPoint = version.Duration, OutPoint = duration,
DisableWatermarks = !fallbackPreset.AllowWatermarks, DisableWatermarks = !fallbackPreset.AllowWatermarks,
Watermarks = [], Watermarks = [],
PlayoutItemWatermarks = [], PlayoutItemWatermarks = [],

3
ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs

@ -48,6 +48,7 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW
true, true,
Option<long>.None, Option<long>.None,
Option<int>.None, Option<int>.None,
Option<TimeSpan>.None); Option<TimeSpan>.None,
false);
} }
} }

14
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -404,7 +404,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
graphicsElementContexts.AddRange(watermarks.Map(wm => new WatermarkElementContext(wm))); graphicsElementContexts.AddRange(watermarks.Map(wm => new WatermarkElementContext(wm)));
} }
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, fillerKind); HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings);
string videoFormat = GetVideoFormat(playbackSettings); string videoFormat = GetVideoFormat(playbackSettings);
Option<string> maybeVideoProfile = GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile); Option<string> maybeVideoProfile = GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile);
@ -499,7 +499,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var desiredState = new FrameState( var desiredState = new FrameState(
playbackSettings.RealtimeOutput, playbackSettings.RealtimeOutput,
fillerKind == FillerKind.Fallback, InfiniteLoop: false,
videoFormat, videoFormat,
maybeVideoProfile, maybeVideoProfile,
maybeVideoPreset, maybeVideoPreset,
@ -700,7 +700,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var desiredState = new FrameState( var desiredState = new FrameState(
playbackSettings.RealtimeOutput, playbackSettings.RealtimeOutput,
false, InfiniteLoop: false,
videoFormat, videoFormat,
GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile), GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile),
VideoPreset.Unset, VideoPreset.Unset,
@ -789,7 +789,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream }); var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });
// TODO: ignore accel if this already failed once // TODO: ignore accel if this already failed once
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, FillerKind.None); HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings);
_logger.LogDebug("HW accel mode: {HwAccel}", hwAccel); _logger.LogDebug("HW accel mode: {HwAccel}", hwAccel);
var ffmpegState = new FFmpegState( var ffmpegState = new FFmpegState(
@ -1187,12 +1187,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
.ForAccelAndFormat(hardwareAccelerationMode, videoFormat, bitDepth) .ForAccelAndFormat(hardwareAccelerationMode, videoFormat, bitDepth)
.Find(p => string.Equals(p, videoPreset, StringComparison.OrdinalIgnoreCase)); .Find(p => string.Equals(p, videoPreset, StringComparison.OrdinalIgnoreCase));
private static HardwareAccelerationMode GetHardwareAccelerationMode( private static HardwareAccelerationMode GetHardwareAccelerationMode(FFmpegPlaybackSettings playbackSettings) =>
FFmpegPlaybackSettings playbackSettings,
FillerKind fillerKind) =>
playbackSettings.HardwareAcceleration switch playbackSettings.HardwareAcceleration switch
{ {
_ when fillerKind == FillerKind.Fallback => HardwareAccelerationMode.None, //_ when fillerKind == FillerKind.Fallback => HardwareAccelerationMode.None,
HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc, HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc,
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv, HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi, HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,

Loading…
Cancel
Save