Browse Source

fix on demand for block and yaml schedules (#2290)

pull/2291/head
Jason Dove 1 week ago committed by GitHub
parent
commit
3f247288d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 57
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 4
      ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs
  4. 2
      ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs
  5. 4
      ErsatzTV.Application/Playouts/Commands/CreateYamlPlayoutHandler.cs
  6. 2
      ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayout.cs
  7. 27
      ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayoutHandler.cs
  8. 3
      ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpointHandler.cs
  9. 13
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  10. 13
      ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs
  11. 20
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  12. 2
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  13. 4
      ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs
  14. 5
      ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs
  15. 11
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  16. 90
      ErsatzTV.Core/Scheduling/PlayoutTimeShifter.cs
  17. 128
      ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs

1
CHANGELOG.md

@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fix
- Fix database operations that were slowing down playout builds
- YAML playouts in particular should build significantly faster
- Fix channel playout mode `On Demand` for Block and YAML schedules
### Changed
- Allow multiple watermarks in playback troubleshooting

57
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -23,6 +23,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -23,6 +23,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IPlayoutBuilder _playoutBuilder;
@ -39,6 +40,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -39,6 +40,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
IExternalJsonPlayoutBuilder externalJsonPlayoutBuilder,
IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker,
IPlayoutTimeShifter playoutTimeShifter,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_client = client;
@ -50,33 +52,60 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -50,33 +52,60 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_externalJsonPlayoutBuilder = externalJsonPlayoutBuilder;
_ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker;
_playoutTimeShifter = playoutTimeShifter;
_workerChannel = workerChannel;
}
public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await validation.Match(
playout => ApplyUpdateRequest(dbContext, request, playout, cancellationToken),
error => Task.FromResult<Either<BaseError, Unit>>(error.Join()));
try
{
await _entityLocker.LockPlayout(request.PlayoutId);
if (request.Mode is not PlayoutBuildMode.Reset)
{
// this needs to happen before we load the playout in this handler because it modifies items, etc
await _playoutTimeShifter.TimeShift(request.PlayoutId, DateTimeOffset.Now, false);
}
Either<BaseError, PlayoutBuildResult> result;
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
result = await validation.Match(
playout => ApplyUpdateRequest(dbContext, request, playout, cancellationToken),
error => Task.FromResult<Either<BaseError, PlayoutBuildResult>>(error.Join()));
}
// after dbcontext is closed
foreach (var playoutBuildResult in result.RightToSeq())
{
foreach (var timeShiftTo in playoutBuildResult.TimeShiftTo)
{
await _playoutTimeShifter.TimeShift(request.PlayoutId, timeShiftTo, false);
}
}
return result.Map(_ => Unit.Default);
}
finally
{
await _entityLocker.UnlockPlayout(request.PlayoutId);
}
}
private async Task<Either<BaseError, Unit>> ApplyUpdateRequest(
private async Task<Either<BaseError, PlayoutBuildResult>> ApplyUpdateRequest(
TvContext dbContext,
BuildPlayout request,
Playout playout,
CancellationToken cancellationToken)
{
string channelNumber;
string channelName = "[unknown]";
try
{
await _entityLocker.LockPlayout(playout.Id);
var referenceData = await GetReferenceData(dbContext, playout.Id, playout.ProgramSchedulePlayoutType);
channelNumber = referenceData.Channel.Number;
var channelNumber = referenceData.Channel.Number;
channelName = referenceData.Channel.Name;
var result = PlayoutBuildResult.Empty;
@ -207,6 +236,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -207,6 +236,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
}
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id), cancellationToken);
return result;
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{
@ -222,12 +253,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -222,12 +253,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
return BaseError.New(
$"Unexpected error building playout for channel {channelName}: {ex.Message}");
}
finally
{
await _entityLocker.UnlockPlayout(playout.Id);
}
return Unit.Default;
}
private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, BuildPlayout request) =>

4
ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs

@ -29,6 +29,10 @@ public class CreateBlockPlayoutHandler( @@ -29,6 +29,10 @@ public class CreateBlockPlayoutHandler(
await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync();
await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset));
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
await channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false));
}
await channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id);
}

2
ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs

@ -39,7 +39,7 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit @@ -39,7 +39,7 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset));
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Channel.Number, DateTimeOffset.Now, false));
await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false));
}
await _channel.WriteAsync(new RefreshChannelList());

4
ErsatzTV.Application/Playouts/Commands/CreateYamlPlayoutHandler.cs

@ -42,6 +42,10 @@ public class CreateYamlPlayoutHandler @@ -42,6 +42,10 @@ public class CreateYamlPlayoutHandler
await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync();
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset));
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false));
}
await _channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id);
}

2
ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayout.cs

@ -2,5 +2,5 @@ using ErsatzTV.Core; @@ -2,5 +2,5 @@ using ErsatzTV.Core;
namespace ErsatzTV.Application.Playouts;
public record TimeShiftOnDemandPlayout(string ChannelNumber, DateTimeOffset Now, bool Force)
public record TimeShiftOnDemandPlayout(int PlayoutId, DateTimeOffset Now, bool Force)
: IRequest<Option<BaseError>>, IBackgroundServiceRequest;

27
ErsatzTV.Application/Playouts/Commands/TimeShiftOnDemandPlayoutHandler.cs

@ -1,41 +1,22 @@ @@ -1,41 +1,22 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts;
public class TimeShiftOnDemandPlayoutHandler(
IPlayoutTimeShifter playoutTimeShifter,
IDbContextFactory<TvContext> dbContextFactory)
public class TimeShiftOnDemandPlayoutHandler(IPlayoutTimeShifter playoutTimeShifter)
: IRequestHandler<TimeShiftOnDemandPlayout, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(TimeShiftOnDemandPlayout request, CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.Anchor)
.Include(p => p.ProgramScheduleAnchors)
.SelectOneAsync(p => p.Channel.Number, p => p.Channel.Number == request.ChannelNumber);
foreach (Playout playout in maybePlayout)
{
playoutTimeShifter.TimeShift(playout, request.Now, request.Force);
await dbContext.SaveChangesAsync(cancellationToken);
}
return Option<BaseError>.None;
await playoutTimeShifter.TimeShift(request.PlayoutId, request.Now, request.Force);
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
return Option<BaseError>.None;
}
}

3
ErsatzTV.Application/Playouts/Commands/UpdateOnDemandCheckpointHandler.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
@ -29,6 +30,8 @@ public class UpdateOnDemandCheckpointHandler( @@ -29,6 +30,8 @@ public class UpdateOnDemandCheckpointHandler(
return;
}
playout.OnDemandCheckpoint ??= SystemTime.MinValueUtc;
int timeout = await (await configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout))
.IfNoneAsync(60);

13
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -183,11 +183,18 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -183,11 +183,18 @@ public class HlsSessionWorker : IHlsSessionWorker
PlaylistStart = _transcodedUntil;
_channelStart = _transcodedUntil;
// time shift on-demand playout if needed
await _mediator.Send(
new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true),
var maybePlayoutId = await _mediator.Send(
new GetPlayoutIdByChannelNumber(_channelNumber),
cancellationToken);
// time shift on-demand playout if needed
foreach (var playoutId in maybePlayoutId)
{
await _mediator.Send(
new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true),
cancellationToken);
}
bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit();
_state = initialWorkAhead ? HlsSessionState.SeekAndWorkAhead : HlsSessionState.SeekAndRealtime;

13
ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs

@ -130,11 +130,18 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker @@ -130,11 +130,18 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
PlaylistStart = _transcodedUntil;
_channelStart = _transcodedUntil;
// time shift on-demand playout if needed
await _mediator.Send(
new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true),
var maybePlayoutId = await _mediator.Send(
new GetPlayoutIdByChannelNumber(_channelNumber),
cancellationToken);
// time shift on-demand playout if needed
foreach (var playoutId in maybePlayoutId)
{
await _mediator.Send(
new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true),
cancellationToken);
}
// start concat/segmenter process
// other transcode processes will be started by incoming requests from concat/segmenter process

20
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -565,7 +565,6 @@ public class PlayoutBuilderTests @@ -565,7 +565,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -664,7 +663,6 @@ public class PlayoutBuilderTests @@ -664,7 +663,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -814,7 +812,6 @@ public class PlayoutBuilderTests @@ -814,7 +812,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -923,7 +920,6 @@ public class PlayoutBuilderTests @@ -923,7 +920,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1031,7 +1027,6 @@ public class PlayoutBuilderTests @@ -1031,7 +1027,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1148,7 +1143,6 @@ public class PlayoutBuilderTests @@ -1148,7 +1143,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1258,7 +1252,6 @@ public class PlayoutBuilderTests @@ -1258,7 +1252,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1372,7 +1365,6 @@ public class PlayoutBuilderTests @@ -1372,7 +1365,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1485,7 +1477,6 @@ public class PlayoutBuilderTests @@ -1485,7 +1477,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1595,7 +1586,6 @@ public class PlayoutBuilderTests @@ -1595,7 +1586,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1708,7 +1698,6 @@ public class PlayoutBuilderTests @@ -1708,7 +1698,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1831,7 +1820,6 @@ public class PlayoutBuilderTests @@ -1831,7 +1820,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -1947,7 +1935,6 @@ public class PlayoutBuilderTests @@ -1947,7 +1935,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -2029,7 +2016,6 @@ public class PlayoutBuilderTests @@ -2029,7 +2016,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -2247,7 +2233,6 @@ public class PlayoutBuilderTests @@ -2247,7 +2233,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -2735,7 +2720,6 @@ public class PlayoutBuilderTests @@ -2735,7 +2720,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -2846,7 +2830,6 @@ public class PlayoutBuilderTests @@ -2846,7 +2830,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -2957,7 +2940,6 @@ public class PlayoutBuilderTests @@ -2957,7 +2940,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
fakeRepository,
televisionRepo,
@ -3066,7 +3048,6 @@ public class PlayoutBuilderTests @@ -3066,7 +3048,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
collectionRepo,
televisionRepo,
@ -3124,7 +3105,6 @@ public class PlayoutBuilderTests @@ -3124,7 +3105,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo,
collectionRepo,
televisionRepo,

2
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -117,7 +117,6 @@ public class ScheduleIntegrationTests @@ -117,7 +117,6 @@ public class ScheduleIntegrationTests
provider.GetRequiredService<IFallbackMetadataProvider>());
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
new ConfigElementRepository(factory),
new MediaCollectionRepository(Substitute.For<IClient>(), searchIndex, factory),
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
@ -297,7 +296,6 @@ public class ScheduleIntegrationTests @@ -297,7 +296,6 @@ public class ScheduleIntegrationTests
DateTimeOffset finish = start.AddDays(2);
var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
new ConfigElementRepository(factory),
new MediaCollectionRepository(Substitute.For<IClient>(), Substitute.For<ISearchIndex>(), factory),
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),

4
ErsatzTV.Core/Interfaces/Scheduling/IPlayoutTimeShifter.cs

@ -1,8 +1,6 @@ @@ -1,8 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IPlayoutTimeShifter
{
public void TimeShift(Playout playout, DateTimeOffset now, bool force);
public Task TimeShift(int playoutId, DateTimeOffset now, bool force);
}

5
ErsatzTV.Core/Scheduling/PlayoutBuildResult.cs

@ -10,8 +10,9 @@ public record PlayoutBuildResult( @@ -10,8 +10,9 @@ public record PlayoutBuildResult(
List<PlayoutItem> AddedItems,
List<int> ItemsToRemove,
List<PlayoutHistory> AddedHistory,
List<int> HistoryToRemove)
List<int> HistoryToRemove,
Option<DateTimeOffset> TimeShiftTo)
{
public static PlayoutBuildResult Empty =>
new(false, Option<DateTimeOffset>.None, Option<DateTimeOffset>.None, [], [], [], []);
new(false, Option<DateTimeOffset>.None, Option<DateTimeOffset>.None, [], [], [], [], Option<DateTimeOffset>.None);
}

11
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -22,13 +22,11 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -22,13 +22,11 @@ public class PlayoutBuilder : IPlayoutBuilder
private readonly ILocalFileSystem _localFileSystem;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly ITelevisionRepository _televisionRepository;
private Playlist _debugPlaylist;
private ILogger<PlayoutBuilder> _logger;
public PlayoutBuilder(
IPlayoutTimeShifter playoutTimeShifter,
IConfigElementRepository configElementRepository,
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
@ -37,7 +35,6 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -37,7 +35,6 @@ public class PlayoutBuilder : IPlayoutBuilder
ILocalFileSystem localFileSystem,
ILogger<PlayoutBuilder> logger)
{
_playoutTimeShifter = playoutTimeShifter;
_configElementRepository = configElementRepository;
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
@ -89,12 +86,6 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -89,12 +86,6 @@ public class PlayoutBuilder : IPlayoutBuilder
// return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) });
// }
// time shift on demand channel if needed
if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand && mode is not PlayoutBuildMode.Reset)
{
_playoutTimeShifter.TimeShift(playout, parameters.Start, false);
}
result = await Build(playout, referenceData, result, mode, parameters, cancellationToken);
}
@ -285,7 +276,7 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -285,7 +276,7 @@ public class PlayoutBuilder : IPlayoutBuilder
// time shift on demand channel if needed
if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{
_playoutTimeShifter.TimeShift(playout, parameters.Start, false);
result = result with { TimeShiftTo = parameters.Start };
}
return result;

90
ErsatzTV.Core/Scheduling/PlayoutTimeShifter.cs

@ -1,90 +0,0 @@ @@ -1,90 +0,0 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling;
public class PlayoutTimeShifter(IFFmpegSegmenterService segmenterService, ILogger<PlayoutTimeShifter> logger)
: IPlayoutTimeShifter
{
public void TimeShift(Playout playout, DateTimeOffset now, bool force)
{
if (playout.Channel.PlayoutMode is not ChannelPlayoutMode.OnDemand)
{
return;
}
if (!force && segmenterService.IsActive(playout.Channel.Number))
{
logger.LogDebug(
"Will not time shift on demand playout that is active for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
return;
}
if (playout.Items.Count == 0)
{
logger.LogDebug(
"Unable to time shift empty playout for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
return;
}
if (playout.OnDemandCheckpoint is null)
{
logger.LogDebug(
"Time shifting unwatched playout for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
playout.OnDemandCheckpoint = playout.Items.Min(p => p.StartOffset);
}
TimeSpan toOffset = now - playout.OnDemandCheckpoint.IfNone(now);
logger.LogDebug(
"Time shifting playout for channel {Number} - {Name} forward by {Time}",
playout.Channel.Number,
playout.Channel.Name,
toOffset);
// time shift items
foreach (PlayoutItem playoutItem in playout.Items)
{
playoutItem.Start += toOffset;
playoutItem.Finish += toOffset;
if (playoutItem.GuideStart.HasValue)
{
playoutItem.GuideStart += toOffset;
}
if (playoutItem.GuideFinish.HasValue)
{
playoutItem.GuideFinish += toOffset;
}
}
// time shift anchors
foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors)
{
if (anchor.AnchorDate.HasValue)
{
anchor.AnchorDate += toOffset;
}
}
// time shift anchor
if (playout.Anchor is not null)
{
playout.Anchor.NextStart += toOffset;
}
playout.OnDemandCheckpoint = now;
}
}

128
ErsatzTV.Infrastructure/Scheduling/PlayoutTimeShifter.cs

@ -0,0 +1,128 @@ @@ -0,0 +1,128 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Scheduling;
public class PlayoutTimeShifter(
IDbContextFactory<TvContext> dbContextFactory,
IFFmpegSegmenterService segmenterService,
ILogger<PlayoutTimeShifter> logger)
: IPlayoutTimeShifter
{
public async Task TimeShift(int playoutId, DateTimeOffset now, bool force)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
Option<Channel> maybeChannel = await dbContext.Playouts
.AsNoTracking()
.Include(p => p.Channel)
.SelectOneAsync(p => p.Id, p => p.Id == playoutId)
.MapT(p => p.Channel);
foreach (var channel in maybeChannel.Where(c => c.PlayoutMode is ChannelPlayoutMode.OnDemand))
{
Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.Anchor)
.Include(p => p.ProgramScheduleAnchors)
.Include(p => p.PlayoutHistory)
.SelectOneAsync(p => p.ChannelId, p => p.ChannelId == channel.Id);
foreach (var playout in maybePlayout)
{
if (playout.Channel.PlayoutMode is not ChannelPlayoutMode.OnDemand)
{
return;
}
if (!force && segmenterService.IsActive(playout.Channel.Number))
{
logger.LogDebug(
"Will not time shift on demand playout that is active for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
return;
}
if (playout.Items.Count == 0)
{
logger.LogDebug(
"Unable to time shift empty playout for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
return;
}
if (playout.OnDemandCheckpoint is null)
{
logger.LogDebug(
"Time shifting unwatched playout for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
playout.OnDemandCheckpoint = playout.Items.Min(p => p.StartOffset);
}
TimeSpan toOffset = now - playout.OnDemandCheckpoint.IfNone(now);
logger.LogDebug(
"Time shifting playout for channel {Number} - {Name} forward by {Time}",
playout.Channel.Number,
playout.Channel.Name,
toOffset);
// time shift history
foreach (PlayoutHistory history in playout.PlayoutHistory)
{
history.When += toOffset;
history.Finish += toOffset;
}
// time shift items
foreach (PlayoutItem playoutItem in playout.Items)
{
playoutItem.Start += toOffset;
playoutItem.Finish += toOffset;
if (playoutItem.GuideStart.HasValue)
{
playoutItem.GuideStart += toOffset;
}
if (playoutItem.GuideFinish.HasValue)
{
playoutItem.GuideFinish += toOffset;
}
}
// time shift anchors
foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors)
{
if (anchor.AnchorDate.HasValue)
{
anchor.AnchorDate += toOffset;
}
}
// time shift anchor
if (playout.Anchor is not null)
{
playout.Anchor.NextStart += toOffset;
}
playout.OnDemandCheckpoint = now;
await dbContext.SaveChangesAsync();
}
}
}
}
Loading…
Cancel
Save