Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd3ef90880 | ||
|
|
696b29c9e9 | ||
|
|
70c37df596 | ||
|
|
040785b0d7 | ||
|
|
b25f783343 | ||
|
|
a21f62ff8c | ||
|
|
78fdc9c57a | ||
|
|
f6c42f3ff5 | ||
|
|
c92b6cb909 | ||
|
|
a2e1dc8bfb | ||
|
|
8a6093ce8d | ||
|
|
1d6279cee8 | ||
|
|
66ab0b3990 | ||
|
|
a7922beaed | ||
|
|
a1d9d6790e | ||
|
|
2f2d7952dd | ||
|
|
c96b800b52 | ||
|
|
c05882f4a6 | ||
|
|
5a442a06a0 | ||
|
|
640fed0a43 |
+20
-1
@@ -5,6 +5,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.2-alpha] - 2022-02-26
|
||||
### Fixed
|
||||
- Add improved but experimental transcoder logic, which can be toggled on and off in `Settings`
|
||||
- Fix `HLS Segmenter` bug when source video packet contains no duration (`N/A`)
|
||||
- Fix green line at the bottom of some content scaled using QSV acceleration
|
||||
|
||||
### Added
|
||||
- Add configurable channel group (M3U) and categories (XMLTV)
|
||||
- Add `Shuffle Schedule Items` option to schedule configuration
|
||||
- When this is enabled, schedule items will be shuffled rather than looped in order
|
||||
- **To support this, all playouts will be rebuilt (one time) after upgrading to this version**
|
||||
|
||||
### Changed
|
||||
- Disable framerate normalization by default and on all ffmpeg profiles
|
||||
- If framerate normalization is desired (not typically needed), it can be re-enabled manually
|
||||
- Show watermarks over songs
|
||||
- Hide unused local libraries
|
||||
|
||||
## [0.4.1-alpha] - 2022-02-10
|
||||
### Fixed
|
||||
- Normalize smart quotes in search queries as they are unsupported by the search library
|
||||
@@ -967,7 +985,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.1-alpha...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.2-alpha...HEAD
|
||||
[0.4.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.1-alpha...v0.4.2-alpha
|
||||
[0.4.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.0-alpha...v0.4.1-alpha
|
||||
[0.4.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
|
||||
[0.3.8-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ErsatzTV.Application.Channels
|
||||
int Id,
|
||||
string Number,
|
||||
string Name,
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
(
|
||||
string Name,
|
||||
string Number,
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
CreateChannel request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => PersistChannel(dbContext, c));
|
||||
}
|
||||
@@ -65,6 +65,8 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
{
|
||||
Name = name,
|
||||
Number = number,
|
||||
Group = request.Group,
|
||||
Categories = request.Categories,
|
||||
FFmpegProfileId = ffmpegProfileId,
|
||||
StreamingMode = request.StreamingMode,
|
||||
Artwork = artwork,
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
int ChannelId,
|
||||
string Name,
|
||||
string Number,
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
UpdateChannel request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||
}
|
||||
@@ -37,6 +37,8 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
{
|
||||
c.Name = update.Name;
|
||||
c.Number = update.Number;
|
||||
c.Group = update.Group;
|
||||
c.Categories = update.Categories;
|
||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||
c.PreferredLanguageCode = update.PreferredLanguageCode;
|
||||
c.Artwork ??= new List<Artwork>();
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace ErsatzTV.Application.Channels
|
||||
channel.Id,
|
||||
channel.Number,
|
||||
channel.Name,
|
||||
channel.Group,
|
||||
channel.Categories,
|
||||
channel.FFmpegProfileId,
|
||||
GetLogo(channel),
|
||||
channel.PreferredLanguageCode,
|
||||
|
||||
@@ -85,6 +85,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
|
||||
}
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegUseExperimentalTranscoder,
|
||||
request.Settings.UseExperimentalTranscoder.ToString());
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegPreferredLanguageCode,
|
||||
request.Settings.PreferredLanguageCode);
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
public int HlsSegmenterIdleTimeout { get; set; }
|
||||
public int WorkAheadSegmenterLimit { get; set; }
|
||||
public int InitialSegmentCount { get; set; }
|
||||
public bool UseExperimentalTranscoder { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
|
||||
Option<int> initialSegmentCount =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
|
||||
Option<bool> useExperimentalTranscoder =
|
||||
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseExperimentalTranscoder);
|
||||
|
||||
var result = new FFmpegSettingsViewModel
|
||||
{
|
||||
@@ -46,7 +48,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
|
||||
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
|
||||
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
|
||||
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1)
|
||||
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1),
|
||||
UseExperimentalTranscoder = await useExperimentalTranscoder.IfNoneAsync(false)
|
||||
};
|
||||
|
||||
foreach (int watermarkId in watermark)
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@ using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries.Queries
|
||||
{
|
||||
public record GetAllLibraries : IRequest<List<LibraryViewModel>>;
|
||||
public record GetConfiguredLibraries : IRequest<List<LibraryViewModel>>;
|
||||
}
|
||||
+7
-4
@@ -10,13 +10,16 @@ using static ErsatzTV.Application.Libraries.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries.Queries
|
||||
{
|
||||
public class GetAllLibrariesHandler : IRequestHandler<GetAllLibraries, List<LibraryViewModel>>
|
||||
public class GetConfiguredLibrariesHandler : IRequestHandler<GetConfiguredLibraries, List<LibraryViewModel>>
|
||||
{
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
|
||||
public GetAllLibrariesHandler(ILibraryRepository libraryRepository) => _libraryRepository = libraryRepository;
|
||||
public GetConfiguredLibrariesHandler(ILibraryRepository libraryRepository) =>
|
||||
_libraryRepository = libraryRepository;
|
||||
|
||||
public Task<List<LibraryViewModel>> Handle(GetAllLibraries request, CancellationToken cancellationToken) =>
|
||||
public Task<List<LibraryViewModel>> Handle(
|
||||
GetConfiguredLibraries request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_libraryRepository.GetAll()
|
||||
.Map(
|
||||
list => list.Filter(ShouldIncludeLibrary)
|
||||
@@ -28,7 +31,7 @@ namespace ErsatzTV.Application.Libraries.Queries
|
||||
private static bool ShouldIncludeLibrary(Library library) =>
|
||||
library switch
|
||||
{
|
||||
LocalLibrary => true,
|
||||
LocalLibrary => library.Paths.Count > 0,
|
||||
PlexLibrary plex => plex.ShouldSyncItems,
|
||||
JellyfinLibrary jellyfin => jellyfin.ShouldSyncItems,
|
||||
EmbyLibrary emby => emby.ShouldSyncItems,
|
||||
@@ -8,5 +8,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
public record CreateProgramSchedule(
|
||||
string Name,
|
||||
bool KeepMultiPartEpisodesTogether,
|
||||
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;
|
||||
bool TreatCollectionsAsShows,
|
||||
bool ShuffleScheduleItems) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
Name = name,
|
||||
KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
|
||||
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows
|
||||
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows,
|
||||
ShuffleScheduleItems = request.ShuffleScheduleItems
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -10,5 +10,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int ProgramScheduleId,
|
||||
string Name,
|
||||
bool KeepMultiPartEpisodesTogether,
|
||||
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;
|
||||
bool TreatCollectionsAsShows,
|
||||
bool ShuffleScheduleItems) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
UpdateProgramSchedule request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
|
||||
@@ -45,12 +45,14 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
// we need to rebuild playouts if the playback order or keep multi-episodes has been modified
|
||||
bool needToRebuildPlayout =
|
||||
programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether ||
|
||||
programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows;
|
||||
programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows ||
|
||||
programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems;
|
||||
|
||||
programSchedule.Name = request.Name;
|
||||
programSchedule.KeepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether;
|
||||
programSchedule.TreatCollectionsAsShows = programSchedule.KeepMultiPartEpisodesTogether &&
|
||||
request.TreatCollectionsAsShows;
|
||||
programSchedule.ShuffleScheduleItems = request.ShuffleScheduleItems;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
programSchedule.Id,
|
||||
programSchedule.Name,
|
||||
programSchedule.KeepMultiPartEpisodesTogether,
|
||||
programSchedule.TreatCollectionsAsShows);
|
||||
programSchedule.TreatCollectionsAsShows,
|
||||
programSchedule.ShuffleScheduleItems);
|
||||
|
||||
internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) =>
|
||||
programScheduleItem switch
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
int Id,
|
||||
string Name,
|
||||
bool KeepMultiPartEpisodesTogether,
|
||||
bool TreatCollectionsAsShows);
|
||||
bool TreatCollectionsAsShows,
|
||||
bool ShuffleScheduleItems);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
|
||||
ps.Id,
|
||||
ps.Name,
|
||||
ps.KeepMultiPartEpisodesTogether,
|
||||
ps.TreatCollectionsAsShows))
|
||||
ps.TreatCollectionsAsShows,
|
||||
ps.ShuffleScheduleItems))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
|
||||
GetProgramScheduleById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
return await dbContext.ProgramSchedules
|
||||
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -23,7 +24,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
|
||||
GetProgramScheduleItems request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Option<ProgramSchedule> maybeProgramSchedule =
|
||||
await dbContext.ProgramSchedules.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id);
|
||||
|
||||
return await dbContext.ProgramScheduleItems
|
||||
.Filter(psi => psi.ProgramScheduleId == request.Id)
|
||||
.Include(i => i.Collection)
|
||||
@@ -51,7 +56,29 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
|
||||
.Include(i => i.TailFiller)
|
||||
.Include(i => i.FallbackFiller)
|
||||
.ToListAsync(cancellationToken)
|
||||
.Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel).ToList());
|
||||
.Map(
|
||||
programScheduleItems => programScheduleItems.Map(ProjectToViewModel)
|
||||
.Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList());
|
||||
}
|
||||
|
||||
// shuffled schedule items supports a limited set of properly values
|
||||
private ProgramScheduleItemViewModel EnforceProperties(
|
||||
Option<ProgramSchedule> maybeProgramSchedule,
|
||||
ProgramScheduleItemViewModel item)
|
||||
{
|
||||
foreach (ProgramSchedule programSchedule in maybeProgramSchedule)
|
||||
{
|
||||
if (programSchedule.ShuffleScheduleItems)
|
||||
{
|
||||
item = item with { StartType = StartType.Dynamic };
|
||||
if (item.PlayoutMode == PlayoutMode.Flood)
|
||||
{
|
||||
item = item with { PlayoutMode = PlayoutMode.One };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace ErsatzTV.Application.Streaming.Commands
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
|
||||
public StartFFmpegSessionHandler(
|
||||
@@ -29,13 +30,15 @@ namespace ErsatzTV.Application.Streaming.Commands
|
||||
ILogger<StartFFmpegSessionHandler> logger,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IFFmpegSegmenterService ffmpegSegmenterService,
|
||||
IConfigElementRepository configElementRepository)
|
||||
IConfigElementRepository configElementRepository,
|
||||
IHlsPlaylistFilter hlsPlaylistFilter)
|
||||
{
|
||||
_localFileSystem = localFileSystem;
|
||||
_logger = logger;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_ffmpegSegmenterService = ffmpegSegmenterService;
|
||||
_configElementRepository = configElementRepository;
|
||||
_hlsPlaylistFilter = hlsPlaylistFilter;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(StartFFmpegSession request, CancellationToken cancellationToken) =>
|
||||
@@ -78,7 +81,7 @@ namespace ErsatzTV.Application.Streaming.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private static async Task WaitForPlaylistSegments(string playlistFileName, int initialSegmentCount, IHlsSessionWorker worker)
|
||||
private async Task WaitForPlaylistSegments(string playlistFileName, int initialSegmentCount, IHlsSessionWorker worker)
|
||||
{
|
||||
while (!File.Exists(playlistFileName))
|
||||
{
|
||||
@@ -92,7 +95,7 @@ namespace ErsatzTV.Application.Streaming.Commands
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.Now.AddSeconds(-30);
|
||||
string[] input = await File.ReadAllLinesAsync(playlistFileName);
|
||||
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(worker.PlaylistStart, now, input);
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(worker.PlaylistStart, now, input);
|
||||
segmentCount = result.SegmentCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace ErsatzTV.Application.Streaming
|
||||
public class HlsSessionWorker : IHlsSessionWorker
|
||||
{
|
||||
private static int _workAheadCount;
|
||||
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILogger<HlsSessionWorker> _logger;
|
||||
private DateTimeOffset _lastAccess;
|
||||
@@ -33,8 +34,9 @@ namespace ErsatzTV.Application.Streaming
|
||||
private DateTimeOffset _playlistStart;
|
||||
private Option<int> _targetFramerate;
|
||||
|
||||
public HlsSessionWorker(IServiceScopeFactory serviceScopeFactory, ILogger<HlsSessionWorker> logger)
|
||||
public HlsSessionWorker(IHlsPlaylistFilter hlsPlaylistFilter, IServiceScopeFactory serviceScopeFactory, ILogger<HlsSessionWorker> logger)
|
||||
{
|
||||
_hlsPlaylistFilter = hlsPlaylistFilter;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -123,7 +125,11 @@ namespace ErsatzTV.Application.Streaming
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> Transcode(string channelNumber, bool firstProcess, bool realtime, CancellationToken cancellationToken)
|
||||
private async Task<bool> Transcode(
|
||||
string channelNumber,
|
||||
bool firstProcess,
|
||||
bool realtime,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -212,7 +218,7 @@ namespace ErsatzTV.Application.Streaming
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private async Task TrimAndDelete(string channelNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
string playlistFileName = Path.Combine(
|
||||
@@ -224,7 +230,7 @@ namespace ErsatzTV.Application.Streaming
|
||||
{
|
||||
// trim playlist and insert discontinuity before appending with new ffmpeg process
|
||||
string[] lines = await File.ReadAllLinesAsync(playlistFileName, cancellationToken);
|
||||
TrimPlaylistResult trimResult = HlsPlaylistFilter.TrimPlaylistWithDiscontinuity(
|
||||
TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity(
|
||||
_playlistStart,
|
||||
DateTimeOffset.Now.AddMinutes(-1),
|
||||
lines);
|
||||
@@ -246,13 +252,13 @@ namespace ErsatzTV.Application.Streaming
|
||||
var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList();
|
||||
// if (toDelete.Count > 0)
|
||||
// {
|
||||
// _logger.LogInformation(
|
||||
// "Deleting HLS segments {Min} to {Max} (less than {StartSequence})",
|
||||
// toDelete.Map(s => s.SequenceNumber).Min(),
|
||||
// toDelete.Map(s => s.SequenceNumber).Max(),
|
||||
// trimResult.Sequence);
|
||||
// _logger.LogInformation(
|
||||
// "Deleting HLS segments {Min} to {Max} (less than {StartSequence})",
|
||||
// toDelete.Map(s => s.SequenceNumber).Min(),
|
||||
// toDelete.Map(s => s.SequenceNumber).Max(),
|
||||
// trimResult.Sequence);
|
||||
// }
|
||||
|
||||
|
||||
foreach (Segment segment in toDelete)
|
||||
{
|
||||
File.Delete(segment.File);
|
||||
@@ -261,7 +267,7 @@ namespace ErsatzTV.Application.Streaming
|
||||
_playlistStart = trimResult.PlaylistStart;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<long> GetPtsOffset(IMediator mediator, string channelNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber));
|
||||
|
||||
@@ -6,7 +6,11 @@ public record PtsAndDuration(long Pts, long Duration)
|
||||
{
|
||||
string[] split = ffprobeLine.Split("|");
|
||||
var left = long.Parse(split[0]);
|
||||
var right = long.Parse(split[1]);
|
||||
if (!long.TryParse(split[1], out long right))
|
||||
{
|
||||
// some durations are N/A, so we have to guess at something
|
||||
right = 10_000;
|
||||
}
|
||||
return new PtsAndDuration(left, right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
@@ -13,14 +14,14 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
|
||||
{
|
||||
private readonly IFFmpegProcessService _ffmpegProcessService;
|
||||
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
|
||||
|
||||
public GetConcatProcessByChannelNumberHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IFFmpegProcessService ffmpegProcessService)
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
|
||||
: base(dbContextFactory)
|
||||
{
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
|
||||
@@ -33,7 +34,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
Process process = _ffmpegProcessService.ConcatChannel(
|
||||
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
|
||||
Process process = ffmpegProcessService.ConcatChannel(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
channel,
|
||||
|
||||
+10
-7
@@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||
@@ -30,15 +31,15 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly IFFmpegProcessService _ffmpegProcessService;
|
||||
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
|
||||
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly IPlexPathReplacementService _plexPathReplacementService;
|
||||
private readonly ISongVideoGenerator _songVideoGenerator;
|
||||
|
||||
public GetPlayoutItemProcessByChannelNumberHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IFFmpegProcessService ffmpegProcessService,
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IPlexPathReplacementService plexPathReplacementService,
|
||||
IJellyfinPathReplacementService jellyfinPathReplacementService,
|
||||
@@ -49,7 +50,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
ISongVideoGenerator songVideoGenerator)
|
||||
: base(dbContextFactory)
|
||||
{
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
|
||||
_localFileSystem = localFileSystem;
|
||||
_plexPathReplacementService = plexPathReplacementService;
|
||||
_jellyfinPathReplacementService = jellyfinPathReplacementService;
|
||||
@@ -111,6 +112,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
maybePlayoutItem = await CheckForFallbackFiller(dbContext, channel, now);
|
||||
}
|
||||
|
||||
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
|
||||
|
||||
return await maybePlayoutItem.Match(
|
||||
async playoutItemWithPath =>
|
||||
{
|
||||
@@ -141,7 +144,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
Process process = await _ffmpegProcessService.ForPlayoutItem(
|
||||
Process process = await ffmpegProcessService.ForPlayoutItem(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
channel,
|
||||
@@ -190,7 +193,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
case UnableToLocatePlayoutItem:
|
||||
if (channel.FFmpegProfile.Transcode)
|
||||
{
|
||||
Process errorProcess = await _ffmpegProcessService.ForError(
|
||||
Process errorProcess = await ffmpegProcessService.ForError(
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
@@ -210,7 +213,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
case PlayoutItemDoesNotExistOnDisk:
|
||||
if (channel.FFmpegProfile.Transcode)
|
||||
{
|
||||
Process errorProcess = await _ffmpegProcessService.ForError(
|
||||
Process errorProcess = await ffmpegProcessService.ForError(
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
@@ -230,7 +233,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
default:
|
||||
if (channel.FFmpegProfile.Transcode)
|
||||
{
|
||||
Process errorProcess = await _ffmpegProcessService.ForError(
|
||||
Process errorProcess = await ffmpegProcessService.ForError(
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
@@ -13,14 +14,14 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetWrappedProcessByChannelNumber>
|
||||
{
|
||||
private readonly IFFmpegProcessService _ffmpegProcessService;
|
||||
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
|
||||
|
||||
public GetWrappedProcessByChannelNumberHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IFFmpegProcessService ffmpegProcessService)
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
|
||||
: base(dbContextFactory)
|
||||
{
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
|
||||
@@ -33,7 +34,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
Process process = _ffmpegProcessService.WrapSegmenter(
|
||||
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
|
||||
Process process = ffmpegProcessService.WrapSegmenter(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
channel,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
@@ -10,8 +11,8 @@ namespace ErsatzTV.Application.Watermarks.Commands
|
||||
string Image,
|
||||
ChannelWatermarkMode Mode,
|
||||
ChannelWatermarkImageSource ImageSource,
|
||||
ChannelWatermarkLocation Location,
|
||||
ChannelWatermarkSize Size,
|
||||
WatermarkLocation Location,
|
||||
WatermarkSize Size,
|
||||
int Width,
|
||||
int HorizontalMargin,
|
||||
int VerticalMargin,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
@@ -11,8 +12,8 @@ namespace ErsatzTV.Application.Watermarks.Commands
|
||||
string Image,
|
||||
ChannelWatermarkMode Mode,
|
||||
ChannelWatermarkImageSource ImageSource,
|
||||
ChannelWatermarkLocation Location,
|
||||
ChannelWatermarkSize Size,
|
||||
WatermarkLocation Location,
|
||||
WatermarkSize Size,
|
||||
int Width,
|
||||
int HorizontalMargin,
|
||||
int VerticalMargin,
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace ErsatzTV.Application.Watermarks.Commands
|
||||
UpdateWatermark request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, ChannelWatermark> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
|
||||
namespace ErsatzTV.Application.Watermarks
|
||||
{
|
||||
@@ -8,8 +9,8 @@ namespace ErsatzTV.Application.Watermarks
|
||||
string Name,
|
||||
ChannelWatermarkMode Mode,
|
||||
ChannelWatermarkImageSource ImageSource,
|
||||
ChannelWatermarkLocation Location,
|
||||
ChannelWatermarkSize Size,
|
||||
WatermarkLocation Location,
|
||||
WatermarkSize Size,
|
||||
int Width,
|
||||
int HorizontalMargin,
|
||||
int VerticalMargin,
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.4.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using FluentAssertions;
|
||||
using LanguageExt;
|
||||
using NUnit.Framework;
|
||||
@@ -141,7 +142,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.BottomLeft,
|
||||
WatermarkLocation.BottomLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:0][1:v]overlay=x=134:y=H-h-54[v]",
|
||||
@@ -151,7 +152,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.BottomRight,
|
||||
WatermarkLocation.BottomRight,
|
||||
false,
|
||||
100,
|
||||
"[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]",
|
||||
@@ -161,7 +162,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:0][1:v]overlay=x=134:y=54[v]",
|
||||
@@ -171,7 +172,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopRight,
|
||||
WatermarkLocation.TopRight,
|
||||
false,
|
||||
100,
|
||||
"[0:0][1:v]overlay=x=W-w-134:y=54[v]",
|
||||
@@ -181,7 +182,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=nv12[v]",
|
||||
@@ -191,7 +192,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
true,
|
||||
100,
|
||||
"[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]",
|
||||
@@ -201,7 +202,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
90,
|
||||
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]",
|
||||
@@ -211,7 +212,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
|
||||
@@ -221,7 +222,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
true,
|
||||
100,
|
||||
"[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]",
|
||||
@@ -231,7 +232,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
|
||||
@@ -241,7 +242,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]",
|
||||
@@ -251,7 +252,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
bool alignAudio,
|
||||
bool deinterlace,
|
||||
bool intermittent,
|
||||
ChannelWatermarkLocation location,
|
||||
WatermarkLocation location,
|
||||
bool scaled,
|
||||
int opacity,
|
||||
string expectedVideoFilter,
|
||||
@@ -266,7 +267,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
DurationSeconds = intermittent ? 15 : 0,
|
||||
FrequencyMinutes = intermittent ? 10 : 0,
|
||||
Location = location,
|
||||
Size = scaled ? ChannelWatermarkSize.Scaled : ChannelWatermarkSize.ActualSize,
|
||||
Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize,
|
||||
WidthPercent = scaled ? 20 : 0,
|
||||
Opacity = opacity,
|
||||
HorizontalMarginPercent = 7,
|
||||
@@ -310,7 +311,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.BottomLeft,
|
||||
WatermarkLocation.BottomLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54[v]",
|
||||
@@ -321,7 +322,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.BottomLeft,
|
||||
WatermarkLocation.BottomLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54,hwupload[v]",
|
||||
@@ -332,7 +333,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
|
||||
@@ -343,7 +344,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
|
||||
@@ -354,7 +355,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
true,
|
||||
100,
|
||||
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
|
||||
@@ -365,7 +366,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
true,
|
||||
100,
|
||||
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
|
||||
@@ -376,7 +377,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
90,
|
||||
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
|
||||
@@ -387,7 +388,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
90,
|
||||
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
|
||||
@@ -399,7 +400,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// ChannelWatermarkLocation.TopLeft,
|
||||
// WatermarkLocation.TopLeft,
|
||||
// false,
|
||||
// 100,
|
||||
// "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
|
||||
@@ -409,7 +410,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
// false,
|
||||
// true,
|
||||
// false,
|
||||
// ChannelWatermarkLocation.TopLeft,
|
||||
// WatermarkLocation.TopLeft,
|
||||
// true,
|
||||
// 100,
|
||||
// "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]",
|
||||
@@ -419,7 +420,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
// true,
|
||||
// true,
|
||||
// false,
|
||||
// ChannelWatermarkLocation.TopLeft,
|
||||
// WatermarkLocation.TopLeft,
|
||||
// false,
|
||||
// 100,
|
||||
// "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
|
||||
@@ -429,7 +430,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
|
||||
@@ -440,7 +441,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
ChannelWatermarkLocation.TopLeft,
|
||||
WatermarkLocation.TopLeft,
|
||||
false,
|
||||
100,
|
||||
"[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
|
||||
@@ -451,7 +452,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
bool alignAudio,
|
||||
bool deinterlace,
|
||||
bool intermittent,
|
||||
ChannelWatermarkLocation location,
|
||||
WatermarkLocation location,
|
||||
bool scaled,
|
||||
int opacity,
|
||||
string expectedVideoFilter,
|
||||
@@ -467,7 +468,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
DurationSeconds = intermittent ? 15 : 0,
|
||||
FrequencyMinutes = intermittent ? 10 : 0,
|
||||
Location = location,
|
||||
Size = scaled ? ChannelWatermarkSize.Scaled : ChannelWatermarkSize.ActualSize,
|
||||
Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize,
|
||||
WidthPercent = scaled ? 20 : 0,
|
||||
Opacity = opacity,
|
||||
HorizontalMarginPercent = 7,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
@@ -8,8 +11,19 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
[TestFixture]
|
||||
public class HlsPlaylistFilterTests
|
||||
{
|
||||
private HlsPlaylistFilter _hlsPlaylistFilter;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_hlsPlaylistFilter = new HlsPlaylistFilter(
|
||||
new Mock<ITempFilePool>().Object,
|
||||
new Mock<ILogger<HlsPlaylistFilter>>().Object
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HlsPlaylistFilter_ShouldRewriteProgramDateTime()
|
||||
public void _hlsPlaylistFilter_ShouldRewriteProgramDateTime()
|
||||
{
|
||||
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
|
||||
string[] input = NormalizeLineEndings(@"#EXTM3U
|
||||
@@ -28,7 +42,7 @@ live001138.ts
|
||||
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
|
||||
live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input);
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input);
|
||||
|
||||
result.PlaylistStart.Should().Be(start);
|
||||
result.Sequence.Should().Be(1137);
|
||||
@@ -53,7 +67,7 @@ live001139.ts
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HlsPlaylistFilter_ShouldLimitSegments()
|
||||
public void _hlsPlaylistFilter_ShouldLimitSegments()
|
||||
{
|
||||
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
|
||||
string[] input = NormalizeLineEndings(@"#EXTM3U
|
||||
@@ -72,7 +86,7 @@ live001138.ts
|
||||
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
|
||||
live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input, 2);
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input, 2);
|
||||
|
||||
result.PlaylistStart.Should().Be(start);
|
||||
result.Sequence.Should().Be(1137);
|
||||
@@ -94,7 +108,7 @@ live001138.ts
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HlsPlaylistFilter_ShouldAddDiscontinuity()
|
||||
public void _hlsPlaylistFilter_ShouldAddDiscontinuity()
|
||||
{
|
||||
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
|
||||
string[] input = NormalizeLineEndings(@"#EXTM3U
|
||||
@@ -113,7 +127,7 @@ live001138.ts
|
||||
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
|
||||
live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(
|
||||
start,
|
||||
start.AddSeconds(-30),
|
||||
input,
|
||||
@@ -144,7 +158,7 @@ live001139.ts
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HlsPlaylistFilter_ShouldFilterOldSegments()
|
||||
public void _hlsPlaylistFilter_ShouldFilterOldSegments()
|
||||
{
|
||||
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
|
||||
string[] input = NormalizeLineEndings(@"#EXTM3U
|
||||
@@ -163,7 +177,7 @@ live001138.ts
|
||||
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
|
||||
live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input);
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input);
|
||||
|
||||
result.PlaylistStart.Should().Be(start.AddSeconds(8));
|
||||
result.Sequence.Should().Be(1139);
|
||||
@@ -182,7 +196,7 @@ live001139.ts
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HlsPlaylistFilter_ShouldFilterOldDiscontinuity()
|
||||
public void _hlsPlaylistFilter_ShouldFilterOldDiscontinuity()
|
||||
{
|
||||
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
|
||||
string[] input = NormalizeLineEndings(@"#EXTM3U
|
||||
@@ -202,7 +216,7 @@ live001138.ts
|
||||
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
|
||||
live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input);
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input);
|
||||
|
||||
result.PlaylistStart.Should().Be(start.AddSeconds(8));
|
||||
result.Sequence.Should().Be(1139);
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
@@ -37,6 +39,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
public record InputFormat(string Encoder, string PixelFormat);
|
||||
|
||||
public enum Padding
|
||||
{
|
||||
NoPadding,
|
||||
@@ -56,22 +60,32 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
VideoScanKind.Progressive,
|
||||
VideoScanKind.Interlaced
|
||||
};
|
||||
|
||||
public static string[] InputCodecs =
|
||||
{
|
||||
"h264",
|
||||
"mpeg2video",
|
||||
"hevc",
|
||||
"mpeg4"
|
||||
};
|
||||
|
||||
public static string[] InputPixelFormats =
|
||||
public static InputFormat[] InputFormats =
|
||||
{
|
||||
"yuv420p",
|
||||
"yuv420p10le",
|
||||
// "yuvj420p",
|
||||
// "yuv444p",
|
||||
// "yuv444p10le"
|
||||
new("libx264", "yuv420p"),
|
||||
new("libx264", "yuvj420p"),
|
||||
new("libx264", "yuv420p10le"),
|
||||
// new("libx264", "yuv444p10le"),
|
||||
|
||||
new("mpeg1video", "yuv420p"),
|
||||
|
||||
new("mpeg2video", "yuv420p"),
|
||||
|
||||
new("libx265", "yuv420p"),
|
||||
new("libx265", "yuv420p10le"),
|
||||
|
||||
new("mpeg4", "yuv420p"),
|
||||
|
||||
new("libvpx-vp9", "yuv420p"),
|
||||
|
||||
// new("libaom-av1", "yuv420p")
|
||||
// av1 yuv420p10le 51
|
||||
|
||||
new("msmpeg4v2", "yuv420p"),
|
||||
new("msmpeg4v3", "yuv420p")
|
||||
|
||||
// wmv3 yuv420p 1
|
||||
};
|
||||
|
||||
public static Resolution[] Resolutions =
|
||||
@@ -123,14 +137,23 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
HardwareAccelerationKind.VideoToolbox
|
||||
};
|
||||
|
||||
public static string[] QsvCodecs =
|
||||
{
|
||||
"h264_qsv",
|
||||
"hevc_qsv"
|
||||
};
|
||||
|
||||
public static HardwareAccelerationKind[] QsvAcceleration =
|
||||
{
|
||||
HardwareAccelerationKind.Qsv
|
||||
};
|
||||
}
|
||||
|
||||
[Test, Combinatorial]
|
||||
public async Task Transcode(
|
||||
[ValueSource(typeof(TestData), nameof(TestData.InputCodecs))]
|
||||
string inputCodec,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.InputPixelFormats))]
|
||||
string inputPixelFormat,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.InputFormats))]
|
||||
InputFormat inputFormat,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
|
||||
Resolution profileResolution,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.Paddings))]
|
||||
@@ -143,11 +166,22 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec,
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.QsvCodecs))] string profileCodec,
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec,
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||
{
|
||||
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
|
||||
{
|
||||
if (videoScanKind == VideoScanKind.Interlaced)
|
||||
{
|
||||
Assert.Inconclusive($"{inputFormat.Encoder} does not support interlaced content");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string name = GetStringSha256Hash(
|
||||
$"{inputCodec}_{inputPixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}");
|
||||
$"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}");
|
||||
|
||||
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
|
||||
if (!File.Exists(file))
|
||||
@@ -158,7 +192,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
string flags = videoScanKind == VideoScanKind.Interlaced ? "-flags +ildct+ilme" : string.Empty;
|
||||
|
||||
string args =
|
||||
$"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {flags} {file}";
|
||||
$"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputFormat.Encoder} -shortest -pix_fmt {inputFormat.PixelFormat} -strict -2 {flags} {file}";
|
||||
var p1 = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
@@ -175,19 +209,35 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
p1.ExitCode.Should().Be(0);
|
||||
}
|
||||
|
||||
var service = new FFmpegProcessService(
|
||||
var oldService = new FFmpegProcessService(
|
||||
new FFmpegPlaybackSettingsCalculator(),
|
||||
new FakeStreamSelector(),
|
||||
new Mock<IImageCache>().Object,
|
||||
new Mock<ITempFilePool>().Object,
|
||||
new Mock<ILogger<FFmpegProcessService>>().Object);
|
||||
|
||||
MediaVersion v = new MediaVersion();
|
||||
var service = new FFmpegLibraryProcessService(
|
||||
oldService,
|
||||
new FFmpegPlaybackSettingsCalculator(),
|
||||
new FakeStreamSelector(),
|
||||
new Mock<ILogger<FFmpegLibraryProcessService>>().Object);
|
||||
|
||||
var v = new MediaVersion
|
||||
{
|
||||
MediaFiles = new List<MediaFile>
|
||||
{
|
||||
new() { Path = file }
|
||||
}
|
||||
};
|
||||
|
||||
var metadataRepository = new Mock<IMetadataRepository>();
|
||||
metadataRepository
|
||||
.Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
|
||||
.Callback<MediaItem, MediaVersion, bool>((_, version, _) => v = version);
|
||||
.Callback<MediaItem, MediaVersion, bool>((_, version, _) =>
|
||||
{
|
||||
version.MediaFiles = v.MediaFiles;
|
||||
v = version;
|
||||
});
|
||||
|
||||
var localStatisticsProvider = new LocalStatisticsProvider(
|
||||
metadataRepository.Object,
|
||||
@@ -217,10 +267,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
new Channel(Guid.NewGuid())
|
||||
{
|
||||
Number = "1",
|
||||
FFmpegProfile = FFmpegProfile.New("test", profileResolution) with
|
||||
{
|
||||
HardwareAcceleration = profileAcceleration,
|
||||
VideoCodec = profileCodec
|
||||
VideoCodec = profileCodec,
|
||||
AudioCodec = "aac"
|
||||
},
|
||||
StreamingMode = StreamingMode.TransportStream
|
||||
},
|
||||
@@ -242,25 +294,51 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
None);
|
||||
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.EnableRaisingEvents = true;
|
||||
|
||||
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
|
||||
|
||||
process.Start().Should().BeTrue();
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
string error = await process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
process.WaitForExit();
|
||||
|
||||
string[] unsupportedMessages =
|
||||
{
|
||||
"No support for codec",
|
||||
"No usable",
|
||||
"Provided device doesn't support"
|
||||
};
|
||||
|
||||
var errorBuffer = new StringBuilder();
|
||||
|
||||
if (profileAcceleration != HardwareAccelerationKind.None && unsupportedMessages.Any(error.Contains))
|
||||
process.ErrorDataReceived += (_, errorLine) =>
|
||||
{
|
||||
string data = errorLine.Data ?? string.Empty;
|
||||
errorBuffer.AppendLine(data);
|
||||
};
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
// string error = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(timeoutSignal.Token);
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill();
|
||||
|
||||
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
|
||||
Assert.Fail($"Transcode failure (timeout): ffmpeg {string.Join(" ", quotedArgs)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var error = errorBuffer.ToString();
|
||||
bool isUnsupported = unsupportedMessages.Any(error.Contains);
|
||||
|
||||
if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported)
|
||||
{
|
||||
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
|
||||
process.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}");
|
||||
@@ -273,8 +351,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
else
|
||||
{
|
||||
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
|
||||
process.ExitCode.Should().Be(0, error + Environment.NewLine + string.Join(" ", quotedArgs));
|
||||
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
|
||||
process.ExitCode.Should().Be(0, errorBuffer + Environment.NewLine + string.Join(" ", quotedArgs));
|
||||
if (process.ExitCode == 0)
|
||||
{
|
||||
Console.WriteLine(string.Join(" ", quotedArgs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -624,8 +624,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextStart = HoursAfterMidnight(9).UtcDateTime,
|
||||
NextScheduleItem = items[0],
|
||||
NextScheduleItemId = 1,
|
||||
ScheduleItemsEnumeratorState = new CollectionEnumeratorState
|
||||
{
|
||||
Index = 0,
|
||||
Seed = 1
|
||||
},
|
||||
InFlood = true
|
||||
}
|
||||
};
|
||||
@@ -918,8 +921,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextStart = HoursAfterMidnight(1).UtcDateTime,
|
||||
NextScheduleItem = items[0],
|
||||
NextScheduleItemId = 1,
|
||||
ScheduleItemsEnumeratorState = new CollectionEnumeratorState
|
||||
{
|
||||
Index = 0,
|
||||
Seed = 1
|
||||
},
|
||||
MultipleRemaining = 2
|
||||
}
|
||||
};
|
||||
@@ -951,7 +957,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[3].MediaItemId.Should().Be(2);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1);
|
||||
result.Anchor.MultipleRemaining.Should().Be(1);
|
||||
}
|
||||
|
||||
@@ -1048,7 +1054,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[4].MediaItemId.Should().Be(5);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[0]);
|
||||
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
|
||||
result.Anchor.MultipleRemaining.Should().BeNull();
|
||||
}
|
||||
|
||||
@@ -1116,8 +1122,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextStart = HoursAfterMidnight(1).UtcDateTime,
|
||||
NextScheduleItem = items[0],
|
||||
NextScheduleItemId = 1,
|
||||
ScheduleItemsEnumeratorState = new CollectionEnumeratorState
|
||||
{
|
||||
Index = 0,
|
||||
Seed = 1
|
||||
},
|
||||
DurationFinish = HoursAfterMidnight(3).UtcDateTime
|
||||
}
|
||||
};
|
||||
@@ -1149,7 +1158,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[3].MediaItemId.Should().Be(2);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1);
|
||||
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
|
||||
}
|
||||
|
||||
@@ -1284,7 +1293,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0));
|
||||
result.Items[11].MediaItemId.Should().Be(3);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[0]);
|
||||
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
|
||||
result.Anchor.DurationFinish.Should().BeNull();
|
||||
}
|
||||
|
||||
@@ -1361,7 +1370,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5));
|
||||
result.Items[5].MediaItemId.Should().Be(4);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[0]);
|
||||
result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
|
||||
result.Anchor.DurationFinish.Should().BeNull();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,19 +31,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -51,26 +57,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
@@ -93,20 +99,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
CustomTitle = "Custom Title"
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -114,28 +126,28 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
playoutItems[0].CustomTitle.Should().Be("Custom Title");
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
playoutItems[1].CustomTitle.Should().Be("Custom Title");
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
@@ -158,20 +170,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
TailMode = TailMode.None,
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -179,26 +197,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
@@ -220,48 +238,54 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
TailMode = TailMode.Offline,
|
||||
PlaybackOrder = PlaybackOrder.Chronological
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
// duration block should end after exact duration, with gap
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
@@ -290,6 +314,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -299,15 +327,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -315,7 +345,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -323,25 +353,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
@@ -370,6 +400,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -379,15 +413,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -395,7 +431,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -403,37 +439,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
@@ -462,6 +498,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -471,23 +511,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -495,37 +537,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[4].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[5].GuideFinish.HasValue.Should().BeFalse();
|
||||
@@ -561,6 +603,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -574,9 +620,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
@@ -586,9 +634,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -596,7 +644,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -605,43 +653,43 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(7);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[4].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
playoutItems[5].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
playoutItems[6].MediaItemId.Should().Be(5);
|
||||
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].GuideGroup.Should().Be(3);
|
||||
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
playoutItems[6].GuideFinish.HasValue.Should().BeFalse();
|
||||
|
||||
@@ -40,16 +40,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -57,24 +63,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
@@ -109,16 +115,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
StartTime = TimeSpan.FromHours(3),
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -126,24 +138,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
@@ -188,15 +200,21 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -204,7 +222,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -212,32 +230,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(2);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[2].GuideGroup.Should().Be(2);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(4);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 55, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 55, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(2);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(1);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
}
|
||||
@@ -268,16 +286,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -285,24 +309,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
@@ -343,16 +367,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -360,7 +390,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -368,32 +398,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
@@ -434,16 +464,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -451,7 +487,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -459,22 +495,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
@@ -515,16 +551,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -532,7 +574,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -540,32 +582,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
@@ -616,10 +658,16 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
@@ -629,9 +677,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -639,7 +687,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -648,37 +696,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(7);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[6].MediaItemId.Should().Be(5);
|
||||
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].GuideGroup.Should().Be(3);
|
||||
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
@@ -729,10 +777,16 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
scheduleItem,
|
||||
NextScheduleItem
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
sortedScheduleItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object);
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
@@ -742,9 +796,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -752,7 +806,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(0);
|
||||
@@ -761,17 +815,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
FallbackFiller = null,
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -42,16 +46,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -59,24 +65,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
@@ -98,6 +104,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
FallbackFiller = null,
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -107,16 +117,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -124,24 +136,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
@@ -169,6 +181,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
FallbackFiller = null,
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -183,16 +199,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -200,7 +218,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -208,32 +226,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
@@ -261,6 +279,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
},
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -275,16 +297,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionTwo.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -292,7 +316,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -300,22 +324,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
@@ -343,6 +367,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
FallbackFiller = null,
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -357,16 +385,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -374,7 +404,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -382,32 +412,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(6);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
@@ -441,6 +471,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
},
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -461,9 +495,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
@@ -473,9 +509,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -483,7 +519,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -492,37 +528,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(7);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(3);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(4);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(3);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[5].MediaItemId.Should().Be(3);
|
||||
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[5].GuideGroup.Should().Be(3);
|
||||
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[6].MediaItemId.Should().Be(5);
|
||||
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[6].GuideGroup.Should().Be(3);
|
||||
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
@@ -556,6 +592,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
},
|
||||
Count = 3
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -575,10 +615,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems },
|
||||
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems }
|
||||
}.ToMap();
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
@@ -588,9 +630,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(4);
|
||||
@@ -598,7 +640,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(0);
|
||||
@@ -607,17 +649,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(3);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(2);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(2);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||
playoutItems[2].GuideGroup.Should().Be(3);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
|
||||
@@ -30,20 +30,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
TailFiller = null,
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -51,14 +57,14 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator.State.Index.Should().Be(1);
|
||||
|
||||
playoutItems.Count.Should().Be(1);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
@@ -91,6 +97,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
Collection = collectionThree
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -104,15 +114,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -120,7 +132,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(0);
|
||||
@@ -129,7 +141,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(1);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
@@ -156,6 +168,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
},
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -164,16 +180,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -181,7 +199,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -189,22 +207,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(4);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
@@ -231,6 +249,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionTwo.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -239,16 +261,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -256,7 +280,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -264,12 +288,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(2);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
@@ -296,6 +320,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
},
|
||||
FallbackFiller = null
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -304,16 +332,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
var enumerator2 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -321,7 +351,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -329,22 +359,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(4);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
}
|
||||
@@ -377,6 +407,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -389,10 +423,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
@@ -402,9 +438,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -412,7 +448,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -421,27 +457,27 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(5);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(4);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
|
||||
|
||||
playoutItems[4].MediaItemId.Should().Be(5);
|
||||
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
|
||||
playoutItems[4].GuideGroup.Should().Be(1);
|
||||
playoutItems[4].FillerKind.Should().Be(FillerKind.Fallback);
|
||||
}
|
||||
@@ -474,6 +510,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -486,10 +526,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
var enumerator3 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator1,
|
||||
@@ -499,9 +541,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -509,7 +551,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(0);
|
||||
@@ -518,7 +560,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(1);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
}
|
||||
@@ -553,6 +595,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -566,15 +612,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -582,7 +630,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -590,22 +638,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(4);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
}
|
||||
@@ -640,6 +688,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
CollectionId = collectionThree.Id
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
@@ -653,15 +705,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
collectionThree.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
|
||||
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
|
||||
StartState,
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
|
||||
scheduleItem,
|
||||
NextScheduleItem,
|
||||
HardStop);
|
||||
HardStop(scheduleItemsEnumerator));
|
||||
|
||||
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3));
|
||||
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
|
||||
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||
|
||||
playoutBuilderState.NextGuideGroup.Should().Be(2);
|
||||
@@ -669,7 +723,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutBuilderState.InFlood.Should().BeFalse();
|
||||
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||
playoutBuilderState.ScheduleItemIndex.Should().Be(1);
|
||||
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||
|
||||
enumerator1.State.Index.Should().Be(1);
|
||||
enumerator2.State.Index.Should().Be(1);
|
||||
@@ -678,22 +732,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
playoutItems.Count.Should().Be(4);
|
||||
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[0].GuideGroup.Should().Be(1);
|
||||
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
|
||||
playoutItems[1].GuideGroup.Should().Be(1);
|
||||
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
|
||||
playoutItems[2].MediaItemId.Should().Be(4);
|
||||
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
|
||||
playoutItems[2].GuideGroup.Should().Be(1);
|
||||
playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
|
||||
playoutItems[3].MediaItemId.Should().Be(3);
|
||||
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
|
||||
playoutItems[3].GuideGroup.Should().Be(1);
|
||||
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
public abstract class SchedulerTestBase
|
||||
{
|
||||
protected static PlayoutBuilderState StartState => new(
|
||||
0,
|
||||
protected static PlayoutBuilderState StartState(IScheduleItemsEnumerator scheduleItemsEnumerator) => new(
|
||||
scheduleItemsEnumerator,
|
||||
Prelude.None,
|
||||
Prelude.None,
|
||||
false,
|
||||
@@ -24,7 +24,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
StartTime = null
|
||||
};
|
||||
|
||||
protected static DateTimeOffset HardStop => StartState.CurrentTime.AddHours(6);
|
||||
protected static DateTimeOffset HardStop(IScheduleItemsEnumerator scheduleItemsEnumerator) =>
|
||||
StartState(scheduleItemsEnumerator).CurrentTime.AddHours(6);
|
||||
|
||||
protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators(
|
||||
ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator) =>
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace ErsatzTV.Core.Domain
|
||||
public Guid UniqueId { get; init; }
|
||||
public string Number { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Group { get; set; }
|
||||
public string Categories { get; set; }
|
||||
public int FFmpegProfileId { get; set; }
|
||||
public FFmpegProfile FFmpegProfile { get; set; }
|
||||
public int? WatermarkId { get; set; }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class ChannelWatermark
|
||||
{
|
||||
@@ -7,8 +9,8 @@
|
||||
public ChannelWatermarkMode Mode { get; set; }
|
||||
public ChannelWatermarkImageSource ImageSource { get; set; }
|
||||
public string Image { get; set; }
|
||||
public ChannelWatermarkLocation Location { get; set; }
|
||||
public ChannelWatermarkSize Size { get; set; }
|
||||
public WatermarkLocation Location { get; set; }
|
||||
public WatermarkSize Size { get; set; }
|
||||
public int WidthPercent { get; set; }
|
||||
public int HorizontalMarginPercent { get; set; }
|
||||
public int VerticalMarginPercent { get; set; }
|
||||
@@ -17,24 +19,6 @@
|
||||
public int Opacity { get; set; }
|
||||
}
|
||||
|
||||
public enum ChannelWatermarkLocation
|
||||
{
|
||||
BottomRight = 0,
|
||||
BottomLeft = 1,
|
||||
TopRight = 2,
|
||||
TopLeft = 3,
|
||||
TopMiddle = 4,
|
||||
RightMiddle = 5,
|
||||
BottomMiddle = 6,
|
||||
LeftMiddle = 7
|
||||
}
|
||||
|
||||
public enum ChannelWatermarkSize
|
||||
{
|
||||
Scaled = 0,
|
||||
ActualSize = 1
|
||||
}
|
||||
|
||||
public enum ChannelWatermarkMode
|
||||
{
|
||||
None = 0,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds");
|
||||
public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit");
|
||||
public static ConfigElementKey FFmpegInitialSegmentCount => new("ffmpeg.segmenter.initial_segment_count");
|
||||
public static ConfigElementKey FFmpegUseExperimentalTranscoder => new("ffmpeg.use_experimental_transcoder");
|
||||
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
|
||||
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
|
||||
public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size");
|
||||
|
||||
@@ -6,10 +6,7 @@ namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class PlayoutAnchor
|
||||
{
|
||||
public int NextScheduleItemId { get; set; }
|
||||
|
||||
public ProgramScheduleItem NextScheduleItem { get; set; }
|
||||
|
||||
public CollectionEnumeratorState ScheduleItemsEnumeratorState { get; set; }
|
||||
public DateTime NextStart { get; set; }
|
||||
public int? MultipleRemaining { get; set; }
|
||||
public DateTime? DurationFinish { get; set; }
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace ErsatzTV.Core.Domain
|
||||
public string Name { get; set; }
|
||||
public bool KeepMultiPartEpisodesTogether { get; set; }
|
||||
public bool TreatCollectionsAsShows { get; set; }
|
||||
public bool ShuffleScheduleItems { get; set; }
|
||||
public List<ProgramScheduleItem> Items { get; set; }
|
||||
public List<Playout> Playouts { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<PackageReference Include="Flurl" Version="3.0.4" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||
<PackageReference Include="MediatR" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
||||
@@ -25,5 +26,9 @@
|
||||
<_Parameter1>ErsatzTV.Core.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ErsatzTV.FFmpeg\ErsatzTV.FFmpeg.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
public override string ToString() =>
|
||||
$@"ffconcat version 1.0
|
||||
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}
|
||||
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}";
|
||||
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}?mode=ts-legacy
|
||||
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}?mode=ts-legacy";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -330,13 +331,13 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
string position = watermark.Location switch
|
||||
{
|
||||
ChannelWatermarkLocation.BottomLeft => $"x={horizontalMargin}:y=H-h-{verticalMargin}",
|
||||
ChannelWatermarkLocation.TopLeft => $"x={horizontalMargin}:y={verticalMargin}",
|
||||
ChannelWatermarkLocation.TopRight => $"x=W-w-{horizontalMargin}:y={verticalMargin}",
|
||||
ChannelWatermarkLocation.TopMiddle => $"x=(W-w)/2:y={verticalMargin}",
|
||||
ChannelWatermarkLocation.RightMiddle => $"x=W-w-{horizontalMargin}:y=(H-h)/2",
|
||||
ChannelWatermarkLocation.BottomMiddle => $"x=(W-w)/2:y=H-h-{verticalMargin}",
|
||||
ChannelWatermarkLocation.LeftMiddle => $"x={horizontalMargin}:y=(H-h)/2",
|
||||
WatermarkLocation.BottomLeft => $"x={horizontalMargin}:y=H-h-{verticalMargin}",
|
||||
WatermarkLocation.TopLeft => $"x={horizontalMargin}:y={verticalMargin}",
|
||||
WatermarkLocation.TopRight => $"x=W-w-{horizontalMargin}:y={verticalMargin}",
|
||||
WatermarkLocation.TopMiddle => $"x=(W-w)/2:y={verticalMargin}",
|
||||
WatermarkLocation.RightMiddle => $"x=W-w-{horizontalMargin}:y=(H-h)/2",
|
||||
WatermarkLocation.BottomMiddle => $"x=(W-w)/2:y=H-h-{verticalMargin}",
|
||||
WatermarkLocation.LeftMiddle => $"x={horizontalMargin}:y=(H-h)/2",
|
||||
_ => $"x=W-w-{horizontalMargin}:y=H-h-{verticalMargin}"
|
||||
};
|
||||
|
||||
@@ -346,7 +347,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
watermarkPreprocess.Add($"colorchannelmixer=aa={opacity:F2}");
|
||||
}
|
||||
|
||||
if (watermark.Size == ChannelWatermarkSize.Scaled)
|
||||
if (watermark.Size == WatermarkSize.Scaled)
|
||||
{
|
||||
double width = Math.Round(watermark.WidthPercent / 100.0 * _resolution.Width);
|
||||
watermarkPreprocess.Add($"scale={width}:-1");
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.FFmpeg;
|
||||
using ErsatzTV.FFmpeg.Environment;
|
||||
using ErsatzTV.FFmpeg.Format;
|
||||
using ErsatzTV.FFmpeg.OutputFormat;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
using FFmpegState = ErsatzTV.FFmpeg.FFmpegState;
|
||||
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg;
|
||||
|
||||
public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
{
|
||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
|
||||
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
|
||||
private readonly ILogger<FFmpegLibraryProcessService> _logger;
|
||||
|
||||
public FFmpegLibraryProcessService(
|
||||
FFmpegProcessService ffmpegProcessService,
|
||||
FFmpegPlaybackSettingsCalculator playbackSettingsCalculator,
|
||||
IFFmpegStreamSelector ffmpegStreamSelector,
|
||||
ILogger<FFmpegLibraryProcessService> logger)
|
||||
{
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_playbackSettingsCalculator = playbackSettingsCalculator;
|
||||
_ffmpegStreamSelector = ffmpegStreamSelector;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Process> ForPlayoutItem(
|
||||
string ffmpegPath,
|
||||
bool saveReports,
|
||||
Channel channel,
|
||||
MediaVersion videoVersion,
|
||||
MediaVersion audioVersion,
|
||||
string videoPath,
|
||||
string audioPath,
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset finish,
|
||||
DateTimeOffset now,
|
||||
Option<ChannelWatermark> globalWatermark,
|
||||
VaapiDriver vaapiDriver,
|
||||
string vaapiDevice,
|
||||
bool hlsRealtime,
|
||||
FillerKind fillerKind,
|
||||
TimeSpan inPoint,
|
||||
TimeSpan outPoint,
|
||||
long ptsOffset,
|
||||
Option<int> targetFramerate)
|
||||
{
|
||||
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
|
||||
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
|
||||
|
||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
|
||||
channel.StreamingMode,
|
||||
channel.FFmpegProfile,
|
||||
videoVersion,
|
||||
videoStream,
|
||||
maybeAudioStream,
|
||||
start,
|
||||
now,
|
||||
inPoint,
|
||||
outPoint,
|
||||
hlsRealtime,
|
||||
targetFramerate);
|
||||
|
||||
Option<WatermarkOptions> watermarkOptions =
|
||||
await _ffmpegProcessService.GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None);
|
||||
|
||||
Option<List<FadePoint>> maybeFadePoints = watermarkOptions
|
||||
.Map(o => o.Watermark)
|
||||
.Flatten()
|
||||
.Where(wm => wm.Mode == ChannelWatermarkMode.Intermittent)
|
||||
.Map(
|
||||
wm =>
|
||||
WatermarkCalculator.CalculateFadePoints(
|
||||
start,
|
||||
inPoint,
|
||||
outPoint,
|
||||
playbackSettings.StreamSeek,
|
||||
wm.FrequencyMinutes,
|
||||
wm.DurationSeconds));
|
||||
|
||||
var audioState = new AudioState(
|
||||
playbackSettings.AudioCodec,
|
||||
playbackSettings.AudioChannels,
|
||||
playbackSettings.AudioBitrate,
|
||||
playbackSettings.AudioBufferSize,
|
||||
playbackSettings.AudioSampleRate,
|
||||
videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None,
|
||||
playbackSettings.NormalizeLoudness);
|
||||
|
||||
var ffmpegVideoStream = new VideoStream(
|
||||
videoStream.Index,
|
||||
videoStream.Codec,
|
||||
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
|
||||
new FrameSize(videoVersion.Width, videoVersion.Height),
|
||||
videoVersion.RFrameRate,
|
||||
videoPath != audioPath); // still image when paths are different
|
||||
|
||||
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });
|
||||
|
||||
Option<AudioInputFile> audioInputFile = maybeAudioStream.Map(
|
||||
audioStream =>
|
||||
{
|
||||
var ffmpegAudioStream = new AudioStream(audioStream.Index, audioStream.Codec, audioStream.Channels);
|
||||
return new AudioInputFile(audioPath, new List<AudioStream> { ffmpegAudioStream }, audioState);
|
||||
});
|
||||
|
||||
var watermarkInputFile = GetWatermarkInputFile(watermarkOptions, maybeFadePoints);
|
||||
|
||||
// TODO: need formats for these codecs
|
||||
string videoFormat = playbackSettings.VideoCodec switch
|
||||
{
|
||||
"libx265" or "hevc_nvenc" or "hevc_qsv" or "hevc_vaapi" or "hevc_videotoolbox" => VideoFormat.Hevc,
|
||||
"libx264" or "h264_nvenc" or "h264_qsv" or "h264_vaapi" or "h264_videotoolbox" => VideoFormat.H264,
|
||||
"mpeg2video" => VideoFormat.Mpeg2Video,
|
||||
"copy" => VideoFormat.Copy,
|
||||
_ => throw new ArgumentOutOfRangeException($"unexpected video codec {playbackSettings.VideoCodec}")
|
||||
};
|
||||
|
||||
HardwareAccelerationMode hwAccel = playbackSettings.HardwareAcceleration switch
|
||||
{
|
||||
HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc,
|
||||
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
|
||||
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,
|
||||
HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox,
|
||||
_ => HardwareAccelerationMode.None
|
||||
};
|
||||
|
||||
OutputFormatKind outputFormat = channel.StreamingMode == StreamingMode.HttpLiveStreamingSegmenter
|
||||
? OutputFormatKind.Hls
|
||||
: OutputFormatKind.MpegTs;
|
||||
|
||||
Option<string> hlsPlaylistPath = outputFormat == OutputFormatKind.Hls
|
||||
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8")
|
||||
: Option<string>.None;
|
||||
|
||||
Option<string> hlsSegmentTemplate = outputFormat == OutputFormatKind.Hls
|
||||
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts")
|
||||
: Option<string>.None;
|
||||
|
||||
// normalize songs to yuv420p
|
||||
Option<IPixelFormat> desiredPixelFormat =
|
||||
videoPath == audioPath ? ffmpegVideoStream.PixelFormat : new PixelFormatYuv420P();
|
||||
|
||||
var desiredState = new FrameState(
|
||||
playbackSettings.RealtimeOutput,
|
||||
false, // TODO: fallback filler needs to loop
|
||||
videoFormat,
|
||||
desiredPixelFormat,
|
||||
await playbackSettings.ScaledSize.Map(ss => new FrameSize(ss.Width, ss.Height))
|
||||
.IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)),
|
||||
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
|
||||
playbackSettings.FrameRate,
|
||||
playbackSettings.VideoBitrate,
|
||||
playbackSettings.VideoBufferSize,
|
||||
playbackSettings.VideoTrackTimeScale,
|
||||
playbackSettings.Deinterlace);
|
||||
|
||||
var ffmpegState = new FFmpegState(
|
||||
saveReports,
|
||||
hwAccel,
|
||||
VaapiDriverName(hwAccel, vaapiDriver),
|
||||
VaapiDeviceName(hwAccel, vaapiDevice),
|
||||
playbackSettings.StreamSeek,
|
||||
finish - now,
|
||||
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect,
|
||||
"ErsatzTV",
|
||||
channel.Name,
|
||||
maybeAudioStream.Map(s => Optional(s.Language)).Flatten(),
|
||||
outputFormat,
|
||||
hlsPlaylistPath,
|
||||
hlsSegmentTemplate,
|
||||
ptsOffset);
|
||||
|
||||
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
|
||||
|
||||
var pipelineBuilder = new PipelineBuilder(
|
||||
videoInputFile,
|
||||
audioInputFile,
|
||||
watermarkInputFile,
|
||||
FileSystemLayout.FFmpegReportsFolder,
|
||||
_logger);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
|
||||
|
||||
return GetProcess(ffmpegPath, videoInputFile, audioInputFile, watermarkInputFile, None, pipeline);
|
||||
}
|
||||
|
||||
private Option<WatermarkInputFile> GetWatermarkInputFile(
|
||||
Option<WatermarkOptions> watermarkOptions,
|
||||
Option<List<FadePoint>> maybeFadePoints)
|
||||
{
|
||||
foreach (WatermarkOptions options in watermarkOptions)
|
||||
{
|
||||
foreach (ChannelWatermark watermark in options.Watermark)
|
||||
{
|
||||
// skip watermark if intermittent and no fade points
|
||||
if (watermark.Mode != ChannelWatermarkMode.None &&
|
||||
(watermark.Mode != ChannelWatermarkMode.Intermittent ||
|
||||
maybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)))
|
||||
{
|
||||
foreach (string path in options.ImagePath)
|
||||
{
|
||||
var watermarkInputFile = new WatermarkInputFile(
|
||||
path,
|
||||
new List<VideoStream>
|
||||
{
|
||||
new(
|
||||
options.ImageStreamIndex.IfNone(0),
|
||||
"unknown",
|
||||
new PixelFormatUnknown(),
|
||||
new FrameSize(1, 1),
|
||||
Option<string>.None,
|
||||
!options.IsAnimated)
|
||||
},
|
||||
new WatermarkState(
|
||||
maybeFadePoints.Map(
|
||||
lst => lst.Map(
|
||||
fp =>
|
||||
{
|
||||
return fp switch
|
||||
{
|
||||
FadeInPoint fip => (WatermarkFadePoint)new WatermarkFadeIn(
|
||||
fip.Time,
|
||||
fip.EnableStart,
|
||||
fip.EnableFinish),
|
||||
FadeOutPoint fop => new WatermarkFadeOut(
|
||||
fop.Time,
|
||||
fop.EnableStart,
|
||||
fop.EnableFinish),
|
||||
_ => throw new NotSupportedException() // this will never happen
|
||||
};
|
||||
}).ToList()),
|
||||
watermark.Location,
|
||||
watermark.Size,
|
||||
watermark.WidthPercent,
|
||||
watermark.HorizontalMarginPercent,
|
||||
watermark.VerticalMarginPercent,
|
||||
watermark.Opacity));
|
||||
|
||||
return watermarkInputFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
public Task<Process> ForError(
|
||||
string ffmpegPath,
|
||||
Channel channel,
|
||||
Option<TimeSpan> duration,
|
||||
string errorMessage,
|
||||
bool hlsRealtime,
|
||||
long ptsOffset) =>
|
||||
_ffmpegProcessService.ForError(ffmpegPath, channel, duration, errorMessage, hlsRealtime, ptsOffset);
|
||||
|
||||
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||
{
|
||||
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
|
||||
|
||||
var concatInputFile = new ConcatInputFile(
|
||||
$"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}",
|
||||
resolution);
|
||||
|
||||
var pipelineBuilder = new PipelineBuilder(None, None, None, FileSystemLayout.FFmpegReportsFolder, _logger);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Concat(
|
||||
concatInputFile,
|
||||
FFmpegState.Concat(saveReports, channel.Name));
|
||||
|
||||
return GetProcess(ffmpegPath, None, None, None, concatInputFile, pipeline);
|
||||
}
|
||||
|
||||
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) =>
|
||||
_ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host);
|
||||
|
||||
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile) =>
|
||||
_ffmpegProcessService.ConvertToPng(ffmpegPath, inputFile, outputFile);
|
||||
|
||||
public Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) =>
|
||||
_ffmpegProcessService.ExtractAttachedPicAsPng(ffmpegPath, inputFile, streamIndex, outputFile);
|
||||
|
||||
public Task<Either<BaseError, string>> GenerateSongImage(
|
||||
string ffmpegPath,
|
||||
Option<string> subtitleFile,
|
||||
Channel channel,
|
||||
Option<ChannelWatermark> globalWatermark,
|
||||
MediaVersion videoVersion,
|
||||
string videoPath,
|
||||
bool boxBlur,
|
||||
Option<string> watermarkPath,
|
||||
WatermarkLocation watermarkLocation,
|
||||
int horizontalMarginPercent,
|
||||
int verticalMarginPercent,
|
||||
int watermarkWidthPercent) =>
|
||||
_ffmpegProcessService.GenerateSongImage(
|
||||
ffmpegPath,
|
||||
subtitleFile,
|
||||
channel,
|
||||
globalWatermark,
|
||||
videoVersion,
|
||||
videoPath,
|
||||
boxBlur,
|
||||
watermarkPath,
|
||||
watermarkLocation,
|
||||
horizontalMarginPercent,
|
||||
verticalMarginPercent,
|
||||
watermarkWidthPercent);
|
||||
|
||||
private Process GetProcess(
|
||||
string ffmpegPath,
|
||||
Option<VideoInputFile> videoInputFile,
|
||||
Option<AudioInputFile> audioInputFile,
|
||||
Option<WatermarkInputFile> watermarkInputFile,
|
||||
Option<ConcatInputFile> concatInputFile,
|
||||
FFmpegPipeline pipeline)
|
||||
{
|
||||
IEnumerable<string> loggedSteps = pipeline.PipelineSteps.Map(ps => ps.GetType().Name);
|
||||
IEnumerable<string> loggedVideoFilters =
|
||||
videoInputFile.Map(f => f.FilterSteps.Map(vf => vf.GetType().Name)).Flatten();
|
||||
IEnumerable<string> loggedAudioFilters =
|
||||
audioInputFile.Map(f => f.FilterSteps.Map(af => af.GetType().Name)).Flatten();
|
||||
|
||||
_logger.LogDebug(
|
||||
"FFmpeg pipeline {PipelineSteps}, {AudioFilters}, {VideoFilters}",
|
||||
loggedSteps,
|
||||
loggedAudioFilters,
|
||||
loggedVideoFilters
|
||||
);
|
||||
|
||||
IList<EnvironmentVariable> environmentVariables =
|
||||
CommandGenerator.GenerateEnvironmentVariables(pipeline.PipelineSteps);
|
||||
IList<string> arguments = CommandGenerator.GenerateArguments(
|
||||
videoInputFile,
|
||||
audioInputFile,
|
||||
watermarkInputFile,
|
||||
concatInputFile,
|
||||
pipeline.PipelineSteps);
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ffmpegPath,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = false,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
if (environmentVariables.Any())
|
||||
{
|
||||
_logger.LogDebug("FFmpeg environment variables {EnvVars}", environmentVariables);
|
||||
}
|
||||
|
||||
foreach ((string key, string value) in environmentVariables)
|
||||
{
|
||||
startInfo.EnvironmentVariables[key] = value;
|
||||
}
|
||||
|
||||
foreach (string argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
return new Process
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
}
|
||||
|
||||
private static Option<string> VaapiDriverName(HardwareAccelerationMode accelerationMode, VaapiDriver driver)
|
||||
{
|
||||
if (accelerationMode == HardwareAccelerationMode.Vaapi)
|
||||
{
|
||||
switch (driver)
|
||||
{
|
||||
case VaapiDriver.i965:
|
||||
return "i965";
|
||||
case VaapiDriver.iHD:
|
||||
return "iHD";
|
||||
case VaapiDriver.RadeonSI:
|
||||
return "radeonsi";
|
||||
}
|
||||
}
|
||||
|
||||
return Option<string>.None;
|
||||
}
|
||||
|
||||
private static Option<string> VaapiDeviceName(HardwareAccelerationMode accelerationMode, string vaapiDevice)
|
||||
{
|
||||
return accelerationMode == HardwareAccelerationMode.Vaapi ? vaapiDevice : Option<string>.None;
|
||||
}
|
||||
}
|
||||
@@ -222,7 +222,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
_arguments.Add($"{fr}");
|
||||
|
||||
_arguments.Add("-vsync");
|
||||
_arguments.Add("1");
|
||||
_arguments.Add("cfr");
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
@@ -7,9 +7,11 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
@@ -182,23 +184,18 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithMetadata(channel, maybeAudioStream)
|
||||
.WithDuration(finish - now);
|
||||
|
||||
switch (channel.StreamingMode)
|
||||
return channel.StreamingMode switch
|
||||
{
|
||||
// HLS needs to segment and generate playlist
|
||||
case StreamingMode.HttpLiveStreamingSegmenter:
|
||||
return builder.WithHls(
|
||||
channel.Number,
|
||||
videoVersion,
|
||||
ptsOffset,
|
||||
playbackSettings.VideoTrackTimeScale,
|
||||
playbackSettings.FrameRate)
|
||||
.Build();
|
||||
default:
|
||||
return builder.WithFormat("mpegts")
|
||||
.WithInitialDiscontinuity()
|
||||
.WithPipe()
|
||||
.Build();
|
||||
}
|
||||
StreamingMode.HttpLiveStreamingSegmenter => builder.WithHls(
|
||||
channel.Number,
|
||||
videoVersion,
|
||||
ptsOffset,
|
||||
playbackSettings.VideoTrackTimeScale,
|
||||
playbackSettings.FrameRate)
|
||||
.Build(),
|
||||
_ => builder.WithFormat("mpegts").WithInitialDiscontinuity().WithPipe().Build()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Process> ForError(
|
||||
@@ -334,7 +331,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
string videoPath,
|
||||
bool boxBlur,
|
||||
Option<string> watermarkPath,
|
||||
ChannelWatermarkLocation watermarkLocation,
|
||||
WatermarkLocation watermarkLocation,
|
||||
int horizontalMarginPercent,
|
||||
int verticalMarginPercent,
|
||||
int watermarkWidthPercent)
|
||||
@@ -353,7 +350,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
HorizontalMarginPercent = horizontalMarginPercent,
|
||||
VerticalMarginPercent = verticalMarginPercent,
|
||||
Location = watermarkLocation,
|
||||
Size = ChannelWatermarkSize.Scaled,
|
||||
Size = WatermarkSize.Scaled,
|
||||
WidthPercent = watermarkWidthPercent,
|
||||
Opacity = 100
|
||||
}
|
||||
@@ -425,18 +422,13 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) =>
|
||||
displaySize.Width != target.Width || displaySize.Height != target.Height;
|
||||
|
||||
private async Task<WatermarkOptions> GetWatermarkOptions(
|
||||
internal async Task<WatermarkOptions> GetWatermarkOptions(
|
||||
Channel channel,
|
||||
Option<ChannelWatermark> globalWatermark,
|
||||
MediaVersion videoVersion,
|
||||
Option<ChannelWatermark> watermarkOverride,
|
||||
Option<string> watermarkPath)
|
||||
{
|
||||
if (videoVersion is BackgroundImageMediaVersion)
|
||||
{
|
||||
return new WatermarkOptions(None, None, None, false);
|
||||
}
|
||||
|
||||
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && channel.FFmpegProfile.Transcode &&
|
||||
channel.FFmpegProfile.NormalizeVideo)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg;
|
||||
|
||||
public class FFmpegProcessServiceFactory : IFFmpegProcessServiceFactory
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public FFmpegProcessServiceFactory(IConfigElementRepository configElementRepository, IServiceProvider serviceProvider)
|
||||
{
|
||||
_configElementRepository = configElementRepository;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task<IFFmpegProcessService> GetService()
|
||||
{
|
||||
Option<bool> useExperimentalTranscoder =
|
||||
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseExperimentalTranscoder);
|
||||
|
||||
return await useExperimentalTranscoder.IfNoneAsync(false)
|
||||
? _serviceProvider.GetRequiredService<FFmpegLibraryProcessService>()
|
||||
: _serviceProvider.GetRequiredService<FFmpegProcessService>();
|
||||
}
|
||||
}
|
||||
@@ -1,110 +1,144 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
public class HlsPlaylistFilter
|
||||
public class HlsPlaylistFilter : IHlsPlaylistFilter
|
||||
{
|
||||
public static TrimPlaylistResult TrimPlaylist(
|
||||
private readonly ITempFilePool _tempFilePool;
|
||||
private readonly ILogger<HlsPlaylistFilter> _logger;
|
||||
|
||||
public HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger<HlsPlaylistFilter> logger)
|
||||
{
|
||||
_tempFilePool = tempFilePool;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public TrimPlaylistResult TrimPlaylist(
|
||||
DateTimeOffset playlistStart,
|
||||
DateTimeOffset filterBefore,
|
||||
string[] lines,
|
||||
int maxSegments = 10,
|
||||
bool endWithDiscontinuity = false)
|
||||
{
|
||||
DateTimeOffset currentTime = playlistStart;
|
||||
DateTimeOffset nextPlaylistStart = DateTimeOffset.MaxValue;
|
||||
|
||||
var discontinuitySequence = 0;
|
||||
var startSequence = 0;
|
||||
var output = new StringBuilder();
|
||||
var started = false;
|
||||
var i = 0;
|
||||
var segments = 0;
|
||||
while (!lines[i].StartsWith("#EXTINF:"))
|
||||
try
|
||||
{
|
||||
if (lines[i].StartsWith("#EXT-X-DISCONTINUITY-SEQUENCE"))
|
||||
{
|
||||
discontinuitySequence = int.Parse(lines[i].Split(':')[1]);
|
||||
}
|
||||
DateTimeOffset currentTime = playlistStart;
|
||||
DateTimeOffset nextPlaylistStart = DateTimeOffset.MaxValue;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
while (i < lines.Length)
|
||||
{
|
||||
if (segments >= maxSegments)
|
||||
var discontinuitySequence = 0;
|
||||
var startSequence = 0;
|
||||
var output = new StringBuilder();
|
||||
var started = false;
|
||||
var i = 0;
|
||||
var segments = 0;
|
||||
while (!lines[i].StartsWith("#EXTINF:"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
string line = lines[i];
|
||||
// _logger.LogInformation("Line: {Line}", line);
|
||||
if (line.StartsWith("#EXT-X-DISCONTINUITY"))
|
||||
{
|
||||
if (started)
|
||||
if (lines[i].StartsWith("#EXT-X-DISCONTINUITY-SEQUENCE"))
|
||||
{
|
||||
output.AppendLine("#EXT-X-DISCONTINUITY");
|
||||
}
|
||||
else
|
||||
{
|
||||
discontinuitySequence++;
|
||||
discontinuitySequence = int.Parse(lines[i].Split(':')[1]);
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var duration = TimeSpan.FromSeconds(
|
||||
double.Parse(
|
||||
lines[i].TrimEnd(',').Split(':')[1],
|
||||
NumberStyles.Number,
|
||||
CultureInfo.InvariantCulture));
|
||||
if (currentTime < filterBefore)
|
||||
while (i < lines.Length)
|
||||
{
|
||||
if (segments >= maxSegments)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
string line = lines[i];
|
||||
// _logger.LogInformation("Line: {Line}", line);
|
||||
if (line.StartsWith("#EXT-X-DISCONTINUITY"))
|
||||
{
|
||||
if (started)
|
||||
{
|
||||
output.AppendLine("#EXT-X-DISCONTINUITY");
|
||||
}
|
||||
else
|
||||
{
|
||||
discontinuitySequence++;
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var duration = TimeSpan.FromSeconds(
|
||||
double.Parse(
|
||||
lines[i].TrimEnd(',').Split(':')[1],
|
||||
NumberStyles.Number,
|
||||
CultureInfo.InvariantCulture));
|
||||
if (currentTime < filterBefore)
|
||||
{
|
||||
currentTime += duration;
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
nextPlaylistStart = currentTime < nextPlaylistStart ? currentTime : nextPlaylistStart;
|
||||
|
||||
if (!started)
|
||||
{
|
||||
startSequence = int.Parse(lines[i + 2].Replace("live", string.Empty).Split('.')[0]);
|
||||
|
||||
output.AppendLine("#EXTM3U");
|
||||
output.AppendLine("#EXT-X-VERSION:6");
|
||||
output.AppendLine("#EXT-X-TARGETDURATION:4");
|
||||
output.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{startSequence}");
|
||||
output.AppendLine($"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuitySequence}");
|
||||
output.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
|
||||
output.AppendLine("#EXT-X-DISCONTINUITY");
|
||||
|
||||
started = true;
|
||||
}
|
||||
|
||||
output.AppendLine(lines[i]);
|
||||
string offset = currentTime.ToString("zzz").Replace(":", string.Empty);
|
||||
output.AppendLine($"#EXT-X-PROGRAM-DATE-TIME:{currentTime:yyyy-MM-ddTHH:mm:ss.fff}{offset}");
|
||||
output.AppendLine(lines[i + 2]);
|
||||
|
||||
currentTime += duration;
|
||||
segments++;
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
nextPlaylistStart = currentTime < nextPlaylistStart ? currentTime : nextPlaylistStart;
|
||||
|
||||
if (!started)
|
||||
var playlist = output.ToString();
|
||||
if (endWithDiscontinuity && !playlist.EndsWith($"#EXT-X-DISCONTINUITY{Environment.NewLine}"))
|
||||
{
|
||||
startSequence = int.Parse(lines[i + 2].Replace("live", string.Empty).Split('.')[0]);
|
||||
|
||||
output.AppendLine("#EXTM3U");
|
||||
output.AppendLine("#EXT-X-VERSION:6");
|
||||
output.AppendLine("#EXT-X-TARGETDURATION:4");
|
||||
output.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{startSequence}");
|
||||
output.AppendLine($"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuitySequence}");
|
||||
output.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
|
||||
output.AppendLine("#EXT-X-DISCONTINUITY");
|
||||
|
||||
started = true;
|
||||
playlist += "#EXT-X-DISCONTINUITY" + Environment.NewLine;
|
||||
}
|
||||
|
||||
output.AppendLine(lines[i]);
|
||||
string offset = currentTime.ToString("zzz").Replace(":", string.Empty);
|
||||
output.AppendLine($"#EXT-X-PROGRAM-DATE-TIME:{currentTime:yyyy-MM-ddTHH:mm:ss.fff}{offset}");
|
||||
output.AppendLine(lines[i + 2]);
|
||||
|
||||
currentTime += duration;
|
||||
segments++;
|
||||
i += 3;
|
||||
return new TrimPlaylistResult(nextPlaylistStart, startSequence, playlist, segments);
|
||||
}
|
||||
|
||||
var playlist = output.ToString();
|
||||
if (endWithDiscontinuity && !playlist.EndsWith($"#EXT-X-DISCONTINUITY{Environment.NewLine}"))
|
||||
catch (Exception ex)
|
||||
{
|
||||
playlist += "#EXT-X-DISCONTINUITY" + Environment.NewLine;
|
||||
}
|
||||
try
|
||||
{
|
||||
string file = _tempFilePool.GetNextTempFile(TempFileCategory.BadPlaylist);
|
||||
File.WriteAllLines(file, lines);
|
||||
|
||||
return new TrimPlaylistResult(nextPlaylistStart, startSequence, playlist, segments);
|
||||
_logger.LogError(ex, "Error filtering playlist. Bad playlist saved to {BadPlaylistFile}", file);
|
||||
|
||||
// TODO: better error result?
|
||||
return new TrimPlaylistResult(playlistStart, 0, string.Empty, 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static TrimPlaylistResult TrimPlaylistWithDiscontinuity(
|
||||
public TrimPlaylistResult TrimPlaylistWithDiscontinuity(
|
||||
DateTimeOffset playlistStart,
|
||||
DateTimeOffset filterBefore,
|
||||
string[] lines)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg;
|
||||
|
||||
public interface IFFmpegProcessServiceFactory
|
||||
{
|
||||
Task<IFFmpegProcessService> GetService();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg;
|
||||
|
||||
public interface IHlsPlaylistFilter
|
||||
{
|
||||
TrimPlaylistResult TrimPlaylist(
|
||||
DateTimeOffset playlistStart,
|
||||
DateTimeOffset filterBefore,
|
||||
string[] lines,
|
||||
int maxSegments = 10,
|
||||
bool endWithDiscontinuity = false);
|
||||
|
||||
TrimPlaylistResult TrimPlaylistWithDiscontinuity(
|
||||
DateTimeOffset playlistStart,
|
||||
DateTimeOffset filterBefore,
|
||||
string[] lines);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.FFmpeg.Format;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -19,16 +21,16 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
private readonly ITempFilePool _tempFilePool;
|
||||
private readonly IImageCache _imageCache;
|
||||
private readonly IFFmpegProcessService _ffmpegProcessService;
|
||||
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
|
||||
|
||||
public SongVideoGenerator(
|
||||
ITempFilePool tempFilePool,
|
||||
IImageCache imageCache,
|
||||
IFFmpegProcessService ffmpegProcessService)
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
|
||||
{
|
||||
_tempFilePool = tempFilePool;
|
||||
_imageCache = imageCache;
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
|
||||
}
|
||||
|
||||
public async Task<Tuple<string, MediaVersion>> GenerateSongVideo(
|
||||
@@ -48,7 +50,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
SampleAspectRatio = "1:1",
|
||||
Streams = new List<MediaStream>
|
||||
{
|
||||
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }
|
||||
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0, PixelFormat = "yuv420p" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,9 +73,9 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
const int HORIZONTAL_MARGIN_PERCENT = 3;
|
||||
const int VERTICAL_MARGIN_PERCENT = 5;
|
||||
const int WATERMARK_WIDTH_PERCENT = 25;
|
||||
ChannelWatermarkLocation watermarkLocation = NextRandom(2) == 0
|
||||
? ChannelWatermarkLocation.BottomLeft
|
||||
: ChannelWatermarkLocation.BottomRight;
|
||||
WatermarkLocation watermarkLocation = NextRandom(2) == 0
|
||||
? WatermarkLocation.BottomLeft
|
||||
: WatermarkLocation.BottomRight;
|
||||
|
||||
foreach (SongMetadata metadata in song.SongMetadata)
|
||||
{
|
||||
@@ -120,10 +122,10 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
switch (watermarkLocation)
|
||||
{
|
||||
case ChannelWatermarkLocation.BottomLeft:
|
||||
case WatermarkLocation.BottomLeft:
|
||||
leftMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT;
|
||||
break;
|
||||
case ChannelWatermarkLocation.BottomRight:
|
||||
case WatermarkLocation.BottomRight:
|
||||
leftMarginPercent = rightMarginPercent = HORIZONTAL_MARGIN_PERCENT;
|
||||
rightMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT;
|
||||
break;
|
||||
@@ -208,7 +210,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
new() { Path = videoPath }
|
||||
};
|
||||
|
||||
Either<BaseError, string> maybeSongImage = await _ffmpegProcessService.GenerateSongImage(
|
||||
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
|
||||
Either<BaseError, string> maybeSongImage = await ffmpegProcessService.GenerateSongImage(
|
||||
ffmpegPath,
|
||||
subtitleFile,
|
||||
channel,
|
||||
@@ -234,7 +237,13 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
SampleAspectRatio = "1:1",
|
||||
Streams = new List<MediaStream>
|
||||
{
|
||||
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 },
|
||||
new()
|
||||
{
|
||||
MediaStreamKind = MediaStreamKind.Video,
|
||||
Index = 0,
|
||||
Codec = VideoFormat.GeneratedImage,
|
||||
PixelFormat = new PixelFormatUnknown().Name // the resulting pixel format is unknown
|
||||
},
|
||||
},
|
||||
MediaFiles = new List<MediaFile>
|
||||
{
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
Subtitle = 0,
|
||||
SongBackground = 1,
|
||||
CoverArt = 2,
|
||||
CachedArtwork = 3
|
||||
CachedArtwork = 3,
|
||||
|
||||
BadPlaylist = 99
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.FFmpeg
|
||||
@@ -56,7 +57,7 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
|
||||
string videoPath,
|
||||
bool boxBlur,
|
||||
Option<string> watermarkPath,
|
||||
ChannelWatermarkLocation watermarkLocation,
|
||||
WatermarkLocation watermarkLocation,
|
||||
int horizontalMarginPercent,
|
||||
int verticalMarginPercent,
|
||||
int watermarkWidthPercent);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
public interface IScheduleItemsEnumerator
|
||||
{
|
||||
CollectionEnumeratorState State { get; }
|
||||
ProgramScheduleItem Current { get; }
|
||||
void MoveNext();
|
||||
ProgramScheduleItem Peek(int offset);
|
||||
}
|
||||
@@ -53,6 +53,14 @@ namespace ErsatzTV.Core.Iptv
|
||||
xml.WriteString(channel.Name);
|
||||
xml.WriteEndElement(); // display-name
|
||||
|
||||
foreach (string category in GetCategories(channel.Categories))
|
||||
{
|
||||
xml.WriteStartElement("category");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteString(category);
|
||||
xml.WriteEndElement(); // category
|
||||
}
|
||||
|
||||
xml.WriteStartElement("icon");
|
||||
string logo = Optional(channel.Artwork).Flatten()
|
||||
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||
@@ -491,6 +499,13 @@ namespace ErsatzTV.Core.Iptv
|
||||
}).Flatten();
|
||||
}
|
||||
|
||||
private static List<string> GetCategories(string categories) =>
|
||||
(categories ?? string.Empty).Split(',')
|
||||
.Map(s => s.Trim())
|
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
private record ContentRating(Option<string> System, string Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace ErsatzTV.Core.Iptv
|
||||
string acodec = channel.FFmpegProfile.AudioCodec;
|
||||
|
||||
sb.AppendLine(
|
||||
$"#EXTINF:0 tvg-id=\"{channel.Number}.etv\" channel-id=\"{shortUniqueId}\" channel-number=\"{channel.Number}\" CUID=\"{shortUniqueId}\" tvg-chno=\"{channel.Number}\" tvg-name=\"{channel.Name}\" tvg-logo=\"{logo}\" group-title=\"ErsatzTV\" tvc-stream-vcodec=\"{vcodec}\" tvc-stream-acodec=\"{acodec}\", {channel.Name}");
|
||||
$"#EXTINF:0 tvg-id=\"{channel.Number}.etv\" channel-id=\"{shortUniqueId}\" channel-number=\"{channel.Number}\" CUID=\"{shortUniqueId}\" tvg-chno=\"{channel.Number}\" tvg-name=\"{channel.Name}\" tvg-logo=\"{logo}\" group-title=\"{channel.Group}\" tvc-stream-vcodec=\"{vcodec}\" tvc-stream-acodec=\"{acodec}\", {channel.Name}");
|
||||
sb.AppendLine($"{_scheme}://{_host}/iptv/channel/{channel.Number}.{format}");
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
.ToList();
|
||||
|
||||
private readonly IImageCache _imageCache;
|
||||
private readonly IFFmpegProcessService _ffmpegProcessService;
|
||||
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
|
||||
private readonly ITempFilePool _tempFilePool;
|
||||
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
@@ -65,7 +65,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
IMetadataRepository metadataRepository,
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IImageCache imageCache,
|
||||
IFFmpegProcessService ffmpegProcessService,
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
|
||||
ITempFilePool tempFilePool,
|
||||
ILogger logger)
|
||||
{
|
||||
@@ -74,7 +74,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
_metadataRepository = metadataRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_imageCache = imageCache;
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
|
||||
_tempFilePool = tempFilePool;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -156,12 +156,14 @@ namespace ErsatzTV.Core.Metadata
|
||||
// if ffmpeg path is passed, we need pre-processing
|
||||
foreach (string path in ffmpegPath)
|
||||
{
|
||||
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
|
||||
|
||||
artworkFile = await attachedPicIndex.Match(
|
||||
async picIndex =>
|
||||
{
|
||||
// extract attached pic (and convert to png)
|
||||
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt);
|
||||
using Process process = _ffmpegProcessService.ExtractAttachedPicAsPng(
|
||||
using Process process = ffmpegProcessService.ExtractAttachedPicAsPng(
|
||||
path,
|
||||
artworkFile,
|
||||
picIndex,
|
||||
@@ -175,7 +177,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
// no attached pic index means convert to png
|
||||
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt);
|
||||
using Process process = _ffmpegProcessService.ConvertToPng(path, artworkFile, tempName);
|
||||
using Process process = ffmpegProcessService.ConvertToPng(path, artworkFile, tempName);
|
||||
process.Start();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
@@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
ILibraryRepository libraryRepository,
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IMediator mediator,
|
||||
IFFmpegProcessService ffmpegProcessService,
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
|
||||
ITempFilePool tempFilePool,
|
||||
ILogger<MovieFolderScanner> logger)
|
||||
: base(
|
||||
@@ -50,7 +51,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
metadataRepository,
|
||||
mediaItemRepository,
|
||||
imageCache,
|
||||
ffmpegProcessService,
|
||||
ffmpegProcessServiceFactory,
|
||||
tempFilePool,
|
||||
logger)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
@@ -42,7 +43,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
ILibraryRepository libraryRepository,
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IMediator mediator,
|
||||
IFFmpegProcessService ffmpegProcessService,
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
|
||||
ITempFilePool tempFilePool,
|
||||
ILogger<MusicVideoFolderScanner> logger) : base(
|
||||
localFileSystem,
|
||||
@@ -50,7 +51,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
metadataRepository,
|
||||
mediaItemRepository,
|
||||
imageCache,
|
||||
ffmpegProcessService,
|
||||
ffmpegProcessServiceFactory,
|
||||
tempFilePool,
|
||||
logger)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
@@ -40,7 +41,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
IOtherVideoRepository otherVideoRepository,
|
||||
ILibraryRepository libraryRepository,
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegProcessService ffmpegProcessService,
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
|
||||
ITempFilePool tempFilePool,
|
||||
ILogger<OtherVideoFolderScanner> logger) : base(
|
||||
localFileSystem,
|
||||
@@ -48,7 +49,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
metadataRepository,
|
||||
mediaItemRepository,
|
||||
imageCache,
|
||||
ffmpegProcessService,
|
||||
ffmpegProcessServiceFactory,
|
||||
tempFilePool,
|
||||
logger)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
@@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
ISongRepository songRepository,
|
||||
ILibraryRepository libraryRepository,
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IFFmpegProcessService ffmpegProcessService,
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
|
||||
ITempFilePool tempFilePool,
|
||||
ILogger<SongFolderScanner> logger) : base(
|
||||
localFileSystem,
|
||||
@@ -49,7 +50,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
metadataRepository,
|
||||
mediaItemRepository,
|
||||
imageCache,
|
||||
ffmpegProcessService,
|
||||
ffmpegProcessServiceFactory,
|
||||
tempFilePool,
|
||||
logger)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
@@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
ILibraryRepository libraryRepository,
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IMediator mediator,
|
||||
IFFmpegProcessService ffmpegProcessService,
|
||||
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
|
||||
ITempFilePool tempFilePool,
|
||||
ILogger<TelevisionFolderScanner> logger) : base(
|
||||
localFileSystem,
|
||||
@@ -49,7 +50,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
metadataRepository,
|
||||
mediaItemRepository,
|
||||
imageCache,
|
||||
ffmpegProcessService,
|
||||
ffmpegProcessServiceFactory,
|
||||
tempFilePool,
|
||||
logger)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
|
||||
public class OrderedScheduleItemsEnumerator : IScheduleItemsEnumerator
|
||||
{
|
||||
private readonly IList<ProgramScheduleItem> _sortedScheduleItems;
|
||||
|
||||
public OrderedScheduleItemsEnumerator(
|
||||
IEnumerable<ProgramScheduleItem> scheduleItems,
|
||||
CollectionEnumeratorState state)
|
||||
{
|
||||
_sortedScheduleItems = scheduleItems.OrderBy(i => i.Index).ToList();
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
|
||||
if (state.Index >= _sortedScheduleItems.Count)
|
||||
{
|
||||
state.Index = 0;
|
||||
state.Seed = 0;
|
||||
}
|
||||
|
||||
while (State.Index < state.Index)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
public CollectionEnumeratorState State { get; }
|
||||
|
||||
public ProgramScheduleItem Current => _sortedScheduleItems[State.Index];
|
||||
|
||||
public void MoveNext() => State.Index = (State.Index + 1) % _sortedScheduleItems.Count;
|
||||
|
||||
public ProgramScheduleItem Peek(int offset) =>
|
||||
_sortedScheduleItems[(State.Index + offset) % _sortedScheduleItems.Count];
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt;
|
||||
@@ -105,6 +104,12 @@ namespace ErsatzTV.Core.Scheduling
|
||||
}
|
||||
|
||||
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
|
||||
CollectionEnumeratorState scheduleItemsEnumeratorState =
|
||||
playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState
|
||||
{ Seed = Random.Next(), Index = 0 };
|
||||
IScheduleItemsEnumerator scheduleItemsEnumerator = playout.ProgramSchedule.ShuffleScheduleItems
|
||||
? new ShuffledScheduleItemsEnumerator(playout.ProgramSchedule.Items, scheduleItemsEnumeratorState)
|
||||
: new OrderedScheduleItemsEnumerator(playout.ProgramSchedule.Items, scheduleItemsEnumeratorState);
|
||||
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
|
||||
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
|
||||
{
|
||||
@@ -119,7 +124,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
}
|
||||
|
||||
// find start anchor
|
||||
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, sortedScheduleItems);
|
||||
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, scheduleItemsEnumerator);
|
||||
|
||||
// start at the previously-decided time
|
||||
DateTimeOffset currentTime = startAnchor.NextStartOffset.ToLocalTime();
|
||||
@@ -142,7 +147,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
// start with the previously-decided schedule item
|
||||
// start with the previous multiple/duration states
|
||||
var playoutBuilderState = new PlayoutBuilderState(
|
||||
sortedScheduleItems.IndexOf(startAnchor.NextScheduleItem),
|
||||
scheduleItemsEnumerator,
|
||||
Optional(startAnchor.MultipleRemaining),
|
||||
startAnchor.DurationFinishOffset,
|
||||
startAnchor.InFlood,
|
||||
@@ -153,17 +158,15 @@ namespace ErsatzTV.Core.Scheduling
|
||||
var schedulerOne = new PlayoutModeSchedulerOne(_logger);
|
||||
var schedulerMultiple = new PlayoutModeSchedulerMultiple(collectionMediaItems, _logger);
|
||||
var schedulerDuration = new PlayoutModeSchedulerDuration(_logger);
|
||||
var schedulerFlood = new PlayoutModeSchedulerFlood(sortedScheduleItems, _logger);
|
||||
var schedulerFlood = new PlayoutModeSchedulerFlood(_logger);
|
||||
|
||||
// loop until we're done filling the desired amount of time
|
||||
while (playoutBuilderState.CurrentTime < playoutFinish)
|
||||
{
|
||||
// get the schedule item out of the sorted list
|
||||
ProgramScheduleItem scheduleItem =
|
||||
sortedScheduleItems[playoutBuilderState.ScheduleItemIndex % sortedScheduleItems.Count];
|
||||
ProgramScheduleItem scheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;
|
||||
|
||||
ProgramScheduleItem nextScheduleItem =
|
||||
sortedScheduleItems[(playoutBuilderState.ScheduleItemIndex + 1) % sortedScheduleItems.Count];
|
||||
ProgramScheduleItem nextScheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Peek(1);
|
||||
|
||||
Tuple<PlayoutBuilderState, List<PlayoutItem>> result = scheduleItem switch
|
||||
{
|
||||
@@ -205,8 +208,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
}
|
||||
|
||||
// once more to get playout anchor
|
||||
ProgramScheduleItem anchorScheduleItem =
|
||||
sortedScheduleItems[playoutBuilderState.ScheduleItemIndex % sortedScheduleItems.Count];
|
||||
ProgramScheduleItem anchorScheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;
|
||||
|
||||
// build program schedule anchors
|
||||
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
|
||||
@@ -226,8 +228,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
|
||||
playout.Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextScheduleItem = anchorScheduleItem,
|
||||
NextScheduleItemId = anchorScheduleItem.Id,
|
||||
ScheduleItemsEnumeratorState = playoutBuilderState.ScheduleItemsEnumerator.State,
|
||||
NextStart = PlayoutModeSchedulerBase<ProgramScheduleItem>.GetStartTimeAfter(playoutBuilderState, anchorScheduleItem)
|
||||
.UtcDateTime,
|
||||
MultipleRemaining = playoutBuilderState.MultipleRemaining.IsSome
|
||||
@@ -320,18 +321,17 @@ namespace ErsatzTV.Core.Scheduling
|
||||
private static PlayoutAnchor FindStartAnchor(
|
||||
Playout playout,
|
||||
DateTimeOffset start,
|
||||
IReadOnlyCollection<ProgramScheduleItem> sortedScheduleItems) =>
|
||||
IScheduleItemsEnumerator enumerator) =>
|
||||
Optional(playout.Anchor).IfNone(
|
||||
() =>
|
||||
{
|
||||
ProgramScheduleItem schedule = sortedScheduleItems.Head();
|
||||
ProgramScheduleItem schedule = enumerator.Current;
|
||||
switch (schedule.StartType)
|
||||
{
|
||||
case StartType.Fixed:
|
||||
return new PlayoutAnchor
|
||||
{
|
||||
NextScheduleItem = schedule,
|
||||
NextScheduleItemId = schedule.Id,
|
||||
ScheduleItemsEnumeratorState = enumerator.State,
|
||||
NextStart = (start - start.TimeOfDay).UtcDateTime +
|
||||
schedule.StartTime.GetValueOrDefault()
|
||||
};
|
||||
@@ -339,8 +339,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
default:
|
||||
return new PlayoutAnchor
|
||||
{
|
||||
NextScheduleItem = schedule,
|
||||
NextScheduleItemId = schedule.Id,
|
||||
ScheduleItemsEnumeratorState = enumerator.State,
|
||||
NextStart = (start - start.TimeOfDay).UtcDateTime
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public record PlayoutBuilderState(
|
||||
int ScheduleItemIndex,
|
||||
IScheduleItemsEnumerator ScheduleItemsEnumerator,
|
||||
Option<int> MultipleRemaining,
|
||||
Option<DateTimeOffset> DurationFinish,
|
||||
bool InFlood,
|
||||
|
||||
@@ -122,9 +122,10 @@ namespace ErsatzTV.Core.Scheduling
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
DurationFinish = None,
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1
|
||||
DurationFinish = None
|
||||
};
|
||||
|
||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +134,10 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
nextState = nextState with
|
||||
{
|
||||
DurationFinish = None,
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1
|
||||
DurationFinish = None
|
||||
};
|
||||
|
||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||
}
|
||||
|
||||
if (playoutItems.Select(pi => pi.GuideGroup).Distinct().Count() != 1)
|
||||
|
||||
@@ -12,12 +12,9 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramScheduleItemFlood>
|
||||
{
|
||||
private readonly List<ProgramScheduleItem> _sortedScheduleItems;
|
||||
|
||||
public PlayoutModeSchedulerFlood(List<ProgramScheduleItem> sortedScheduleItems, ILogger logger)
|
||||
public PlayoutModeSchedulerFlood(ILogger logger)
|
||||
: base(logger)
|
||||
{
|
||||
_sortedScheduleItems = sortedScheduleItems;
|
||||
}
|
||||
|
||||
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
|
||||
@@ -35,8 +32,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
IMediaCollectionEnumerator contentEnumerator =
|
||||
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
|
||||
|
||||
ProgramScheduleItem peekScheduleItem =
|
||||
_sortedScheduleItems[(nextState.ScheduleItemIndex + 1) % _sortedScheduleItems.Count];
|
||||
ProgramScheduleItem peekScheduleItem = nextScheduleItem;
|
||||
|
||||
while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime)
|
||||
{
|
||||
@@ -81,7 +77,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
playoutItems.AddRange(
|
||||
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
|
||||
DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset);
|
||||
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
|
||||
@@ -105,19 +101,19 @@ namespace ErsatzTV.Core.Scheduling
|
||||
}
|
||||
}
|
||||
|
||||
// _logger.LogDebug(
|
||||
// "Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
// "Flood");
|
||||
_logger.LogDebug(
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"Flood");
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
|
||||
InFlood = nextState.CurrentTime >= hardStop,
|
||||
NextGuideGroup = nextState.DecrementGuideGroup
|
||||
};
|
||||
|
||||
ProgramScheduleItem peekItem =
|
||||
_sortedScheduleItems[nextState.ScheduleItemIndex % _sortedScheduleItems.Count];
|
||||
|
||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||
|
||||
ProgramScheduleItem peekItem = nextScheduleItem;
|
||||
DateTimeOffset peekItemStart = GetStartTimeAfter(nextState, peekItem);
|
||||
|
||||
if (scheduleItem.TailFiller != null)
|
||||
|
||||
@@ -97,10 +97,11 @@ namespace ErsatzTV.Core.Scheduling
|
||||
|
||||
nextState = nextState with
|
||||
{
|
||||
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
|
||||
MultipleRemaining = None,
|
||||
NextGuideGroup = nextState.DecrementGuideGroup
|
||||
};
|
||||
|
||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||
}
|
||||
|
||||
DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem);
|
||||
|
||||
@@ -67,10 +67,10 @@ namespace ErsatzTV.Core.Scheduling
|
||||
|
||||
PlayoutBuilderState nextState = playoutBuilderState with
|
||||
{
|
||||
CurrentTime = itemEndTimeWithFiller,
|
||||
ScheduleItemIndex = playoutBuilderState.ScheduleItemIndex + 1
|
||||
CurrentTime = itemEndTimeWithFiller
|
||||
};
|
||||
|
||||
nextState.ScheduleItemsEnumerator.MoveNext();
|
||||
contentEnumerator.MoveNext();
|
||||
|
||||
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
|
||||
public class ShuffledScheduleItemsEnumerator : IScheduleItemsEnumerator
|
||||
{
|
||||
private readonly int _scheduleItemsCount;
|
||||
private readonly IList<ProgramScheduleItem> _scheduleItems;
|
||||
private CloneableRandom _random;
|
||||
private IList<ProgramScheduleItem> _shuffled;
|
||||
|
||||
public ShuffledScheduleItemsEnumerator(
|
||||
IList<ProgramScheduleItem> scheduleItems,
|
||||
CollectionEnumeratorState state)
|
||||
{
|
||||
_scheduleItemsCount = scheduleItems.Count;
|
||||
_scheduleItems = scheduleItems;
|
||||
|
||||
if (state.Index >= _scheduleItems.Count)
|
||||
{
|
||||
state.Index = 0;
|
||||
state.Seed = new Random(state.Seed).Next();
|
||||
}
|
||||
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
_shuffled = Shuffle(_scheduleItems, _random);
|
||||
|
||||
State = new CollectionEnumeratorState { Seed = state.Seed };
|
||||
while (State.Index < state.Index)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
public CollectionEnumeratorState State { get; }
|
||||
|
||||
public ProgramScheduleItem Current => _shuffled[State.Index % _scheduleItemsCount];
|
||||
|
||||
public void MoveNext()
|
||||
{
|
||||
if ((State.Index + 1) % _scheduleItemsCount == 0)
|
||||
{
|
||||
ProgramScheduleItem tail = Current;
|
||||
|
||||
State.Index = 0;
|
||||
do
|
||||
{
|
||||
State.Seed = _random.Next();
|
||||
_random = new CloneableRandom(State.Seed);
|
||||
_shuffled = Shuffle(_scheduleItems, _random);
|
||||
} while (_scheduleItems.Count > 1 && Current == tail);
|
||||
}
|
||||
else
|
||||
{
|
||||
State.Index++;
|
||||
}
|
||||
|
||||
State.Index %= _scheduleItemsCount;
|
||||
}
|
||||
|
||||
public ProgramScheduleItem Peek(int offset)
|
||||
{
|
||||
if (offset == 0)
|
||||
{
|
||||
return Current;
|
||||
}
|
||||
|
||||
if ((State.Index + offset) % _scheduleItemsCount == 0)
|
||||
{
|
||||
IList<ProgramScheduleItem> shuffled;
|
||||
ProgramScheduleItem tail = Current;
|
||||
|
||||
// clone the random
|
||||
CloneableRandom randomCopy = _random.Clone();
|
||||
|
||||
do
|
||||
{
|
||||
int newSeed = randomCopy.Next();
|
||||
randomCopy = new CloneableRandom(newSeed);
|
||||
shuffled = Shuffle(_scheduleItems, randomCopy);
|
||||
} while (_scheduleItems.Count > 1 && shuffled[0] == tail);
|
||||
|
||||
return shuffled[0];
|
||||
}
|
||||
|
||||
return _shuffled[(State.Index + offset) % _scheduleItemsCount];
|
||||
}
|
||||
|
||||
private IList<ProgramScheduleItem> Shuffle(IEnumerable<ProgramScheduleItem> list, CloneableRandom random)
|
||||
{
|
||||
ProgramScheduleItem[] copy = list.ToArray();
|
||||
|
||||
int n = copy.Length;
|
||||
while (n > 1)
|
||||
{
|
||||
n--;
|
||||
int k = random.Next(n + 1);
|
||||
(copy[k], copy[n]) = (copy[n], copy[k]);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ErsatzTV.FFmpeg\ErsatzTV.FFmpeg.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
|
||||
<HintPath>..\..\..\..\..\..\usr\share\dotnet\shared\Microsoft.AspNetCore.App\6.0.2\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.FFmpeg.Encoder;
|
||||
using ErsatzTV.FFmpeg.Format;
|
||||
using ErsatzTV.FFmpeg.OutputFormat;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using NUnit.Framework;
|
||||
using FluentAssertions;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.FFmpeg.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class PipelineGeneratorTests
|
||||
{
|
||||
private readonly ILogger _logger = new Mock<ILogger>().Object;
|
||||
|
||||
[Test]
|
||||
public void Incorrect_Video_Codec_Should_Use_Encoder()
|
||||
{
|
||||
var videoInputFile = new VideoInputFile(
|
||||
"/tmp/whatever.mkv",
|
||||
new List<VideoStream>
|
||||
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24", false) });
|
||||
|
||||
var audioInputFile = new AudioInputFile(
|
||||
"/tmp/whatever.mkv",
|
||||
new List<AudioStream> { new(1, AudioFormat.Aac, 2) },
|
||||
new AudioState(
|
||||
AudioFormat.Aac,
|
||||
2,
|
||||
320,
|
||||
640,
|
||||
48,
|
||||
Option<TimeSpan>.None,
|
||||
false));
|
||||
|
||||
var desiredState = new FrameState(
|
||||
true,
|
||||
false,
|
||||
VideoFormat.Hevc,
|
||||
new PixelFormatYuv420P(),
|
||||
new FrameSize(1920, 1080),
|
||||
new FrameSize(1920, 1080),
|
||||
Option<int>.None,
|
||||
2000,
|
||||
4000,
|
||||
90_000,
|
||||
false);
|
||||
|
||||
var ffmpegState = new FFmpegState(
|
||||
false,
|
||||
HardwareAccelerationMode.None,
|
||||
Option<string>.None,
|
||||
Option<string>.None,
|
||||
Option<TimeSpan>.None,
|
||||
Option<TimeSpan>.None,
|
||||
false,
|
||||
Option<string>.None,
|
||||
Option<string>.None,
|
||||
Option<string>.None,
|
||||
OutputFormatKind.MpegTs,
|
||||
Option<string>.None,
|
||||
Option<string>.None,
|
||||
0);
|
||||
|
||||
var builder = new PipelineBuilder(videoInputFile, audioInputFile, None, "", _logger);
|
||||
FFmpegPipeline result = builder.Build(ffmpegState, desiredState);
|
||||
|
||||
result.PipelineSteps.Should().HaveCountGreaterThan(0);
|
||||
result.PipelineSteps.Should().Contain(ps => ps is EncoderLibx265);
|
||||
|
||||
PrintCommand(videoInputFile, audioInputFile, None, None, result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Concat_Test()
|
||||
{
|
||||
var resolution = new FrameSize(1920, 1080);
|
||||
var concatInputFile = new ConcatInputFile("http://localhost:8080/ffmpeg/concat/1", resolution);
|
||||
|
||||
var builder = new PipelineBuilder(None, None, None, "", _logger);
|
||||
FFmpegPipeline result = builder.Concat(concatInputFile, FFmpegState.Concat(false, "Some Channel"));
|
||||
|
||||
result.PipelineSteps.Should().HaveCountGreaterThan(0);
|
||||
|
||||
string command = PrintCommand(None, None, None, concatInputFile, result);
|
||||
|
||||
command.Should().Be(
|
||||
"-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -f concat -safe 0 -protocol_whitelist file,http,tcp,https,tcp,tls -probesize 32 -re -stream_loop -1 -i http://localhost:8080/ffmpeg/concat/1 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c copy -map_metadata -1 -metadata service_provider=\"ErsatzTV\" -metadata service_name=\"Some Channel\" -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||
}
|
||||
|
||||
private static string PrintCommand(
|
||||
Option<VideoInputFile> videoInputFile,
|
||||
Option<AudioInputFile> audioInputFile,
|
||||
Option<WatermarkInputFile> watermarkInputFile,
|
||||
Option<ConcatInputFile> concatInputFile,
|
||||
FFmpegPipeline pipeline)
|
||||
{
|
||||
IList<string> arguments = CommandGenerator.GenerateArguments(
|
||||
videoInputFile,
|
||||
audioInputFile,
|
||||
watermarkInputFile,
|
||||
concatInputFile,
|
||||
pipeline.PipelineSteps);
|
||||
|
||||
var command = string.Join(" ", arguments);
|
||||
|
||||
Console.WriteLine($"Generated command: ffmpeg {string.Join(" ", arguments)}");
|
||||
|
||||
return command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using ErsatzTV.FFmpeg.Environment;
|
||||
using ErsatzTV.FFmpeg.Option;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.FFmpeg;
|
||||
|
||||
public static class CommandGenerator
|
||||
{
|
||||
public static IList<EnvironmentVariable> GenerateEnvironmentVariables(IEnumerable<IPipelineStep> pipelineSteps)
|
||||
{
|
||||
return pipelineSteps.SelectMany(ps => ps.EnvironmentVariables).ToList();
|
||||
}
|
||||
|
||||
public static IList<string> GenerateArguments(
|
||||
Option<VideoInputFile> maybeVideoInputFile,
|
||||
Option<AudioInputFile> maybeAudioInputFile,
|
||||
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
||||
Option<ConcatInputFile> maybeConcatInputFile,
|
||||
IList<IPipelineStep> pipelineSteps)
|
||||
{
|
||||
var arguments = new List<string>();
|
||||
|
||||
foreach (IPipelineStep step in pipelineSteps)
|
||||
{
|
||||
arguments.AddRange(step.GlobalOptions);
|
||||
}
|
||||
|
||||
var includedPaths = new System.Collections.Generic.HashSet<string>();
|
||||
foreach (VideoInputFile videoInputFile in maybeVideoInputFile)
|
||||
{
|
||||
includedPaths.Add(videoInputFile.Path);
|
||||
|
||||
foreach (IInputOption step in videoInputFile.InputOptions)
|
||||
{
|
||||
arguments.AddRange(step.InputOptions(videoInputFile));
|
||||
}
|
||||
|
||||
arguments.AddRange(new[] { "-i", videoInputFile.Path });
|
||||
}
|
||||
|
||||
foreach (AudioInputFile audioInputFile in maybeAudioInputFile)
|
||||
{
|
||||
if (!includedPaths.Contains(audioInputFile.Path))
|
||||
{
|
||||
includedPaths.Add(audioInputFile.Path);
|
||||
|
||||
foreach (IInputOption step in audioInputFile.InputOptions)
|
||||
{
|
||||
arguments.AddRange(step.InputOptions(audioInputFile));
|
||||
}
|
||||
|
||||
arguments.AddRange(new[] { "-i", audioInputFile.Path });
|
||||
}
|
||||
}
|
||||
|
||||
foreach (WatermarkInputFile watermarkInputFile in maybeWatermarkInputFile)
|
||||
{
|
||||
if (!includedPaths.Contains(watermarkInputFile.Path))
|
||||
{
|
||||
includedPaths.Add(watermarkInputFile.Path);
|
||||
|
||||
foreach (IInputOption step in watermarkInputFile.InputOptions)
|
||||
{
|
||||
arguments.AddRange(step.InputOptions(watermarkInputFile));
|
||||
}
|
||||
|
||||
arguments.AddRange(new[] { "-i", watermarkInputFile.Path });
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ConcatInputFile concatInputFile in maybeConcatInputFile)
|
||||
{
|
||||
foreach (IInputOption step in concatInputFile.InputOptions)
|
||||
{
|
||||
arguments.AddRange(step.InputOptions(concatInputFile));
|
||||
}
|
||||
|
||||
arguments.AddRange(new[] { "-i", concatInputFile.Path });
|
||||
}
|
||||
|
||||
foreach (IPipelineStep step in pipelineSteps)
|
||||
{
|
||||
arguments.AddRange(step.FilterOptions);
|
||||
}
|
||||
|
||||
foreach (IPipelineStep step in pipelineSteps)
|
||||
{
|
||||
arguments.AddRange(step.OutputOptions);
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using ErsatzTV.FFmpeg.Decoder.Cuvid;
|
||||
using ErsatzTV.FFmpeg.Decoder.Qsv;
|
||||
using ErsatzTV.FFmpeg.Format;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public static class AvailableDecoders
|
||||
{
|
||||
public static Option<IDecoder> ForVideoFormat(FFmpegState ffmpegState, FrameState currentState, ILogger logger)
|
||||
{
|
||||
return (ffmpegState.HardwareAccelerationMode, currentState.VideoFormat,
|
||||
currentState.PixelFormat.Match(pf => pf.Name, () => string.Empty)) switch
|
||||
{
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Hevc, _) => new DecoderHevcCuvid(),
|
||||
|
||||
// nvenc doesn't support hardware decoding of 10-bit content
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.H264, PixelFormat.YUV420P10LE or PixelFormat.YUV444P10LE)
|
||||
=> new DecoderH264(),
|
||||
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.H264, _) => new DecoderH264Cuvid(),
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg2Video, _) => new DecoderMpeg2Cuvid(),
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Vc1, _) => new DecoderVc1Cuvid(),
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Vp9, _) => new DecoderVp9Cuvid(),
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg4, _) => new DecoderMpeg4Cuvid(),
|
||||
|
||||
// hevc_qsv decoder sometimes causes green lines with 10-bit content
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Hevc, PixelFormat.YUV420P10LE) => new DecoderHevc(),
|
||||
|
||||
// h264_qsv does not support decoding 10-bit content
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.H264, PixelFormat.YUV420P10LE or PixelFormat.YUV444P10LE) =>
|
||||
new DecoderH264(),
|
||||
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Hevc, _) => new DecoderHevcQsv(),
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.H264, _) => new DecoderH264Qsv(),
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Mpeg2Video, _) => new DecoderMpeg2Qsv(),
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Vc1, _) => new DecoderVc1Qsv(),
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Vp9, _) => new DecoderVp9Qsv(),
|
||||
|
||||
// vaapi should use implicit decoders
|
||||
(HardwareAccelerationMode.Vaapi, _, _) => new DecoderVaapi(),
|
||||
|
||||
// videotoolbox should use implicit decoders
|
||||
(HardwareAccelerationMode.VideoToolbox, _, _) => new DecoderVideoToolbox(),
|
||||
|
||||
(_, VideoFormat.Hevc, _) => new DecoderHevc(),
|
||||
(_, VideoFormat.H264, _) => new DecoderH264(),
|
||||
(_, VideoFormat.Mpeg1Video, _) => new DecoderMpeg1Video(),
|
||||
(_, VideoFormat.Mpeg2Video, _) => new DecoderMpeg2Video(),
|
||||
(_, VideoFormat.Vc1, _) => new DecoderVc1(),
|
||||
(_, VideoFormat.MsMpeg4V2, _) => new DecoderMsMpeg4V2(),
|
||||
(_, VideoFormat.MsMpeg4V3, _) => new DecoderMsMpeg4V3(),
|
||||
(_, VideoFormat.Mpeg4, _) => new DecoderMpeg4(),
|
||||
(_, VideoFormat.Vp9, _) => new DecoderVp9(),
|
||||
|
||||
(_, VideoFormat.Undetermined, _) => new DecoderImplicit(),
|
||||
(_, VideoFormat.Copy, _) => new DecoderImplicit(),
|
||||
(_, VideoFormat.GeneratedImage, _) => new DecoderImplicit(),
|
||||
|
||||
var (accel, videoFormat, pixelFormat) => LogUnknownDecoder(accel, videoFormat, pixelFormat, logger)
|
||||
};
|
||||
}
|
||||
|
||||
private static Option<IDecoder> LogUnknownDecoder(
|
||||
HardwareAccelerationMode hardwareAccelerationMode,
|
||||
string videoFormat,
|
||||
string pixelFormat,
|
||||
ILogger logger)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Unable to determine decoder for {AccelMode} - {VideoFormat} - {PixelFormat}; may have playback issues",
|
||||
hardwareAccelerationMode,
|
||||
videoFormat,
|
||||
pixelFormat);
|
||||
return Option<IDecoder>.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid;
|
||||
|
||||
public class DecoderH264Cuvid : DecoderBase
|
||||
{
|
||||
public override string Name => "h264_cuvid";
|
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile)
|
||||
{
|
||||
IList<string> result = base.InputOptions(inputFile);
|
||||
|
||||
result.Add("-hwaccel_output_format");
|
||||
result.Add("cuda");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid;
|
||||
|
||||
public class DecoderHevcCuvid : DecoderBase
|
||||
{
|
||||
public override string Name => "hevc_cuvid";
|
||||
public override IList<string> InputOptions(InputFile inputFile)
|
||||
{
|
||||
IList<string> result = base.InputOptions(inputFile);
|
||||
|
||||
result.Add("-hwaccel_output_format");
|
||||
result.Add("cuda");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid;
|
||||
|
||||
public class DecoderMpeg2Cuvid : DecoderBase
|
||||
{
|
||||
public override string Name => "mpeg2_cuvid";
|
||||
public override IList<string> InputOptions(InputFile inputFile)
|
||||
{
|
||||
IList<string> result = base.InputOptions(inputFile);
|
||||
|
||||
result.Add("-hwaccel_output_format");
|
||||
result.Add("cuda");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid;
|
||||
|
||||
public class DecoderMpeg4Cuvid : DecoderBase
|
||||
{
|
||||
public override string Name => "mpeg4_cuvid";
|
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile)
|
||||
{
|
||||
IList<string> result = base.InputOptions(inputFile);
|
||||
|
||||
result.Add("-hwaccel_output_format");
|
||||
result.Add("cuda");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid;
|
||||
|
||||
public class DecoderVc1Cuvid : DecoderBase
|
||||
{
|
||||
public override string Name => "vc1_cuvid";
|
||||
public override IList<string> InputOptions(InputFile inputFile)
|
||||
{
|
||||
IList<string> result = base.InputOptions(inputFile);
|
||||
|
||||
result.Add("-hwaccel_output_format");
|
||||
result.Add("cuda");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid;
|
||||
|
||||
public class DecoderVp9Cuvid : DecoderBase
|
||||
{
|
||||
public override string Name => "vp9_cuvid";
|
||||
public override IList<string> InputOptions(InputFile inputFile)
|
||||
{
|
||||
IList<string> result = base.InputOptions(inputFile);
|
||||
|
||||
result.Add("-hwaccel_output_format");
|
||||
result.Add("cuda");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.FFmpeg.Environment;
|
||||
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public abstract class DecoderBase : IDecoder
|
||||
{
|
||||
protected abstract FrameDataLocation OutputFrameDataLocation { get; }
|
||||
public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>();
|
||||
public IList<string> GlobalOptions => Array.Empty<string>();
|
||||
public virtual IList<string> InputOptions(InputFile inputFile) => new List<string> { "-c:v", Name };
|
||||
public IList<string> FilterOptions => Array.Empty<string>();
|
||||
public IList<string> OutputOptions => Array.Empty<string>();
|
||||
public virtual FrameState NextState(FrameState currentState) =>
|
||||
currentState with { FrameDataLocation = OutputFrameDataLocation };
|
||||
public abstract string Name { get; }
|
||||
public bool AppliesTo(AudioInputFile audioInputFile) => false;
|
||||
|
||||
public bool AppliesTo(VideoInputFile videoInputFile) => true;
|
||||
|
||||
public bool AppliesTo(ConcatInputFile concatInputFile) => false;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderH264 : DecoderBase
|
||||
{
|
||||
public override string Name => "h264";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderHevc : DecoderBase
|
||||
{
|
||||
public override string Name => "hevc";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderImplicit : DecoderBase
|
||||
{
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
public override string Name => string.Empty;
|
||||
public override IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderMpeg1Video : DecoderBase
|
||||
{
|
||||
public override string Name => "mpeg1video";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderMpeg2Video : DecoderBase
|
||||
{
|
||||
public override string Name => "mpeg2video";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderMpeg4 : DecoderBase
|
||||
{
|
||||
public override string Name => "mpeg4";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderMsMpeg4V2 : DecoderBase
|
||||
{
|
||||
public override string Name => "msmpeg4v2";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderMsMpeg4V3 : DecoderBase
|
||||
{
|
||||
public override string Name => "msmpeg4v3";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.FFmpeg.Format;
|
||||
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderVaapi : DecoderBase
|
||||
{
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
public override string Name => "implicit_vaapi";
|
||||
public override IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>();
|
||||
|
||||
public override FrameState NextState(FrameState currentState)
|
||||
{
|
||||
FrameState nextState = base.NextState(currentState);
|
||||
|
||||
return currentState.PixelFormat.Match(
|
||||
pixelFormat => nextState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) },
|
||||
() => nextState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderVc1 : DecoderBase
|
||||
{
|
||||
public override string Name => "vc1";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderVideoToolbox : DecoderBase
|
||||
{
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
public override string Name => "implicit_videotoolbox";
|
||||
public override IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public class DecoderVp9 : DecoderBase
|
||||
{
|
||||
public override string Name => "vp9";
|
||||
protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.FFmpeg.Option;
|
||||
|
||||
namespace ErsatzTV.FFmpeg.Decoder;
|
||||
|
||||
public interface IDecoder : IInputOption
|
||||
{
|
||||
string Name { get; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user