Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

248 lines
8.7 KiB

using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
using TimeSpanParserUtil;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers;
public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlPlayoutContentHandler(enumeratorCache)
{
public override async Task<bool> Handle(
YamlPlayoutContext context,
YamlPlayoutInstruction instruction,
PlayoutBuildMode mode,
Func<string, Task> executeSequence,
ILogger<YamlPlayoutBuilder> logger,
CancellationToken cancellationToken)
{
if (instruction is not YamlPlayoutDurationInstruction duration)
{
return false;
}
// TODO: move to up-front validation somewhere
if (!TimeSpanParser.TryParse(duration.Duration, out TimeSpan timeSpan))
{
return false;
}
if (duration.StopBeforeEnd == false && duration.OfflineTail)
{
logger.LogError("offline_tail must be false when stop_before_end is false");
return false;
}
DateTimeOffset targetTime = context.CurrentTime.Add(timeSpan);
Option<IMediaCollectionEnumerator> maybeEnumerator = await GetContentEnumerator(
context,
duration.Content,
logger,
cancellationToken);
Option<IMediaCollectionEnumerator> fallbackEnumerator = await GetContentEnumerator(
context,
duration.Fallback,
logger,
cancellationToken);
foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator)
{
context.CurrentTime = await Schedule(
context,
instruction.Content,
duration.Fallback,
targetTime,
duration.StopBeforeEnd,
duration.DiscardAttempts,
duration.Trim,
duration.OfflineTail,
GetFillerKind(duration, context),
duration.CustomTitle,
duration.DisableWatermarks,
enumerator,
fallbackEnumerator,
executeSequence,
logger);
return true;
}
return false;
}
protected static async Task<DateTimeOffset> Schedule(
YamlPlayoutContext context,
string contentKey,
string fallbackContentKey,
DateTimeOffset targetTime,
bool stopBeforeEnd,
int discardAttempts,
bool trim,
bool offlineTail,
FillerKind fillerKind,
string customTitle,
bool disableWatermarks,
IMediaCollectionEnumerator enumerator,
Option<IMediaCollectionEnumerator> fallbackEnumerator,
Func<string, Task> executeSequence,
ILogger<YamlPlayoutBuilder> logger)
{
var done = false;
TimeSpan remainingToFill = targetTime - context.CurrentTime;
while (!done && enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero)
{
foreach (string preRollSequence in context.GetPreRollSequence())
{
context.PushFillerKind(FillerKind.PreRoll);
await executeSequence(preRollSequence);
context.PopFillerKind();
remainingToFill = targetTime - context.CurrentTime;
if (remainingToFill <= TimeSpan.Zero)
{
break;
}
}
foreach (MediaItem mediaItem in enumerator.Current)
{
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = context.CurrentTime.UtcDateTime,
Finish = context.CurrentTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
GuideGroup = context.PeekNextGuideGroup(),
FillerKind = fillerKind,
CustomTitle = string.IsNullOrWhiteSpace(customTitle) ? null : customTitle,
DisableWatermarks = disableWatermarks
};
foreach (int watermarkId in context.GetChannelWatermarkId())
{
playoutItem.WatermarkId = watermarkId;
}
if (remainingToFill - itemDuration >= TimeSpan.Zero || !stopBeforeEnd)
{
context.Playout.Items.Add(playoutItem);
context.AdvanceGuideGroup();
// create history record
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
contentKey,
enumerator,
playoutItem,
mediaItem,
logger);
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
}
remainingToFill -= itemDuration;
context.CurrentTime += itemDuration;
enumerator.MoveNext();
}
else if (discardAttempts > 0)
{
// item won't fit; try the next one
discardAttempts--;
enumerator.MoveNext();
}
else if (trim)
{
// trim item to exactly fit
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start;
context.Playout.Items.Add(playoutItem);
context.AdvanceGuideGroup();
// create history record
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
contentKey,
enumerator,
playoutItem,
mediaItem,
logger);
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
}
remainingToFill = TimeSpan.Zero;
context.CurrentTime = targetTime;
enumerator.MoveNext();
}
else if (fallbackEnumerator.IsSome)
{
foreach (IMediaCollectionEnumerator fallback in fallbackEnumerator)
{
remainingToFill = TimeSpan.Zero;
context.CurrentTime = targetTime;
done = true;
// replace with fallback content
foreach (MediaItem fallbackItem in fallback.Current)
{
playoutItem.MediaItemId = fallbackItem.Id;
playoutItem.Finish = targetTime.UtcDateTime;
playoutItem.FillerKind = FillerKind.Fallback;
context.Playout.Items.Add(playoutItem);
// create history record
List<PlayoutHistory> maybeHistory = GetHistoryForItem(
context,
fallbackContentKey,
fallback,
playoutItem,
mediaItem,
logger);
foreach (PlayoutHistory history in maybeHistory)
{
context.Playout.PlayoutHistory.Add(history);
}
fallback.MoveNext();
}
}
}
else
{
// item won't fit; we're done
done = true;
}
}
foreach (string postRollSequence in context.GetPostRollSequence())
{
context.PushFillerKind(FillerKind.PostRoll);
await executeSequence(postRollSequence);
context.PopFillerKind();
}
}
if (!stopBeforeEnd)
{
return context.CurrentTime;
}
return offlineTail ? targetTime : context.CurrentTime;
}
}