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
133 changed files with 9739 additions and 244 deletions
+1 -1
View File
@@ -198,7 +198,7 @@ jobs:
id: downloadffmpeg
name: Download ffmpeg
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/
- name: Build
+52 -2
View File
@@ -5,6 +5,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [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
@@ -1276,7 +1323,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.4-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
@@ -1377,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.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.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>
<PackageReference Include="CliWrap" Version="3.4.4" />
<PackageReference Include="CliWrap" Version="3.5.0" />
</ItemGroup>
<ItemGroup>
@@ -30,4 +30,4 @@
</Compile>
</ItemGroup>
</Project>
</Project>
@@ -11,6 +11,7 @@ public record ChannelViewModel(
int FFmpegProfileId,
string Logo,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
@@ -12,6 +12,7 @@ public record CreateChannel
int FFmpegProfileId,
string Logo,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
@@ -71,6 +71,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
StreamingMode = request.StreamingMode,
Artwork = artwork,
PreferredAudioLanguageCode = preferredAudioLanguageCode,
PreferredAudioTitle = request.PreferredAudioTitle,
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
SubtitleMode = request.SubtitleMode,
MusicVideoCreditsMode = request.MusicVideoCreditsMode
@@ -13,6 +13,7 @@ public record UpdateChannel
int FFmpegProfileId,
string Logo,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
@@ -31,7 +31,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
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)
@@ -42,6 +42,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
c.Categories = update.Categories;
c.FFmpegProfileId = update.FFmpegProfileId;
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
c.PreferredAudioTitle = update.PreferredAudioTitle;
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
c.SubtitleMode = update.SubtitleMode;
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
+1
View File
@@ -15,6 +15,7 @@ internal static class Mapper
channel.FFmpegProfileId,
GetLogo(channel),
channel.PreferredAudioLanguageCode,
channel.PreferredAudioTitle,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,
@@ -8,11 +8,11 @@
<ItemGroup>
<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="MediatR" Version="10.0.1" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -10,6 +10,7 @@ public record CreateFFmpegProfile(
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int? QsvExtraHardwareFrames,
int ResolutionId,
FFmpegProfileVideoFormat VideoFormat,
int VideoBitrate,
@@ -44,6 +44,7 @@ public class CreateFFmpegProfileHandler :
HardwareAcceleration = request.HardwareAcceleration,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
VideoFormat = request.VideoFormat,
VideoBitrate = request.VideoBitrate,
@@ -15,7 +15,7 @@ public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegP
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
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
@@ -11,6 +11,7 @@ public record UpdateFFmpegProfile(
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int? QsvExtraHardwareFrames,
int ResolutionId,
FFmpegProfileVideoFormat VideoFormat,
int VideoBitrate,
@@ -20,7 +20,7 @@ public class
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
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(
@@ -33,6 +33,7 @@ public class
p.HardwareAcceleration = update.HardwareAcceleration;
p.VaapiDriver = update.VaapiDriver;
p.VaapiDevice = update.VaapiDevice;
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
p.ResolutionId = update.ResolutionId;
p.VideoFormat = update.VideoFormat;
p.VideoBitrate = update.VideoBitrate;
@@ -11,6 +11,7 @@ public record FFmpegProfileViewModel(
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int? QsvExtraHardwareFrames,
ResolutionViewModel Resolution,
FFmpegProfileVideoFormat VideoFormat,
int VideoBitrate,
@@ -14,6 +14,7 @@ internal static class Mapper
profile.HardwareAcceleration,
profile.VaapiDriver,
profile.VaapiDevice,
profile.QsvExtraHardwareFrames,
Project(profile.Resolution),
profile.VideoFormat,
profile.VideoBitrate,
@@ -26,6 +26,7 @@ public record AddProgramScheduleItem(
int? FallbackFillerId,
int? WatermarkId,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode? SubtitleMode) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>,
IProgramScheduleItemRequest;
@@ -24,6 +24,7 @@ public interface IProgramScheduleItemRequest
int? FallbackFillerId { get; }
int? WatermarkId { get; }
string PreferredAudioLanguageCode { get; }
string PreferredAudioTitle { get; }
string PreferredSubtitleLanguageCode { get; }
ChannelSubtitleMode? SubtitleMode { get; }
}
@@ -180,6 +180,7 @@ public abstract class ProgramScheduleItemCommandBase
FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
},
@@ -203,6 +204,7 @@ public abstract class ProgramScheduleItemCommandBase
FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
},
@@ -227,6 +229,7 @@ public abstract class ProgramScheduleItemCommandBase
FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
},
@@ -252,6 +255,7 @@ public abstract class ProgramScheduleItemCommandBase
FallbackFillerId = item.FallbackFillerId,
WatermarkId = item.WatermarkId,
PreferredAudioLanguageCode = item.PreferredAudioLanguageCode,
PreferredAudioTitle = item.PreferredAudioTitle,
PreferredSubtitleLanguageCode = item.PreferredSubtitleLanguageCode,
SubtitleMode = item.SubtitleMode
},
@@ -26,6 +26,7 @@ public record ReplaceProgramScheduleItem(
int? FallbackFillerId,
int? WatermarkId,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode? SubtitleMode) : IProgramScheduleItemRequest;
@@ -63,6 +63,7 @@ internal static class Mapper
? Watermarks.Mapper.ProjectToViewModel(duration.Watermark)
: null,
duration.PreferredAudioLanguageCode,
duration.PreferredAudioTitle,
duration.PreferredSubtitleLanguageCode,
duration.SubtitleMode),
ProgramScheduleItemFlood flood =>
@@ -110,6 +111,7 @@ internal static class Mapper
? Watermarks.Mapper.ProjectToViewModel(flood.Watermark)
: null,
flood.PreferredAudioLanguageCode,
flood.PreferredAudioTitle,
flood.PreferredSubtitleLanguageCode,
flood.SubtitleMode),
ProgramScheduleItemMultiple multiple =>
@@ -158,6 +160,7 @@ internal static class Mapper
? Watermarks.Mapper.ProjectToViewModel(multiple.Watermark)
: null,
multiple.PreferredAudioLanguageCode,
multiple.PreferredAudioTitle,
multiple.PreferredSubtitleLanguageCode,
multiple.SubtitleMode),
ProgramScheduleItemOne one =>
@@ -205,6 +208,7 @@ internal static class Mapper
? Watermarks.Mapper.ProjectToViewModel(one.Watermark)
: null,
one.PreferredAudioLanguageCode,
one.PreferredAudioTitle,
one.PreferredSubtitleLanguageCode,
one.SubtitleMode),
_ => throw new NotSupportedException(
@@ -30,6 +30,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark,
string preferredAudioLanguageCode,
string preferredAudioTitle,
string preferredSubtitleLanguageCode,
ChannelSubtitleMode? subtitleMode) : base(
id,
@@ -52,6 +53,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
fallbackFiller,
watermark,
preferredAudioLanguageCode,
preferredAudioTitle,
preferredSubtitleLanguageCode,
subtitleMode)
{
@@ -28,6 +28,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark,
string preferredAudioLanguageCode,
string preferredAudioTitle,
string preferredSubtitleLanguageCode,
ChannelSubtitleMode? subtitleMode) : base(
id,
@@ -50,6 +51,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
fallbackFiller,
watermark,
preferredAudioLanguageCode,
preferredAudioTitle,
preferredSubtitleLanguageCode,
subtitleMode)
{
@@ -29,6 +29,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark,
string preferredAudioLanguageCode,
string preferredAudioTitle,
string preferredSubtitleLanguageCode,
ChannelSubtitleMode? subtitleMode) : base(
id,
@@ -51,6 +52,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
fallbackFiller,
watermark,
preferredAudioLanguageCode,
preferredAudioTitle,
preferredSubtitleLanguageCode,
subtitleMode) =>
Count = count;
@@ -28,6 +28,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
FillerPresetViewModel fallbackFiller,
WatermarkViewModel watermark,
string preferredAudioLanguageCode,
string preferredAudioTitle,
string preferredSubtitleLanguageCode,
ChannelSubtitleMode? subtitleMode) : base(
id,
@@ -50,6 +51,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
fallbackFiller,
watermark,
preferredAudioLanguageCode,
preferredAudioTitle,
preferredSubtitleLanguageCode,
subtitleMode)
{
@@ -27,6 +27,7 @@ public abstract record ProgramScheduleItemViewModel(
FillerPresetViewModel FallbackFiller,
WatermarkViewModel Watermark,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode? SubtitleMode)
{
@@ -1,4 +1,5 @@
using ErsatzTV.Core;
using System.Diagnostics;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
@@ -78,22 +79,47 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
IHlsSessionWorker worker,
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;
while (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)
_logger.LogDebug("Waiting for playlist to exist");
while (!File.Exists(playlistFileName))
{
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 Bugsnag;
using CliWrap;
@@ -64,6 +65,7 @@ public class HlsSessionWorker : IHlsSessionWorker
DateTimeOffset filterBefore,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
await Slim.WaitAsync(cancellationToken);
try
{
@@ -85,6 +87,8 @@ public class HlsSessionWorker : IHlsSessionWorker
finally
{
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();
if (toDelete.Count > 0)
{
_logger.LogDebug(
"Deleting HLS segments {Min} to {Max} (less than {StartSequence})",
toDelete.Map(s => s.SequenceNumber).Min(),
toDelete.Map(s => s.SequenceNumber).Max(),
trimResult.Sequence);
// _logger.LogDebug(
// "Deleting HLS segments {Min} to {Max} (less than {StartSequence})",
// toDelete.Map(s => s.SequenceNumber).Min(),
// toDelete.Map(s => s.SequenceNumber).Max(),
// trimResult.Sequence);
}
foreach (Segment segment in toDelete)
@@ -33,7 +33,8 @@ public class GetErrorProcessHandler : FFmpegProcessHandler<GetErrorProcess>
request.HlsRealtime,
request.PtsOffset,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice);
channel.FFmpegProfile.VaapiDevice,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
return new PlayoutItemProcessModel(process, request.MaybeDuration, request.Until);
}
@@ -174,6 +174,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
audioPath,
subtitles,
playoutItemWithPath.PlayoutItem.PreferredAudioLanguageCode ?? channel.PreferredAudioLanguageCode,
playoutItemWithPath.PlayoutItem.PreferredAudioTitle ?? channel.PreferredAudioTitle,
playoutItemWithPath.PlayoutItem.PreferredSubtitleLanguageCode ?? channel.PreferredSubtitleLanguageCode,
playoutItemWithPath.PlayoutItem.SubtitleMode ?? channel.SubtitleMode,
playoutItemWithPath.PlayoutItem.StartOffset,
@@ -183,6 +184,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
maybeGlobalWatermark,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
request.HlsRealtime,
playoutItemWithPath.PlayoutItem.FillerKind,
playoutItemWithPath.PlayoutItem.InPoint,
@@ -229,7 +231,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
request.HlsRealtime,
request.PtsOffset,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice);
channel.FFmpegProfile.VaapiDevice,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
return new PlayoutItemProcessModel(offlineProcess, maybeDuration, finish);
case PlayoutItemDoesNotExistOnDisk:
@@ -241,7 +244,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
request.HlsRealtime,
request.PtsOffset,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice);
channel.FFmpegProfile.VaapiDevice,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
return new PlayoutItemProcessModel(doesNotExistProcess, maybeDuration, finish);
default:
@@ -253,7 +257,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
request.HlsRealtime,
request.PtsOffset,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice);
channel.FFmpegProfile.VaapiDevice,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
return new PlayoutItemProcessModel(errorProcess, maybeDuration, finish);
}
@@ -2,7 +2,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.FFmpeg.Runtime;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
@@ -8,7 +8,7 @@
<ItemGroup>
<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="LanguageExt.Core" Version="4.2.9" />
<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.Abstractions" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.3.44">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Serilog" Version="2.11.0" />
+13 -3
View File
@@ -13,6 +13,7 @@ using ErsatzTV.Core.Metadata;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.State;
using ErsatzTV.Infrastructure.Runtime;
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
@@ -168,6 +169,11 @@ public class TranscodingTests
HardwareAccelerationKind.VideoToolbox
};
public static HardwareAccelerationKind[] AmfAcceleration =
{
HardwareAccelerationKind.Amf
};
public static HardwareAccelerationKind[] QsvAcceleration =
{
HardwareAccelerationKind.Qsv
@@ -192,11 +198,11 @@ public class TranscodingTests
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))]
FFmpegProfileVideoFormat profileVideoFormat,
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))]
HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] 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")
{
@@ -309,6 +315,7 @@ public class TranscodingTests
// new HardwareCapabilitiesFactory(
// new MemoryCache(new MemoryCacheOptions()),
// LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
new RuntimeInfo(),
LoggerFactory.CreateLogger<FFmpegLibraryProcessService>());
var v = new MediaVersion
@@ -483,6 +490,7 @@ public class TranscodingTests
subtitles,
string.Empty,
string.Empty,
string.Empty,
subtitleMode,
now,
now + TimeSpan.FromSeconds(5),
@@ -491,6 +499,7 @@ public class TranscodingTests
channelWatermark,
VaapiDriver.Default,
"/dev/dri/renderD128",
Option<int>.None,
false,
FillerKind.None,
TimeSpan.Zero,
@@ -569,7 +578,8 @@ public class TranscodingTests
MediaVersion version,
StreamingMode streamingMode,
string channelNumber,
string preferredAudioLanguage) =>
string preferredAudioLanguage,
string preferredAudioTitle) =>
Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
public Task<Option<Domain.Subtitle>> SelectSubtitleStream(
@@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Core.Jellyfin;
using FluentAssertions;
using Microsoft.Extensions.Logging;
@@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Core.Plex;
using FluentAssertions;
using Microsoft.Extensions.Logging;
@@ -515,6 +515,7 @@ public class PlayoutBuilderTests
{
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = floodCollection,
CollectionId = floodCollection.Id,
@@ -523,6 +524,7 @@ public class PlayoutBuilderTests
},
new ProgramScheduleItemOne
{
Id = 2,
Index = 2,
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
@@ -605,6 +607,7 @@ public class PlayoutBuilderTests
{
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = floodCollection,
CollectionId = floodCollection.Id,
@@ -613,6 +616,7 @@ public class PlayoutBuilderTests
},
new ProgramScheduleItemOne
{
Id = 2,
Index = 2,
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
@@ -742,6 +746,7 @@ public class PlayoutBuilderTests
{
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = floodCollection,
CollectionId = floodCollection.Id,
@@ -750,6 +755,7 @@ public class PlayoutBuilderTests
},
new ProgramScheduleItemMultiple
{
Id = 2,
Index = 2,
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
@@ -839,6 +845,7 @@ public class PlayoutBuilderTests
{
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = floodCollection,
CollectionId = floodCollection.Id,
@@ -847,6 +854,7 @@ public class PlayoutBuilderTests
},
new ProgramScheduleItemOne
{
Id = 2,
Index = 2,
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
@@ -934,6 +942,7 @@ public class PlayoutBuilderTests
{
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = floodCollection,
CollectionId = floodCollection.Id,
@@ -942,6 +951,7 @@ public class PlayoutBuilderTests
},
new ProgramScheduleItemOne
{
Id = 2,
Index = 2,
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
@@ -1040,6 +1050,7 @@ public class PlayoutBuilderTests
{
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = floodCollection,
CollectionId = floodCollection.Id,
@@ -1048,6 +1059,7 @@ public class PlayoutBuilderTests
},
new ProgramScheduleItemDuration
{
Id = 2,
Index = 2,
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
@@ -1141,6 +1153,7 @@ public class PlayoutBuilderTests
{
new ProgramScheduleItemMultiple
{
Id = 1,
Index = 1,
Collection = multipleCollection,
CollectionId = multipleCollection.Id,
@@ -1150,6 +1163,7 @@ public class PlayoutBuilderTests
},
new ProgramScheduleItemDuration
{
Id = 2,
Index = 2,
Collection = dynamicCollection,
CollectionId = dynamicCollection.Id,
@@ -2329,6 +2343,7 @@ public class PlayoutBuilderTests
{
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = floodCollection,
CollectionId = floodCollection.Id,
@@ -2337,6 +2352,7 @@ public class PlayoutBuilderTests
},
new ProgramScheduleItemOne
{
Id = 2,
Index = 2,
Collection = fixedCollection,
CollectionId = fixedCollection.Id,
@@ -2622,6 +2638,7 @@ public class PlayoutBuilderTests
private static ProgramScheduleItem Flood(Collection mediaCollection, PlaybackOrder playbackOrder) =>
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
Collection = mediaCollection,
CollectionId = mediaCollection.Id,
@@ -85,6 +85,98 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
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]
public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood()
+1
View File
@@ -23,6 +23,7 @@ public class Channel
public List<Playout> Playouts { get; set; }
public List<Artwork> Artwork { get; set; }
public string PreferredAudioLanguageCode { get; set; }
public string PreferredAudioTitle { get; set; }
public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode SubtitleMode { get; set; }
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
+3 -1
View File
@@ -10,6 +10,7 @@ public record FFmpegProfile
public HardwareAccelerationKind HardwareAcceleration { get; set; }
public VaapiDriver VaapiDriver { get; set; }
public string VaapiDevice { get; set; }
public int? QsvExtraHardwareFrames { get; set; }
public int ResolutionId { get; set; }
public Resolution Resolution { get; set; }
public FFmpegProfileVideoFormat VideoFormat { get; set; }
@@ -42,6 +43,7 @@ public record FFmpegProfile
AudioSampleRate = 48,
DeinterlaceVideo = true,
NormalizeFramerate = false,
HardwareAcceleration = HardwareAccelerationKind.None
HardwareAcceleration = HardwareAccelerationKind.None,
QsvExtraHardwareFrames = 64
};
}
@@ -6,5 +6,6 @@ public enum HardwareAccelerationKind
Qsv = 1,
Nvenc = 2,
Vaapi = 3,
VideoToolbox = 4
VideoToolbox = 4,
Amf = 5
}
+1
View File
@@ -24,6 +24,7 @@ public class PlayoutItem
public int? WatermarkId { get; set; }
public bool DisableWatermarks { get; set; }
public string PreferredAudioLanguageCode { get; set; }
public string PreferredAudioTitle { get; set; }
public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode? SubtitleMode { get; set; }
public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime();
@@ -35,6 +35,7 @@ public abstract class ProgramScheduleItem
public ChannelWatermark Watermark { get; set; }
public int? WatermarkId { get; set; }
public string PreferredAudioLanguageCode { get; set; }
public string PreferredAudioTitle { get; set; }
public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode? SubtitleMode { get; set; }
}
@@ -2,7 +2,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.FFmpeg.Runtime;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Emby;
+2 -2
View File
@@ -17,8 +17,8 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.3.44">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -169,6 +169,7 @@ public class FFmpegComplexFilterBuilder
_videoDecoder.Contains("cuvid")),
HardwareAccelerationKind.Qsv => !isSong,
HardwareAccelerationKind.VideoToolbox => false,
HardwareAccelerationKind.Amf => false,
_ => false
};
@@ -200,6 +201,7 @@ public class FFmpegComplexFilterBuilder
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None &&
acceleration != HardwareAccelerationKind.VideoToolbox &&
acceleration != HardwareAccelerationKind.Amf &&
!isHardwareDecode &&
(_deinterlace || _scaleToSize.IsSome);
@@ -7,6 +7,7 @@ using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.OutputFormat;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.FFmpeg.State;
using Microsoft.Extensions.Logging;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
@@ -18,6 +19,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
private readonly IRuntimeInfo _runtimeInfo;
private readonly ILogger<FFmpegLibraryProcessService> _logger;
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
private readonly ITempFilePool _tempFilePool;
@@ -28,6 +30,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
IFFmpegStreamSelector ffmpegStreamSelector,
ITempFilePool tempFilePool,
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
IRuntimeInfo runtimeInfo,
ILogger<FFmpegLibraryProcessService> logger)
{
_ffmpegProcessService = ffmpegProcessService;
@@ -35,6 +38,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
_ffmpegStreamSelector = ffmpegStreamSelector;
_tempFilePool = tempFilePool;
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
_runtimeInfo = runtimeInfo;
_logger = logger;
}
@@ -49,6 +53,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
string audioPath,
List<Subtitle> subtitles,
string preferredAudioLanguage,
string preferredAudioTitle,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode,
DateTimeOffset start,
@@ -58,6 +63,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
Option<ChannelWatermark> globalWatermark,
VaapiDriver vaapiDriver,
string vaapiDevice,
Option<int> qsvExtraHardwareFrames,
bool hlsRealtime,
FillerKind fillerKind,
TimeSpan inPoint,
@@ -72,7 +78,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
audioVersion,
channel.StreamingMode,
channel.Number,
preferredAudioLanguage);
preferredAudioLanguage,
preferredAudioTitle);
Option<Subtitle> maybeSubtitle =
await _ffmpegStreamSelector.SelectSubtitleStream(
subtitles,
@@ -140,6 +147,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoStream.Codec,
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.DisplayAspectRatio,
videoVersion.RFrameRate,
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))
.IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)),
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
channel.FFmpegProfile.Resolution.Width == 640 ? "4:3" : "16:9",
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
@@ -231,11 +240,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsPlaylistPath,
hlsSegmentTemplate,
ptsOffset,
playbackSettings.ThreadCount);
playbackSettings.ThreadCount,
qsvExtraHardwareFrames);
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
var pipelineBuilder = new PipelineBuilder(
_runtimeInfo,
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hwAccel),
videoInputFile,
audioInputFile,
@@ -258,7 +269,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
bool hlsRealtime,
long ptsOffset,
VaapiDriver vaapiDriver,
string vaapiDevice)
string vaapiDevice,
Option<int> qsvExtraHardwareFrames)
{
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateErrorSettings(
channel.StreamingMode,
@@ -302,6 +314,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
new PixelFormatYuv420P(),
new FrameSize(desiredResolution.Width, desiredResolution.Height),
new FrameSize(desiredResolution.Width, desiredResolution.Height),
desiredResolution.Width == 640 ? "4:3" : "16:9",
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
@@ -329,6 +342,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
VideoFormat.GeneratedImage,
new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p
new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.DisplayAspectRatio,
None,
true);
@@ -352,7 +366,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsPlaylistPath,
hlsSegmentTemplate,
ptsOffset,
Option<int>.None);
Option<int>.None,
qsvExtraHardwareFrames);
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);
var pipelineBuilder = new PipelineBuilder(
_runtimeInfo,
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hwAccel),
videoInputFile,
audioInputFile,
@@ -394,6 +410,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
resolution);
var pipelineBuilder = new PipelineBuilder(
_runtimeInfo,
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None),
None,
None,
@@ -417,9 +434,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
{
var videoInputFile = new VideoInputFile(
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(
_runtimeInfo,
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None),
videoInputFile,
None,
@@ -497,6 +515,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
"unknown",
new PixelFormatUnknown(),
new FrameSize(1, 1),
string.Empty,
Option<string>.None,
!options.IsAnimated)
},
@@ -620,6 +639,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,
HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox,
HardwareAccelerationKind.Amf => HardwareAccelerationMode.Amf,
_ => HardwareAccelerationMode.None
};
}
+38 -7
View File
@@ -28,13 +28,14 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
MediaVersion version,
StreamingMode streamingMode,
string channelNumber,
string preferredAudioLanguage)
string preferredAudioLanguage,
string preferredAudioTitle)
{
if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(preferredAudioLanguage))
string.IsNullOrWhiteSpace(preferredAudioLanguage) && string.IsNullOrWhiteSpace(preferredAudioTitle))
{
_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);
return None;
}
@@ -71,18 +72,18 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
if (correctLanguage.Any())
{
_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,
allCodes);
return correctLanguage.OrderByDescending(s => s.Channels).Head();
return PrioritizeAudioTitle(correctLanguage, preferredAudioTitle ?? string.Empty);
}
_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);
return audioStreams.OrderByDescending(s => s.Channels).HeadOrNone();
return PrioritizeAudioTitle(audioStreams, preferredAudioTitle ?? string.Empty);
}
public async Task<Option<Subtitle>> SelectSubtitleStream(
@@ -163,4 +164,34 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
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,
List<Subtitle> subtitles,
string preferredAudioLanguage,
string preferredAudioTitle,
string preferredSubtitleLanguage,
ChannelSubtitleMode subtitleMode,
DateTimeOffset start,
@@ -28,6 +29,7 @@ public interface IFFmpegProcessService
Option<ChannelWatermark> globalWatermark,
VaapiDriver vaapiDriver,
string vaapiDevice,
Option<int> qsvExtraHardwareFrames,
bool hlsRealtime,
FillerKind fillerKind,
TimeSpan inPoint,
@@ -44,7 +46,8 @@ public interface IFFmpegProcessService
bool hlsRealtime,
long ptsOffset,
VaapiDriver vaapiDriver,
string vaapiDevice);
string vaapiDevice,
Option<int> qsvExtraHardwareFrames);
Task<Command> ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
@@ -10,7 +10,8 @@ public interface IFFmpegStreamSelector
MediaVersion version,
StreamingMode streamingMode,
string channelNumber,
string preferredAudioLanguage);
string preferredAudioLanguage,
string preferredAudioTitle);
Task<Option<Subtitle>> SelectSubtitleStream(
List<Subtitle> subtitles,
+3 -2
View File
@@ -79,6 +79,7 @@ public class ChannelGuide
foreach ((Channel channel, List<PlayoutItem> sorted) in sortedChannelItems.OrderBy(
kvp => decimal.Parse(kvp.Key.Number)))
{
// skip all filler that isn't pre-roll
var i = 0;
while (i < sorted.Count && sorted[i].FillerKind != FillerKind.None &&
sorted[i].FillerKind != FillerKind.PreRoll)
@@ -90,7 +91,7 @@ public class ChannelGuide
{
PlayoutItem startItem = sorted[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++;
}
@@ -98,7 +99,7 @@ public class ChannelGuide
PlayoutItem displayItem = sorted[j];
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
int finishIndex = i;
int finishIndex = j;
while (finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup)
{
finishIndex++;
@@ -2,7 +2,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.FFmpeg.Runtime;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Jellyfin;
+11 -2
View File
@@ -33,8 +33,17 @@ public class LocalFileSystem : ILocalFileSystem
return Unit.Default;
}
public DateTime GetLastWriteTime(string path) =>
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => SystemTime.MinValueUtc);
public DateTime GetLastWriteTime(string path)
{
try
{
return File.GetLastWriteTimeUtc(path);
}
catch
{
return SystemTime.MinValueUtc;
}
}
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
Directory.Exists(libraryPath.Path);
+2 -1
View File
@@ -21,7 +21,8 @@ public abstract class LocalFolderScanner
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()
@@ -3,6 +3,8 @@ using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using Bugsnag;
using CliWrap;
using CliWrap.Buffered;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
@@ -157,42 +159,48 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
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,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
"-hide_banner",
"-print_format", "json",
"-show_format",
"-show_streams",
"-show_chapters",
"-i", filePath
};
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add("quiet");
startInfo.ArgumentList.Add("-print_format");
startInfo.ArgumentList.Add("json");
startInfo.ArgumentList.Add("-show_format");
startInfo.ArgumentList.Add("-show_streams");
startInfo.ArgumentList.Add("-show_chapters");
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(filePath);
BufferedCommandResult probe = await Cli.Wrap(ffprobePath)
.WithArguments(arguments)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8);
var probe = new Process
if (probe.ExitCode != 0)
{
StartInfo = startInfo
};
return BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
}
probe.Start();
return probe.StandardOutput.ReadToEndAsync().MapAsync<string, Either<BaseError, FFprobe>>(
async output =>
FFprobe ffprobe = JsonConvert.DeserializeObject<FFprobe>(probe.StandardOutput);
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();
return probe.ExitCode == 0
? JsonConvert.DeserializeObject<FFprobe>(output)
: BaseError.New($"FFprobe at {ffprobePath} exited with code {probe.ExitCode}");
});
string sar = match.Groups[1].Value;
string dar = match.Groups[2].Value;
foreach (FFprobeStream stream in ffprobe.streams.Where(s => s.codec_type == "video").ToList())
{
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)
@@ -2,7 +2,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.FFmpeg.Runtime;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Plex;
@@ -423,9 +423,29 @@ public class PlayoutBuilder : IPlayoutBuilder
var schedulerDuration = new PlayoutModeSchedulerDuration(_logger);
var schedulerFlood = new PlayoutModeSchedulerFlood(_logger);
var timeCount = new Dictionary<DateTimeOffset, int>();
// loop until we're done filling the desired amount of time
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);
// get the schedule item out of the sorted list
@@ -81,6 +81,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase<ProgramSche
CustomTitle = scheduleItem.CustomTitle,
WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
};
@@ -62,12 +62,14 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramSchedul
CustomTitle = scheduleItem.CustomTitle,
WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
};
// never block scheduling when there is only one schedule item (with fixed start and flood)
DateTimeOffset peekScheduleItemStart =
peekScheduleItem.StartType == StartType.Fixed
scheduleItem.Id != peekScheduleItem.Id && peekScheduleItem.StartType == StartType.Fixed
? GetStartTimeAfter(nextState with { InFlood = false }, peekScheduleItem)
: DateTimeOffset.MaxValue;
@@ -70,6 +70,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase<ProgramSche
CustomTitle = scheduleItem.CustomTitle,
WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
};
@@ -50,6 +50,7 @@ public class PlayoutModeSchedulerOne : PlayoutModeSchedulerBase<ProgramScheduleI
CustomTitle = scheduleItem.CustomTitle,
WatermarkId = scheduleItem.WatermarkId,
PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
SubtitleMode = scheduleItem.SubtitleMode
};
@@ -9,8 +9,8 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
+90 -5
View File
@@ -4,6 +4,7 @@ using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Encoder;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.OutputFormat;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.FFmpeg.State;
using FluentAssertions;
using LanguageExt;
@@ -25,7 +26,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
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(
"/tmp/whatever.mkv",
@@ -46,6 +47,7 @@ public class PipelineGeneratorTests
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
"16:9",
Option<int>.None,
2000,
4000,
@@ -68,9 +70,11 @@ public class PipelineGeneratorTests
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,
@@ -95,7 +99,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
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(
"/tmp/whatever.mkv",
@@ -116,6 +120,7 @@ public class PipelineGeneratorTests
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
"16:9",
Option<int>.None,
2000,
4000,
@@ -138,9 +143,11 @@ public class PipelineGeneratorTests
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,
@@ -165,7 +172,16 @@ public class PipelineGeneratorTests
var resolution = new FrameSize(1920, 1080);
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"));
result.PipelineSteps.Should().HaveCountGreaterThan(0);
@@ -182,7 +198,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
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(
"/tmp/whatever.mkv",
@@ -203,6 +219,7 @@ public class PipelineGeneratorTests
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
"16:9",
Option<int>.None,
2000,
4000,
@@ -225,9 +242,11 @@ public class PipelineGeneratorTests
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,
@@ -248,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");
}
[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]
public void Resize_Image_Test()
{
@@ -257,10 +341,11 @@ public class PipelineGeneratorTests
"/test/input/file.png",
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(
new Mock<IRuntimeInfo>().Object,
new DefaultHardwareCapabilities(),
videoInputFile,
Option<AudioInputFile>.None,
@@ -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.Encoder.Amf;
using ErsatzTV.FFmpeg.Encoder.Nvenc;
using ErsatzTV.FFmpeg.Encoder.Qsv;
using ErsatzTV.FFmpeg.Encoder.Vaapi;
@@ -42,14 +43,16 @@ public static class AvailableEncoders
new EncoderHevcQsv(
currentState,
maybeWatermarkInputFile,
maybeSubtitleInputFile),
maybeSubtitleInputFile,
ffmpegState.QsvExtraHardwareFrames),
(HardwareAccelerationMode.Qsv, VideoFormat.H264) when hardwareCapabilities.CanEncode(
VideoFormat.H264,
desiredState.PixelFormat) =>
new EncoderH264Qsv(
currentState,
maybeWatermarkInputFile,
maybeSubtitleInputFile),
maybeSubtitleInputFile,
ffmpegState.QsvExtraHardwareFrames),
(HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) when hardwareCapabilities.CanEncode(
VideoFormat.Hevc,
@@ -72,6 +75,13 @@ public static class AvailableEncoders
(HardwareAccelerationMode.VideoToolbox, VideoFormat.H264) when hardwareCapabilities.CanEncode(
VideoFormat.H264,
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.H264) => new EncoderLibx264(),
@@ -6,21 +6,26 @@ public class EncoderH264Qsv : EncoderBase
{
private readonly FrameState _currentState;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly int _extraHardwareFrames;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
public EncoderH264Qsv(
FrameState currentState,
Option<WatermarkInputFile> maybeWatermarkInputFile,
Option<SubtitleInputFile> maybeSubtitleInputFile)
Option<SubtitleInputFile> maybeSubtitleInputFile,
int extraHardwareFrames)
{
_currentState = currentState;
_maybeWatermarkInputFile = maybeWatermarkInputFile;
_maybeSubtitleInputFile = maybeSubtitleInputFile;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Name => "h264_qsv";
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
public override string Filter
@@ -37,11 +42,11 @@ public class EncoderH264Qsv : EncoderBase
// pixel format should already be converted to a supported format by QsvHardwareAccelerationOption
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
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 Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly int _extraHardwareFrames;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
public EncoderHevcQsv(
FrameState currentState,
Option<WatermarkInputFile> maybeWatermarkInputFile,
Option<SubtitleInputFile> maybeSubtitleInputFile)
Option<SubtitleInputFile> maybeSubtitleInputFile,
int extraHardwareFrames)
{
_currentState = currentState;
_maybeWatermarkInputFile = maybeWatermarkInputFile;
_maybeSubtitleInputFile = maybeSubtitleInputFile;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Name => "hevc_qsv";
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
public override string Filter
@@ -37,11 +42,11 @@ public class EncoderHevcQsv : EncoderBase
// pixel format should already be converted to a supported format by QsvHardwareAccelerationOption
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
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>
<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="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<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> HlsSegmentTemplate,
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) =>
new(
saveReport,
@@ -37,5 +40,6 @@ public record FFmpegState(
Option<string>.None,
Option<string>.None,
0,
Option<int>.None,
Option<int>.None);
}
@@ -1,22 +1,25 @@
using ErsatzTV.FFmpeg.Filter.Cuda;
using ErsatzTV.FFmpeg.Filter.Qsv;
using ErsatzTV.FFmpeg.Filter.Vaapi;
using ErsatzTV.FFmpeg.Runtime;
namespace ErsatzTV.FFmpeg.Filter;
public static class AvailableScaleFilters
{
public static IPipelineFilterStep ForAcceleration(
IRuntimeInfo runtimeInfo,
HardwareAccelerationMode accelMode,
FrameState currentState,
FrameSize scaledSize,
FrameSize paddedSize) =>
FrameSize paddedSize,
int extraHardwareFrames) =>
accelMode switch
{
HardwareAccelerationMode.Nvenc => new ScaleCudaFilter(currentState, scaledSize, paddedSize),
HardwareAccelerationMode.Qsv when currentState.FrameDataLocation == FrameDataLocation.Hardware ||
scaledSize == paddedSize =>
new ScaleQsvFilter(currentState, scaledSize),
new ScaleQsvFilter(runtimeInfo, currentState, scaledSize, paddedSize, extraHardwareFrames),
HardwareAccelerationMode.Vaapi => new ScaleVaapiFilter(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)
};
}
+5 -5
View File
@@ -173,7 +173,8 @@ public class ComplexFilter : IPipelineStep
if (_maybeSubtitleInputFile.Map(s => !s.IsImageBased).IfNone(false) &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Vaapi &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.VideoToolbox)
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.VideoToolbox &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Amf)
{
uploadDownloadFilter = new HardwareDownloadFilter(_currentState).Filter;
}
@@ -230,10 +231,9 @@ public class ComplexFilter : IPipelineStep
if (filter != string.Empty)
{
string tempVideoLabel = string.IsNullOrWhiteSpace(videoFilterComplex) &&
string.IsNullOrWhiteSpace(watermarkFilterComplex)
? $"[{videoLabel}]"
: videoLabel;
string tempVideoLabel = videoLabel.StartsWith('[') && videoLabel.EndsWith(']')
? videoLabel
: $"[{videoLabel}]";
// vaapi uses software overlay and needs to upload
// videotoolbox seems to require a hwupload for hevc
@@ -37,13 +37,7 @@ public class ScaleCudaFilter : BaseFilter
format = $":format={pixelFormat.FFmpegName}";
}
string aspectRatio = string.Empty;
if (_scaledSize != _paddedSize)
{
aspectRatio = ":force_original_aspect_ratio=1";
}
scale = $"scale_cuda={targetSize}{aspectRatio}{format}";
scale = $"scale_cuda={targetSize}{format}";
}
// TODO: this might not always upload to hardware, so NextState could be inaccurate
@@ -0,0 +1,40 @@
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Filter.Cuda;
public class SubtitleScaleNppFilter : BaseFilter
{
private readonly FrameState _currentState;
private readonly FrameSize _paddedSize;
private readonly FrameSize _scaledSize;
public SubtitleScaleNppFilter(FrameState currentState, FrameSize scaledSize, FrameSize paddedSize)
{
_currentState = currentState;
_scaledSize = scaledSize;
_paddedSize = paddedSize;
}
public override string Filter
{
get
{
string scale = string.Empty;
if (_currentState.ScaledSize != _scaledSize)
{
string targetSize = $"{_paddedSize.Width}:{_paddedSize.Height}";
string format = string.Empty;
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
{
format = $":format={pixelFormat.FFmpegName}";
}
scale = $"scale_npp={targetSize}{format}";
}
return scale;
}
}
public override FrameState NextState(FrameState currentState) => currentState;
}
@@ -4,13 +4,16 @@ public class HardwareUploadFilter : BaseFilter
{
private readonly FFmpegState _ffmpegState;
public HardwareUploadFilter(FFmpegState ffmpegState) => _ffmpegState = ffmpegState;
public HardwareUploadFilter(FFmpegState ffmpegState)
{
_ffmpegState = ffmpegState;
}
public override string Filter => _ffmpegState.EncoderHardwareAccelerationMode switch
{
HardwareAccelerationMode.None => string.Empty,
HardwareAccelerationMode.Nvenc => "hwupload_cuda",
HardwareAccelerationMode.Qsv => "hwupload=extra_hw_frames=64",
HardwareAccelerationMode.Qsv => $"hwupload=extra_hw_frames={_ffmpegState.QsvExtraHardwareFrames}",
HardwareAccelerationMode.Vaapi => "format=nv12|vaapi,hwupload",
_ => "hwupload"
};
@@ -5,12 +5,17 @@ namespace ErsatzTV.FFmpeg.Filter.Qsv;
public class DeinterlaceQsvFilter : BaseFilter
{
private readonly FrameState _currentState;
private readonly int _extraHardwareFrames;
public DeinterlaceQsvFilter(FrameState currentState) => _currentState = currentState;
public DeinterlaceQsvFilter(FrameState currentState, int extraHardwareFrames)
{
_currentState = currentState;
_extraHardwareFrames = extraHardwareFrames;
}
// deinterlace_qsv seems to only support nv12, not p010le
public override string Filter => _currentState.FrameDataLocation == FrameDataLocation.Software
? "format=nv12,hwupload=extra_hw_frames=64,deinterlace_qsv"
? $"format=nv12,hwupload=extra_hw_frames={_extraHardwareFrames},deinterlace_qsv"
: "deinterlace_qsv";
public override FrameState NextState(FrameState currentState)
+20 -5
View File
@@ -1,16 +1,29 @@
using ErsatzTV.FFmpeg.Format;
using System.Runtime.InteropServices;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.Runtime;
namespace ErsatzTV.FFmpeg.Filter.Qsv;
public class ScaleQsvFilter : BaseFilter
{
private readonly IRuntimeInfo _runtimeInfo;
private readonly FrameState _currentState;
private readonly FrameSize _scaledSize;
private readonly FrameSize _paddedSize;
private readonly int _extraHardwareFrames;
public ScaleQsvFilter(FrameState currentState, FrameSize scaledSize)
public ScaleQsvFilter(
IRuntimeInfo runtimeInfo,
FrameState currentState,
FrameSize scaledSize,
FrameSize paddedSize,
int extraHardwareFrames)
{
_runtimeInfo = runtimeInfo;
_currentState = currentState;
_scaledSize = scaledSize;
_paddedSize = paddedSize;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Filter
@@ -37,7 +50,9 @@ public class ScaleQsvFilter : BaseFilter
format = $":format={pixelFormat.FFmpegName}";
}
string targetSize = $"w={_scaledSize.Width}:h={_scaledSize.Height}";
string targetSize = _runtimeInfo.IsOSPlatform(OSPlatform.Windows)
? $"w={_paddedSize.Width}:h={_paddedSize.Height}"
: $"w={_scaledSize.Width}:h={_scaledSize.Height}";
scale = $"vpp_qsv={targetSize}{format}";
}
@@ -49,10 +64,10 @@ public class ScaleQsvFilter : BaseFilter
string initialPixelFormat = _currentState.PixelFormat.Match(pf => pf.FFmpegName, FFmpegFormat.NV12);
if (!string.IsNullOrWhiteSpace(scale))
{
return $"format={initialPixelFormat},hwupload=extra_hw_frames=64,{scale}";
return $"format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames},{scale}";
}
return $"format={initialPixelFormat},hwupload=extra_hw_frames=64";
return $"format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames}";
}
}
@@ -0,0 +1,48 @@
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Filter.Qsv;
public class SubtitleScaleQsvFilter : BaseFilter
{
private readonly FrameState _currentState;
private readonly FrameSize _scaledSize;
private readonly FrameSize _paddedSize;
private readonly int _extraHardwareFrames;
public SubtitleScaleQsvFilter(
FrameState currentState,
FrameSize scaledSize,
FrameSize paddedSize,
int extraHardwareFrames)
{
_currentState = currentState;
_scaledSize = scaledSize;
_paddedSize = paddedSize;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Filter
{
get
{
string scale = string.Empty;
if (_currentState.ScaledSize != _scaledSize)
{
string targetSize = $"w={_paddedSize.Width}:h={_paddedSize.Height}";
// use software scaling
scale = $"scale={targetSize}";
}
string initialPixelFormat = _currentState.PixelFormat.Match(pf => pf.FFmpegName, FFmpegFormat.NV12);
if (!string.IsNullOrWhiteSpace(scale))
{
return $"{scale},format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames}";
}
return $"format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames}";
}
}
public override FrameState NextState(FrameState currentState) => currentState;
}
+14
View File
@@ -0,0 +1,14 @@
namespace ErsatzTV.FFmpeg.Filter;
public class SetDarFilter : BaseFilter
{
private readonly string _displayAspectRatio;
public SetDarFilter(string displayAspectRatio)
{
_displayAspectRatio = displayAspectRatio;
}
public override string Filter => $"setdar=dar={_displayAspectRatio.Replace(':', '/')}";
public override FrameState NextState(FrameState currentState) => currentState;
}
-7
View File
@@ -1,7 +0,0 @@
namespace ErsatzTV.FFmpeg.Filter;
public class SetSarFilter : BaseFilter
{
public override string Filter => "setsar=1";
public override FrameState NextState(FrameState currentState) => currentState;
}
@@ -16,7 +16,7 @@ public class SubtitleHardwareUploadFilter : BaseFilter
{
HardwareAccelerationMode.None => string.Empty,
HardwareAccelerationMode.Nvenc => "hwupload_cuda",
HardwareAccelerationMode.Qsv => "hwupload=extra_hw_frames=64",
HardwareAccelerationMode.Qsv => string.Empty,
// leave vaapi in software since we don't (yet) use overlay_vaapi
HardwareAccelerationMode.Vaapi when _currentState.FrameDataLocation == FrameDataLocation.Software =>
@@ -25,6 +25,9 @@ public class SubtitleHardwareUploadFilter : BaseFilter
// leave videotoolbox in software since we use a software overlay filter
HardwareAccelerationMode.VideoToolbox => string.Empty,
// leave amf in software since we use a software overlay filter
HardwareAccelerationMode.Amf => string.Empty,
_ => "hwupload"
};
@@ -6,20 +6,14 @@ public class SubtitlePixelFormatFilter : BaseFilter
public SubtitlePixelFormatFilter(FFmpegState ffmpegState) => _ffmpegState = ffmpegState;
public override string Filter
{
get
{
Option<string> maybeFormat = _ffmpegState.EncoderHardwareAccelerationMode switch
{
HardwareAccelerationMode.Nvenc => "yuva420p",
HardwareAccelerationMode.Qsv => "yuva420p",
_ => None
};
public override string Filter => MaybeFormat.Match(f => $"format={f}", () => string.Empty);
return maybeFormat.Match(f => $"format={f}", () => string.Empty);
}
}
public Option<string> MaybeFormat =>_ffmpegState.EncoderHardwareAccelerationMode switch
{
HardwareAccelerationMode.Nvenc => "yuva420p",
HardwareAccelerationMode.Qsv => "yuva420p",
_ => None
};
public override FrameState NextState(FrameState currentState) => currentState;
}
@@ -37,14 +37,8 @@ public class ScaleVaapiFilter : BaseFilter
format = $":format={pixelFormat.FFmpegName}";
}
string aspectRatio = string.Empty;
if (_scaledSize != _paddedSize)
{
aspectRatio = ":force_original_aspect_ratio=1";
}
string targetSize = $"{_paddedSize.Width}:{_paddedSize.Height}";
scale = $"scale_vaapi={targetSize}{aspectRatio}:force_divisible_by=2{format}";
scale = $"scale_vaapi={targetSize}:force_divisible_by=2{format}";
}
if (_currentState.FrameDataLocation == FrameDataLocation.Hardware)
@@ -15,7 +15,7 @@ public class WatermarkHardwareUploadFilter : BaseFilter
{
HardwareAccelerationMode.None => string.Empty,
HardwareAccelerationMode.Nvenc => "hwupload_cuda",
HardwareAccelerationMode.Qsv => "hwupload=extra_hw_frames=64",
HardwareAccelerationMode.Qsv => $"hwupload=extra_hw_frames={_ffmpegState.QsvExtraHardwareFrames}",
// leave vaapi in software since we don't (yet) use overlay_vaapi
HardwareAccelerationMode.Vaapi when _currentState.FrameDataLocation == FrameDataLocation.Software =>
@@ -24,6 +24,9 @@ public class WatermarkHardwareUploadFilter : BaseFilter
// leave videotoolbox in software since we use a software overlay filter
HardwareAccelerationMode.VideoToolbox => string.Empty,
// leave amf in software since we use a software overlay filter
HardwareAccelerationMode.Amf => string.Empty,
_ => "hwupload"
};
@@ -11,6 +11,7 @@ public static class AvailablePixelFormats
PixelFormat.YUV420P10LE => new PixelFormatYuv420P10Le(),
PixelFormat.YUVJ420P => new PixelFormatYuvJ420P(),
PixelFormat.YUV444P => new PixelFormatYuv444P(),
PixelFormat.YUVA420P => new PixelFormatYuva420P(),
_ => LogUnknownPixelFormat(pixelFormat, logger)
};
+1
View File
@@ -4,6 +4,7 @@ public class FFmpegFormat
{
public const string YUV420P = "yuv420p";
public const string YUV444P = "yuv444p";
public const string YUVA420P = "yuva420p";
public const string P010LE = "p010le";
public const string NV12 = "nv12";
}
+1
View File
@@ -4,6 +4,7 @@ public static class PixelFormat
{
public const string YUV420P = "yuv420p";
public const string YUV420P10LE = "yuv420p10le";
public const string YUVA420P = "yuva420p";
public const string YUVJ420P = "yuvj420p";
public const string YUV444P = "yuv444p";
public const string YUV444P10LE = "yuv444p10le";
@@ -0,0 +1,8 @@
namespace ErsatzTV.FFmpeg.Format;
public class PixelFormatYuva420P : IPixelFormat
{
public string Name => PixelFormat.YUVA420P;
public string FFmpegName => FFmpegFormat.YUVA420P;
public int BitDepth => 8;
}
+1
View File
@@ -9,6 +9,7 @@ public record FrameState(
Option<IPixelFormat> PixelFormat,
FrameSize ScaledSize,
FrameSize PaddedSize,
string DisplayAspectRatio,
Option<int> FrameRate,
Option<int> VideoBitrate,
Option<int> VideoBufferSize,
+2 -1
View File
@@ -6,5 +6,6 @@ public enum HardwareAccelerationMode
Qsv = 1,
Nvenc = 2,
Vaapi = 3,
VideoToolbox = 4
VideoToolbox = 4,
Amf = 5
}
+1
View File
@@ -19,6 +19,7 @@ public record ConcatInputFile(string Url, FrameSize Resolution) : InputFile(
string.Empty,
Option<IPixelFormat>.None,
Resolution,
string.Empty,
Option<string>.None,
false)
})
+1
View File
@@ -14,6 +14,7 @@ public record VideoStream(
string Codec,
Option<IPixelFormat> PixelFormat,
FrameSize FrameSize,
string DisplayAspectRatio,
Option<string> FrameRate,
bool StillImage) : MediaStream(
Index,
@@ -0,0 +1,11 @@
namespace ErsatzTV.FFmpeg.Option.HardwareAcceleration;
public class AmfHardwareAccelerationOption : GlobalOption
{
public override IList<string> GlobalOptions => new List<string> { "-hwaccel", "dxva2" };
public override FrameState NextState(FrameState currentState) => currentState with
{
FrameDataLocation = FrameDataLocation.Software
};
}
@@ -14,6 +14,7 @@ public static class AvailableHardwareAccelerationOptions
HardwareAccelerationMode.Qsv => new QsvHardwareAccelerationOption(),
HardwareAccelerationMode.Vaapi => GetVaapiAcceleration(vaapiDevice, logger),
HardwareAccelerationMode.VideoToolbox => new VideoToolboxHardwareAccelerationOption(),
HardwareAccelerationMode.Amf => new AmfHardwareAccelerationOption(),
HardwareAccelerationMode.None => Option<IPipelineStep>.None,
_ => LogUnknownMode(mode, logger)
};
+77 -15
View File
@@ -10,6 +10,7 @@ using ErsatzTV.FFmpeg.Option.HardwareAcceleration;
using ErsatzTV.FFmpeg.Option.Metadata;
using ErsatzTV.FFmpeg.OutputFormat;
using ErsatzTV.FFmpeg.Protocol;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.FFmpeg.State;
using Microsoft.Extensions.Logging;
@@ -19,6 +20,7 @@ public class PipelineBuilder
{
private readonly Option<AudioInputFile> _audioInputFile;
private readonly string _fontsFolder;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IHardwareCapabilities _hardwareCapabilities;
private readonly ILogger _logger;
private readonly List<IPipelineStep> _pipelineSteps;
@@ -28,6 +30,7 @@ public class PipelineBuilder
private readonly Option<WatermarkInputFile> _watermarkInputFile;
public PipelineBuilder(
IRuntimeInfo runtimeInfo,
IHardwareCapabilities hardwareCapabilities,
Option<VideoInputFile> videoInputFile,
Option<AudioInputFile> audioInputFile,
@@ -49,6 +52,7 @@ public class PipelineBuilder
new ClosedGopOutputOption()
};
_runtimeInfo = runtimeInfo;
_hardwareCapabilities = hardwareCapabilities;
_videoInputFile = videoInputFile;
_audioInputFile = audioInputFile;
@@ -188,6 +192,7 @@ public class PipelineBuilder
videoStream.PixelFormat,
videoStream.FrameSize,
videoStream.FrameSize,
videoStream.DisplayAspectRatio,
initialFrameRate,
Option<int>.None,
Option<int>.None,
@@ -380,18 +385,23 @@ public class PipelineBuilder
currentState = padStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(padStep));
IPipelineFilterStep sarStep = new SetSarFilter();
currentState = sarStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(sarStep));
if (videoStream.DisplayAspectRatio == desiredState.DisplayAspectRatio)
{
IPipelineFilterStep darStep = new SetDarFilter(desiredState.DisplayAspectRatio);
currentState = darStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(darStep));
}
}
}
else if (currentState.ScaledSize != desiredState.ScaledSize)
{
IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration(
_runtimeInfo,
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter));
@@ -403,17 +413,23 @@ public class PipelineBuilder
_videoInputFile.Iter(f => f.FilterSteps.Add(padStep));
}
IPipelineFilterStep sarStep = new SetSarFilter();
currentState = sarStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(sarStep));
if (videoStream.DisplayAspectRatio == desiredState.DisplayAspectRatio ||
ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Qsv)
{
IPipelineFilterStep darStep = new SetDarFilter(desiredState.DisplayAspectRatio);
currentState = darStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(darStep));
}
}
else if (currentState.PaddedSize != desiredState.PaddedSize)
{
IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration(
_runtimeInfo,
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter));
@@ -424,9 +440,13 @@ public class PipelineBuilder
_videoInputFile.Iter(f => f.FilterSteps.Add(padStep));
}
IPipelineFilterStep sarStep = new SetSarFilter();
currentState = sarStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(sarStep));
if (videoStream.DisplayAspectRatio == desiredState.DisplayAspectRatio ||
ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Qsv)
{
IPipelineFilterStep darStep = new SetDarFilter(desiredState.DisplayAspectRatio);
currentState = darStep.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(darStep));
}
}
if (hasOverlay && currentState.PixelFormat.Map(pf => pf.FFmpegName) !=
@@ -460,10 +480,12 @@ public class PipelineBuilder
currentState = currentState with { PixelFormat = desiredState.PixelFormat };
IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration(
_runtimeInfo,
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter));
}
@@ -489,10 +511,12 @@ public class PipelineBuilder
currentState = currentState with { PixelFormat = desiredState.PixelFormat };
IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration(
_runtimeInfo,
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
videoInputFile.FilterSteps.Add(scaleFilter);
}
@@ -529,6 +553,16 @@ public class PipelineBuilder
// TODO: if all video filters are software, use software pixel format for hwaccel output
// might be able to skip scale_cuda=format=whatever,hwdownload,format=whatever
if (_audioInputFile.IsNone)
{
// always need to specify audio codec so ffmpeg doesn't default to a codec we don't want
foreach (IEncoder step in AvailableEncoders.ForAudioFormat(AudioState.Copy, _logger))
{
currentState = step.NextState(currentState);
_pipelineSteps.Add(step);
}
}
foreach (AudioInputFile audioInputFile in _audioInputFile)
{
// always need to specify audio codec so ffmpeg doesn't default to a codec we don't want
@@ -589,9 +623,35 @@ public class PipelineBuilder
_videoInputFile.Iter(f => f.FilterSteps.Add(downloadFilter));
}
subtitleInputFile.FilterSteps.Add(new SubtitlePixelFormatFilter(ffmpegState));
var pixelFormatFilter = new SubtitlePixelFormatFilter(ffmpegState);
subtitleInputFile.FilterSteps.Add(pixelFormatFilter);
subtitleInputFile.FilterSteps.Add(new SubtitleHardwareUploadFilter(currentState, ffmpegState));
FrameState fakeState = currentState;
foreach (string format in pixelFormatFilter.MaybeFormat)
{
fakeState = fakeState with
{
PixelFormat = AvailablePixelFormats.ForPixelFormat(format, _logger)
};
}
// hacky check for actual scaling or padding
if (_videoInputFile.Exists(
v => v.FilterSteps.Any(s => s.Filter.Contains(currentState.PaddedSize.Height.ToString()))))
{
// enable scaling the subtitle stream
fakeState = fakeState with { ScaledSize = new FrameSize(1, 1) };
}
IPipelineFilterStep scaleFilter = AvailableSubtitleScaleFilters.ForAcceleration(
ffmpegState.EncoderHardwareAccelerationMode,
fakeState,
desiredState.ScaledSize,
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
subtitleInputFile.FilterSteps.Add(scaleFilter);
}
else
{
@@ -605,10 +665,12 @@ public class PipelineBuilder
_videoInputFile.Map(f => f.FilterSteps.Count).IfNone(1) == 0)
{
IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration(
_runtimeInfo,
ffmpegState.EncoderHardwareAccelerationMode,
currentState,
desiredState.ScaledSize,
desiredState.PaddedSize);
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = scaleFilter.NextState(currentState);
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter));
}
@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace ErsatzTV.Core.Interfaces.Runtime;
namespace ErsatzTV.FFmpeg.Runtime;
public interface IRuntimeInfo
{
+12 -1
View File
@@ -7,4 +7,15 @@ public record AudioState(
Option<int> AudioBufferSize,
Option<int> AudioSampleRate,
Option<TimeSpan> AudioDuration,
bool NormalizeLoudness);
bool NormalizeLoudness)
{
public static readonly AudioState Copy = new(
Format.AudioFormat.Copy,
Option<int>.None,
Option<int>.None,
Option<int>.None,
Option<int>.None,
Option<TimeSpan>.None,
false
);
}
@@ -10,8 +10,8 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
@@ -44,7 +44,7 @@ public static class DbInitializer
await context.SaveChangesAsync(cancellationToken);
}
if (context.Resolutions.Any())
if (context.Resolutions.Any(x => x.Width == 1920))
{
return Unit.Default;
}

Some files were not shown because too many files have changed in this diff Show More