Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b8dc0ed80 | ||
|
|
3cc1286271 | ||
|
|
df281758b7 | ||
|
|
25273c18c8 | ||
|
|
f1be945423 | ||
|
|
9a4f772f53 | ||
|
|
d669e8114b | ||
|
|
3972e3603b | ||
|
|
acc22fcb62 | ||
|
|
2df360d7fb | ||
|
|
46331ed2c6 | ||
|
|
3aee3b0515 | ||
|
|
72c45692b2 | ||
|
|
8edf71ca55 | ||
|
|
612b9e6524 | ||
|
|
7aff65f07b | ||
|
|
5d350fcfad | ||
|
|
5546ad204c | ||
|
|
d66efa0a1d | ||
|
|
36d3d38530 | ||
|
|
8e79141860 | ||
|
|
9b3545f7ca | ||
|
|
56db20faa0 | ||
|
|
b0bd4c9fed | ||
|
|
ba079452e2 | ||
|
|
f0f2b3da4b | ||
|
|
866049543c | ||
|
|
40ed4b8b0e | ||
|
|
b43d08ca67 | ||
|
|
5e7e386108 | ||
|
|
4176df9940 | ||
|
|
de2ef959fe | ||
|
|
b53cfebac1 | ||
|
|
6895b9cc6b | ||
|
|
c60d6e46f1 |
@@ -198,7 +198,7 @@ jobs:
|
|||||||
id: downloadffmpeg
|
id: downloadffmpeg
|
||||||
name: Download ffmpeg
|
name: Download ffmpeg
|
||||||
with:
|
with:
|
||||||
url: "https://github.com/GyanD/codexffmpeg/releases/download/5.0/ffmpeg-5.0-full_build.7z"
|
url: "https://github.com/GyanD/codexffmpeg/releases/download/5.1/ffmpeg-5.1-full_build.7z"
|
||||||
target: ffmpeg/
|
target: ffmpeg/
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|||||||
+66
-2
@@ -5,6 +5,65 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.6.7-beta] - 2022-09-05
|
||||||
|
### Fixed
|
||||||
|
- When all audio streams are selected with `HLS Direct`, explicitly copy them without transcoding
|
||||||
|
- This only happens when the channel does not have a `Preferred Audio Language`
|
||||||
|
- Fix scanner crash caused by invalid mtime
|
||||||
|
- `VAAPI`: Downgrade libva from 2.15 to 2.14
|
||||||
|
- Fix bug with XMLTV that caused some filler to display with primary content details
|
||||||
|
- Multiple fixes for content scaling with `Nvidia`, `Qsv` and `Vaapi` accelerations
|
||||||
|
- Properly scale image-based subtitles
|
||||||
|
- Fix bug where a schedule containing a single item (fixed start and flood) would never finish building a playout
|
||||||
|
- Logic was also added to detect infinite playout build loops in the future and stop them
|
||||||
|
- Fix bug where `Other Videos` wouldn't be included in scheduling mode `Shuffle In Order`
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add `Preferred Audio Title` feature
|
||||||
|
- Preference can be configured in channel settings and overridden on schedule items
|
||||||
|
- When a title is specified, audio streams that contain that title (case-insensitive search) will be prioritized
|
||||||
|
- This can be helpful for creating channels that use commentary tracks
|
||||||
|
- External tooling exists to easily update title/name metadata if your audio streams don't already have this metadata
|
||||||
|
- Add `Amf` hardware acceleration option for AMD GPUs on Windows
|
||||||
|
- Add `QSV Extra Hardware Frames` parameter for tuning QSV acceleration
|
||||||
|
- Performance may improve on some systems after doubling or halving the default value of `64`
|
||||||
|
|
||||||
|
## [0.6.6-beta] - 2022-08-17
|
||||||
|
### Fixed
|
||||||
|
- Use MIME Type `application/x-mpegurl` for all playlists instead of `application/vnd.apple.mpegurl`
|
||||||
|
- Replace `setsar` filter with `setdar` filter
|
||||||
|
- `setsar` caused issues scaling between two different aspect ratios
|
||||||
|
- For example, some 4:3 content would appear stretched when scaled to a 16:9 resolution
|
||||||
|
- `setdar` is now only used when aspect ratios match
|
||||||
|
- Prioritize aspect ratio from container when video stream contains conflicting aspect ratio
|
||||||
|
- This is usually caused by bad authoring, but the change should improve scaling behavior for edge cases
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Support DSD audio file formats (DFF and DSF) in local song libraries
|
||||||
|
- Support OGG audio file formats (OGG, OPUS, OGA, OGX, SPX) in local song libraries
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Always return playlist after a maximum of 8 seconds while starting up an HLS Segmenter session
|
||||||
|
- Use multi-variant playlists instead of redirects for HLS Segmenter sessions
|
||||||
|
- Upgrade ffmpeg from 5.0 to 5.1 in most docker images (not ARM variants)
|
||||||
|
- Upgrading from 5.0 to 5.1 is also recommended for other installations (Windows, Linux)
|
||||||
|
|
||||||
|
## [0.6.5-beta] - 2022-08-02
|
||||||
|
### Fixed
|
||||||
|
- Fix database initializer; fresh installs with v0.6.4-beta are missing some config data and should upgrade
|
||||||
|
|
||||||
|
## [0.6.4-beta] - 2022-07-28
|
||||||
|
### Fixed
|
||||||
|
- Fix subtitle stream selection when subtitle language is different than audio language
|
||||||
|
- Fix bug with unsupported AAC channel layouts
|
||||||
|
- Fix NVIDIA second-gen maxwell capabilities detection
|
||||||
|
- Return distinct search results for episodes and other videos that have the same title
|
||||||
|
- For example, two other videos both named `Trailer` would previously have displayed as one item in search results
|
||||||
|
- Fix schedules that would begin to repeat the same content in the same order after a couple of days
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add `640x480` resolution
|
||||||
|
|
||||||
## [0.6.3-beta] - 2022-07-04
|
## [0.6.3-beta] - 2022-07-04
|
||||||
### Fixed
|
### Fixed
|
||||||
- Maintain stream continuity when playout is rebuilt for a channel that is actively being streamed
|
- Maintain stream continuity when playout is rebuilt for a channel that is actively being streamed
|
||||||
@@ -1264,7 +1323,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Initial release to facilitate testing outside of Docker.
|
- Initial release to facilitate testing outside of Docker.
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.2-beta...HEAD
|
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.7-beta...HEAD
|
||||||
|
[0.6.7-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.6-beta...v0.6.7-beta
|
||||||
|
[0.6.6-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.5-beta...v0.6.6-beta
|
||||||
|
[0.6.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.4-beta...v0.6.5-beta
|
||||||
|
[0.6.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.3-beta...v0.6.4-beta
|
||||||
|
[0.6.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.2-beta...v0.6.3-beta
|
||||||
[0.6.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.1-beta...v0.6.2-beta
|
[0.6.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.1-beta...v0.6.2-beta
|
||||||
[0.6.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.0-beta...v0.6.1-beta
|
[0.6.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.0-beta...v0.6.1-beta
|
||||||
[0.6.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.8-beta...v0.6.0-beta
|
[0.6.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.8-beta...v0.6.0-beta
|
||||||
@@ -1363,4 +1427,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[0.0.5-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.4-prealpha...v0.0.5-prealpha
|
[0.0.5-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.4-prealpha...v0.0.5-prealpha
|
||||||
[0.0.4-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.3-prealpha...v0.0.4-prealpha
|
[0.0.4-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.3-prealpha...v0.0.4-prealpha
|
||||||
[0.0.3-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.1-prealpha...v0.0.3-prealpha
|
[0.0.3-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.1-prealpha...v0.0.3-prealpha
|
||||||
[0.0.1-prealpha]: https://github.com/jasongdove/ErsatzTV/releases/tag/v0.0.1-prealpha
|
[0.0.1-prealpha]: https://github.com/jasongdove/ErsatzTV/releases/tag/v0.0.1-prealpha
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliWrap" Version="3.4.4" />
|
<PackageReference Include="CliWrap" Version="3.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -30,4 +30,4 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public record ChannelViewModel(
|
|||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
string Logo,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
|
string PreferredAudioTitle,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public record CreateChannel
|
|||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
string Logo,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
|
string PreferredAudioTitle,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
|||||||
StreamingMode = request.StreamingMode,
|
StreamingMode = request.StreamingMode,
|
||||||
Artwork = artwork,
|
Artwork = artwork,
|
||||||
PreferredAudioLanguageCode = preferredAudioLanguageCode,
|
PreferredAudioLanguageCode = preferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = request.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
|
||||||
SubtitleMode = request.SubtitleMode,
|
SubtitleMode = request.SubtitleMode,
|
||||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode
|
MusicVideoCreditsMode = request.MusicVideoCreditsMode
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public record UpdateChannel
|
|||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
string Logo,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
|
string PreferredAudioTitle,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
|||||||
{
|
{
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||||
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
|
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
|
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
|
||||||
@@ -42,6 +42,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
|||||||
c.Categories = update.Categories;
|
c.Categories = update.Categories;
|
||||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||||
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
|
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
|
||||||
|
c.PreferredAudioTitle = update.PreferredAudioTitle;
|
||||||
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
|
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
|
||||||
c.SubtitleMode = update.SubtitleMode;
|
c.SubtitleMode = update.SubtitleMode;
|
||||||
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ internal static class Mapper
|
|||||||
channel.FFmpegProfileId,
|
channel.FFmpegProfileId,
|
||||||
GetLogo(channel),
|
GetLogo(channel),
|
||||||
channel.PreferredAudioLanguageCode,
|
channel.PreferredAudioLanguageCode,
|
||||||
|
channel.PreferredAudioTitle,
|
||||||
channel.StreamingMode,
|
channel.StreamingMode,
|
||||||
channel.WatermarkId,
|
channel.WatermarkId,
|
||||||
channel.FallbackFillerId,
|
channel.FallbackFillerId,
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
||||||
<PackageReference Include="CliWrap" Version="3.4.4" />
|
<PackageReference Include="CliWrap" Version="3.5.0" />
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||||
<PackageReference Include="MediatR" Version="10.0.1" />
|
<PackageReference Include="MediatR" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
|
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.3.44">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public record CreateFFmpegProfile(
|
|||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
VaapiDriver VaapiDriver,
|
VaapiDriver VaapiDriver,
|
||||||
string VaapiDevice,
|
string VaapiDevice,
|
||||||
|
int? QsvExtraHardwareFrames,
|
||||||
int ResolutionId,
|
int ResolutionId,
|
||||||
FFmpegProfileVideoFormat VideoFormat,
|
FFmpegProfileVideoFormat VideoFormat,
|
||||||
int VideoBitrate,
|
int VideoBitrate,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public class CreateFFmpegProfileHandler :
|
|||||||
HardwareAcceleration = request.HardwareAcceleration,
|
HardwareAcceleration = request.HardwareAcceleration,
|
||||||
VaapiDriver = request.VaapiDriver,
|
VaapiDriver = request.VaapiDriver,
|
||||||
VaapiDevice = request.VaapiDevice,
|
VaapiDevice = request.VaapiDevice,
|
||||||
|
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
|
||||||
ResolutionId = resolutionId,
|
ResolutionId = resolutionId,
|
||||||
VideoFormat = request.VideoFormat,
|
VideoFormat = request.VideoFormat,
|
||||||
VideoBitrate = request.VideoBitrate,
|
VideoBitrate = request.VideoBitrate,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegP
|
|||||||
|
|
||||||
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
|
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
int defaultResolutionId = await dbContext.ConfigElements
|
int defaultResolutionId = await dbContext.ConfigElements
|
||||||
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
|
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public record UpdateFFmpegProfile(
|
|||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
VaapiDriver VaapiDriver,
|
VaapiDriver VaapiDriver,
|
||||||
string VaapiDevice,
|
string VaapiDevice,
|
||||||
|
int? QsvExtraHardwareFrames,
|
||||||
int ResolutionId,
|
int ResolutionId,
|
||||||
FFmpegProfileVideoFormat VideoFormat,
|
FFmpegProfileVideoFormat VideoFormat,
|
||||||
int VideoBitrate,
|
int VideoBitrate,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class
|
|||||||
{
|
{
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
|
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
|
||||||
return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request));
|
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
|
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
|
||||||
@@ -33,6 +33,7 @@ public class
|
|||||||
p.HardwareAcceleration = update.HardwareAcceleration;
|
p.HardwareAcceleration = update.HardwareAcceleration;
|
||||||
p.VaapiDriver = update.VaapiDriver;
|
p.VaapiDriver = update.VaapiDriver;
|
||||||
p.VaapiDevice = update.VaapiDevice;
|
p.VaapiDevice = update.VaapiDevice;
|
||||||
|
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
|
||||||
p.ResolutionId = update.ResolutionId;
|
p.ResolutionId = update.ResolutionId;
|
||||||
p.VideoFormat = update.VideoFormat;
|
p.VideoFormat = update.VideoFormat;
|
||||||
p.VideoBitrate = update.VideoBitrate;
|
p.VideoBitrate = update.VideoBitrate;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public record FFmpegProfileViewModel(
|
|||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
VaapiDriver VaapiDriver,
|
VaapiDriver VaapiDriver,
|
||||||
string VaapiDevice,
|
string VaapiDevice,
|
||||||
|
int? QsvExtraHardwareFrames,
|
||||||
ResolutionViewModel Resolution,
|
ResolutionViewModel Resolution,
|
||||||
FFmpegProfileVideoFormat VideoFormat,
|
FFmpegProfileVideoFormat VideoFormat,
|
||||||
int VideoBitrate,
|
int VideoBitrate,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ internal static class Mapper
|
|||||||
profile.HardwareAcceleration,
|
profile.HardwareAcceleration,
|
||||||
profile.VaapiDriver,
|
profile.VaapiDriver,
|
||||||
profile.VaapiDevice,
|
profile.VaapiDevice,
|
||||||
|
profile.QsvExtraHardwareFrames,
|
||||||
Project(profile.Resolution),
|
Project(profile.Resolution),
|
||||||
profile.VideoFormat,
|
profile.VideoFormat,
|
||||||
profile.VideoBitrate,
|
profile.VideoBitrate,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
@@ -17,11 +17,11 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
|
|||||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||||
private readonly ILogger<MoveLocalLibraryPathHandler> _logger;
|
private readonly ILogger<MoveLocalLibraryPathHandler> _logger;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
public MoveLocalLibraryPathHandler(
|
public MoveLocalLibraryPathHandler(
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IDbContextFactory<TvContext> dbContextFactory,
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
ILogger<MoveLocalLibraryPathHandler> logger)
|
ILogger<MoveLocalLibraryPathHandler> logger)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Interfaces.Trakt;
|
using ErsatzTV.Core.Interfaces.Trakt;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
@@ -18,7 +18,7 @@ public class AddTraktListHandler : TraktCommandBase, IRequestHandler<AddTraktLis
|
|||||||
|
|
||||||
public AddTraktListHandler(
|
public AddTraktListHandler(
|
||||||
ITraktApiClient traktApiClient,
|
ITraktApiClient traktApiClient,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IDbContextFactory<TvContext> dbContextFactory,
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Interfaces.Trakt;
|
using ErsatzTV.Core.Interfaces.Trakt;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
@@ -17,11 +17,11 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler<DeleteTr
|
|||||||
private readonly IEntityLocker _entityLocker;
|
private readonly IEntityLocker _entityLocker;
|
||||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
public DeleteTraktListHandler(
|
public DeleteTraktListHandler(
|
||||||
ITraktApiClient traktApiClient,
|
ITraktApiClient traktApiClient,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IDbContextFactory<TvContext> dbContextFactory,
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Interfaces.Trakt;
|
using ErsatzTV.Core.Interfaces.Trakt;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
@@ -19,7 +19,7 @@ public class MatchTraktListItemsHandler : TraktCommandBase,
|
|||||||
|
|
||||||
public MatchTraktListItemsHandler(
|
public MatchTraktListItemsHandler(
|
||||||
ITraktApiClient traktApiClient,
|
ITraktApiClient traktApiClient,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IDbContextFactory<TvContext> dbContextFactory,
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Interfaces.Trakt;
|
using ErsatzTV.Core.Interfaces.Trakt;
|
||||||
using ErsatzTV.Core.Trakt;
|
using ErsatzTV.Core.Trakt;
|
||||||
@@ -17,11 +17,11 @@ public abstract class TraktCommandBase
|
|||||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
protected TraktCommandBase(
|
protected TraktCommandBase(
|
||||||
ITraktApiClient traktApiClient,
|
ITraktApiClient traktApiClient,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
ILogger logger)
|
ILogger logger)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public record AddProgramScheduleItem(
|
|||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
|
string PreferredAudioTitle,
|
||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode? SubtitleMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>,
|
ChannelSubtitleMode? SubtitleMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>,
|
||||||
IProgramScheduleItemRequest;
|
IProgramScheduleItemRequest;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public interface IProgramScheduleItemRequest
|
|||||||
int? FallbackFillerId { get; }
|
int? FallbackFillerId { get; }
|
||||||
int? WatermarkId { get; }
|
int? WatermarkId { get; }
|
||||||
string PreferredAudioLanguageCode { get; }
|
string PreferredAudioLanguageCode { get; }
|
||||||
|
string PreferredAudioTitle { get; }
|
||||||
string PreferredSubtitleLanguageCode { get; }
|
string PreferredSubtitleLanguageCode { get; }
|
||||||
ChannelSubtitleMode? SubtitleMode { get; }
|
ChannelSubtitleMode? SubtitleMode { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ public abstract class ProgramScheduleItemCommandBase
|
|||||||
FallbackFillerId = item.FallbackFillerId,
|
FallbackFillerId = item.FallbackFillerId,
|
||||||
WatermarkId = item.WatermarkId,
|
WatermarkId = item.WatermarkId,
|
||||||
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = item.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = item.SubtitleMode
|
SubtitleMode = item.SubtitleMode
|
||||||
},
|
},
|
||||||
@@ -203,6 +204,7 @@ public abstract class ProgramScheduleItemCommandBase
|
|||||||
FallbackFillerId = item.FallbackFillerId,
|
FallbackFillerId = item.FallbackFillerId,
|
||||||
WatermarkId = item.WatermarkId,
|
WatermarkId = item.WatermarkId,
|
||||||
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = item.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = item.SubtitleMode
|
SubtitleMode = item.SubtitleMode
|
||||||
},
|
},
|
||||||
@@ -227,6 +229,7 @@ public abstract class ProgramScheduleItemCommandBase
|
|||||||
FallbackFillerId = item.FallbackFillerId,
|
FallbackFillerId = item.FallbackFillerId,
|
||||||
WatermarkId = item.WatermarkId,
|
WatermarkId = item.WatermarkId,
|
||||||
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = item.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = item.SubtitleMode
|
SubtitleMode = item.SubtitleMode
|
||||||
},
|
},
|
||||||
@@ -252,6 +255,7 @@ public abstract class ProgramScheduleItemCommandBase
|
|||||||
FallbackFillerId = item.FallbackFillerId,
|
FallbackFillerId = item.FallbackFillerId,
|
||||||
WatermarkId = item.WatermarkId,
|
WatermarkId = item.WatermarkId,
|
||||||
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = item.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = item.SubtitleMode
|
SubtitleMode = item.SubtitleMode
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public record ReplaceProgramScheduleItem(
|
|||||||
int? FallbackFillerId,
|
int? FallbackFillerId,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
|
string PreferredAudioTitle,
|
||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode? SubtitleMode) : IProgramScheduleItemRequest;
|
ChannelSubtitleMode? SubtitleMode) : IProgramScheduleItemRequest;
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ internal static class Mapper
|
|||||||
? Watermarks.Mapper.ProjectToViewModel(duration.Watermark)
|
? Watermarks.Mapper.ProjectToViewModel(duration.Watermark)
|
||||||
: null,
|
: null,
|
||||||
duration.PreferredAudioLanguageCode,
|
duration.PreferredAudioLanguageCode,
|
||||||
|
duration.PreferredAudioTitle,
|
||||||
duration.PreferredSubtitleLanguageCode,
|
duration.PreferredSubtitleLanguageCode,
|
||||||
duration.SubtitleMode),
|
duration.SubtitleMode),
|
||||||
ProgramScheduleItemFlood flood =>
|
ProgramScheduleItemFlood flood =>
|
||||||
@@ -110,6 +111,7 @@ internal static class Mapper
|
|||||||
? Watermarks.Mapper.ProjectToViewModel(flood.Watermark)
|
? Watermarks.Mapper.ProjectToViewModel(flood.Watermark)
|
||||||
: null,
|
: null,
|
||||||
flood.PreferredAudioLanguageCode,
|
flood.PreferredAudioLanguageCode,
|
||||||
|
flood.PreferredAudioTitle,
|
||||||
flood.PreferredSubtitleLanguageCode,
|
flood.PreferredSubtitleLanguageCode,
|
||||||
flood.SubtitleMode),
|
flood.SubtitleMode),
|
||||||
ProgramScheduleItemMultiple multiple =>
|
ProgramScheduleItemMultiple multiple =>
|
||||||
@@ -158,6 +160,7 @@ internal static class Mapper
|
|||||||
? Watermarks.Mapper.ProjectToViewModel(multiple.Watermark)
|
? Watermarks.Mapper.ProjectToViewModel(multiple.Watermark)
|
||||||
: null,
|
: null,
|
||||||
multiple.PreferredAudioLanguageCode,
|
multiple.PreferredAudioLanguageCode,
|
||||||
|
multiple.PreferredAudioTitle,
|
||||||
multiple.PreferredSubtitleLanguageCode,
|
multiple.PreferredSubtitleLanguageCode,
|
||||||
multiple.SubtitleMode),
|
multiple.SubtitleMode),
|
||||||
ProgramScheduleItemOne one =>
|
ProgramScheduleItemOne one =>
|
||||||
@@ -205,6 +208,7 @@ internal static class Mapper
|
|||||||
? Watermarks.Mapper.ProjectToViewModel(one.Watermark)
|
? Watermarks.Mapper.ProjectToViewModel(one.Watermark)
|
||||||
: null,
|
: null,
|
||||||
one.PreferredAudioLanguageCode,
|
one.PreferredAudioLanguageCode,
|
||||||
|
one.PreferredAudioTitle,
|
||||||
one.PreferredSubtitleLanguageCode,
|
one.PreferredSubtitleLanguageCode,
|
||||||
one.SubtitleMode),
|
one.SubtitleMode),
|
||||||
_ => throw new NotSupportedException(
|
_ => throw new NotSupportedException(
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
|
|||||||
FillerPresetViewModel fallbackFiller,
|
FillerPresetViewModel fallbackFiller,
|
||||||
WatermarkViewModel watermark,
|
WatermarkViewModel watermark,
|
||||||
string preferredAudioLanguageCode,
|
string preferredAudioLanguageCode,
|
||||||
|
string preferredAudioTitle,
|
||||||
string preferredSubtitleLanguageCode,
|
string preferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode? subtitleMode) : base(
|
ChannelSubtitleMode? subtitleMode) : base(
|
||||||
id,
|
id,
|
||||||
@@ -52,6 +53,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
|
|||||||
fallbackFiller,
|
fallbackFiller,
|
||||||
watermark,
|
watermark,
|
||||||
preferredAudioLanguageCode,
|
preferredAudioLanguageCode,
|
||||||
|
preferredAudioTitle,
|
||||||
preferredSubtitleLanguageCode,
|
preferredSubtitleLanguageCode,
|
||||||
subtitleMode)
|
subtitleMode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
|
|||||||
FillerPresetViewModel fallbackFiller,
|
FillerPresetViewModel fallbackFiller,
|
||||||
WatermarkViewModel watermark,
|
WatermarkViewModel watermark,
|
||||||
string preferredAudioLanguageCode,
|
string preferredAudioLanguageCode,
|
||||||
|
string preferredAudioTitle,
|
||||||
string preferredSubtitleLanguageCode,
|
string preferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode? subtitleMode) : base(
|
ChannelSubtitleMode? subtitleMode) : base(
|
||||||
id,
|
id,
|
||||||
@@ -50,6 +51,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
|
|||||||
fallbackFiller,
|
fallbackFiller,
|
||||||
watermark,
|
watermark,
|
||||||
preferredAudioLanguageCode,
|
preferredAudioLanguageCode,
|
||||||
|
preferredAudioTitle,
|
||||||
preferredSubtitleLanguageCode,
|
preferredSubtitleLanguageCode,
|
||||||
subtitleMode)
|
subtitleMode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
|
|||||||
FillerPresetViewModel fallbackFiller,
|
FillerPresetViewModel fallbackFiller,
|
||||||
WatermarkViewModel watermark,
|
WatermarkViewModel watermark,
|
||||||
string preferredAudioLanguageCode,
|
string preferredAudioLanguageCode,
|
||||||
|
string preferredAudioTitle,
|
||||||
string preferredSubtitleLanguageCode,
|
string preferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode? subtitleMode) : base(
|
ChannelSubtitleMode? subtitleMode) : base(
|
||||||
id,
|
id,
|
||||||
@@ -51,6 +52,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
|
|||||||
fallbackFiller,
|
fallbackFiller,
|
||||||
watermark,
|
watermark,
|
||||||
preferredAudioLanguageCode,
|
preferredAudioLanguageCode,
|
||||||
|
preferredAudioTitle,
|
||||||
preferredSubtitleLanguageCode,
|
preferredSubtitleLanguageCode,
|
||||||
subtitleMode) =>
|
subtitleMode) =>
|
||||||
Count = count;
|
Count = count;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
|
|||||||
FillerPresetViewModel fallbackFiller,
|
FillerPresetViewModel fallbackFiller,
|
||||||
WatermarkViewModel watermark,
|
WatermarkViewModel watermark,
|
||||||
string preferredAudioLanguageCode,
|
string preferredAudioLanguageCode,
|
||||||
|
string preferredAudioTitle,
|
||||||
string preferredSubtitleLanguageCode,
|
string preferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode? subtitleMode) : base(
|
ChannelSubtitleMode? subtitleMode) : base(
|
||||||
id,
|
id,
|
||||||
@@ -50,6 +51,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
|
|||||||
fallbackFiller,
|
fallbackFiller,
|
||||||
watermark,
|
watermark,
|
||||||
preferredAudioLanguageCode,
|
preferredAudioLanguageCode,
|
||||||
|
preferredAudioTitle,
|
||||||
preferredSubtitleLanguageCode,
|
preferredSubtitleLanguageCode,
|
||||||
subtitleMode)
|
subtitleMode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public abstract record ProgramScheduleItemViewModel(
|
|||||||
FillerPresetViewModel FallbackFiller,
|
FillerPresetViewModel FallbackFiller,
|
||||||
WatermarkViewModel Watermark,
|
WatermarkViewModel Watermark,
|
||||||
string PreferredAudioLanguageCode,
|
string PreferredAudioLanguageCode,
|
||||||
|
string PreferredAudioTitle,
|
||||||
string PreferredSubtitleLanguageCode,
|
string PreferredSubtitleLanguageCode,
|
||||||
ChannelSubtitleMode? SubtitleMode)
|
ChannelSubtitleMode? SubtitleMode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core;
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -16,11 +17,11 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Uni
|
|||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
private readonly ILogger<RebuildSearchIndexHandler> _logger;
|
private readonly ILogger<RebuildSearchIndexHandler> _logger;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
public RebuildSearchIndexHandler(
|
public RebuildSearchIndexHandler(
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IConfigElementRepository configElementRepository,
|
IConfigElementRepository configElementRepository,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using ErsatzTV.Core;
|
using System.Diagnostics;
|
||||||
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Errors;
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.FFmpeg;
|
using ErsatzTV.Core.FFmpeg;
|
||||||
@@ -78,22 +79,47 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
|
|||||||
IHlsSessionWorker worker,
|
IHlsSessionWorker worker,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
while (!File.Exists(playlistFileName))
|
var sw = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
|
DateTimeOffset start = DateTimeOffset.Now;
|
||||||
}
|
DateTimeOffset finish = start.AddSeconds(8);
|
||||||
|
|
||||||
var segmentCount = 0;
|
_logger.LogDebug("Waiting for playlist to exist");
|
||||||
while (segmentCount < initialSegmentCount)
|
while (!File.Exists(playlistFileName))
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);
|
|
||||||
|
|
||||||
DateTimeOffset now = DateTimeOffset.Now.AddSeconds(-30);
|
|
||||||
Option<TrimPlaylistResult> maybeResult = await worker.TrimPlaylist(now, cancellationToken);
|
|
||||||
foreach (TrimPlaylistResult result in maybeResult)
|
|
||||||
{
|
{
|
||||||
segmentCount = result.SegmentCount;
|
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Playlist exists");
|
||||||
|
|
||||||
|
var segmentCount = 0;
|
||||||
|
var lastSegmentCount = -1;
|
||||||
|
while (DateTimeOffset.Now < finish && segmentCount < initialSegmentCount)
|
||||||
|
{
|
||||||
|
if (segmentCount != lastSegmentCount)
|
||||||
|
{
|
||||||
|
lastSegmentCount = segmentCount;
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Segment count {SegmentCount} of {InitialSegmentCount}",
|
||||||
|
segmentCount,
|
||||||
|
initialSegmentCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.Now.AddSeconds(-30);
|
||||||
|
Option<TrimPlaylistResult> maybeResult = await worker.TrimPlaylist(now, cancellationToken);
|
||||||
|
foreach (TrimPlaylistResult result in maybeResult)
|
||||||
|
{
|
||||||
|
segmentCount = result.SegmentCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
sw.Stop();
|
||||||
|
_logger.LogDebug("WaitForPlaylistSegments took {Duration}", sw.Elapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
using System.Timers;
|
using System.Timers;
|
||||||
using Bugsnag;
|
using Bugsnag;
|
||||||
using CliWrap;
|
using CliWrap;
|
||||||
@@ -64,6 +65,7 @@ public class HlsSessionWorker : IHlsSessionWorker
|
|||||||
DateTimeOffset filterBefore,
|
DateTimeOffset filterBefore,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
await Slim.WaitAsync(cancellationToken);
|
await Slim.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -85,6 +87,8 @@ public class HlsSessionWorker : IHlsSessionWorker
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Slim.Release();
|
Slim.Release();
|
||||||
|
sw.Stop();
|
||||||
|
// _logger.LogDebug("TrimPlaylist took {Duration}", sw.Elapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,11 +401,11 @@ public class HlsSessionWorker : IHlsSessionWorker
|
|||||||
var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList();
|
var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList();
|
||||||
if (toDelete.Count > 0)
|
if (toDelete.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(
|
// _logger.LogDebug(
|
||||||
"Deleting HLS segments {Min} to {Max} (less than {StartSequence})",
|
// "Deleting HLS segments {Min} to {Max} (less than {StartSequence})",
|
||||||
toDelete.Map(s => s.SequenceNumber).Min(),
|
// toDelete.Map(s => s.SequenceNumber).Min(),
|
||||||
toDelete.Map(s => s.SequenceNumber).Max(),
|
// toDelete.Map(s => s.SequenceNumber).Max(),
|
||||||
trimResult.Sequence);
|
// trimResult.Sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (Segment segment in toDelete)
|
foreach (Segment segment in toDelete)
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ public class GetErrorProcessHandler : FFmpegProcessHandler<GetErrorProcess>
|
|||||||
request.HlsRealtime,
|
request.HlsRealtime,
|
||||||
request.PtsOffset,
|
request.PtsOffset,
|
||||||
channel.FFmpegProfile.VaapiDriver,
|
channel.FFmpegProfile.VaapiDriver,
|
||||||
channel.FFmpegProfile.VaapiDevice);
|
channel.FFmpegProfile.VaapiDevice,
|
||||||
|
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||||
|
|
||||||
return new PlayoutItemProcessModel(process, request.MaybeDuration, request.Until);
|
return new PlayoutItemProcessModel(process, request.MaybeDuration, request.Until);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -174,6 +174,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
audioPath,
|
audioPath,
|
||||||
subtitles,
|
subtitles,
|
||||||
playoutItemWithPath.PlayoutItem.PreferredAudioLanguageCode ?? channel.PreferredAudioLanguageCode,
|
playoutItemWithPath.PlayoutItem.PreferredAudioLanguageCode ?? channel.PreferredAudioLanguageCode,
|
||||||
|
playoutItemWithPath.PlayoutItem.PreferredAudioTitle ?? channel.PreferredAudioTitle,
|
||||||
playoutItemWithPath.PlayoutItem.PreferredSubtitleLanguageCode ?? channel.PreferredSubtitleLanguageCode,
|
playoutItemWithPath.PlayoutItem.PreferredSubtitleLanguageCode ?? channel.PreferredSubtitleLanguageCode,
|
||||||
playoutItemWithPath.PlayoutItem.SubtitleMode ?? channel.SubtitleMode,
|
playoutItemWithPath.PlayoutItem.SubtitleMode ?? channel.SubtitleMode,
|
||||||
playoutItemWithPath.PlayoutItem.StartOffset,
|
playoutItemWithPath.PlayoutItem.StartOffset,
|
||||||
@@ -183,6 +184,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
maybeGlobalWatermark,
|
maybeGlobalWatermark,
|
||||||
channel.FFmpegProfile.VaapiDriver,
|
channel.FFmpegProfile.VaapiDriver,
|
||||||
channel.FFmpegProfile.VaapiDevice,
|
channel.FFmpegProfile.VaapiDevice,
|
||||||
|
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
|
||||||
request.HlsRealtime,
|
request.HlsRealtime,
|
||||||
playoutItemWithPath.PlayoutItem.FillerKind,
|
playoutItemWithPath.PlayoutItem.FillerKind,
|
||||||
playoutItemWithPath.PlayoutItem.InPoint,
|
playoutItemWithPath.PlayoutItem.InPoint,
|
||||||
@@ -229,7 +231,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
request.HlsRealtime,
|
request.HlsRealtime,
|
||||||
request.PtsOffset,
|
request.PtsOffset,
|
||||||
channel.FFmpegProfile.VaapiDriver,
|
channel.FFmpegProfile.VaapiDriver,
|
||||||
channel.FFmpegProfile.VaapiDevice);
|
channel.FFmpegProfile.VaapiDevice,
|
||||||
|
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||||
|
|
||||||
return new PlayoutItemProcessModel(offlineProcess, maybeDuration, finish);
|
return new PlayoutItemProcessModel(offlineProcess, maybeDuration, finish);
|
||||||
case PlayoutItemDoesNotExistOnDisk:
|
case PlayoutItemDoesNotExistOnDisk:
|
||||||
@@ -241,7 +244,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
request.HlsRealtime,
|
request.HlsRealtime,
|
||||||
request.PtsOffset,
|
request.PtsOffset,
|
||||||
channel.FFmpegProfile.VaapiDriver,
|
channel.FFmpegProfile.VaapiDriver,
|
||||||
channel.FFmpegProfile.VaapiDevice);
|
channel.FFmpegProfile.VaapiDevice,
|
||||||
|
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||||
|
|
||||||
return new PlayoutItemProcessModel(doesNotExistProcess, maybeDuration, finish);
|
return new PlayoutItemProcessModel(doesNotExistProcess, maybeDuration, finish);
|
||||||
default:
|
default:
|
||||||
@@ -253,7 +257,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
|||||||
request.HlsRealtime,
|
request.HlsRealtime,
|
||||||
request.PtsOffset,
|
request.PtsOffset,
|
||||||
channel.FFmpegProfile.VaapiDriver,
|
channel.FFmpegProfile.VaapiDriver,
|
||||||
channel.FFmpegProfile.VaapiDevice);
|
channel.FFmpegProfile.VaapiDevice,
|
||||||
|
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||||
|
|
||||||
return new PlayoutItemProcessModel(errorProcess, maybeDuration, finish);
|
return new PlayoutItemProcessModel(errorProcess, maybeDuration, finish);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Emby;
|
using ErsatzTV.Core.Emby;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
||||||
<PackageReference Include="CliWrap" Version="3.4.4" />
|
<PackageReference Include="CliWrap" Version="3.5.0" />
|
||||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||||
<PackageReference Include="LanguageExt.Core" Version="4.2.9" />
|
<PackageReference Include="LanguageExt.Core" Version="4.2.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
|
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.3.44">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.18.1" />
|
<PackageReference Include="Moq" Version="4.18.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using ErsatzTV.Core.Metadata;
|
|||||||
using ErsatzTV.FFmpeg;
|
using ErsatzTV.FFmpeg;
|
||||||
using ErsatzTV.FFmpeg.Capabilities;
|
using ErsatzTV.FFmpeg.Capabilities;
|
||||||
using ErsatzTV.FFmpeg.State;
|
using ErsatzTV.FFmpeg.State;
|
||||||
|
using ErsatzTV.Infrastructure.Runtime;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -168,6 +169,11 @@ public class TranscodingTests
|
|||||||
HardwareAccelerationKind.VideoToolbox
|
HardwareAccelerationKind.VideoToolbox
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static HardwareAccelerationKind[] AmfAcceleration =
|
||||||
|
{
|
||||||
|
HardwareAccelerationKind.Amf
|
||||||
|
};
|
||||||
|
|
||||||
public static HardwareAccelerationKind[] QsvAcceleration =
|
public static HardwareAccelerationKind[] QsvAcceleration =
|
||||||
{
|
{
|
||||||
HardwareAccelerationKind.Qsv
|
HardwareAccelerationKind.Qsv
|
||||||
@@ -192,11 +198,11 @@ public class TranscodingTests
|
|||||||
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))]
|
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))]
|
||||||
FFmpegProfileVideoFormat profileVideoFormat,
|
FFmpegProfileVideoFormat profileVideoFormat,
|
||||||
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
|
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))]
|
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
HardwareAccelerationKind profileAcceleration)
|
|
||||||
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
|
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
// [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration)
|
// [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
|
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
|
// [ValueSource(typeof(TestData), nameof(TestData.AmfAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
{
|
{
|
||||||
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
|
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
|
||||||
{
|
{
|
||||||
@@ -309,6 +315,7 @@ public class TranscodingTests
|
|||||||
// new HardwareCapabilitiesFactory(
|
// new HardwareCapabilitiesFactory(
|
||||||
// new MemoryCache(new MemoryCacheOptions()),
|
// new MemoryCache(new MemoryCacheOptions()),
|
||||||
// LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
|
// LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
|
||||||
|
new RuntimeInfo(),
|
||||||
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
|
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
|
||||||
|
|
||||||
var v = new MediaVersion
|
var v = new MediaVersion
|
||||||
@@ -483,6 +490,7 @@ public class TranscodingTests
|
|||||||
subtitles,
|
subtitles,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
|
string.Empty,
|
||||||
subtitleMode,
|
subtitleMode,
|
||||||
now,
|
now,
|
||||||
now + TimeSpan.FromSeconds(5),
|
now + TimeSpan.FromSeconds(5),
|
||||||
@@ -491,6 +499,7 @@ public class TranscodingTests
|
|||||||
channelWatermark,
|
channelWatermark,
|
||||||
VaapiDriver.Default,
|
VaapiDriver.Default,
|
||||||
"/dev/dri/renderD128",
|
"/dev/dri/renderD128",
|
||||||
|
Option<int>.None,
|
||||||
false,
|
false,
|
||||||
FillerKind.None,
|
FillerKind.None,
|
||||||
TimeSpan.Zero,
|
TimeSpan.Zero,
|
||||||
@@ -569,7 +578,8 @@ public class TranscodingTests
|
|||||||
MediaVersion version,
|
MediaVersion version,
|
||||||
StreamingMode streamingMode,
|
StreamingMode streamingMode,
|
||||||
string channelNumber,
|
string channelNumber,
|
||||||
string preferredAudioLanguage) =>
|
string preferredAudioLanguage,
|
||||||
|
string preferredAudioTitle) =>
|
||||||
Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
|
Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
|
||||||
|
|
||||||
public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
|
public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
|
||||||
@@ -585,7 +595,7 @@ public class TranscodingTests
|
|||||||
public Task<IHardwareCapabilities> GetHardwareCapabilities(
|
public Task<IHardwareCapabilities> GetHardwareCapabilities(
|
||||||
string ffmpegPath,
|
string ffmpegPath,
|
||||||
HardwareAccelerationMode hardwareAccelerationMode) =>
|
HardwareAccelerationMode hardwareAccelerationMode) =>
|
||||||
Task.FromResult<IHardwareCapabilities>(new NvidiaHardwareCapabilities(61));
|
Task.FromResult<IHardwareCapabilities>(new NvidiaHardwareCapabilities(61, string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ExecutableName(string baseName) =>
|
private static string ExecutableName(string baseName) =>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
using ErsatzTV.Core.Jellyfin;
|
using ErsatzTV.Core.Jellyfin;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
|
|||||||
using ErsatzTV.Core.Interfaces.Images;
|
using ErsatzTV.Core.Interfaces.Images;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using ErsatzTV.Core.Tests.Fakes;
|
using ErsatzTV.Core.Tests.Fakes;
|
||||||
@@ -622,7 +623,7 @@ public class MovieFolderScannerTests
|
|||||||
new Mock<IMetadataRepository>().Object,
|
new Mock<IMetadataRepository>().Object,
|
||||||
_imageCache.Object,
|
_imageCache.Object,
|
||||||
new Mock<ISearchIndex>().Object,
|
new Mock<ISearchIndex>().Object,
|
||||||
new Mock<ISearchRepository>().Object,
|
new Mock<ICachingSearchRepository>().Object,
|
||||||
new Mock<IFallbackMetadataProvider>().Object,
|
new Mock<IFallbackMetadataProvider>().Object,
|
||||||
new Mock<ILibraryRepository>().Object,
|
new Mock<ILibraryRepository>().Object,
|
||||||
_mediaItemRepository.Object,
|
_mediaItemRepository.Object,
|
||||||
@@ -643,7 +644,7 @@ public class MovieFolderScannerTests
|
|||||||
new Mock<IMetadataRepository>().Object,
|
new Mock<IMetadataRepository>().Object,
|
||||||
_imageCache.Object,
|
_imageCache.Object,
|
||||||
new Mock<ISearchIndex>().Object,
|
new Mock<ISearchIndex>().Object,
|
||||||
new Mock<ISearchRepository>().Object,
|
new Mock<ICachingSearchRepository>().Object,
|
||||||
new Mock<IFallbackMetadataProvider>().Object,
|
new Mock<IFallbackMetadataProvider>().Object,
|
||||||
new Mock<ILibraryRepository>().Object,
|
new Mock<ILibraryRepository>().Object,
|
||||||
_mediaItemRepository.Object,
|
_mediaItemRepository.Object,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
using ErsatzTV.Core.Plex;
|
using ErsatzTV.Core.Plex;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|||||||
@@ -515,6 +515,7 @@ public class PlayoutBuilderTests
|
|||||||
{
|
{
|
||||||
new ProgramScheduleItemFlood
|
new ProgramScheduleItemFlood
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = floodCollection,
|
Collection = floodCollection,
|
||||||
CollectionId = floodCollection.Id,
|
CollectionId = floodCollection.Id,
|
||||||
@@ -523,6 +524,7 @@ public class PlayoutBuilderTests
|
|||||||
},
|
},
|
||||||
new ProgramScheduleItemOne
|
new ProgramScheduleItemOne
|
||||||
{
|
{
|
||||||
|
Id = 2,
|
||||||
Index = 2,
|
Index = 2,
|
||||||
Collection = fixedCollection,
|
Collection = fixedCollection,
|
||||||
CollectionId = fixedCollection.Id,
|
CollectionId = fixedCollection.Id,
|
||||||
@@ -605,6 +607,7 @@ public class PlayoutBuilderTests
|
|||||||
{
|
{
|
||||||
new ProgramScheduleItemFlood
|
new ProgramScheduleItemFlood
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = floodCollection,
|
Collection = floodCollection,
|
||||||
CollectionId = floodCollection.Id,
|
CollectionId = floodCollection.Id,
|
||||||
@@ -613,6 +616,7 @@ public class PlayoutBuilderTests
|
|||||||
},
|
},
|
||||||
new ProgramScheduleItemOne
|
new ProgramScheduleItemOne
|
||||||
{
|
{
|
||||||
|
Id = 2,
|
||||||
Index = 2,
|
Index = 2,
|
||||||
Collection = fixedCollection,
|
Collection = fixedCollection,
|
||||||
CollectionId = fixedCollection.Id,
|
CollectionId = fixedCollection.Id,
|
||||||
@@ -742,6 +746,7 @@ public class PlayoutBuilderTests
|
|||||||
{
|
{
|
||||||
new ProgramScheduleItemFlood
|
new ProgramScheduleItemFlood
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = floodCollection,
|
Collection = floodCollection,
|
||||||
CollectionId = floodCollection.Id,
|
CollectionId = floodCollection.Id,
|
||||||
@@ -750,6 +755,7 @@ public class PlayoutBuilderTests
|
|||||||
},
|
},
|
||||||
new ProgramScheduleItemMultiple
|
new ProgramScheduleItemMultiple
|
||||||
{
|
{
|
||||||
|
Id = 2,
|
||||||
Index = 2,
|
Index = 2,
|
||||||
Collection = fixedCollection,
|
Collection = fixedCollection,
|
||||||
CollectionId = fixedCollection.Id,
|
CollectionId = fixedCollection.Id,
|
||||||
@@ -839,6 +845,7 @@ public class PlayoutBuilderTests
|
|||||||
{
|
{
|
||||||
new ProgramScheduleItemFlood
|
new ProgramScheduleItemFlood
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = floodCollection,
|
Collection = floodCollection,
|
||||||
CollectionId = floodCollection.Id,
|
CollectionId = floodCollection.Id,
|
||||||
@@ -847,6 +854,7 @@ public class PlayoutBuilderTests
|
|||||||
},
|
},
|
||||||
new ProgramScheduleItemOne
|
new ProgramScheduleItemOne
|
||||||
{
|
{
|
||||||
|
Id = 2,
|
||||||
Index = 2,
|
Index = 2,
|
||||||
Collection = fixedCollection,
|
Collection = fixedCollection,
|
||||||
CollectionId = fixedCollection.Id,
|
CollectionId = fixedCollection.Id,
|
||||||
@@ -934,6 +942,7 @@ public class PlayoutBuilderTests
|
|||||||
{
|
{
|
||||||
new ProgramScheduleItemFlood
|
new ProgramScheduleItemFlood
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = floodCollection,
|
Collection = floodCollection,
|
||||||
CollectionId = floodCollection.Id,
|
CollectionId = floodCollection.Id,
|
||||||
@@ -942,6 +951,7 @@ public class PlayoutBuilderTests
|
|||||||
},
|
},
|
||||||
new ProgramScheduleItemOne
|
new ProgramScheduleItemOne
|
||||||
{
|
{
|
||||||
|
Id = 2,
|
||||||
Index = 2,
|
Index = 2,
|
||||||
Collection = fixedCollection,
|
Collection = fixedCollection,
|
||||||
CollectionId = fixedCollection.Id,
|
CollectionId = fixedCollection.Id,
|
||||||
@@ -1040,6 +1050,7 @@ public class PlayoutBuilderTests
|
|||||||
{
|
{
|
||||||
new ProgramScheduleItemFlood
|
new ProgramScheduleItemFlood
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = floodCollection,
|
Collection = floodCollection,
|
||||||
CollectionId = floodCollection.Id,
|
CollectionId = floodCollection.Id,
|
||||||
@@ -1048,6 +1059,7 @@ public class PlayoutBuilderTests
|
|||||||
},
|
},
|
||||||
new ProgramScheduleItemDuration
|
new ProgramScheduleItemDuration
|
||||||
{
|
{
|
||||||
|
Id = 2,
|
||||||
Index = 2,
|
Index = 2,
|
||||||
Collection = fixedCollection,
|
Collection = fixedCollection,
|
||||||
CollectionId = fixedCollection.Id,
|
CollectionId = fixedCollection.Id,
|
||||||
@@ -1141,6 +1153,7 @@ public class PlayoutBuilderTests
|
|||||||
{
|
{
|
||||||
new ProgramScheduleItemMultiple
|
new ProgramScheduleItemMultiple
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = multipleCollection,
|
Collection = multipleCollection,
|
||||||
CollectionId = multipleCollection.Id,
|
CollectionId = multipleCollection.Id,
|
||||||
@@ -1150,6 +1163,7 @@ public class PlayoutBuilderTests
|
|||||||
},
|
},
|
||||||
new ProgramScheduleItemDuration
|
new ProgramScheduleItemDuration
|
||||||
{
|
{
|
||||||
|
Id = 2,
|
||||||
Index = 2,
|
Index = 2,
|
||||||
Collection = dynamicCollection,
|
Collection = dynamicCollection,
|
||||||
CollectionId = dynamicCollection.Id,
|
CollectionId = dynamicCollection.Id,
|
||||||
@@ -2235,7 +2249,7 @@ public class PlayoutBuilderTests
|
|||||||
DateTimeOffset start2 = HoursAfterMidnight(0);
|
DateTimeOffset start2 = HoursAfterMidnight(0);
|
||||||
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
|
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
|
||||||
|
|
||||||
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2);
|
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
|
||||||
|
|
||||||
int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed;
|
int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed;
|
||||||
|
|
||||||
@@ -2244,6 +2258,57 @@ public class PlayoutBuilderTests
|
|||||||
result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0);
|
result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ShuffleFlood_Should_MaintainRandomSeed_MultipleDays()
|
||||||
|
{
|
||||||
|
var mediaItems = new List<MediaItem>();
|
||||||
|
for (int i = 1; i <= 25; i++)
|
||||||
|
{
|
||||||
|
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
(PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle);
|
||||||
|
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
|
||||||
|
DateTimeOffset finish = start + TimeSpan.FromDays(2);
|
||||||
|
|
||||||
|
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
|
||||||
|
|
||||||
|
result.Items.Count.Should().Be(53);
|
||||||
|
result.ProgramScheduleAnchors.Count.Should().Be(2);
|
||||||
|
|
||||||
|
result.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).Should().BeTrue();
|
||||||
|
PlayoutProgramScheduleAnchor lastCheckpoint = result.ProgramScheduleAnchors
|
||||||
|
.OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue)
|
||||||
|
.First();
|
||||||
|
lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0);
|
||||||
|
lastCheckpoint.EnumeratorState.Index.Should().Be(3);
|
||||||
|
|
||||||
|
// we need to mess up the ordering to trigger the problematic behavior
|
||||||
|
// this simulates the way the rows are loaded with EF
|
||||||
|
PlayoutProgramScheduleAnchor oldest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate).Last();
|
||||||
|
PlayoutProgramScheduleAnchor newest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate).First();
|
||||||
|
|
||||||
|
result.ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>
|
||||||
|
{
|
||||||
|
oldest,
|
||||||
|
newest
|
||||||
|
};
|
||||||
|
|
||||||
|
int firstSeedValue = lastCheckpoint.EnumeratorState.Seed;
|
||||||
|
|
||||||
|
DateTimeOffset start2 = start.AddHours(1);
|
||||||
|
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2);
|
||||||
|
|
||||||
|
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
|
||||||
|
|
||||||
|
PlayoutProgramScheduleAnchor continueAnchor =
|
||||||
|
result2.ProgramScheduleAnchors.First(x => x.AnchorDate is null);
|
||||||
|
int secondSeedValue = continueAnchor.EnumeratorState.Seed;
|
||||||
|
|
||||||
|
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
|
||||||
|
firstSeedValue.Should().Be(secondSeedValue);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor()
|
public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor()
|
||||||
{
|
{
|
||||||
@@ -2278,6 +2343,7 @@ public class PlayoutBuilderTests
|
|||||||
{
|
{
|
||||||
new ProgramScheduleItemFlood
|
new ProgramScheduleItemFlood
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = floodCollection,
|
Collection = floodCollection,
|
||||||
CollectionId = floodCollection.Id,
|
CollectionId = floodCollection.Id,
|
||||||
@@ -2286,6 +2352,7 @@ public class PlayoutBuilderTests
|
|||||||
},
|
},
|
||||||
new ProgramScheduleItemOne
|
new ProgramScheduleItemOne
|
||||||
{
|
{
|
||||||
|
Id = 2,
|
||||||
Index = 2,
|
Index = 2,
|
||||||
Collection = fixedCollection,
|
Collection = fixedCollection,
|
||||||
CollectionId = fixedCollection.Id,
|
CollectionId = fixedCollection.Id,
|
||||||
@@ -2571,6 +2638,7 @@ public class PlayoutBuilderTests
|
|||||||
private static ProgramScheduleItem Flood(Collection mediaCollection, PlaybackOrder playbackOrder) =>
|
private static ProgramScheduleItem Flood(Collection mediaCollection, PlaybackOrder playbackOrder) =>
|
||||||
new ProgramScheduleItemFlood
|
new ProgramScheduleItemFlood
|
||||||
{
|
{
|
||||||
|
Id = 1,
|
||||||
Index = 1,
|
Index = 1,
|
||||||
Collection = mediaCollection,
|
Collection = mediaCollection,
|
||||||
CollectionId = mediaCollection.Id,
|
CollectionId = mediaCollection.Id,
|
||||||
|
|||||||
@@ -85,6 +85,98 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
|
|||||||
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||||
playoutItems[2].CustomTitle.Should().Be("CustomTitle");
|
playoutItems[2].CustomTitle.Should().Be("CustomTitle");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Should_Schedule_Single_Item_Fixed_Start_Flood()
|
||||||
|
{
|
||||||
|
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
var scheduleItem = new ProgramScheduleItemFlood
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Index = 1,
|
||||||
|
Collection = collectionOne,
|
||||||
|
CollectionId = collectionOne.Id,
|
||||||
|
StartTime = TimeSpan.Zero,
|
||||||
|
PlaybackOrder = PlaybackOrder.Chronological,
|
||||||
|
TailFiller = null,
|
||||||
|
FallbackFiller = null,
|
||||||
|
CustomTitle = "CustomTitle"
|
||||||
|
};
|
||||||
|
|
||||||
|
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||||
|
collectionOne.MediaItems,
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
|
var sortedScheduleItems = new List<ProgramScheduleItem>
|
||||||
|
{
|
||||||
|
scheduleItem
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
CollectionEnumerators(scheduleItem, enumerator),
|
||||||
|
scheduleItem,
|
||||||
|
scheduleItem,
|
||||||
|
HardStop(scheduleItemsEnumerator));
|
||||||
|
|
||||||
|
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(6));
|
||||||
|
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
|
||||||
|
|
||||||
|
playoutBuilderState.NextGuideGroup.Should().Be(2); // one guide group here because of custom title
|
||||||
|
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
|
||||||
|
playoutBuilderState.InFlood.Should().BeTrue();
|
||||||
|
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
|
||||||
|
playoutBuilderState.InDurationFiller.Should().BeFalse();
|
||||||
|
playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
|
||||||
|
|
||||||
|
enumerator.State.Index.Should().Be(0);
|
||||||
|
|
||||||
|
playoutItems.Count.Should().Be(6);
|
||||||
|
|
||||||
|
playoutItems[0].MediaItemId.Should().Be(1);
|
||||||
|
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||||
|
playoutItems[0].GuideGroup.Should().Be(1);
|
||||||
|
playoutItems[0].FillerKind.Should().Be(FillerKind.None);
|
||||||
|
playoutItems[0].CustomTitle.Should().Be("CustomTitle");
|
||||||
|
|
||||||
|
playoutItems[1].MediaItemId.Should().Be(2);
|
||||||
|
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
|
||||||
|
playoutItems[1].GuideGroup.Should().Be(1);
|
||||||
|
playoutItems[1].FillerKind.Should().Be(FillerKind.None);
|
||||||
|
playoutItems[1].CustomTitle.Should().Be("CustomTitle");
|
||||||
|
|
||||||
|
playoutItems[2].MediaItemId.Should().Be(1);
|
||||||
|
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
|
||||||
|
playoutItems[2].GuideGroup.Should().Be(1);
|
||||||
|
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
|
||||||
|
playoutItems[2].CustomTitle.Should().Be("CustomTitle");
|
||||||
|
|
||||||
|
playoutItems[3].MediaItemId.Should().Be(2);
|
||||||
|
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.AddHours(3));
|
||||||
|
playoutItems[3].GuideGroup.Should().Be(1);
|
||||||
|
playoutItems[3].FillerKind.Should().Be(FillerKind.None);
|
||||||
|
playoutItems[3].CustomTitle.Should().Be("CustomTitle");
|
||||||
|
|
||||||
|
playoutItems[4].MediaItemId.Should().Be(1);
|
||||||
|
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddHours(4));
|
||||||
|
playoutItems[4].GuideGroup.Should().Be(1);
|
||||||
|
playoutItems[4].FillerKind.Should().Be(FillerKind.None);
|
||||||
|
playoutItems[4].CustomTitle.Should().Be("CustomTitle");
|
||||||
|
|
||||||
|
playoutItems[5].MediaItemId.Should().Be(2);
|
||||||
|
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.AddHours(5));
|
||||||
|
playoutItems[5].GuideGroup.Should().Be(1);
|
||||||
|
playoutItems[5].FillerKind.Should().Be(FillerKind.None);
|
||||||
|
playoutItems[5].CustomTitle.Should().Be("CustomTitle");
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood()
|
public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood()
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Destructurama;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using ErsatzTV.Core.Scheduling;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Data.Repositories;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt.UnsafeValueAccess;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
using Serilog.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Tests.Scheduling;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Explicit]
|
||||||
|
public class ScheduleIntegrationTests
|
||||||
|
{
|
||||||
|
public ScheduleIntegrationTests()
|
||||||
|
{
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Information()
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
|
||||||
|
.WriteTo.Console()
|
||||||
|
.Destructure.UsingAttributes()
|
||||||
|
.CreateLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Test()
|
||||||
|
{
|
||||||
|
string dbFileName = Path.GetTempFileName() + ".sqlite3";
|
||||||
|
|
||||||
|
IServiceCollection services = new ServiceCollection()
|
||||||
|
.AddLogging();
|
||||||
|
|
||||||
|
var connectionString = $"Data Source={dbFileName};foreign keys=true;";
|
||||||
|
|
||||||
|
services.AddDbContext<TvContext>(
|
||||||
|
options => options.UseSqlite(
|
||||||
|
connectionString,
|
||||||
|
o =>
|
||||||
|
{
|
||||||
|
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||||
|
o.MigrationsAssembly("ErsatzTV.Infrastructure");
|
||||||
|
}),
|
||||||
|
ServiceLifetime.Scoped,
|
||||||
|
ServiceLifetime.Singleton);
|
||||||
|
|
||||||
|
services.AddDbContextFactory<TvContext>(
|
||||||
|
options => options.UseSqlite(
|
||||||
|
connectionString,
|
||||||
|
o =>
|
||||||
|
{
|
||||||
|
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
|
||||||
|
o.MigrationsAssembly("ErsatzTV.Infrastructure");
|
||||||
|
}));
|
||||||
|
|
||||||
|
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
|
||||||
|
SqlMapper.AddTypeHandler(new GuidHandler());
|
||||||
|
SqlMapper.AddTypeHandler(new TimeSpanHandler());
|
||||||
|
|
||||||
|
services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory()));
|
||||||
|
|
||||||
|
ServiceProvider provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
IDbContextFactory<TvContext> factory = provider.GetRequiredService<IDbContextFactory<TvContext>>();
|
||||||
|
|
||||||
|
ILogger<ScheduleIntegrationTests> logger = provider.GetRequiredService<ILogger<ScheduleIntegrationTests>>();
|
||||||
|
logger.LogInformation("Database is at {File}", dbFileName);
|
||||||
|
|
||||||
|
await using TvContext dbContext = await factory.CreateDbContextAsync(CancellationToken.None);
|
||||||
|
await dbContext.Database.MigrateAsync(CancellationToken.None);
|
||||||
|
await DbInitializer.Initialize(dbContext, CancellationToken.None);
|
||||||
|
|
||||||
|
var path = new LibraryPath
|
||||||
|
{
|
||||||
|
Path = "Test LibraryPath"
|
||||||
|
};
|
||||||
|
|
||||||
|
var library = new LocalLibrary
|
||||||
|
{
|
||||||
|
MediaKind = LibraryMediaKind.Movies,
|
||||||
|
Paths = new List<LibraryPath> { path },
|
||||||
|
MediaSource = new LocalMediaSource()
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.Libraries.AddAsync(library);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var movies = new List<Movie>();
|
||||||
|
for (var i = 1; i < 25; i++)
|
||||||
|
{
|
||||||
|
var movie = new Movie
|
||||||
|
{
|
||||||
|
MediaVersions = new List<MediaVersion>
|
||||||
|
{
|
||||||
|
new() { Duration = TimeSpan.FromMinutes(55) }
|
||||||
|
},
|
||||||
|
MovieMetadata = new List<MovieMetadata>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Title = $"Movie {i}",
|
||||||
|
ReleaseDate = new DateTime(2000, 1, 1).AddDays(i)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LibraryPath = path,
|
||||||
|
LibraryPathId = path.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
movies.Add(movie);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.Movies.AddRangeAsync(movies);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var collection = new Collection
|
||||||
|
{
|
||||||
|
Name = "Test Collection",
|
||||||
|
MediaItems = movies.Cast<MediaItem>().ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.Collections.AddAsync(collection);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var scheduleItems = new List<ProgramScheduleItem>
|
||||||
|
{
|
||||||
|
new ProgramScheduleItemDuration
|
||||||
|
{
|
||||||
|
Collection = collection,
|
||||||
|
CollectionId = collection.Id,
|
||||||
|
PlayoutDuration = TimeSpan.FromHours(1),
|
||||||
|
TailMode = TailMode.None, // immediately continue
|
||||||
|
PlaybackOrder = PlaybackOrder.Shuffle
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int playoutId = await AddTestData(dbContext, scheduleItems);
|
||||||
|
|
||||||
|
DateTimeOffset start = new DateTimeOffset(2022, 7, 26, 8, 0, 5, TimeSpan.FromHours(-5));
|
||||||
|
DateTimeOffset finish = start.AddDays(2);
|
||||||
|
|
||||||
|
var builder = new PlayoutBuilder(
|
||||||
|
new ConfigElementRepository(factory),
|
||||||
|
new MediaCollectionRepository(new Mock<ISearchIndex>().Object, factory),
|
||||||
|
new TelevisionRepository(factory),
|
||||||
|
new ArtistRepository(factory),
|
||||||
|
provider.GetRequiredService<ILogger<PlayoutBuilder>>());
|
||||||
|
|
||||||
|
for (var i = 0; i <= (24 * 4); i++)
|
||||||
|
{
|
||||||
|
await using TvContext context = await factory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
Option<Playout> maybePlayout = await GetPlayout(context, playoutId);
|
||||||
|
Playout playout = maybePlayout.ValueUnsafe();
|
||||||
|
|
||||||
|
await builder.Build(playout, PlayoutBuildMode.Continue, start.AddHours(i), finish.AddHours(i));
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> AddTestData(TvContext dbContext, List<ProgramScheduleItem> scheduleItems)
|
||||||
|
{
|
||||||
|
var ffmpegProfile = new FFmpegProfile
|
||||||
|
{
|
||||||
|
Name = "Test FFmpeg Profile"
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var channel = new Channel(Guid.Parse("00000000-0000-0000-0000-000000000001"))
|
||||||
|
{
|
||||||
|
Name = "Test Channel",
|
||||||
|
FFmpegProfile = ffmpegProfile,
|
||||||
|
FFmpegProfileId = ffmpegProfile.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.Channels.AddAsync(channel);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var schedule = new ProgramSchedule
|
||||||
|
{
|
||||||
|
Name = "Test Schedule",
|
||||||
|
Items = scheduleItems
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.ProgramSchedules.AddAsync(schedule);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var playout = new Playout
|
||||||
|
{
|
||||||
|
Channel = channel,
|
||||||
|
ChannelId = channel.Id,
|
||||||
|
ProgramSchedule = schedule,
|
||||||
|
ProgramScheduleId = schedule.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.Playouts.AddAsync(playout);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return playout.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Option<Playout>> GetPlayout(TvContext dbContext, int playoutId)
|
||||||
|
{
|
||||||
|
return await dbContext.Playouts
|
||||||
|
.Include(p => p.Channel)
|
||||||
|
.Include(p => p.Items)
|
||||||
|
.Include(p => p.ProgramScheduleAnchors)
|
||||||
|
.ThenInclude(a => a.MediaItem)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.Collection)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.MediaItem)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.PreRollFiller)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.MidRollFiller)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.PostRollFiller)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.TailFiller)
|
||||||
|
.Include(p => p.ProgramSchedule)
|
||||||
|
.ThenInclude(ps => ps.Items)
|
||||||
|
.ThenInclude(psi => psi.FallbackFiller)
|
||||||
|
.SelectOneAsync(p => p.Id, p => p.Id == playoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ public class Channel
|
|||||||
public List<Playout> Playouts { get; set; }
|
public List<Playout> Playouts { get; set; }
|
||||||
public List<Artwork> Artwork { get; set; }
|
public List<Artwork> Artwork { get; set; }
|
||||||
public string PreferredAudioLanguageCode { get; set; }
|
public string PreferredAudioLanguageCode { get; set; }
|
||||||
|
public string PreferredAudioTitle { get; set; }
|
||||||
public string PreferredSubtitleLanguageCode { get; set; }
|
public string PreferredSubtitleLanguageCode { get; set; }
|
||||||
public ChannelSubtitleMode SubtitleMode { get; set; }
|
public ChannelSubtitleMode SubtitleMode { get; set; }
|
||||||
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public record FFmpegProfile
|
|||||||
public HardwareAccelerationKind HardwareAcceleration { get; set; }
|
public HardwareAccelerationKind HardwareAcceleration { get; set; }
|
||||||
public VaapiDriver VaapiDriver { get; set; }
|
public VaapiDriver VaapiDriver { get; set; }
|
||||||
public string VaapiDevice { get; set; }
|
public string VaapiDevice { get; set; }
|
||||||
|
public int? QsvExtraHardwareFrames { get; set; }
|
||||||
public int ResolutionId { get; set; }
|
public int ResolutionId { get; set; }
|
||||||
public Resolution Resolution { get; set; }
|
public Resolution Resolution { get; set; }
|
||||||
public FFmpegProfileVideoFormat VideoFormat { get; set; }
|
public FFmpegProfileVideoFormat VideoFormat { get; set; }
|
||||||
@@ -42,6 +43,7 @@ public record FFmpegProfile
|
|||||||
AudioSampleRate = 48,
|
AudioSampleRate = 48,
|
||||||
DeinterlaceVideo = true,
|
DeinterlaceVideo = true,
|
||||||
NormalizeFramerate = false,
|
NormalizeFramerate = false,
|
||||||
HardwareAcceleration = HardwareAccelerationKind.None
|
HardwareAcceleration = HardwareAccelerationKind.None,
|
||||||
|
QsvExtraHardwareFrames = 64
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ public enum HardwareAccelerationKind
|
|||||||
Qsv = 1,
|
Qsv = 1,
|
||||||
Nvenc = 2,
|
Nvenc = 2,
|
||||||
Vaapi = 3,
|
Vaapi = 3,
|
||||||
VideoToolbox = 4
|
VideoToolbox = 4,
|
||||||
|
Amf = 5
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class PlayoutItem
|
|||||||
public int? WatermarkId { get; set; }
|
public int? WatermarkId { get; set; }
|
||||||
public bool DisableWatermarks { get; set; }
|
public bool DisableWatermarks { get; set; }
|
||||||
public string PreferredAudioLanguageCode { get; set; }
|
public string PreferredAudioLanguageCode { get; set; }
|
||||||
|
public string PreferredAudioTitle { get; set; }
|
||||||
public string PreferredSubtitleLanguageCode { get; set; }
|
public string PreferredSubtitleLanguageCode { get; set; }
|
||||||
public ChannelSubtitleMode? SubtitleMode { get; set; }
|
public ChannelSubtitleMode? SubtitleMode { get; set; }
|
||||||
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
|
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public abstract class ProgramScheduleItem
|
|||||||
public ChannelWatermark Watermark { get; set; }
|
public ChannelWatermark Watermark { get; set; }
|
||||||
public int? WatermarkId { get; set; }
|
public int? WatermarkId { get; set; }
|
||||||
public string PreferredAudioLanguageCode { get; set; }
|
public string PreferredAudioLanguageCode { get; set; }
|
||||||
|
public string PreferredAudioTitle { get; set; }
|
||||||
public string PreferredSubtitleLanguageCode { get; set; }
|
public string PreferredSubtitleLanguageCode { get; set; }
|
||||||
public ChannelSubtitleMode? SubtitleMode { get; set; }
|
public ChannelSubtitleMode? SubtitleMode { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using ErsatzTV.Core.Interfaces.Emby;
|
using ErsatzTV.Core.Interfaces.Emby;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -14,12 +15,12 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
|
|||||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||||
private readonly ILogger<EmbyCollectionScanner> _logger;
|
private readonly ILogger<EmbyCollectionScanner> _logger;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
public EmbyCollectionScanner(
|
public EmbyCollectionScanner(
|
||||||
IEmbyCollectionRepository embyCollectionRepository,
|
IEmbyCollectionRepository embyCollectionRepository,
|
||||||
IEmbyApiClient embyApiClient,
|
IEmbyApiClient embyApiClient,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
ILogger<EmbyCollectionScanner> logger)
|
ILogger<EmbyCollectionScanner> logger)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
|
|||||||
using ErsatzTV.Core.Interfaces.Emby;
|
using ErsatzTV.Core.Interfaces.Emby;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -25,7 +26,7 @@ public class EmbyMovieLibraryScanner :
|
|||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
IEmbyMovieRepository embyMovieRepository,
|
IEmbyMovieRepository embyMovieRepository,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IEmbyPathReplacementService pathReplacementService,
|
IEmbyPathReplacementService pathReplacementService,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Emby;
|
using ErsatzTV.Core.Interfaces.Emby;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Emby;
|
namespace ErsatzTV.Core.Emby;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
|
|||||||
using ErsatzTV.Core.Interfaces.Emby;
|
using ErsatzTV.Core.Interfaces.Emby;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -24,7 +25,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
|
|||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
IEmbyTelevisionRepository televisionRepository,
|
IEmbyTelevisionRepository televisionRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IEmbyPathReplacementService pathReplacementService,
|
IEmbyPathReplacementService pathReplacementService,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
|
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.3.44">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ public class FFmpegComplexFilterBuilder
|
|||||||
_videoDecoder.Contains("cuvid")),
|
_videoDecoder.Contains("cuvid")),
|
||||||
HardwareAccelerationKind.Qsv => !isSong,
|
HardwareAccelerationKind.Qsv => !isSong,
|
||||||
HardwareAccelerationKind.VideoToolbox => false,
|
HardwareAccelerationKind.VideoToolbox => false,
|
||||||
|
HardwareAccelerationKind.Amf => false,
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,6 +201,7 @@ public class FFmpegComplexFilterBuilder
|
|||||||
|
|
||||||
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None &&
|
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None &&
|
||||||
acceleration != HardwareAccelerationKind.VideoToolbox &&
|
acceleration != HardwareAccelerationKind.VideoToolbox &&
|
||||||
|
acceleration != HardwareAccelerationKind.Amf &&
|
||||||
!isHardwareDecode &&
|
!isHardwareDecode &&
|
||||||
(_deinterlace || _scaleToSize.IsSome);
|
(_deinterlace || _scaleToSize.IsSome);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using ErsatzTV.FFmpeg.Capabilities;
|
|||||||
using ErsatzTV.FFmpeg.Environment;
|
using ErsatzTV.FFmpeg.Environment;
|
||||||
using ErsatzTV.FFmpeg.Format;
|
using ErsatzTV.FFmpeg.Format;
|
||||||
using ErsatzTV.FFmpeg.OutputFormat;
|
using ErsatzTV.FFmpeg.OutputFormat;
|
||||||
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
using ErsatzTV.FFmpeg.State;
|
using ErsatzTV.FFmpeg.State;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
|
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
|
||||||
@@ -18,6 +19,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||||
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
|
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
|
||||||
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
|
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
|
||||||
|
private readonly IRuntimeInfo _runtimeInfo;
|
||||||
private readonly ILogger<FFmpegLibraryProcessService> _logger;
|
private readonly ILogger<FFmpegLibraryProcessService> _logger;
|
||||||
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
|
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
|
||||||
private readonly ITempFilePool _tempFilePool;
|
private readonly ITempFilePool _tempFilePool;
|
||||||
@@ -28,6 +30,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
IFFmpegStreamSelector ffmpegStreamSelector,
|
IFFmpegStreamSelector ffmpegStreamSelector,
|
||||||
ITempFilePool tempFilePool,
|
ITempFilePool tempFilePool,
|
||||||
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
|
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
|
||||||
|
IRuntimeInfo runtimeInfo,
|
||||||
ILogger<FFmpegLibraryProcessService> logger)
|
ILogger<FFmpegLibraryProcessService> logger)
|
||||||
{
|
{
|
||||||
_ffmpegProcessService = ffmpegProcessService;
|
_ffmpegProcessService = ffmpegProcessService;
|
||||||
@@ -35,6 +38,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
_ffmpegStreamSelector = ffmpegStreamSelector;
|
_ffmpegStreamSelector = ffmpegStreamSelector;
|
||||||
_tempFilePool = tempFilePool;
|
_tempFilePool = tempFilePool;
|
||||||
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
|
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
|
||||||
|
_runtimeInfo = runtimeInfo;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +53,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
string audioPath,
|
string audioPath,
|
||||||
List<Subtitle> subtitles,
|
List<Subtitle> subtitles,
|
||||||
string preferredAudioLanguage,
|
string preferredAudioLanguage,
|
||||||
|
string preferredAudioTitle,
|
||||||
string preferredSubtitleLanguage,
|
string preferredSubtitleLanguage,
|
||||||
ChannelSubtitleMode subtitleMode,
|
ChannelSubtitleMode subtitleMode,
|
||||||
DateTimeOffset start,
|
DateTimeOffset start,
|
||||||
@@ -58,6 +63,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
Option<ChannelWatermark> globalWatermark,
|
Option<ChannelWatermark> globalWatermark,
|
||||||
VaapiDriver vaapiDriver,
|
VaapiDriver vaapiDriver,
|
||||||
string vaapiDevice,
|
string vaapiDevice,
|
||||||
|
Option<int> qsvExtraHardwareFrames,
|
||||||
bool hlsRealtime,
|
bool hlsRealtime,
|
||||||
FillerKind fillerKind,
|
FillerKind fillerKind,
|
||||||
TimeSpan inPoint,
|
TimeSpan inPoint,
|
||||||
@@ -72,7 +78,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
audioVersion,
|
audioVersion,
|
||||||
channel.StreamingMode,
|
channel.StreamingMode,
|
||||||
channel.Number,
|
channel.Number,
|
||||||
preferredAudioLanguage);
|
preferredAudioLanguage,
|
||||||
|
preferredAudioTitle);
|
||||||
Option<Subtitle> maybeSubtitle =
|
Option<Subtitle> maybeSubtitle =
|
||||||
await _ffmpegStreamSelector.SelectSubtitleStream(
|
await _ffmpegStreamSelector.SelectSubtitleStream(
|
||||||
subtitles,
|
subtitles,
|
||||||
@@ -140,6 +147,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
videoStream.Codec,
|
videoStream.Codec,
|
||||||
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
|
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
|
||||||
new FrameSize(videoVersion.Width, videoVersion.Height),
|
new FrameSize(videoVersion.Width, videoVersion.Height),
|
||||||
|
videoVersion.DisplayAspectRatio,
|
||||||
videoVersion.RFrameRate,
|
videoVersion.RFrameRate,
|
||||||
videoPath != audioPath); // still image when paths are different
|
videoPath != audioPath); // still image when paths are different
|
||||||
|
|
||||||
@@ -209,6 +217,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
await playbackSettings.ScaledSize.Map(ss => new FrameSize(ss.Width, ss.Height))
|
await playbackSettings.ScaledSize.Map(ss => new FrameSize(ss.Width, ss.Height))
|
||||||
.IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)),
|
.IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)),
|
||||||
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
|
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
|
||||||
|
channel.FFmpegProfile.Resolution.Width == 640 ? "4:3" : "16:9",
|
||||||
playbackSettings.FrameRate,
|
playbackSettings.FrameRate,
|
||||||
playbackSettings.VideoBitrate,
|
playbackSettings.VideoBitrate,
|
||||||
playbackSettings.VideoBufferSize,
|
playbackSettings.VideoBufferSize,
|
||||||
@@ -231,11 +240,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
hlsPlaylistPath,
|
hlsPlaylistPath,
|
||||||
hlsSegmentTemplate,
|
hlsSegmentTemplate,
|
||||||
ptsOffset,
|
ptsOffset,
|
||||||
playbackSettings.ThreadCount);
|
playbackSettings.ThreadCount,
|
||||||
|
qsvExtraHardwareFrames);
|
||||||
|
|
||||||
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
|
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
|
||||||
|
|
||||||
var pipelineBuilder = new PipelineBuilder(
|
var pipelineBuilder = new PipelineBuilder(
|
||||||
|
_runtimeInfo,
|
||||||
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hwAccel),
|
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hwAccel),
|
||||||
videoInputFile,
|
videoInputFile,
|
||||||
audioInputFile,
|
audioInputFile,
|
||||||
@@ -258,7 +269,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
bool hlsRealtime,
|
bool hlsRealtime,
|
||||||
long ptsOffset,
|
long ptsOffset,
|
||||||
VaapiDriver vaapiDriver,
|
VaapiDriver vaapiDriver,
|
||||||
string vaapiDevice)
|
string vaapiDevice,
|
||||||
|
Option<int> qsvExtraHardwareFrames)
|
||||||
{
|
{
|
||||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateErrorSettings(
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateErrorSettings(
|
||||||
channel.StreamingMode,
|
channel.StreamingMode,
|
||||||
@@ -302,6 +314,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
new PixelFormatYuv420P(),
|
new PixelFormatYuv420P(),
|
||||||
new FrameSize(desiredResolution.Width, desiredResolution.Height),
|
new FrameSize(desiredResolution.Width, desiredResolution.Height),
|
||||||
new FrameSize(desiredResolution.Width, desiredResolution.Height),
|
new FrameSize(desiredResolution.Width, desiredResolution.Height),
|
||||||
|
desiredResolution.Width == 640 ? "4:3" : "16:9",
|
||||||
playbackSettings.FrameRate,
|
playbackSettings.FrameRate,
|
||||||
playbackSettings.VideoBitrate,
|
playbackSettings.VideoBitrate,
|
||||||
playbackSettings.VideoBufferSize,
|
playbackSettings.VideoBufferSize,
|
||||||
@@ -329,6 +342,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
VideoFormat.GeneratedImage,
|
VideoFormat.GeneratedImage,
|
||||||
new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p
|
new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p
|
||||||
new FrameSize(videoVersion.Width, videoVersion.Height),
|
new FrameSize(videoVersion.Width, videoVersion.Height),
|
||||||
|
videoVersion.DisplayAspectRatio,
|
||||||
None,
|
None,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
@@ -352,7 +366,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
hlsPlaylistPath,
|
hlsPlaylistPath,
|
||||||
hlsSegmentTemplate,
|
hlsSegmentTemplate,
|
||||||
ptsOffset,
|
ptsOffset,
|
||||||
Option<int>.None);
|
Option<int>.None,
|
||||||
|
qsvExtraHardwareFrames);
|
||||||
|
|
||||||
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video);
|
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video);
|
||||||
|
|
||||||
@@ -366,6 +381,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
_logger.LogDebug("FFmpeg desired error state {FrameState}", desiredState);
|
_logger.LogDebug("FFmpeg desired error state {FrameState}", desiredState);
|
||||||
|
|
||||||
var pipelineBuilder = new PipelineBuilder(
|
var pipelineBuilder = new PipelineBuilder(
|
||||||
|
_runtimeInfo,
|
||||||
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hwAccel),
|
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hwAccel),
|
||||||
videoInputFile,
|
videoInputFile,
|
||||||
audioInputFile,
|
audioInputFile,
|
||||||
@@ -394,6 +410,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
resolution);
|
resolution);
|
||||||
|
|
||||||
var pipelineBuilder = new PipelineBuilder(
|
var pipelineBuilder = new PipelineBuilder(
|
||||||
|
_runtimeInfo,
|
||||||
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None),
|
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -417,9 +434,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
{
|
{
|
||||||
var videoInputFile = new VideoInputFile(
|
var videoInputFile = new VideoInputFile(
|
||||||
inputFile,
|
inputFile,
|
||||||
new List<VideoStream> { new(0, string.Empty, None, FrameSize.Unknown, None, true) });
|
new List<VideoStream> { new(0, string.Empty, None, FrameSize.Unknown, string.Empty, None, true) });
|
||||||
|
|
||||||
var pipelineBuilder = new PipelineBuilder(
|
var pipelineBuilder = new PipelineBuilder(
|
||||||
|
_runtimeInfo,
|
||||||
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None),
|
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None),
|
||||||
videoInputFile,
|
videoInputFile,
|
||||||
None,
|
None,
|
||||||
@@ -497,6 +515,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
"unknown",
|
"unknown",
|
||||||
new PixelFormatUnknown(),
|
new PixelFormatUnknown(),
|
||||||
new FrameSize(1, 1),
|
new FrameSize(1, 1),
|
||||||
|
string.Empty,
|
||||||
Option<string>.None,
|
Option<string>.None,
|
||||||
!options.IsAnimated)
|
!options.IsAnimated)
|
||||||
},
|
},
|
||||||
@@ -620,6 +639,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|||||||
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
|
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
|
||||||
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,
|
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,
|
||||||
HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox,
|
HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox,
|
||||||
|
HardwareAccelerationKind.Amf => HardwareAccelerationMode.Amf,
|
||||||
_ => HardwareAccelerationMode.None
|
_ => HardwareAccelerationMode.None
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,14 +148,11 @@ public class FFmpegPlaybackSettingsCalculator
|
|||||||
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
||||||
result.AudioBufferSize = ffmpegProfile.AudioBufferSize;
|
result.AudioBufferSize = ffmpegProfile.AudioBufferSize;
|
||||||
|
|
||||||
audioStream.IfSome(
|
foreach (MediaStream _ in audioStream)
|
||||||
stream =>
|
{
|
||||||
{
|
// this can be optimized out later, depending on the audio codec
|
||||||
if (stream.Channels != ffmpegProfile.AudioChannels)
|
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||||
{
|
}
|
||||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
||||||
result.AudioDuration = outPoint - inPoint;
|
result.AudioDuration = outPoint - inPoint;
|
||||||
|
|||||||
@@ -28,13 +28,14 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
|
|||||||
MediaVersion version,
|
MediaVersion version,
|
||||||
StreamingMode streamingMode,
|
StreamingMode streamingMode,
|
||||||
string channelNumber,
|
string channelNumber,
|
||||||
string preferredAudioLanguage)
|
string preferredAudioLanguage,
|
||||||
|
string preferredAudioTitle)
|
||||||
{
|
{
|
||||||
if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
|
if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
|
||||||
string.IsNullOrWhiteSpace(preferredAudioLanguage))
|
string.IsNullOrWhiteSpace(preferredAudioLanguage) && string.IsNullOrWhiteSpace(preferredAudioTitle))
|
||||||
{
|
{
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Channel {Number} is HLS Direct with no preferred audio language; using all audio streams",
|
"Channel {Number} is HLS Direct with no preferred audio language or title; using all audio streams",
|
||||||
channelNumber);
|
channelNumber);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -71,18 +72,18 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
|
|||||||
if (correctLanguage.Any())
|
if (correctLanguage.Any())
|
||||||
{
|
{
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Found {Count} audio streams with preferred audio language code(s) {Code}; selecting stream with most channels",
|
"Found {Count} audio streams with preferred audio language code(s) {Code}",
|
||||||
correctLanguage.Count,
|
correctLanguage.Count,
|
||||||
allCodes);
|
allCodes);
|
||||||
|
|
||||||
return correctLanguage.OrderByDescending(s => s.Channels).Head();
|
return PrioritizeAudioTitle(correctLanguage, preferredAudioTitle ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Unable to find audio stream with preferred audio language code(s) {Code}; selecting stream with most channels",
|
"Unable to find audio stream with preferred audio language code(s) {Code}",
|
||||||
allCodes);
|
allCodes);
|
||||||
|
|
||||||
return audioStreams.OrderByDescending(s => s.Channels).HeadOrNone();
|
return PrioritizeAudioTitle(audioStreams, preferredAudioTitle ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Option<Subtitle>> SelectSubtitleStream(
|
public async Task<Option<Subtitle>> SelectSubtitleStream(
|
||||||
@@ -163,4 +164,34 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
|
|||||||
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Option<MediaStream> PrioritizeAudioTitle(IReadOnlyCollection<MediaStream> streams, string title)
|
||||||
|
{
|
||||||
|
// return correctLanguage.OrderByDescending(s => s.Channels).Head();
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No audio title has been specified; selecting stream with most channels");
|
||||||
|
return streams.OrderByDescending(s => s.Channels).Head();
|
||||||
|
}
|
||||||
|
|
||||||
|
// prioritize matching titles
|
||||||
|
var matchingTitle = streams
|
||||||
|
.Filter(ms => (ms.Title ?? string.Empty).Contains(title, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
if (matchingTitle.Any())
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Found {Count} audio streams with preferred title {Title}; selecting stream with most channels",
|
||||||
|
matchingTitle.Count,
|
||||||
|
title);
|
||||||
|
|
||||||
|
return matchingTitle.OrderByDescending(s => s.Channels).Head();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Unable to find audio stream with preferred title {Title}; selecting stream with most channels",
|
||||||
|
title);
|
||||||
|
|
||||||
|
return streams.OrderByDescending(s => s.Channels).Head();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public interface IFFmpegProcessService
|
|||||||
string audioPath,
|
string audioPath,
|
||||||
List<Subtitle> subtitles,
|
List<Subtitle> subtitles,
|
||||||
string preferredAudioLanguage,
|
string preferredAudioLanguage,
|
||||||
|
string preferredAudioTitle,
|
||||||
string preferredSubtitleLanguage,
|
string preferredSubtitleLanguage,
|
||||||
ChannelSubtitleMode subtitleMode,
|
ChannelSubtitleMode subtitleMode,
|
||||||
DateTimeOffset start,
|
DateTimeOffset start,
|
||||||
@@ -28,6 +29,7 @@ public interface IFFmpegProcessService
|
|||||||
Option<ChannelWatermark> globalWatermark,
|
Option<ChannelWatermark> globalWatermark,
|
||||||
VaapiDriver vaapiDriver,
|
VaapiDriver vaapiDriver,
|
||||||
string vaapiDevice,
|
string vaapiDevice,
|
||||||
|
Option<int> qsvExtraHardwareFrames,
|
||||||
bool hlsRealtime,
|
bool hlsRealtime,
|
||||||
FillerKind fillerKind,
|
FillerKind fillerKind,
|
||||||
TimeSpan inPoint,
|
TimeSpan inPoint,
|
||||||
@@ -44,7 +46,8 @@ public interface IFFmpegProcessService
|
|||||||
bool hlsRealtime,
|
bool hlsRealtime,
|
||||||
long ptsOffset,
|
long ptsOffset,
|
||||||
VaapiDriver vaapiDriver,
|
VaapiDriver vaapiDriver,
|
||||||
string vaapiDevice);
|
string vaapiDevice,
|
||||||
|
Option<int> qsvExtraHardwareFrames);
|
||||||
|
|
||||||
Task<Command> ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
Task<Command> ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ public interface IFFmpegStreamSelector
|
|||||||
MediaVersion version,
|
MediaVersion version,
|
||||||
StreamingMode streamingMode,
|
StreamingMode streamingMode,
|
||||||
string channelNumber,
|
string channelNumber,
|
||||||
string preferredAudioLanguage);
|
string preferredAudioLanguage,
|
||||||
|
string preferredAudioTitle);
|
||||||
|
|
||||||
Task<Option<Subtitle>> SelectSubtitleStream(
|
Task<Option<Subtitle>> SelectSubtitleStream(
|
||||||
List<Subtitle> subtitles,
|
List<Subtitle> subtitles,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
|
|
||||||
|
public interface ICachingSearchRepository : ISearchRepository
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Search;
|
using ErsatzTV.Core.Search;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Interfaces.Search;
|
namespace ErsatzTV.Core.Interfaces.Search;
|
||||||
@@ -9,15 +10,15 @@ public interface ISearchIndex : IDisposable
|
|||||||
{
|
{
|
||||||
public int Version { get; }
|
public int Version { get; }
|
||||||
Task<bool> Initialize(ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository);
|
Task<bool> Initialize(ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository);
|
||||||
Task<Unit> Rebuild(ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider);
|
Task<Unit> Rebuild(ICachingSearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider);
|
||||||
|
|
||||||
Task<Unit> RebuildItems(
|
Task<Unit> RebuildItems(
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
List<int> itemIds);
|
List<int> itemIds);
|
||||||
|
|
||||||
Task<Unit> UpdateItems(
|
Task<Unit> UpdateItems(
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
List<MediaItem> items);
|
List<MediaItem> items);
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public class ChannelGuide
|
|||||||
foreach ((Channel channel, List<PlayoutItem> sorted) in sortedChannelItems.OrderBy(
|
foreach ((Channel channel, List<PlayoutItem> sorted) in sortedChannelItems.OrderBy(
|
||||||
kvp => decimal.Parse(kvp.Key.Number)))
|
kvp => decimal.Parse(kvp.Key.Number)))
|
||||||
{
|
{
|
||||||
|
// skip all filler that isn't pre-roll
|
||||||
var i = 0;
|
var i = 0;
|
||||||
while (i < sorted.Count && sorted[i].FillerKind != FillerKind.None &&
|
while (i < sorted.Count && sorted[i].FillerKind != FillerKind.None &&
|
||||||
sorted[i].FillerKind != FillerKind.PreRoll)
|
sorted[i].FillerKind != FillerKind.PreRoll)
|
||||||
@@ -90,7 +91,7 @@ public class ChannelGuide
|
|||||||
{
|
{
|
||||||
PlayoutItem startItem = sorted[i];
|
PlayoutItem startItem = sorted[i];
|
||||||
int j = i;
|
int j = i;
|
||||||
while (j + 1 < sorted.Count && sorted[j].FillerKind != FillerKind.None)
|
while (sorted[j].FillerKind != FillerKind.None && j + 1 < sorted.Count)
|
||||||
{
|
{
|
||||||
j++;
|
j++;
|
||||||
}
|
}
|
||||||
@@ -98,7 +99,7 @@ public class ChannelGuide
|
|||||||
PlayoutItem displayItem = sorted[j];
|
PlayoutItem displayItem = sorted[j];
|
||||||
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
|
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
|
||||||
|
|
||||||
int finishIndex = i;
|
int finishIndex = j;
|
||||||
while (finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup)
|
while (finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup)
|
||||||
{
|
{
|
||||||
finishIndex++;
|
finishIndex++;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -14,12 +15,12 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
|
|||||||
private readonly IJellyfinCollectionRepository _jellyfinCollectionRepository;
|
private readonly IJellyfinCollectionRepository _jellyfinCollectionRepository;
|
||||||
private readonly ILogger<JellyfinCollectionScanner> _logger;
|
private readonly ILogger<JellyfinCollectionScanner> _logger;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
public JellyfinCollectionScanner(
|
public JellyfinCollectionScanner(
|
||||||
IJellyfinCollectionRepository jellyfinCollectionRepository,
|
IJellyfinCollectionRepository jellyfinCollectionRepository,
|
||||||
IJellyfinApiClient jellyfinApiClient,
|
IJellyfinApiClient jellyfinApiClient,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
ILogger<JellyfinCollectionScanner> logger)
|
ILogger<JellyfinCollectionScanner> logger)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
|
|||||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -24,7 +25,7 @@ public class JellyfinMovieLibraryScanner :
|
|||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IJellyfinMovieRepository jellyfinMovieRepository,
|
IJellyfinMovieRepository jellyfinMovieRepository,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IJellyfinPathReplacementService pathReplacementService,
|
IJellyfinPathReplacementService pathReplacementService,
|
||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Jellyfin;
|
namespace ErsatzTV.Core.Jellyfin;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
|
|||||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -25,7 +26,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
|
|||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
IJellyfinTelevisionRepository televisionRepository,
|
IJellyfinTelevisionRepository televisionRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IJellyfinPathReplacementService pathReplacementService,
|
IJellyfinPathReplacementService pathReplacementService,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
|
|||||||
@@ -33,8 +33,17 @@ public class LocalFileSystem : ILocalFileSystem
|
|||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime GetLastWriteTime(string path) =>
|
public DateTime GetLastWriteTime(string path)
|
||||||
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => SystemTime.MinValueUtc);
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return File.GetLastWriteTimeUtc(path);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return SystemTime.MinValueUtc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
|
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
|
||||||
Directory.Exists(libraryPath.Path);
|
Directory.Exists(libraryPath.Path);
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ public abstract class LocalFolderScanner
|
|||||||
|
|
||||||
public static readonly List<string> AudioFileExtensions = new()
|
public static readonly List<string> AudioFileExtensions = new()
|
||||||
{
|
{
|
||||||
".aac", ".alac", ".flac", ".mp3", ".m4a", ".wav", ".wma"
|
".aac", ".alac", ".dff", ".dsf", ".flac", ".mp3", ".m4a", ".ogg", ".opus", ".oga", ".ogx", ".spx", ".wav",
|
||||||
|
".wma"
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> ImageFileExtensions = new()
|
public static readonly List<string> ImageFileExtensions = new()
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ using System.Globalization;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Bugsnag;
|
using Bugsnag;
|
||||||
|
using CliWrap;
|
||||||
|
using CliWrap.Buffered;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Extensions;
|
using ErsatzTV.Core.Extensions;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
@@ -157,42 +159,48 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
|
|||||||
return await _metadataRepository.UpdateLocalStatistics(mediaItem, version) && durationChange;
|
return await _metadataRepository.UpdateLocalStatistics(mediaItem, version) && durationChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
|
private async Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
|
||||||
{
|
{
|
||||||
var startInfo = new ProcessStartInfo
|
string[] arguments =
|
||||||
{
|
{
|
||||||
FileName = ffprobePath,
|
"-hide_banner",
|
||||||
RedirectStandardOutput = true,
|
"-print_format", "json",
|
||||||
RedirectStandardError = true,
|
"-show_format",
|
||||||
UseShellExecute = false,
|
"-show_streams",
|
||||||
StandardOutputEncoding = Encoding.UTF8,
|
"-show_chapters",
|
||||||
StandardErrorEncoding = Encoding.UTF8
|
"-i", filePath
|
||||||
};
|
};
|
||||||
|
|
||||||
startInfo.ArgumentList.Add("-v");
|
BufferedCommandResult probe = await Cli.Wrap(ffprobePath)
|
||||||
startInfo.ArgumentList.Add("quiet");
|
.WithArguments(arguments)
|
||||||
startInfo.ArgumentList.Add("-print_format");
|
.WithValidation(CommandResultValidation.None)
|
||||||
startInfo.ArgumentList.Add("json");
|
.ExecuteBufferedAsync(Encoding.UTF8);
|
||||||
startInfo.ArgumentList.Add("-show_format");
|
|
||||||
startInfo.ArgumentList.Add("-show_streams");
|
|
||||||
startInfo.ArgumentList.Add("-show_chapters");
|
|
||||||
startInfo.ArgumentList.Add("-i");
|
|
||||||
startInfo.ArgumentList.Add(filePath);
|
|
||||||
|
|
||||||
var probe = new Process
|
if (probe.ExitCode != 0)
|
||||||
{
|
{
|
||||||
StartInfo = startInfo
|
return BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
|
||||||
};
|
}
|
||||||
|
|
||||||
probe.Start();
|
FFprobe ffprobe = JsonConvert.DeserializeObject<FFprobe>(probe.StandardOutput);
|
||||||
return probe.StandardOutput.ReadToEndAsync().MapAsync<string, Either<BaseError, FFprobe>>(
|
|
||||||
async output =>
|
if (ffprobe != null)
|
||||||
|
{
|
||||||
|
const string PATTERN = @"\[SAR\s+([0-9]+:[0-9]+)\s+DAR\s+([0-9]+:[0-9]+)\]";
|
||||||
|
Match match = Regex.Match(probe.StandardError, PATTERN);
|
||||||
|
if (match.Success)
|
||||||
{
|
{
|
||||||
await probe.WaitForExitAsync();
|
string sar = match.Groups[1].Value;
|
||||||
return probe.ExitCode == 0
|
string dar = match.Groups[2].Value;
|
||||||
? JsonConvert.DeserializeObject<FFprobe>(output)
|
foreach (FFprobeStream stream in ffprobe.streams.Where(s => s.codec_type == "video").ToList())
|
||||||
: BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
|
{
|
||||||
});
|
FFprobeStream replacement = stream with { sample_aspect_ratio = sar, display_aspect_ratio = dar };
|
||||||
|
ffprobe.streams.Remove(stream);
|
||||||
|
ffprobe.streams.Add(replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ffprobe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AnalyzeDuration(string ffmpegPath, string path, MediaVersion version)
|
private async Task AnalyzeDuration(string ffmpegPath, string path, MediaVersion version)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain.MediaServer;
|
|||||||
using ErsatzTV.Core.Errors;
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -22,7 +23,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
protected MediaServerMovieLibraryScanner(
|
protected MediaServerMovieLibraryScanner(
|
||||||
ILocalStatisticsProvider localStatisticsProvider,
|
ILocalStatisticsProvider localStatisticsProvider,
|
||||||
@@ -30,7 +31,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
|
|||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
ILogger logger)
|
ILogger logger)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain.MediaServer;
|
|||||||
using ErsatzTV.Core.Errors;
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -25,13 +26,13 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
protected MediaServerTelevisionLibraryScanner(
|
protected MediaServerTelevisionLibraryScanner(
|
||||||
ILocalStatisticsProvider localStatisticsProvider,
|
ILocalStatisticsProvider localStatisticsProvider,
|
||||||
ILocalSubtitlesProvider localSubtitlesProvider,
|
ILocalSubtitlesProvider localSubtitlesProvider,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
|
|||||||
using ErsatzTV.Core.Interfaces.Images;
|
using ErsatzTV.Core.Interfaces.Images;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -24,7 +25,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
|||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly IMovieRepository _movieRepository;
|
private readonly IMovieRepository _movieRepository;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
public MovieFolderScanner(
|
public MovieFolderScanner(
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
@@ -35,7 +36,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
|
|||||||
IMetadataRepository metadataRepository,
|
IMetadataRepository metadataRepository,
|
||||||
IImageCache imageCache,
|
IImageCache imageCache,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
IMediaItemRepository mediaItemRepository,
|
IMediaItemRepository mediaItemRepository,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
|
|||||||
using ErsatzTV.Core.Interfaces.Images;
|
using ErsatzTV.Core.Interfaces.Images;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -24,7 +25,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
|||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
public MusicVideoFolderScanner(
|
public MusicVideoFolderScanner(
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
@@ -34,7 +35,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
|
|||||||
IMetadataRepository metadataRepository,
|
IMetadataRepository metadataRepository,
|
||||||
IImageCache imageCache,
|
IImageCache imageCache,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IArtistRepository artistRepository,
|
IArtistRepository artistRepository,
|
||||||
IMusicVideoRepository musicVideoRepository,
|
IMusicVideoRepository musicVideoRepository,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
|
|||||||
using ErsatzTV.Core.Interfaces.Images;
|
using ErsatzTV.Core.Interfaces.Images;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -23,7 +24,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
|||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly IOtherVideoRepository _otherVideoRepository;
|
private readonly IOtherVideoRepository _otherVideoRepository;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
|
|
||||||
public OtherVideoFolderScanner(
|
public OtherVideoFolderScanner(
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
@@ -34,7 +35,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
|
|||||||
IImageCache imageCache,
|
IImageCache imageCache,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IOtherVideoRepository otherVideoRepository,
|
IOtherVideoRepository otherVideoRepository,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
|
|||||||
using ErsatzTV.Core.Interfaces.Images;
|
using ErsatzTV.Core.Interfaces.Images;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -22,7 +23,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
|||||||
private readonly ILogger<SongFolderScanner> _logger;
|
private readonly ILogger<SongFolderScanner> _logger;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
private readonly ISongRepository _songRepository;
|
private readonly ISongRepository _songRepository;
|
||||||
|
|
||||||
public SongFolderScanner(
|
public SongFolderScanner(
|
||||||
@@ -33,7 +34,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
|
|||||||
IImageCache imageCache,
|
IImageCache imageCache,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
ISongRepository songRepository,
|
ISongRepository songRepository,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
|
|||||||
using ErsatzTV.Core.Interfaces.Images;
|
using ErsatzTV.Core.Interfaces.Images;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -23,7 +24,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
|||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly IMetadataRepository _metadataRepository;
|
private readonly IMetadataRepository _metadataRepository;
|
||||||
private readonly ISearchIndex _searchIndex;
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly ISearchRepository _searchRepository;
|
private readonly ICachingSearchRepository _searchRepository;
|
||||||
private readonly ITelevisionRepository _televisionRepository;
|
private readonly ITelevisionRepository _televisionRepository;
|
||||||
|
|
||||||
public TelevisionFolderScanner(
|
public TelevisionFolderScanner(
|
||||||
@@ -36,7 +37,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
|
|||||||
IMetadataRepository metadataRepository,
|
IMetadataRepository metadataRepository,
|
||||||
IImageCache imageCache,
|
IImageCache imageCache,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
IMediaItemRepository mediaItemRepository,
|
IMediaItemRepository mediaItemRepository,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
|
|||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Plex;
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -27,7 +28,7 @@ public class PlexMovieLibraryScanner :
|
|||||||
IMovieRepository movieRepository,
|
IMovieRepository movieRepository,
|
||||||
IMetadataRepository metadataRepository,
|
IMetadataRepository metadataRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Plex;
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Plex;
|
namespace ErsatzTV.Core.Plex;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
|
|||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Plex;
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories.Caching;
|
||||||
using ErsatzTV.Core.Interfaces.Search;
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -27,7 +28,7 @@ public class PlexTelevisionLibraryScanner :
|
|||||||
ITelevisionRepository televisionRepository,
|
ITelevisionRepository televisionRepository,
|
||||||
IMetadataRepository metadataRepository,
|
IMetadataRepository metadataRepository,
|
||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ICachingSearchRepository searchRepository,
|
||||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
|
|||||||
@@ -423,9 +423,29 @@ public class PlayoutBuilder : IPlayoutBuilder
|
|||||||
var schedulerDuration = new PlayoutModeSchedulerDuration(_logger);
|
var schedulerDuration = new PlayoutModeSchedulerDuration(_logger);
|
||||||
var schedulerFlood = new PlayoutModeSchedulerFlood(_logger);
|
var schedulerFlood = new PlayoutModeSchedulerFlood(_logger);
|
||||||
|
|
||||||
|
var timeCount = new Dictionary<DateTimeOffset, int>();
|
||||||
|
|
||||||
// loop until we're done filling the desired amount of time
|
// loop until we're done filling the desired amount of time
|
||||||
while (playoutBuilderState.CurrentTime < playoutFinish)
|
while (playoutBuilderState.CurrentTime < playoutFinish)
|
||||||
{
|
{
|
||||||
|
if (timeCount.TryGetValue(playoutBuilderState.CurrentTime, out int count))
|
||||||
|
{
|
||||||
|
timeCount[playoutBuilderState.CurrentTime] = count + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
timeCount[playoutBuilderState.CurrentTime] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeCount[playoutBuilderState.CurrentTime] == 6)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Failed to schedule beyond {Time}; aborting playout build - this is a bug",
|
||||||
|
playoutBuilderState.CurrentTime);
|
||||||
|
|
||||||
|
throw new ApplicationException("Scheduling loop encountered");
|
||||||
|
}
|
||||||
|
|
||||||
// _logger.LogDebug("Playout time is {CurrentTime}", playoutBuilderState.CurrentTime);
|
// _logger.LogDebug("Playout time is {CurrentTime}", playoutBuilderState.CurrentTime);
|
||||||
|
|
||||||
// get the schedule item out of the sorted list
|
// get the schedule item out of the sorted list
|
||||||
@@ -626,9 +646,7 @@ public class PlayoutBuilder : IPlayoutBuilder
|
|||||||
&& a.MediaItemId == collectionKey.MediaItemId
|
&& a.MediaItemId == collectionKey.MediaItemId
|
||||||
&& a.AnchorDate is null);
|
&& a.AnchorDate is null);
|
||||||
|
|
||||||
var maybeEnumeratorState = collectionEnumerators.GroupBy(e => e.Key, e => e.Value.State).ToDictionary(
|
var maybeEnumeratorState = collectionEnumerators.ToDictionary(e => e.Key, e => e.Value.State);
|
||||||
mcs => mcs.Key,
|
|
||||||
mcs => mcs.Head());
|
|
||||||
|
|
||||||
PlayoutProgramScheduleAnchor scheduleAnchor = maybeExisting.Match(
|
PlayoutProgramScheduleAnchor scheduleAnchor = maybeExisting.Match(
|
||||||
existing =>
|
existing =>
|
||||||
@@ -658,10 +676,10 @@ public class PlayoutBuilder : IPlayoutBuilder
|
|||||||
result.Add(scheduleAnchor);
|
result.Add(scheduleAnchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (PlayoutProgramScheduleAnchor continueAnchor in playout.ProgramScheduleAnchors.Where(
|
foreach (PlayoutProgramScheduleAnchor checkpointAnchor in playout.ProgramScheduleAnchors.Where(
|
||||||
a => a.AnchorDate is not null))
|
a => a.AnchorDate is not null))
|
||||||
{
|
{
|
||||||
result.Add(continueAnchor);
|
result.Add(checkpointAnchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -675,7 +693,7 @@ public class PlayoutBuilder : IPlayoutBuilder
|
|||||||
bool randomStartPoint)
|
bool randomStartPoint)
|
||||||
{
|
{
|
||||||
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors
|
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors
|
||||||
.OrderByDescending(a => a.AnchorDate is null)
|
.OrderByDescending(a => a.AnchorDate ?? DateTime.MaxValue)
|
||||||
.FirstOrDefault(
|
.FirstOrDefault(
|
||||||
a => a.ProgramScheduleId == playout.ProgramScheduleId
|
a => a.ProgramScheduleId == playout.ProgramScheduleId
|
||||||
&& a.CollectionType == collectionKey.CollectionType
|
&& a.CollectionType == collectionKey.CollectionType
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
|
|||||||
CustomTitle = scheduleItem.CustomTitle,
|
CustomTitle = scheduleItem.CustomTitle,
|
||||||
WatermarkId = scheduleItem.WatermarkId,
|
WatermarkId = scheduleItem.WatermarkId,
|
||||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,12 +62,14 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
|
|||||||
CustomTitle = scheduleItem.CustomTitle,
|
CustomTitle = scheduleItem.CustomTitle,
|
||||||
WatermarkId = scheduleItem.WatermarkId,
|
WatermarkId = scheduleItem.WatermarkId,
|
||||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// never block scheduling when there is only one schedule item (with fixed start and flood)
|
||||||
DateTimeOffset peekScheduleItemStart =
|
DateTimeOffset peekScheduleItemStart =
|
||||||
peekScheduleItem.StartType == StartType.Fixed
|
scheduleItem.Id != peekScheduleItem.Id && peekScheduleItem.StartType == StartType.Fixed
|
||||||
? GetStartTimeAfter(nextState with { InFlood = false }, peekScheduleItem)
|
? GetStartTimeAfter(nextState with { InFlood = false }, peekScheduleItem)
|
||||||
: DateTimeOffset.MaxValue;
|
: DateTimeOffset.MaxValue;
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
|
|||||||
CustomTitle = scheduleItem.CustomTitle,
|
CustomTitle = scheduleItem.CustomTitle,
|
||||||
WatermarkId = scheduleItem.WatermarkId,
|
WatermarkId = scheduleItem.WatermarkId,
|
||||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
|
|||||||
CustomTitle = scheduleItem.CustomTitle,
|
CustomTitle = scheduleItem.CustomTitle,
|
||||||
WatermarkId = scheduleItem.WatermarkId,
|
WatermarkId = scheduleItem.WatermarkId,
|
||||||
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||||
|
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||||
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||||
SubtitleMode = scheduleItem.SubtitleMode
|
SubtitleMode = scheduleItem.SubtitleMode
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
|
||||||
<PackageReference Include="Moq" Version="4.18.1" />
|
<PackageReference Include="Moq" Version="4.18.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
|
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using ErsatzTV.FFmpeg.Capabilities;
|
|||||||
using ErsatzTV.FFmpeg.Encoder;
|
using ErsatzTV.FFmpeg.Encoder;
|
||||||
using ErsatzTV.FFmpeg.Format;
|
using ErsatzTV.FFmpeg.Format;
|
||||||
using ErsatzTV.FFmpeg.OutputFormat;
|
using ErsatzTV.FFmpeg.OutputFormat;
|
||||||
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
using ErsatzTV.FFmpeg.State;
|
using ErsatzTV.FFmpeg.State;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -25,7 +26,7 @@ public class PipelineGeneratorTests
|
|||||||
var videoInputFile = new VideoInputFile(
|
var videoInputFile = new VideoInputFile(
|
||||||
"/tmp/whatever.mkv",
|
"/tmp/whatever.mkv",
|
||||||
new List<VideoStream>
|
new List<VideoStream>
|
||||||
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24", false) });
|
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "16:9", "24", false) });
|
||||||
|
|
||||||
var audioInputFile = new AudioInputFile(
|
var audioInputFile = new AudioInputFile(
|
||||||
"/tmp/whatever.mkv",
|
"/tmp/whatever.mkv",
|
||||||
@@ -46,6 +47,7 @@ public class PipelineGeneratorTests
|
|||||||
new PixelFormatYuv420P(),
|
new PixelFormatYuv420P(),
|
||||||
new FrameSize(1920, 1080),
|
new FrameSize(1920, 1080),
|
||||||
new FrameSize(1920, 1080),
|
new FrameSize(1920, 1080),
|
||||||
|
"16:9",
|
||||||
Option<int>.None,
|
Option<int>.None,
|
||||||
2000,
|
2000,
|
||||||
4000,
|
4000,
|
||||||
@@ -68,9 +70,11 @@ public class PipelineGeneratorTests
|
|||||||
Option<string>.None,
|
Option<string>.None,
|
||||||
Option<string>.None,
|
Option<string>.None,
|
||||||
0,
|
0,
|
||||||
|
Option<int>.None,
|
||||||
Option<int>.None);
|
Option<int>.None);
|
||||||
|
|
||||||
var builder = new PipelineBuilder(
|
var builder = new PipelineBuilder(
|
||||||
|
new Mock<IRuntimeInfo>().Object,
|
||||||
new DefaultHardwareCapabilities(),
|
new DefaultHardwareCapabilities(),
|
||||||
videoInputFile,
|
videoInputFile,
|
||||||
audioInputFile,
|
audioInputFile,
|
||||||
@@ -86,7 +90,80 @@ public class PipelineGeneratorTests
|
|||||||
|
|
||||||
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
|
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
|
||||||
command.Should().Be(
|
command.Should().Be(
|
||||||
"-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -re -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:a aac -ac 2 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
"-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -re -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:a aac -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Aac_6_Channel_Should_Specify_Audio_Channels()
|
||||||
|
{
|
||||||
|
var videoInputFile = new VideoInputFile(
|
||||||
|
"/tmp/whatever.mkv",
|
||||||
|
new List<VideoStream>
|
||||||
|
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "16:9", "24", false) });
|
||||||
|
|
||||||
|
var audioInputFile = new AudioInputFile(
|
||||||
|
"/tmp/whatever.mkv",
|
||||||
|
new List<AudioStream> { new(1, AudioFormat.Aac, 6) },
|
||||||
|
new AudioState(
|
||||||
|
AudioFormat.Aac,
|
||||||
|
6,
|
||||||
|
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),
|
||||||
|
"16:9",
|
||||||
|
Option<int>.None,
|
||||||
|
2000,
|
||||||
|
4000,
|
||||||
|
90_000,
|
||||||
|
false);
|
||||||
|
|
||||||
|
var ffmpegState = new FFmpegState(
|
||||||
|
false,
|
||||||
|
HardwareAccelerationMode.None,
|
||||||
|
HardwareAccelerationMode.None,
|
||||||
|
Option<string>.None,
|
||||||
|
Option<string>.None,
|
||||||
|
TimeSpan.FromSeconds(1),
|
||||||
|
Option<TimeSpan>.None,
|
||||||
|
false,
|
||||||
|
Option<string>.None,
|
||||||
|
Option<string>.None,
|
||||||
|
Option<string>.None,
|
||||||
|
OutputFormatKind.MpegTs,
|
||||||
|
Option<string>.None,
|
||||||
|
Option<string>.None,
|
||||||
|
0,
|
||||||
|
Option<int>.None,
|
||||||
|
Option<int>.None);
|
||||||
|
|
||||||
|
var builder = new PipelineBuilder(
|
||||||
|
new Mock<IRuntimeInfo>().Object,
|
||||||
|
new DefaultHardwareCapabilities(),
|
||||||
|
videoInputFile,
|
||||||
|
audioInputFile,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
_logger);
|
||||||
|
FFmpegPipeline result = builder.Build(ffmpegState, desiredState);
|
||||||
|
|
||||||
|
result.PipelineSteps.Should().HaveCountGreaterThan(0);
|
||||||
|
result.PipelineSteps.Should().Contain(ps => ps is EncoderLibx265);
|
||||||
|
|
||||||
|
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
|
||||||
|
command.Should().Be(
|
||||||
|
"-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -re -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:a aac -ac 6 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -95,7 +172,16 @@ public class PipelineGeneratorTests
|
|||||||
var resolution = new FrameSize(1920, 1080);
|
var resolution = new FrameSize(1920, 1080);
|
||||||
var concatInputFile = new ConcatInputFile("http://localhost:8080/ffmpeg/concat/1", resolution);
|
var concatInputFile = new ConcatInputFile("http://localhost:8080/ffmpeg/concat/1", resolution);
|
||||||
|
|
||||||
var builder = new PipelineBuilder(new DefaultHardwareCapabilities(), None, None, None, None, "", "", _logger);
|
var builder = new PipelineBuilder(
|
||||||
|
new Mock<IRuntimeInfo>().Object,
|
||||||
|
new DefaultHardwareCapabilities(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
_logger);
|
||||||
FFmpegPipeline result = builder.Concat(concatInputFile, FFmpegState.Concat(false, "Some Channel"));
|
FFmpegPipeline result = builder.Concat(concatInputFile, FFmpegState.Concat(false, "Some Channel"));
|
||||||
|
|
||||||
result.PipelineSteps.Should().HaveCountGreaterThan(0);
|
result.PipelineSteps.Should().HaveCountGreaterThan(0);
|
||||||
@@ -112,7 +198,7 @@ public class PipelineGeneratorTests
|
|||||||
var videoInputFile = new VideoInputFile(
|
var videoInputFile = new VideoInputFile(
|
||||||
"/tmp/whatever.mkv",
|
"/tmp/whatever.mkv",
|
||||||
new List<VideoStream>
|
new List<VideoStream>
|
||||||
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24", false) });
|
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "16:9", "24", false) });
|
||||||
|
|
||||||
var audioInputFile = new AudioInputFile(
|
var audioInputFile = new AudioInputFile(
|
||||||
"/tmp/whatever.mkv",
|
"/tmp/whatever.mkv",
|
||||||
@@ -133,6 +219,7 @@ public class PipelineGeneratorTests
|
|||||||
new PixelFormatYuv420P(),
|
new PixelFormatYuv420P(),
|
||||||
new FrameSize(1920, 1080),
|
new FrameSize(1920, 1080),
|
||||||
new FrameSize(1920, 1080),
|
new FrameSize(1920, 1080),
|
||||||
|
"16:9",
|
||||||
Option<int>.None,
|
Option<int>.None,
|
||||||
2000,
|
2000,
|
||||||
4000,
|
4000,
|
||||||
@@ -155,9 +242,11 @@ public class PipelineGeneratorTests
|
|||||||
Option<string>.None,
|
Option<string>.None,
|
||||||
Option<string>.None,
|
Option<string>.None,
|
||||||
0,
|
0,
|
||||||
|
Option<int>.None,
|
||||||
Option<int>.None);
|
Option<int>.None);
|
||||||
|
|
||||||
var builder = new PipelineBuilder(
|
var builder = new PipelineBuilder(
|
||||||
|
new Mock<IRuntimeInfo>().Object,
|
||||||
new DefaultHardwareCapabilities(),
|
new DefaultHardwareCapabilities(),
|
||||||
videoInputFile,
|
videoInputFile,
|
||||||
audioInputFile,
|
audioInputFile,
|
||||||
@@ -178,6 +267,71 @@ public class PipelineGeneratorTests
|
|||||||
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void HlsDirect_Test_All_Audio_Streams()
|
||||||
|
{
|
||||||
|
var videoInputFile = new VideoInputFile(
|
||||||
|
"/tmp/whatever.mkv",
|
||||||
|
new List<VideoStream>
|
||||||
|
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "16:9", "24", false) });
|
||||||
|
|
||||||
|
Option<AudioInputFile> audioInputFile = Option<AudioInputFile>.None;
|
||||||
|
|
||||||
|
var desiredState = new FrameState(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
VideoFormat.Copy,
|
||||||
|
new PixelFormatYuv420P(),
|
||||||
|
new FrameSize(1920, 1080),
|
||||||
|
new FrameSize(1920, 1080),
|
||||||
|
"16:9",
|
||||||
|
Option<int>.None,
|
||||||
|
2000,
|
||||||
|
4000,
|
||||||
|
90_000,
|
||||||
|
false);
|
||||||
|
|
||||||
|
var ffmpegState = new FFmpegState(
|
||||||
|
false,
|
||||||
|
HardwareAccelerationMode.None,
|
||||||
|
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,
|
||||||
|
Option<int>.None,
|
||||||
|
Option<int>.None);
|
||||||
|
|
||||||
|
var builder = new PipelineBuilder(
|
||||||
|
new Mock<IRuntimeInfo>().Object,
|
||||||
|
new DefaultHardwareCapabilities(),
|
||||||
|
videoInputFile,
|
||||||
|
audioInputFile,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
_logger);
|
||||||
|
FFmpegPipeline result = builder.Build(ffmpegState, desiredState);
|
||||||
|
|
||||||
|
result.PipelineSteps.Should().HaveCountGreaterThan(0);
|
||||||
|
result.PipelineSteps.Should().Contain(ps => ps is EncoderCopyVideo);
|
||||||
|
result.PipelineSteps.Should().Contain(ps => ps is EncoderCopyAudio);
|
||||||
|
|
||||||
|
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
|
||||||
|
|
||||||
|
command.Should().Be(
|
||||||
|
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -i /tmp/whatever.mkv -map 0:a -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Resize_Image_Test()
|
public void Resize_Image_Test()
|
||||||
{
|
{
|
||||||
@@ -187,10 +341,11 @@ public class PipelineGeneratorTests
|
|||||||
"/test/input/file.png",
|
"/test/input/file.png",
|
||||||
new List<VideoStream>
|
new List<VideoStream>
|
||||||
{
|
{
|
||||||
new(0, string.Empty, Option<IPixelFormat>.None, FrameSize.Unknown, Option<string>.None, true)
|
new(0, string.Empty, Option<IPixelFormat>.None, FrameSize.Unknown, string.Empty, Option<string>.None, true)
|
||||||
});
|
});
|
||||||
|
|
||||||
var pipelineBuilder = new PipelineBuilder(
|
var pipelineBuilder = new PipelineBuilder(
|
||||||
|
new Mock<IRuntimeInfo>().Object,
|
||||||
new DefaultHardwareCapabilities(),
|
new DefaultHardwareCapabilities(),
|
||||||
videoInputFile,
|
videoInputFile,
|
||||||
Option<AudioInputFile>.None,
|
Option<AudioInputFile>.None,
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
|||||||
|
|
||||||
public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
||||||
{
|
{
|
||||||
private const string CacheKey = "ffmpeg.hardware.nvidia.architecture";
|
private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture";
|
||||||
|
private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model";
|
||||||
private readonly ILogger<HardwareCapabilitiesFactory> _logger;
|
private readonly ILogger<HardwareCapabilitiesFactory> _logger;
|
||||||
|
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
@@ -31,9 +32,10 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
|||||||
|
|
||||||
private async Task<IHardwareCapabilities> GetNvidiaCapabilities(string ffmpegPath)
|
private async Task<IHardwareCapabilities> GetNvidiaCapabilities(string ffmpegPath)
|
||||||
{
|
{
|
||||||
if (_memoryCache.TryGetValue(CacheKey, out int cachedArchitecture))
|
if (_memoryCache.TryGetValue(ArchitectureCacheKey, out int cachedArchitecture)
|
||||||
|
&& _memoryCache.TryGetValue(ModelCacheKey, out string cachedModel))
|
||||||
{
|
{
|
||||||
return new NvidiaHardwareCapabilities(cachedArchitecture);
|
return new NvidiaHardwareCapabilities(cachedArchitecture, cachedModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] arguments =
|
string[] arguments =
|
||||||
@@ -57,13 +59,20 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
|||||||
Option<string> maybeLine = Optional(output.Split("\n").FirstOrDefault(x => x.Contains("GPU")));
|
Option<string> maybeLine = Optional(output.Split("\n").FirstOrDefault(x => x.Contains("GPU")));
|
||||||
foreach (string line in maybeLine)
|
foreach (string line in maybeLine)
|
||||||
{
|
{
|
||||||
const string PATTERN = @"SM\s+(\d\.\d)";
|
const string ARCHITECTURE_PATTERN = @"SM\s+(\d\.\d)";
|
||||||
Match match = Regex.Match(line, PATTERN);
|
Match match = Regex.Match(line, ARCHITECTURE_PATTERN);
|
||||||
if (match.Success && int.TryParse(match.Groups[1].Value.Replace(".", string.Empty), out int architecture))
|
if (match.Success && int.TryParse(match.Groups[1].Value.Replace(".", string.Empty), out int architecture))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Detected NVIDIA GPU architecture SM {Architecture}", architecture);
|
const string MODEL_PATTERN = @"(GTX\s+[0-9a-zA-Z]+[\sTtIi]+)";
|
||||||
_memoryCache.Set(CacheKey, architecture);
|
Match modelMatch = Regex.Match(line, MODEL_PATTERN);
|
||||||
return new NvidiaHardwareCapabilities(architecture);
|
string model = modelMatch.Success ? modelMatch.Groups[1].Value.Trim() : "unknown";
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Detected NVIDIA GPU model {Model} architecture SM {Architecture}",
|
||||||
|
model,
|
||||||
|
architecture);
|
||||||
|
_memoryCache.Set(ArchitectureCacheKey, architecture);
|
||||||
|
_memoryCache.Set(ModelCacheKey, model);
|
||||||
|
return new NvidiaHardwareCapabilities(architecture, model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
|||||||
public class NvidiaHardwareCapabilities : IHardwareCapabilities
|
public class NvidiaHardwareCapabilities : IHardwareCapabilities
|
||||||
{
|
{
|
||||||
private readonly int _architecture;
|
private readonly int _architecture;
|
||||||
|
private readonly List<string> _maxwellGm206 = new() { "GTX 750", "GTX 950", "GTX 960", "GTX 965M" };
|
||||||
|
private readonly string _model;
|
||||||
|
|
||||||
public NvidiaHardwareCapabilities(int architecture) => _architecture = architecture;
|
public NvidiaHardwareCapabilities(int architecture, string model)
|
||||||
|
{
|
||||||
|
_architecture = architecture;
|
||||||
|
_model = model;
|
||||||
|
}
|
||||||
|
|
||||||
public bool CanDecode(string videoFormat, Option<IPixelFormat> maybePixelFormat)
|
public bool CanDecode(string videoFormat, Option<IPixelFormat> maybePixelFormat)
|
||||||
{
|
{
|
||||||
@@ -14,14 +20,14 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities
|
|||||||
|
|
||||||
return videoFormat switch
|
return videoFormat switch
|
||||||
{
|
{
|
||||||
// second gen maxwell is required to decode hevc
|
// some second gen maxwell can decode hevc, otherwise pascal is required
|
||||||
VideoFormat.Hevc => _architecture >= 52,
|
VideoFormat.Hevc => _architecture == 52 && _maxwellGm206.Contains(_model) || _architecture >= 60,
|
||||||
|
|
||||||
// pascal is required to decode vp9 10-bit
|
// pascal is required to decode vp9 10-bit
|
||||||
VideoFormat.Vp9 when bitDepth == 10 => _architecture >= 60,
|
VideoFormat.Vp9 when bitDepth == 10 => _architecture >= 60,
|
||||||
|
|
||||||
// second gen maxwell is required to decode vp9
|
// some second gen maxwell can decode vp9, otherwise pascal is required
|
||||||
VideoFormat.Vp9 => _architecture >= 52,
|
VideoFormat.Vp9 => _architecture == 52 && _maxwellGm206.Contains(_model) || _architecture >= 60,
|
||||||
|
|
||||||
_ => true
|
_ => true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using ErsatzTV.FFmpeg.Format;
|
||||||
|
|
||||||
|
namespace ErsatzTV.FFmpeg.Encoder.Amf;
|
||||||
|
|
||||||
|
public class EncoderH264Amf : EncoderBase
|
||||||
|
{
|
||||||
|
public override string Name => "h264_amf";
|
||||||
|
public override StreamKind Kind => StreamKind.Video;
|
||||||
|
|
||||||
|
public override FrameState NextState(FrameState currentState) => currentState with
|
||||||
|
{
|
||||||
|
VideoFormat = VideoFormat.H264,
|
||||||
|
FrameDataLocation = FrameDataLocation.Hardware
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using ErsatzTV.FFmpeg.Format;
|
||||||
|
|
||||||
|
namespace ErsatzTV.FFmpeg.Encoder.Amf;
|
||||||
|
|
||||||
|
public class EncoderHevcAmf : EncoderBase
|
||||||
|
{
|
||||||
|
public override string Name => "hevc_amf";
|
||||||
|
public override StreamKind Kind => StreamKind.Video;
|
||||||
|
|
||||||
|
public override FrameState NextState(FrameState currentState) => currentState with
|
||||||
|
{
|
||||||
|
VideoFormat = VideoFormat.Hevc,
|
||||||
|
FrameDataLocation = FrameDataLocation.Hardware
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using ErsatzTV.FFmpeg.Capabilities;
|
using ErsatzTV.FFmpeg.Capabilities;
|
||||||
|
using ErsatzTV.FFmpeg.Encoder.Amf;
|
||||||
using ErsatzTV.FFmpeg.Encoder.Nvenc;
|
using ErsatzTV.FFmpeg.Encoder.Nvenc;
|
||||||
using ErsatzTV.FFmpeg.Encoder.Qsv;
|
using ErsatzTV.FFmpeg.Encoder.Qsv;
|
||||||
using ErsatzTV.FFmpeg.Encoder.Vaapi;
|
using ErsatzTV.FFmpeg.Encoder.Vaapi;
|
||||||
@@ -42,14 +43,16 @@ public static class AvailableEncoders
|
|||||||
new EncoderHevcQsv(
|
new EncoderHevcQsv(
|
||||||
currentState,
|
currentState,
|
||||||
maybeWatermarkInputFile,
|
maybeWatermarkInputFile,
|
||||||
maybeSubtitleInputFile),
|
maybeSubtitleInputFile,
|
||||||
|
ffmpegState.QsvExtraHardwareFrames),
|
||||||
(HardwareAccelerationMode.Qsv, VideoFormat.H264) when hardwareCapabilities.CanEncode(
|
(HardwareAccelerationMode.Qsv, VideoFormat.H264) when hardwareCapabilities.CanEncode(
|
||||||
VideoFormat.H264,
|
VideoFormat.H264,
|
||||||
desiredState.PixelFormat) =>
|
desiredState.PixelFormat) =>
|
||||||
new EncoderH264Qsv(
|
new EncoderH264Qsv(
|
||||||
currentState,
|
currentState,
|
||||||
maybeWatermarkInputFile,
|
maybeWatermarkInputFile,
|
||||||
maybeSubtitleInputFile),
|
maybeSubtitleInputFile,
|
||||||
|
ffmpegState.QsvExtraHardwareFrames),
|
||||||
|
|
||||||
(HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) when hardwareCapabilities.CanEncode(
|
(HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) when hardwareCapabilities.CanEncode(
|
||||||
VideoFormat.Hevc,
|
VideoFormat.Hevc,
|
||||||
@@ -72,6 +75,13 @@ public static class AvailableEncoders
|
|||||||
(HardwareAccelerationMode.VideoToolbox, VideoFormat.H264) when hardwareCapabilities.CanEncode(
|
(HardwareAccelerationMode.VideoToolbox, VideoFormat.H264) when hardwareCapabilities.CanEncode(
|
||||||
VideoFormat.H264,
|
VideoFormat.H264,
|
||||||
desiredState.PixelFormat) => new EncoderH264VideoToolbox(),
|
desiredState.PixelFormat) => new EncoderH264VideoToolbox(),
|
||||||
|
|
||||||
|
(HardwareAccelerationMode.Amf, VideoFormat.Hevc) when hardwareCapabilities.CanEncode(
|
||||||
|
VideoFormat.Hevc,
|
||||||
|
desiredState.PixelFormat) => new EncoderHevcAmf(),
|
||||||
|
(HardwareAccelerationMode.Amf, VideoFormat.H264) when hardwareCapabilities.CanEncode(
|
||||||
|
VideoFormat.H264,
|
||||||
|
desiredState.PixelFormat) => new EncoderH264Amf(),
|
||||||
|
|
||||||
(_, VideoFormat.Hevc) => new EncoderLibx265(currentState),
|
(_, VideoFormat.Hevc) => new EncoderLibx265(currentState),
|
||||||
(_, VideoFormat.H264) => new EncoderLibx264(),
|
(_, VideoFormat.H264) => new EncoderLibx264(),
|
||||||
|
|||||||
@@ -6,21 +6,26 @@ public class EncoderH264Qsv : EncoderBase
|
|||||||
{
|
{
|
||||||
private readonly FrameState _currentState;
|
private readonly FrameState _currentState;
|
||||||
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
|
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
|
||||||
|
private readonly int _extraHardwareFrames;
|
||||||
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
|
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
|
||||||
|
|
||||||
public EncoderH264Qsv(
|
public EncoderH264Qsv(
|
||||||
FrameState currentState,
|
FrameState currentState,
|
||||||
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
||||||
Option<SubtitleInputFile> maybeSubtitleInputFile)
|
Option<SubtitleInputFile> maybeSubtitleInputFile,
|
||||||
|
int extraHardwareFrames)
|
||||||
{
|
{
|
||||||
_currentState = currentState;
|
_currentState = currentState;
|
||||||
_maybeWatermarkInputFile = maybeWatermarkInputFile;
|
_maybeWatermarkInputFile = maybeWatermarkInputFile;
|
||||||
_maybeSubtitleInputFile = maybeSubtitleInputFile;
|
_maybeSubtitleInputFile = maybeSubtitleInputFile;
|
||||||
|
_extraHardwareFrames = extraHardwareFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Name => "h264_qsv";
|
public override string Name => "h264_qsv";
|
||||||
public override StreamKind Kind => StreamKind.Video;
|
public override StreamKind Kind => StreamKind.Video;
|
||||||
public override IList<string> OutputOptions => new[] { "-c:v", "h264_qsv", "-low_power", "0" };
|
|
||||||
|
public override IList<string> OutputOptions =>
|
||||||
|
new[] { "-c:v", "h264_qsv", "-low_power", "0", "-look_ahead", "0" };
|
||||||
|
|
||||||
// need to upload if we're still in software and a watermark is used
|
// need to upload if we're still in software and a watermark is used
|
||||||
public override string Filter
|
public override string Filter
|
||||||
@@ -37,11 +42,11 @@ public class EncoderH264Qsv : EncoderBase
|
|||||||
// pixel format should already be converted to a supported format by QsvHardwareAccelerationOption
|
// pixel format should already be converted to a supported format by QsvHardwareAccelerationOption
|
||||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
|
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
|
||||||
{
|
{
|
||||||
return $"format={pixelFormat.FFmpegName},hwupload=extra_hw_frames=64";
|
return $"format={pixelFormat.FFmpegName},hwupload=extra_hw_frames={_extraHardwareFrames}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// default to nv12
|
// default to nv12
|
||||||
return "format=nv12,hwupload=extra_hw_frames=64";
|
return $"format=nv12,hwupload=extra_hw_frames={_extraHardwareFrames}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,21 +6,26 @@ public class EncoderHevcQsv : EncoderBase
|
|||||||
{
|
{
|
||||||
private readonly FrameState _currentState;
|
private readonly FrameState _currentState;
|
||||||
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
|
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
|
||||||
|
private readonly int _extraHardwareFrames;
|
||||||
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
|
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
|
||||||
|
|
||||||
public EncoderHevcQsv(
|
public EncoderHevcQsv(
|
||||||
FrameState currentState,
|
FrameState currentState,
|
||||||
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
||||||
Option<SubtitleInputFile> maybeSubtitleInputFile)
|
Option<SubtitleInputFile> maybeSubtitleInputFile,
|
||||||
|
int extraHardwareFrames)
|
||||||
{
|
{
|
||||||
_currentState = currentState;
|
_currentState = currentState;
|
||||||
_maybeWatermarkInputFile = maybeWatermarkInputFile;
|
_maybeWatermarkInputFile = maybeWatermarkInputFile;
|
||||||
_maybeSubtitleInputFile = maybeSubtitleInputFile;
|
_maybeSubtitleInputFile = maybeSubtitleInputFile;
|
||||||
|
_extraHardwareFrames = extraHardwareFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Name => "hevc_qsv";
|
public override string Name => "hevc_qsv";
|
||||||
public override StreamKind Kind => StreamKind.Video;
|
public override StreamKind Kind => StreamKind.Video;
|
||||||
public override IList<string> OutputOptions => new[] { "-c:v", "hevc_qsv", "-low_power", "0" };
|
|
||||||
|
public override IList<string> OutputOptions =>
|
||||||
|
new[] { "-c:v", "hevc_qsv", "-low_power", "0", "-look_head", "0" };
|
||||||
|
|
||||||
// need to upload if we're still in software and a watermark is used
|
// need to upload if we're still in software and a watermark is used
|
||||||
public override string Filter
|
public override string Filter
|
||||||
@@ -37,11 +42,11 @@ public class EncoderHevcQsv : EncoderBase
|
|||||||
// pixel format should already be converted to a supported format by QsvHardwareAccelerationOption
|
// pixel format should already be converted to a supported format by QsvHardwareAccelerationOption
|
||||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
|
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
|
||||||
{
|
{
|
||||||
return $"format={pixelFormat.FFmpegName},hwupload=extra_hw_frames=64";
|
return $"format={pixelFormat.FFmpegName},hwupload=extra_hw_frames={_extraHardwareFrames}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// default to nv12
|
// default to nv12
|
||||||
return "format=nv12,hwupload=extra_hw_frames=64";
|
return $"format=nv12,hwupload=extra_hw_frames={_extraHardwareFrames}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliWrap" Version="3.4.4" />
|
<PackageReference Include="CliWrap" Version="3.5.0" />
|
||||||
<PackageReference Include="LanguageExt.Core" Version="4.2.9" />
|
<PackageReference Include="LanguageExt.Core" Version="4.2.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ public record FFmpegState(
|
|||||||
Option<string> HlsPlaylistPath,
|
Option<string> HlsPlaylistPath,
|
||||||
Option<string> HlsSegmentTemplate,
|
Option<string> HlsSegmentTemplate,
|
||||||
long PtsOffset,
|
long PtsOffset,
|
||||||
Option<int> ThreadCount)
|
Option<int> ThreadCount,
|
||||||
|
Option<int> MaybeQsvExtraHardwareFrames)
|
||||||
{
|
{
|
||||||
|
public int QsvExtraHardwareFrames => MaybeQsvExtraHardwareFrames.IfNone(64);
|
||||||
|
|
||||||
public static FFmpegState Concat(bool saveReport, string channelName) =>
|
public static FFmpegState Concat(bool saveReport, string channelName) =>
|
||||||
new(
|
new(
|
||||||
saveReport,
|
saveReport,
|
||||||
@@ -37,5 +40,6 @@ public record FFmpegState(
|
|||||||
Option<string>.None,
|
Option<string>.None,
|
||||||
Option<string>.None,
|
Option<string>.None,
|
||||||
0,
|
0,
|
||||||
|
Option<int>.None,
|
||||||
Option<int>.None);
|
Option<int>.None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
using ErsatzTV.FFmpeg.Filter.Cuda;
|
using ErsatzTV.FFmpeg.Filter.Cuda;
|
||||||
using ErsatzTV.FFmpeg.Filter.Qsv;
|
using ErsatzTV.FFmpeg.Filter.Qsv;
|
||||||
using ErsatzTV.FFmpeg.Filter.Vaapi;
|
using ErsatzTV.FFmpeg.Filter.Vaapi;
|
||||||
|
using ErsatzTV.FFmpeg.Runtime;
|
||||||
|
|
||||||
namespace ErsatzTV.FFmpeg.Filter;
|
namespace ErsatzTV.FFmpeg.Filter;
|
||||||
|
|
||||||
public static class AvailableScaleFilters
|
public static class AvailableScaleFilters
|
||||||
{
|
{
|
||||||
public static IPipelineFilterStep ForAcceleration(
|
public static IPipelineFilterStep ForAcceleration(
|
||||||
|
IRuntimeInfo runtimeInfo,
|
||||||
HardwareAccelerationMode accelMode,
|
HardwareAccelerationMode accelMode,
|
||||||
FrameState currentState,
|
FrameState currentState,
|
||||||
FrameSize scaledSize,
|
FrameSize scaledSize,
|
||||||
FrameSize paddedSize) =>
|
FrameSize paddedSize,
|
||||||
|
int extraHardwareFrames) =>
|
||||||
accelMode switch
|
accelMode switch
|
||||||
{
|
{
|
||||||
HardwareAccelerationMode.Nvenc => new ScaleCudaFilter(currentState, scaledSize, paddedSize),
|
HardwareAccelerationMode.Nvenc => new ScaleCudaFilter(currentState, scaledSize, paddedSize),
|
||||||
HardwareAccelerationMode.Qsv when currentState.FrameDataLocation == FrameDataLocation.Hardware ||
|
HardwareAccelerationMode.Qsv when currentState.FrameDataLocation == FrameDataLocation.Hardware ||
|
||||||
scaledSize == paddedSize =>
|
scaledSize == paddedSize =>
|
||||||
new ScaleQsvFilter(currentState, scaledSize),
|
new ScaleQsvFilter(runtimeInfo, currentState, scaledSize, paddedSize, extraHardwareFrames),
|
||||||
HardwareAccelerationMode.Vaapi => new ScaleVaapiFilter(currentState, scaledSize, paddedSize),
|
HardwareAccelerationMode.Vaapi => new ScaleVaapiFilter(currentState, scaledSize, paddedSize),
|
||||||
_ => new ScaleFilter(currentState, scaledSize, paddedSize)
|
_ => new ScaleFilter(currentState, scaledSize, paddedSize)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using ErsatzTV.FFmpeg.Filter.Cuda;
|
||||||
|
using ErsatzTV.FFmpeg.Filter.Qsv;
|
||||||
|
|
||||||
|
namespace ErsatzTV.FFmpeg.Filter;
|
||||||
|
|
||||||
|
public static class AvailableSubtitleScaleFilters
|
||||||
|
{
|
||||||
|
public static IPipelineFilterStep ForAcceleration(
|
||||||
|
HardwareAccelerationMode accelMode,
|
||||||
|
FrameState currentState,
|
||||||
|
FrameSize scaledSize,
|
||||||
|
FrameSize paddedSize,
|
||||||
|
int extraHardwareFrames) =>
|
||||||
|
accelMode switch
|
||||||
|
{
|
||||||
|
HardwareAccelerationMode.Nvenc => new SubtitleScaleNppFilter(currentState, scaledSize, paddedSize),
|
||||||
|
HardwareAccelerationMode.Qsv => new SubtitleScaleQsvFilter(
|
||||||
|
currentState,
|
||||||
|
scaledSize,
|
||||||
|
paddedSize,
|
||||||
|
extraHardwareFrames),
|
||||||
|
_ => new ScaleImageFilter(paddedSize)
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user