Compare commits

..
Author SHA1 Message Date
Jason Dove 9b8dc0ed80 update changelog for release v0.6.7-beta [no ci] 2022-09-05 13:15:04 -05:00
Jason DoveandGitHub 3cc1286271 include other videos (ungrouped) in shuffle in order (#953)
* include other videos (ungrouped) in shuffle in order

* fix id conflict
2022-09-05 09:20:11 -05:00
Jason DoveandGitHub df281758b7 properly fix infinite playout build loop (#952) 2022-09-05 08:41:39 -05:00
Jason DoveandGitHub 25273c18c8 stop infinite playout building loop (#951) 2022-09-04 20:28:28 -05:00
Jason DoveandGitHub f1be945423 add qsv extra hardware frames setting (#950)
* wip add qsv extra_hw_frames setting

* fix ffmpeg profile editor

* update changelog
2022-09-04 18:07:03 -05:00
Jason DoveandGitHub 9a4f772f53 fix image subtitle scaling (#949)
* properly scale image-based subtitles for nvidia and software

* fix vaapi image subtitle scaling

* fix qsv image subtitle scaling

* update changelog
2022-09-04 14:25:05 -05:00
Jason DoveandGitHub d669e8114b more scaling fixes (#948)
* remove force_original_aspect_ratio from scale_cuda

* remove force_original_aspect_ratio from scale_cuda

* fix qsv scaling

* fix qsv scaling on linux

* fix vaapi scaling edge cases

* update changelog
2022-09-03 20:28:18 -05:00
Jason DoveandGitHub 3972e3603b add amf acceleration (#947) 2022-09-03 10:39:54 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
acc22fcb62 Bump MudBlazor from 6.0.14 to 6.0.15 (#945)
Bumps [MudBlazor](https://github.com/MudBlazor/MudBlazor) from 6.0.14 to 6.0.15.
- [Release notes](https://github.com/MudBlazor/MudBlazor/releases)
- [Changelog](https://github.com/MudBlazor/MudBlazor/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/MudBlazor/MudBlazor/compare/v6.0.14...v6.0.15)

---
updated-dependencies:
- dependency-name: MudBlazor
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-01 19:41:34 -05:00
Jason DoveandGitHub 2df360d7fb fix xmltv filler bug (#944) 2022-08-31 20:15:25 -05:00
Jason DoveandGitHub 46331ed2c6 add preferred audio title feature (#943)
* use consistent edit/delete icons

* add preferred audio title feature

* update dependencies
2022-08-30 17:04:41 -05:00
Jason Dove 3aee3b0515 fix windows build 2022-08-26 09:07:46 -05:00
Jason DoveandGitHub 72c45692b2 update dependencies (#937) 2022-08-26 08:52:59 -05:00
Jason DoveandGitHub 8edf71ca55 downgrade libva, include vainfo in docker (#936) 2022-08-25 16:29:20 -05:00
Jason DoveandGitHub 612b9e6524 fix scanner crash caused by invalid mtime (#934) 2022-08-20 19:56:37 -05:00
Jason DoveandGitHub 7aff65f07b explicitly copy all audio streams with hls direct (#933)
* ensure audio streams are always copied with hls direct

* update changelog
2022-08-18 14:23:51 -05:00
Jason Dove 5d350fcfad update changelog for release v0.6.6-beta [no ci] 2022-08-17 20:40:50 -05:00
Jason DoveandGitHub 5546ad204c upgrade to ffmpeg 5.1 (#931)
* use ffmpeg 5.1 on windows

* remove some debug logs

* use latest ffmpeg on arm

* use ffmpeg 5.1 base images

* update ffmpeg health check for 5.1

* update changelog
2022-08-17 14:57:23 -05:00
Jason DoveandGitHub d66efa0a1d prioritize container aspect ratio over stream aspect ratio (#930)
* prioritize container aspect ratio over stream aspect ratio

* use setdar filter
2022-08-16 19:57:38 -05:00
Jason DoveandGitHub 36d3d38530 remove all use of setsar filter (#928) 2022-08-16 12:25:59 -05:00
Jason DoveandGitHub 8e79141860 use multi-variant playlists for hls segmenter (#926)
* use multi-variant playlists for hls segmenter

* use lowercase mime type
2022-08-13 19:58:08 -05:00
Jason DoveandGitHub 9b3545f7ca add some temporary debug logging (#925) 2022-08-13 19:19:47 -05:00
Jason DoveandGitHub 56db20faa0 limit segmenter delay to 8s (#924)
* always return initial hls playlist after 8 seconds

* update dependencies

* make fluentvalidation happy
2022-08-12 19:48:24 -05:00
Jason DoveandGitHub b0bd4c9fed add ogg file formats to local song library scanner (#914)
* add ogg file formats to local song library scanner

* update dependencies
2022-08-04 16:01:17 -05:00
Jason DoveandGitHub ba079452e2 add dff and dsf to local song library scanner (#911) 2022-08-03 11:01:13 -05:00
Jason Dove f0f2b3da4b update changelog for release v0.6.5-beta [no ci] 2022-08-02 07:36:45 -05:00
Jason DoveandGitHub 866049543c fix db initializer (#907) 2022-07-31 12:30:33 -05:00
Jason Dove 40ed4b8b0e update changelog for release v0.6.4-beta [no ci] 2022-07-28 12:23:33 -05:00
Jason DoveandGitHub b43d08ca67 fix repeating schedules (#901) 2022-07-26 13:04:05 -05:00
Jason DoveandGitHub 5e7e386108 fix search result filtering for episodes and other videos (#900) 2022-07-25 20:02:07 -05:00
Jason DoveandGitHub 4176df9940 fix nvidia capabilities for second-gen maxwell (#899) 2022-07-24 12:23:30 -05:00
Jason DoveandGitHub de2ef959fe add 640x480 resolution (#898)
* update dependencies

* add 640x480 resolution
2022-07-24 08:17:27 -05:00
Jason DoveandGitHub b53cfebac1 fix bug with unsupported aac channel layouts (#893)
* fix bug with unsupported aac channel layouts

* update dependencies
2022-07-14 10:52:25 -05:00
Jason DoveandGitHub 6895b9cc6b fix search repo caching bug (#886)
* add failing test

* fix search repo bug

* update dependencies
2022-07-10 15:32:06 -05:00
Jason Dove c60d6e46f1 fix changelog [no ci] 2022-07-04 15:23:26 -05:00
169 changed files with 14693 additions and 351 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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;
+1
View File
@@ -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);
} }
@@ -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>
+14 -4
View File
@@ -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);
}
}
+1
View File
@@ -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; }
+3 -1
View File
@@ -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
} }
+1
View File
@@ -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; }
} }
+3 -2
View File
@@ -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,
+2 -2
View File
@@ -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;
+38 -7
View File
@@ -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);
+3 -2
View File
@@ -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,
+11 -2
View File
@@ -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);
+2 -1
View File
@@ -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,
+3 -2
View File
@@ -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,
+3 -2
View File
@@ -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,
+24 -6
View File
@@ -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" />
+160 -5
View File
@@ -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
};
}
+12 -2
View File
@@ -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}";
} }
} }
+1 -1
View File
@@ -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" />
+5 -1
View File
@@ -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