Browse Source

rework fallback filler

pull/2719/head
Jason Dove 8 months ago
parent
commit
a59b4042c8
No known key found for this signature in database
  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. 53
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  7. 3
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs
  8. 33
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

7
CHANGELOG.md

@ -60,8 +60,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -60,8 +60,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- No longer round framerate to nearest integer when normalizing framerate
- Allow playlists to have no items included in EPG
- Use hardware encoding for fallback filler
- Fallback filler will continue to use software decoding to avoid issues looping
- 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
### Added

5
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -488,6 +488,11 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -488,6 +488,11 @@ public class HlsSessionWorker : IHlsSessionWorker
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);
// increment discontinuity sequence and store with segment key (generated at)

6
ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs

@ -13,7 +13,8 @@ public class PlayoutItemProcessModel @@ -13,7 +13,8 @@ public class PlayoutItemProcessModel
bool isComplete,
Option<long> segmentKey,
Option<int> mediaItemId,
Option<TimeSpan> playoutOffset)
Option<TimeSpan> playoutOffset,
bool isWorkingAhead)
{
Process = process;
GraphicsEngineContext = graphicsEngineContext;
@ -22,6 +23,7 @@ public class PlayoutItemProcessModel @@ -22,6 +23,7 @@ public class PlayoutItemProcessModel
IsComplete = isComplete;
SegmentKey = segmentKey;
MediaItemId = mediaItemId;
IsWorkingAhead = isWorkingAhead;
// undo the offset applied in FFmpegProcessHandler
// so we don't continually walk backward/forward in time by the offset amount
@ -49,4 +51,6 @@ public class PlayoutItemProcessModel @@ -49,4 +51,6 @@ public class PlayoutItemProcessModel
public Option<long> SegmentKey { 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 @@ -46,6 +46,7 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo
true,
Option<long>.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( @@ -42,6 +42,7 @@ public class GetErrorProcessHandler(
true,
request.Now.ToUnixTimeSeconds(),
Option<int>.None,
Optional(channel.PlayoutOffset));
Optional(channel.PlayoutOffset),
!request.HlsRealtime);
}
}

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

@ -284,9 +284,20 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -284,9 +284,20 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
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;
if (!request.HlsRealtime)
if (!effectiveRealtime)
{
// if we are working ahead, limit to 44s (multiple of segment size)
limit = TimeSpan.FromSeconds(44);
@ -319,7 +330,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -319,7 +330,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
now,
duration,
$"DEBUG_NO_SYNC:\n{Mapper.GetDisplayTitle(playoutItemWithPath.PlayoutItem.MediaItem, Option<string>.None)}\nFrom: {start} To: {finish}",
request.HlsRealtime,
effectiveRealtime,
request.PtsOffset,
channel.FFmpegProfile.VaapiDisplay,
channel.FFmpegProfile.VaapiDriver,
@ -334,7 +345,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -334,7 +345,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
true,
effectiveNow.ToUnixTimeSeconds(),
Option<int>.None,
Optional(channel.PlayoutOffset));
Optional(channel.PlayoutOffset),
!effectiveRealtime);
}
MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem.GetHeadVersion();
@ -443,7 +455,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -443,7 +455,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
hlsRealtime: request.HlsRealtime,
effectiveRealtime,
playoutItemWithPath.PlayoutItem.MediaItem is RemoteStream { IsLive: true }
? StreamInputKind.Live
: StreamInputKind.Vod,
@ -465,7 +477,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -465,7 +477,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
isComplete,
effectiveNow.ToUnixTimeSeconds(),
playoutItemResult.MediaItemId,
Optional(channel.PlayoutOffset));
Optional(channel.PlayoutOffset),
!effectiveRealtime);
return Right<BaseError, PlayoutItemProcessModel>(result);
}
@ -529,7 +542,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -529,7 +542,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
true,
now.ToUnixTimeSeconds(),
Option<int>.None,
Optional(channel.PlayoutOffset));
Optional(channel.PlayoutOffset),
!request.HlsRealtime);
case PlayoutItemDoesNotExistOnDisk:
Command doesNotExistProcess = await _ffmpegProcessService.ForError(
ffmpegPath,
@ -552,7 +566,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -552,7 +566,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
true,
now.ToUnixTimeSeconds(),
Option<int>.None,
Optional(channel.PlayoutOffset));
Optional(channel.PlayoutOffset),
!request.HlsRealtime);
default:
Command errorProcess = await _ffmpegProcessService.ForError(
ffmpegPath,
@ -575,7 +590,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -575,7 +590,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
true,
now.ToUnixTimeSeconds(),
Option<int>.None,
Optional(channel.PlayoutOffset));
Optional(channel.PlayoutOffset),
!request.HlsRealtime);
}
}
@ -724,7 +740,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -724,7 +740,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
break;
}
// TODO: shuffle? does it really matter since we loop anyway
// get a random item
MediaItem item = items[new Random().Next(items.Count)];
Option<TimeSpan> maybeDuration = await dbContext.PlayoutItems
@ -747,15 +763,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -747,15 +763,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.Filter(ms => ms.MediaVersionId == version.Id)
.ToListAsync(cancellationToken);
DateTimeOffset finish = maybeDuration.Match(
// next playout item exists
// loop until it starts
now.Add,
// no next playout item exists
// loop for 5 minutes if less than 30s, otherwise play full item
() => version.Duration < TimeSpan.FromSeconds(30)
? now.AddMinutes(5)
: now.Add(version.Duration));
// always play min(duration to next item, version.Duration)
TimeSpan duration = maybeDuration.IfNone(version.Duration);
if (version.Duration < duration)
{
duration = version.Duration;
}
DateTimeOffset finish = now.Add(duration);
var playoutItem = new PlayoutItem
{
@ -765,7 +780,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -765,7 +780,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
Finish = finish.UtcDateTime,
FillerKind = FillerKind.Fallback,
InPoint = TimeSpan.Zero,
OutPoint = version.Duration,
OutPoint = duration,
DisableWatermarks = !fallbackPreset.AllowWatermarks,
Watermarks = [],
PlayoutItemWatermarks = [],

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

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

33
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -404,13 +404,12 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -404,13 +404,12 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
graphicsElementContexts.AddRange(watermarks.Map(wm => new WatermarkElementContext(wm)));
}
HardwareAccelerationMode decodeHwAccel = GetHardwareAccelerationMode(playbackSettings, fillerKind);
HardwareAccelerationMode encodeHwAccel = GetHardwareAccelerationMode(playbackSettings, FillerKind.None);
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings);
string videoFormat = GetVideoFormat(playbackSettings);
Option<string> maybeVideoProfile = GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile);
Option<string> maybeVideoPreset = GetVideoPreset(
encodeHwAccel,
hwAccel,
videoFormat,
channel.FFmpegProfile.VideoPreset,
FFmpegLibraryHelper.MapBitDepth(channel.FFmpegProfile.BitDepth));
@ -500,7 +499,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -500,7 +499,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var desiredState = new FrameState(
playbackSettings.RealtimeOutput,
fillerKind == FillerKind.Fallback,
InfiniteLoop: false,
videoFormat,
maybeVideoProfile,
maybeVideoPreset,
@ -549,10 +548,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -549,10 +548,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var ffmpegState = new FFmpegState(
saveReports,
decodeHwAccel,
encodeHwAccel,
VaapiDriverName(encodeHwAccel, vaapiDriver),
VaapiDeviceName(encodeHwAccel, vaapiDevice),
hwAccel,
hwAccel,
VaapiDriverName(hwAccel, vaapiDriver),
VaapiDeviceName(hwAccel, vaapiDevice),
playbackSettings.StreamSeek,
finish - now,
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect,
@ -577,16 +576,16 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -577,16 +576,16 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder(
encodeHwAccel,
hwAccel,
videoInputFile,
audioInputFile,
watermarkInputFile,
subtitleInputFile,
Option<ConcatInputFile>.None,
graphicsEngineInput,
VaapiDisplayName(encodeHwAccel, vaapiDisplay),
VaapiDriverName(encodeHwAccel, vaapiDriver),
VaapiDeviceName(encodeHwAccel, vaapiDevice),
VaapiDisplayName(hwAccel, vaapiDisplay),
VaapiDriverName(hwAccel, vaapiDriver),
VaapiDeviceName(hwAccel, vaapiDevice),
await customReportsFolder.IfNoneAsync(FileSystemLayout.FFmpegReportsFolder),
FileSystemLayout.FontsCacheFolder,
ffmpegPath);
@ -701,7 +700,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -701,7 +700,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var desiredState = new FrameState(
playbackSettings.RealtimeOutput,
false,
InfiniteLoop: false,
videoFormat,
GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile),
VideoPreset.Unset,
@ -790,7 +789,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -790,7 +789,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });
// 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);
var ffmpegState = new FFmpegState(
@ -1188,12 +1187,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -1188,12 +1187,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
.ForAccelAndFormat(hardwareAccelerationMode, videoFormat, bitDepth)
.Find(p => string.Equals(p, videoPreset, StringComparison.OrdinalIgnoreCase));
private static HardwareAccelerationMode GetHardwareAccelerationMode(
FFmpegPlaybackSettings playbackSettings,
FillerKind fillerKind) =>
private static HardwareAccelerationMode GetHardwareAccelerationMode(FFmpegPlaybackSettings playbackSettings) =>
playbackSettings.HardwareAcceleration switch
{
_ when fillerKind == FillerKind.Fallback => HardwareAccelerationMode.None,
//_ when fillerKind == FillerKind.Fallback => HardwareAccelerationMode.None,
HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc,
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,

Loading…
Cancel
Save