Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b8dc0ed80 | ||
|
|
3cc1286271 | ||
|
|
df281758b7 | ||
|
|
25273c18c8 | ||
|
|
f1be945423 | ||
|
|
9a4f772f53 | ||
|
|
d669e8114b | ||
|
|
3972e3603b | ||
|
|
acc22fcb62 | ||
|
|
2df360d7fb | ||
|
|
46331ed2c6 | ||
|
|
3aee3b0515 | ||
|
|
72c45692b2 | ||
|
|
8edf71ca55 | ||
|
|
612b9e6524 | ||
|
|
7aff65f07b | ||
|
|
5d350fcfad | ||
|
|
5546ad204c | ||
|
|
d66efa0a1d | ||
|
|
36d3d38530 | ||
|
|
8e79141860 | ||
|
|
9b3545f7ca | ||
|
|
56db20faa0 | ||
|
|
b0bd4c9fed | ||
|
|
ba079452e2 |
@@ -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
|
||||
|
||||
+47
-2
@@ -5,6 +5,49 @@ 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
|
||||
@@ -1280,7 +1323,9 @@ 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.5-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
|
||||
@@ -1382,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+8
-3
@@ -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,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()
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -6,5 +6,6 @@ public enum HardwareAccelerationMode
|
||||
Qsv = 1,
|
||||
Nvenc = 2,
|
||||
Vaapi = 3,
|
||||
VideoToolbox = 4
|
||||
VideoToolbox = 4,
|
||||
Amf = 5
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public record ConcatInputFile(string Url, FrameSize Resolution) : InputFile(
|
||||
string.Empty,
|
||||
Option<IPixelFormat>.None,
|
||||
Resolution,
|
||||
string.Empty,
|
||||
Option<string>.None,
|
||||
false)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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
-1
@@ -1,7 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Runtime;
|
||||
namespace ErsatzTV.FFmpeg.Runtime;
|
||||
|
||||
public interface IRuntimeInfo
|
||||
{
|
||||
@@ -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" />
|
||||
|
||||
@@ -419,12 +419,20 @@ public class MediaCollectionRepository : IMediaCollectionRepository
|
||||
|
||||
result.Add(
|
||||
new CollectionWithItems(
|
||||
id,
|
||||
id--,
|
||||
items.OfType<Movie>().Cast<MediaItem>().ToList(),
|
||||
true,
|
||||
PlaybackOrder.Chronological,
|
||||
false));
|
||||
|
||||
result.Add(
|
||||
new CollectionWithItems(
|
||||
id,
|
||||
items.OfType<OtherVideo>().Cast<MediaItem>().ToList(),
|
||||
true,
|
||||
PlaybackOrder.Chronological,
|
||||
false));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user