|
|
|
@ -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 = [], |
|
|
|
|