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/).
### Fix ### Fix
- Fix database operations that were slowing down playout builds - Fix database operations that were slowing down playout builds
- YAML playouts in particular should build significantly faster - YAML playouts in particular should build significantly faster
- Fix channel playout mode `On Demand` for Block and YAML schedules
### Changed ### Changed
- Allow multiple watermarks in playback troubleshooting - Allow multiple watermarks in playback troubleshooting

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

@ -23,6 +23,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IClient _client; private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder; private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IPlayoutBuilder _playoutBuilder; private readonly IPlayoutBuilder _playoutBuilder;
@ -39,6 +40,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
IExternalJsonPlayoutBuilder externalJsonPlayoutBuilder, IExternalJsonPlayoutBuilder externalJsonPlayoutBuilder,
IFFmpegSegmenterService ffmpegSegmenterService, IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker, IEntityLocker entityLocker,
IPlayoutTimeShifter playoutTimeShifter,
ChannelWriter<IBackgroundServiceRequest> workerChannel) ChannelWriter<IBackgroundServiceRequest> workerChannel)
{ {
_client = client; _client = client;
@ -50,33 +52,60 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_externalJsonPlayoutBuilder = externalJsonPlayoutBuilder; _externalJsonPlayoutBuilder = externalJsonPlayoutBuilder;
_ffmpegSegmenterService = ffmpegSegmenterService; _ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker; _entityLocker = entityLocker;
_playoutTimeShifter = playoutTimeShifter;
_workerChannel = workerChannel; _workerChannel = workerChannel;
} }
public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken) public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); try
Validation<BaseError, Playout> validation = await Validate(dbContext, request); {
return await validation.Match( await _entityLocker.LockPlayout(request.PlayoutId);
playout => ApplyUpdateRequest(dbContext, request, playout, cancellationToken), if (request.Mode is not PlayoutBuildMode.Reset)
error => Task.FromResult<Either<BaseError, Unit>>(error.Join())); {
// 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, TvContext dbContext,
BuildPlayout request, BuildPlayout request,
Playout playout, Playout playout,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
string channelNumber;
string channelName = "[unknown]"; string channelName = "[unknown]";
try try
{ {
await _entityLocker.LockPlayout(playout.Id);
var referenceData = await GetReferenceData(dbContext, playout.Id, playout.ProgramSchedulePlayoutType); var referenceData = await GetReferenceData(dbContext, playout.Id, playout.ProgramSchedulePlayoutType);
channelNumber = referenceData.Channel.Number; var channelNumber = referenceData.Channel.Number;
channelName = referenceData.Channel.Name; channelName = referenceData.Channel.Name;
var result = PlayoutBuildResult.Empty; var result = PlayoutBuildResult.Empty;
@ -207,6 +236,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
} }
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id), cancellationToken); await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id), cancellationToken);
return result;
} }
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{ {
@ -222,12 +253,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
return BaseError.New( return BaseError.New(
$"Unexpected error building playout for channel {channelName}: {ex.Message}"); $"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) => 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(
await dbContext.Playouts.AddAsync(playout); await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); 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()); await channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id); return new CreatePlayoutResponse(playout.Id);
} }

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

@ -39,7 +39,7 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset));
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand) 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()); await _channel.WriteAsync(new RefreshChannelList());

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

@ -42,6 +42,10 @@ public class CreateYamlPlayoutHandler
await dbContext.Playouts.AddAsync(playout); await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); 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()); await _channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id); return new CreatePlayoutResponse(playout.Id);
} }

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

@ -2,5 +2,5 @@ using ErsatzTV.Core;
namespace ErsatzTV.Application.Playouts; 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; : IRequest<Option<BaseError>>, IBackgroundServiceRequest;

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

@ -1,41 +1,22 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
public class TimeShiftOnDemandPlayoutHandler( public class TimeShiftOnDemandPlayoutHandler(IPlayoutTimeShifter playoutTimeShifter)
IPlayoutTimeShifter playoutTimeShifter,
IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<TimeShiftOnDemandPlayout, Option<BaseError>> : IRequestHandler<TimeShiftOnDemandPlayout, Option<BaseError>>
{ {
public async Task<Option<BaseError>> Handle(TimeShiftOnDemandPlayout request, CancellationToken cancellationToken) public async Task<Option<BaseError>> Handle(TimeShiftOnDemandPlayout request, CancellationToken cancellationToken)
{ {
try try
{ {
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await playoutTimeShifter.TimeShift(request.PlayoutId, request.Now, request.Force);
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;
} }
catch (Exception ex) catch (Exception ex)
{ {
return BaseError.New(ex.Message); return BaseError.New(ex.Message);
} }
return Option<BaseError>.None;
} }
} }

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

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

13
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

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

13
ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs

@ -130,11 +130,18 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
PlaylistStart = _transcodedUntil; PlaylistStart = _transcodedUntil;
_channelStart = _transcodedUntil; _channelStart = _transcodedUntil;
// time shift on-demand playout if needed var maybePlayoutId = await _mediator.Send(
await _mediator.Send( new GetPlayoutIdByChannelNumber(_channelNumber),
new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true),
cancellationToken); 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 // start concat/segmenter process
// other transcode processes will be started by incoming requests from 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
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -664,7 +663,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -814,7 +812,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -923,7 +920,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1031,7 +1027,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1148,7 +1143,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1258,7 +1252,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1372,7 +1365,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1485,7 +1477,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1595,7 +1586,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1708,7 +1698,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1831,7 +1820,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -1947,7 +1935,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2029,7 +2016,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2247,7 +2233,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2735,7 +2720,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2846,7 +2830,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -2957,7 +2940,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
fakeRepository, fakeRepository,
televisionRepo, televisionRepo,
@ -3066,7 +3048,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
collectionRepo, collectionRepo,
televisionRepo, televisionRepo,
@ -3124,7 +3105,6 @@ public class PlayoutBuilderTests
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>();
var builder = new PlayoutBuilder( var builder = new PlayoutBuilder(
Substitute.For<IPlayoutTimeShifter>(),
configRepo, configRepo,
collectionRepo, collectionRepo,
televisionRepo, televisionRepo,

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

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

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

@ -1,8 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IPlayoutTimeShifter 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(
List<PlayoutItem> AddedItems, List<PlayoutItem> AddedItems,
List<int> ItemsToRemove, List<int> ItemsToRemove,
List<PlayoutHistory> AddedHistory, List<PlayoutHistory> AddedHistory,
List<int> HistoryToRemove) List<int> HistoryToRemove,
Option<DateTimeOffset> TimeShiftTo)
{ {
public static PlayoutBuildResult Empty => 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
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory; private readonly IMultiEpisodeShuffleCollectionEnumeratorFactory _multiEpisodeFactory;
private readonly IPlayoutTimeShifter _playoutTimeShifter;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
private Playlist _debugPlaylist; private Playlist _debugPlaylist;
private ILogger<PlayoutBuilder> _logger; private ILogger<PlayoutBuilder> _logger;
public PlayoutBuilder( public PlayoutBuilder(
IPlayoutTimeShifter playoutTimeShifter,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
@ -37,7 +35,6 @@ public class PlayoutBuilder : IPlayoutBuilder
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILogger<PlayoutBuilder> logger) ILogger<PlayoutBuilder> logger)
{ {
_playoutTimeShifter = playoutTimeShifter;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
@ -89,12 +86,6 @@ public class PlayoutBuilder : IPlayoutBuilder
// return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) }); // 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); result = await Build(playout, referenceData, result, mode, parameters, cancellationToken);
} }
@ -285,7 +276,7 @@ public class PlayoutBuilder : IPlayoutBuilder
// time shift on demand channel if needed // time shift on demand channel if needed
if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand) if (referenceData.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
{ {
_playoutTimeShifter.TimeShift(playout, parameters.Start, false); result = result with { TimeShiftTo = parameters.Start };
} }
return result; return result;

90
ErsatzTV.Core/Scheduling/PlayoutTimeShifter.cs

@ -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 @@
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