Compare commits
8 Commits
v0.5.0-bet
...
v0.5.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60965d0961 | ||
|
|
ff1a7b376f | ||
|
|
741b00fd52 | ||
|
|
7e55681916 | ||
|
|
210630d299 | ||
|
|
0ddbb898d6 | ||
|
|
d6bf579436 | ||
|
|
765df64555 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -19,14 +19,14 @@ jobs:
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
tag2="${tag:1}"
|
||||
short=$(git rev-parse --short HEAD)
|
||||
final="${tag2/alpha/$short}"
|
||||
final="${tag2/beta/$short}"
|
||||
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
||||
- name: Extract Artifacts Version
|
||||
shell: bash
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
short=$(git rev-parse --short HEAD)
|
||||
final="${tag/alpha/$short}"
|
||||
final="${tag/beta/$short}"
|
||||
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
|
||||
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
||||
outputs:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
||||
echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
|
||||
echo "DOCKER_TAG=${tag/-beta/}" >> $GITHUB_ENV
|
||||
- name: Extract Artifacts Version
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -5,6 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.1-beta] - 2022-04-17
|
||||
### Fixed
|
||||
- Fix subtitles edge case with NVENC
|
||||
- Only select picture subtitles (text subtitles are not yet supported)
|
||||
- Supported picture subtitles are `hdmv_pgs_subtitle` and `dvd_subtitle`
|
||||
- Fix subtitles using software encoders, videotoolbox, VAAPI
|
||||
- Fix setting VAAPI driver name
|
||||
- Fix ffmpeg troubleshooting reports
|
||||
- Fix bug where filler would behave as if it were configured to pad even though a different mode was selected
|
||||
- Fix bug where mid-roll count filler would skip scheduling the final chapter in an episode
|
||||
|
||||
### Added
|
||||
- Add `Empty Trash` button to `Trash` page
|
||||
|
||||
## [0.5.0-beta] - 2022-04-13
|
||||
### Fixed
|
||||
- Fix `HLS Segmenter` bug where it would drift off of the schedule if a playout was changed while the segmenter was running
|
||||
@@ -1068,7 +1082,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.0-beta...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.1-beta...HEAD
|
||||
[0.5.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.0-beta...v0.5.1-beta
|
||||
[0.5.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.5-alpha...v0.5.0-beta
|
||||
[0.4.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.4-alpha...v0.4.5-alpha
|
||||
[0.4.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.3-alpha...v0.4.4-alpha
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -70,16 +69,13 @@ public class
|
||||
|
||||
string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None);
|
||||
|
||||
Process process = _ffmpegProcessService.ResizeImage(
|
||||
Command process = _ffmpegProcessService.ResizeImage(
|
||||
ffmpegPath,
|
||||
originalPath,
|
||||
withExtension,
|
||||
request.MaxHeight.Value);
|
||||
|
||||
CommandResult resize = await Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteAsync();
|
||||
CommandResult resize = await process.ExecuteAsync();
|
||||
|
||||
if (resize.ExitCode != 0)
|
||||
{
|
||||
|
||||
5
ErsatzTV.Application/Maintenance/Commands/EmptyTrash.cs
Normal file
5
ErsatzTV.Application/Maintenance/Commands/EmptyTrash.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Maintenance;
|
||||
|
||||
public record EmptyTrash : IRequest<Either<BaseError, Unit>>;
|
||||
@@ -0,0 +1,55 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Search;
|
||||
|
||||
namespace ErsatzTV.Application.Maintenance;
|
||||
|
||||
public class EmptyTrashHandler : IRequestHandler<EmptyTrash, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public EmptyTrashHandler(
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
ISearchIndex searchIndex)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
EmptyTrash request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
string[] types =
|
||||
{
|
||||
SearchIndex.MovieType,
|
||||
SearchIndex.ShowType,
|
||||
SearchIndex.SeasonType,
|
||||
SearchIndex.EpisodeType,
|
||||
SearchIndex.MusicVideoType,
|
||||
SearchIndex.OtherVideoType,
|
||||
SearchIndex.SongType,
|
||||
SearchIndex.ArtistType
|
||||
};
|
||||
|
||||
var ids = new List<int>();
|
||||
|
||||
foreach (string type in types)
|
||||
{
|
||||
SearchResult result = await _searchIndex.Search($"type:{type} AND (state:FileNotFound)", 0, 0);
|
||||
ids.AddRange(result.Items.Map(i => i.Id));
|
||||
}
|
||||
|
||||
Either<BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(ids);
|
||||
if (deleteResult.IsRight)
|
||||
{
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
}
|
||||
|
||||
return deleteResult;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
@@ -207,18 +207,15 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
{
|
||||
await TrimAndDelete(cancellationToken);
|
||||
|
||||
using Process process = processModel.Process;
|
||||
Command process = processModel.Process;
|
||||
|
||||
_logger.LogInformation(
|
||||
"ffmpeg hls arguments {FFmpegArguments}",
|
||||
string.Join(" ", process.StartInfo.ArgumentList));
|
||||
_logger.LogInformation("ffmpeg hls arguments {FFmpegArguments}", process.Arguments);
|
||||
|
||||
try
|
||||
{
|
||||
BufferedCommandResult commandResult = await Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
BufferedCommandResult commandResult = await process
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync(cancellationToken);
|
||||
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
|
||||
|
||||
if (commandResult.ExitCode == 0)
|
||||
{
|
||||
@@ -256,16 +253,15 @@ public class HlsSessionWorker : IHlsSessionWorker
|
||||
|
||||
foreach (PlayoutItemProcessModel errorProcessModel in maybeOfflineProcess.RightAsEnumerable())
|
||||
{
|
||||
Process errorProcess = errorProcessModel.Process;
|
||||
Command errorProcess = errorProcessModel.Process;
|
||||
|
||||
_logger.LogInformation(
|
||||
"ffmpeg hls error arguments {FFmpegArguments}",
|
||||
string.Join(" ", errorProcess.StartInfo.ArgumentList));
|
||||
errorProcess.Arguments);
|
||||
|
||||
commandResult = await Cli.Wrap(errorProcess.StartInfo.FileName)
|
||||
.WithArguments(errorProcess.StartInfo.ArgumentList)
|
||||
commandResult = await errorProcess
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync(cancellationToken);
|
||||
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
|
||||
|
||||
if (commandResult.ExitCode == 0)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming;
|
||||
|
||||
public record PlayoutItemProcessModel(Process Process, Option<TimeSpan> MaybeDuration, DateTimeOffset Until);
|
||||
public record PlayoutItemProcessModel(
|
||||
Command Process,
|
||||
Option<TimeSpan> MaybeDuration,
|
||||
DateTimeOffset Until);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -32,7 +32,7 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
Process process = _ffmpegProcessService.ConcatChannel(
|
||||
Command process = _ffmpegProcessService.ConcatChannel(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
channel,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -27,7 +27,7 @@ public class GetErrorProcessHandler : FFmpegProcessHandler<GetErrorProcess>
|
||||
string ffprobePath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Process process = await _ffmpegProcessService.ForError(
|
||||
Command process = await _ffmpegProcessService.ForError(
|
||||
ffmpegPath,
|
||||
channel,
|
||||
request.MaybeDuration,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
@@ -139,7 +139,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
Process process = await _ffmpegProcessService.ForPlayoutItem(
|
||||
Command process = await _ffmpegProcessService.ForPlayoutItem(
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
saveReports,
|
||||
@@ -185,7 +185,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
switch (error)
|
||||
{
|
||||
case UnableToLocatePlayoutItem:
|
||||
Process offlineProcess = await _ffmpegProcessService.ForError(
|
||||
Command offlineProcess = await _ffmpegProcessService.ForError(
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
@@ -195,7 +195,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
|
||||
return new PlayoutItemProcessModel(offlineProcess, maybeDuration, finish);
|
||||
case PlayoutItemDoesNotExistOnDisk:
|
||||
Process doesNotExistProcess = await _ffmpegProcessService.ForError(
|
||||
Command doesNotExistProcess = await _ffmpegProcessService.ForError(
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
@@ -205,7 +205,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
|
||||
return new PlayoutItemProcessModel(doesNotExistProcess, maybeDuration, finish);
|
||||
default:
|
||||
Process errorProcess = await _ffmpegProcessService.ForError(
|
||||
Command errorProcess = await _ffmpegProcessService.ForError(
|
||||
ffmpegPath,
|
||||
channel,
|
||||
maybeDuration,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -32,7 +32,7 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW
|
||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
Process process = _ffmpegProcessService.WrapSegmenter(
|
||||
Command process = _ffmpegProcessService.WrapSegmenter(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
channel,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<PackageReference Include="CliWrap" Version="3.4.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.6.0" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
||||
@@ -36,6 +37,9 @@
|
||||
<Content Include="Resources\ErsatzTV.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Resources\test.sup">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -65,6 +65,12 @@ public class TranscodingTests
|
||||
// TODO: animated vs static
|
||||
}
|
||||
|
||||
public enum Subtitle
|
||||
{
|
||||
None,
|
||||
Picture
|
||||
}
|
||||
|
||||
private class TestData
|
||||
{
|
||||
public static Watermark[] Watermarks =
|
||||
@@ -73,6 +79,12 @@ public class TranscodingTests
|
||||
Watermark.PermanentOpaque,
|
||||
Watermark.PermanentTransparent
|
||||
};
|
||||
|
||||
public static Subtitle[] Subtitles =
|
||||
{
|
||||
Subtitle.None,
|
||||
Subtitle.Picture
|
||||
};
|
||||
|
||||
public static Padding[] Paddings =
|
||||
{
|
||||
@@ -158,6 +170,7 @@ public class TranscodingTests
|
||||
[ValueSource(typeof(TestData), nameof(TestData.Paddings))] Padding padding,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))] VideoScanKind videoScanKind,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.Watermarks))] Watermark watermark,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.Subtitles))] Subtitle subtitle,
|
||||
[ValueSource(typeof(TestData), nameof(TestData.VideoFormats))] FFmpegProfileVideoFormat profileVideoFormat,
|
||||
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||
@@ -175,7 +188,7 @@ public class TranscodingTests
|
||||
}
|
||||
|
||||
string name = GetStringSha256Hash(
|
||||
$"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileVideoFormat}_{profileAcceleration}");
|
||||
$"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{watermark}_{subtitle}_{profileResolution}_{profileVideoFormat}_{profileAcceleration}");
|
||||
|
||||
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
|
||||
if (!File.Exists(file))
|
||||
@@ -203,6 +216,46 @@ public class TranscodingTests
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
p1.WaitForExit();
|
||||
p1.ExitCode.Should().Be(0);
|
||||
|
||||
switch (subtitle)
|
||||
{
|
||||
case Subtitle.Picture:
|
||||
string sourceFile = Path.GetTempFileName() + ".mkv";
|
||||
File.Move(file, sourceFile, true);
|
||||
|
||||
string tempFileName = Path.GetTempFileName() + ".mkv";
|
||||
string subPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "test.sup");
|
||||
var p2 = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ExecutableName("mkvmerge"),
|
||||
Arguments = $"-o {tempFileName} {sourceFile} {subPath}"
|
||||
}
|
||||
};
|
||||
|
||||
p2.Start();
|
||||
await p2.WaitForExitAsync();
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
p2.WaitForExit();
|
||||
if (p2.ExitCode != 0)
|
||||
{
|
||||
if (File.Exists(sourceFile))
|
||||
{
|
||||
File.Delete(sourceFile);
|
||||
}
|
||||
|
||||
if (File.Exists(file))
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
p2.ExitCode.Should().Be(0);
|
||||
|
||||
File.Move(tempFileName, file, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var imageCache = new Mock<IImageCache>();
|
||||
@@ -221,7 +274,7 @@ public class TranscodingTests
|
||||
imageCache.Object,
|
||||
new Mock<ITempFilePool>().Object,
|
||||
new Mock<IClient>().Object,
|
||||
new Mock<IMemoryCache>().Object,
|
||||
new MemoryCache(new MemoryCacheOptions()),
|
||||
LoggerFactory.CreateLogger<FFmpegProcessService>());
|
||||
|
||||
var service = new FFmpegLibraryProcessService(
|
||||
@@ -318,7 +371,13 @@ public class TranscodingTests
|
||||
break;
|
||||
}
|
||||
|
||||
using Process process = await service.ForPlayoutItem(
|
||||
ChannelSubtitleMode subtitleMode = subtitle switch
|
||||
{
|
||||
Subtitle.Picture => ChannelSubtitleMode.Any,
|
||||
_ => ChannelSubtitleMode.None
|
||||
};
|
||||
|
||||
Command process = await service.ForPlayoutItem(
|
||||
ExecutableName("ffmpeg"),
|
||||
ExecutableName("ffprobe"),
|
||||
false,
|
||||
@@ -329,9 +388,11 @@ public class TranscodingTests
|
||||
{
|
||||
HardwareAcceleration = profileAcceleration,
|
||||
VideoFormat = profileVideoFormat,
|
||||
AudioFormat = FFmpegProfileAudioFormat.Aac
|
||||
AudioFormat = FFmpegProfileAudioFormat.Aac,
|
||||
DeinterlaceVideo = true
|
||||
},
|
||||
StreamingMode = StreamingMode.TransportStream
|
||||
StreamingMode = StreamingMode.TransportStream,
|
||||
SubtitleMode = subtitleMode
|
||||
},
|
||||
v,
|
||||
v,
|
||||
@@ -365,16 +426,14 @@ public class TranscodingTests
|
||||
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
try
|
||||
{
|
||||
result = await Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
result = await process
|
||||
.WithStandardOutputPipe(PipeTarget.ToStream(Stream.Null))
|
||||
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(sb))
|
||||
.ExecuteAsync(timeoutSignal.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
|
||||
Assert.Fail($"Transcode failure (timeout): ffmpeg {string.Join(" ", quotedArgs)}");
|
||||
Assert.Fail($"Transcode failure (timeout): ffmpeg {process.Arguments}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -383,22 +442,19 @@ public class TranscodingTests
|
||||
|
||||
if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported)
|
||||
{
|
||||
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
|
||||
result.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}");
|
||||
Assert.Warn($"Unsupported on this hardware: ffmpeg {string.Join(" ", quotedArgs)}");
|
||||
result.ExitCode.Should().Be(1, $"Error message with successful exit code? {process.Arguments}");
|
||||
Assert.Warn($"Unsupported on this hardware: ffmpeg {process.Arguments}");
|
||||
}
|
||||
else if (error.Contains("Impossible to convert between"))
|
||||
{
|
||||
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
|
||||
Assert.Fail($"Transcode failure: ffmpeg {string.Join(" ", quotedArgs)}");
|
||||
Assert.Fail($"Transcode failure: ffmpeg {process.Arguments}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
|
||||
result.ExitCode.Should().Be(0, error + Environment.NewLine + string.Join(" ", quotedArgs));
|
||||
result.ExitCode.Should().Be(0, error + Environment.NewLine + process.Arguments);
|
||||
if (result.ExitCode == 0)
|
||||
{
|
||||
Console.WriteLine(string.Join(" ", quotedArgs));
|
||||
Console.WriteLine(process.Arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,7 +481,7 @@ public class TranscodingTests
|
||||
Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask();
|
||||
|
||||
public Task<Option<MediaStream>> SelectSubtitleStream(Channel channel, MediaVersion version) =>
|
||||
Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Subtitle)).AsTask();
|
||||
Optional(version.Streams.Find(s => s.MediaStreamKind == MediaStreamKind.Subtitle)).AsTask();
|
||||
}
|
||||
|
||||
private static string ExecutableName(string baseName) =>
|
||||
|
||||
BIN
ErsatzTV.Core.Tests/Resources/test.sup
Executable file
BIN
ErsatzTV.Core.Tests/Resources/test.sup
Executable file
Binary file not shown.
@@ -3,8 +3,10 @@ using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Serilog;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling;
|
||||
|
||||
@@ -222,7 +224,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
List<PlayoutItem> playoutItems = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
||||
List<PlayoutItem> playoutItems = Scheduler()
|
||||
.AddFiller(
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
@@ -273,7 +275,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
// too lazy to make another enumerator for the filler that we don't want
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), enumerator);
|
||||
|
||||
List<PlayoutItem> playoutItems = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
||||
List<PlayoutItem> playoutItems = Scheduler()
|
||||
.AddFiller(
|
||||
startState,
|
||||
enumerators,
|
||||
@@ -283,6 +285,77 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
|
||||
playoutItems.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Schedule_Mid_Roll_Count_Filler_Correctly()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null,
|
||||
MidRollFiller = new FillerPreset
|
||||
{
|
||||
FillerKind = FillerKind.MidRoll,
|
||||
FillerMode = FillerMode.Count,
|
||||
PadToNearestMinute = 60, // this should be ignored
|
||||
Count = 1
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
var fillerEnumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionTwo.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators = CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator);
|
||||
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), fillerEnumerator);
|
||||
|
||||
List<PlayoutItem> playoutItems = Scheduler()
|
||||
.AddFiller(
|
||||
startState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
new PlayoutItem
|
||||
{
|
||||
MediaItemId = 1,
|
||||
Start = startState.CurrentTime.UtcDateTime,
|
||||
Finish = startState.CurrentTime.AddHours(1).UtcDateTime
|
||||
},
|
||||
new List<MediaChapter>
|
||||
{
|
||||
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
|
||||
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(60) }
|
||||
});
|
||||
|
||||
playoutItems.Count.Should().Be(3);
|
||||
playoutItems[0].MediaItemId.Should().Be(1);
|
||||
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
|
||||
playoutItems[1].MediaItemId.Should().Be(3);
|
||||
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6));
|
||||
playoutItems[2].MediaItemId.Should().Be(1);
|
||||
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11));
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
@@ -324,4 +397,34 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
new() { Duration = duration }
|
||||
}
|
||||
};
|
||||
|
||||
private static PlayoutModeSchedulerBase<ProgramScheduleItem> Scheduler() =>
|
||||
new TestScheduler();
|
||||
|
||||
private class TestScheduler : PlayoutModeSchedulerBase<ProgramScheduleItem>
|
||||
{
|
||||
private static readonly ILoggerFactory LoggerFactory;
|
||||
|
||||
static TestScheduler()
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
|
||||
}
|
||||
|
||||
public TestScheduler() : base(LoggerFactory.CreateLogger<TestScheduler>())
|
||||
{
|
||||
}
|
||||
|
||||
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
ProgramScheduleItem nextScheduleItem,
|
||||
DateTimeOffset hardStop) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
@@ -32,7 +31,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Process> ForPlayoutItem(
|
||||
public async Task<Command> ForPlayoutItem(
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool saveReports,
|
||||
@@ -226,7 +225,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
|
||||
|
||||
return GetProcess(ffmpegPath, videoInputFile, audioInputFile, watermarkInputFile, None, pipeline);
|
||||
return GetCommand(ffmpegPath, videoInputFile, audioInputFile, watermarkInputFile, None, pipeline);
|
||||
}
|
||||
|
||||
private Option<WatermarkInputFile> GetWatermarkInputFile(
|
||||
@@ -290,7 +289,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
return None;
|
||||
}
|
||||
|
||||
public Task<Process> ForError(
|
||||
public Task<Command> ForError(
|
||||
string ffmpegPath,
|
||||
Channel channel,
|
||||
Option<TimeSpan> duration,
|
||||
@@ -299,7 +298,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
long ptsOffset) =>
|
||||
_ffmpegProcessService.ForError(ffmpegPath, channel, duration, errorMessage, hlsRealtime, ptsOffset);
|
||||
|
||||
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||
public Command ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||
{
|
||||
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
|
||||
|
||||
@@ -319,13 +318,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
concatInputFile,
|
||||
FFmpegState.Concat(saveReports, channel.Name));
|
||||
|
||||
return GetProcess(ffmpegPath, None, None, None, concatInputFile, pipeline);
|
||||
return GetCommand(ffmpegPath, None, None, None, concatInputFile, pipeline);
|
||||
}
|
||||
|
||||
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) =>
|
||||
public Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) =>
|
||||
_ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host);
|
||||
|
||||
public Process ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height)
|
||||
public Command ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height)
|
||||
{
|
||||
var videoInputFile = new VideoInputFile(
|
||||
inputFile,
|
||||
@@ -341,13 +340,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Resize(outputFile, new FrameSize(-1, height));
|
||||
|
||||
return GetProcess(ffmpegPath, videoInputFile, None, None, None, pipeline, false);
|
||||
return GetCommand(ffmpegPath, videoInputFile, None, None, None, pipeline, false);
|
||||
}
|
||||
|
||||
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile) =>
|
||||
public Command ConvertToPng(string ffmpegPath, string inputFile, string outputFile) =>
|
||||
_ffmpegProcessService.ConvertToPng(ffmpegPath, inputFile, outputFile);
|
||||
|
||||
public Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) =>
|
||||
public Command ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) =>
|
||||
_ffmpegProcessService.ExtractAttachedPicAsPng(ffmpegPath, inputFile, streamIndex, outputFile);
|
||||
|
||||
public Task<Either<BaseError, string>> GenerateSongImage(
|
||||
@@ -383,7 +382,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
watermarkWidthPercent,
|
||||
cancellationToken);
|
||||
|
||||
private Process GetProcess(
|
||||
private Command GetCommand(
|
||||
string ffmpegPath,
|
||||
Option<VideoInputFile> videoInputFile,
|
||||
Option<AudioInputFile> audioInputFile,
|
||||
@@ -417,35 +416,16 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
concatInputFile,
|
||||
pipeline.PipelineSteps);
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = ffmpegPath,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = false,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
if (environmentVariables.Any())
|
||||
{
|
||||
_logger.LogDebug("FFmpeg environment variables {EnvVars}", environmentVariables);
|
||||
}
|
||||
|
||||
foreach ((string key, string value) in environmentVariables)
|
||||
{
|
||||
startInfo.EnvironmentVariables[key] = value;
|
||||
}
|
||||
|
||||
foreach (string argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
return new Process
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
return Cli.Wrap(ffmpegPath)
|
||||
.WithArguments(arguments)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null))
|
||||
.WithEnvironmentVariables(environmentVariables.ToDictionary(e => e.Key, e => e.Value));
|
||||
}
|
||||
|
||||
private static Option<string> VaapiDriverName(HardwareAccelerationMode accelerationMode, VaapiDriver driver)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
@@ -39,7 +40,7 @@ public class FFmpegProcessService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Process> ForError(
|
||||
public async Task<Command> ForError(
|
||||
string ffmpegPath,
|
||||
Channel channel,
|
||||
Option<TimeSpan> duration,
|
||||
@@ -102,29 +103,34 @@ public class FFmpegProcessService
|
||||
|
||||
await duration.IfSomeAsync(d => builder = builder.WithDuration(d));
|
||||
|
||||
switch (channel.StreamingMode)
|
||||
Process process = channel.StreamingMode switch
|
||||
{
|
||||
// HLS needs to segment and generate playlist
|
||||
case StreamingMode.HttpLiveStreamingSegmenter:
|
||||
return builder.WithHls(
|
||||
StreamingMode.HttpLiveStreamingSegmenter =>
|
||||
builder.WithHls(
|
||||
channel.Number,
|
||||
None,
|
||||
ptsOffset,
|
||||
playbackSettings.VideoTrackTimeScale,
|
||||
playbackSettings.FrameRate)
|
||||
.Build();
|
||||
default:
|
||||
return builder.WithFormat("mpegts")
|
||||
.WithPipe()
|
||||
.Build();
|
||||
}
|
||||
.Build(),
|
||||
_ => builder.WithFormat("mpegts")
|
||||
.WithPipe()
|
||||
.Build()
|
||||
};
|
||||
|
||||
return Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
|
||||
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null));
|
||||
}
|
||||
|
||||
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||
public Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||
{
|
||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
|
||||
|
||||
return new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
||||
Process process = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
||||
.WithThreads(1)
|
||||
.WithQuiet()
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
@@ -136,27 +142,45 @@ public class FFmpegProcessService
|
||||
.WithFormat("mpegts")
|
||||
.WithPipe()
|
||||
.Build();
|
||||
|
||||
return Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
|
||||
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null));
|
||||
}
|
||||
|
||||
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile)
|
||||
public Command ConvertToPng(string ffmpegPath, string inputFile, string outputFile)
|
||||
{
|
||||
return new FFmpegProcessBuilder(ffmpegPath, false, _logger)
|
||||
Process process = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
|
||||
.WithThreads(1)
|
||||
.WithQuiet()
|
||||
.WithInput(inputFile)
|
||||
.WithOutputFormat("apng", outputFile)
|
||||
.Build();
|
||||
|
||||
return Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
|
||||
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null));
|
||||
}
|
||||
|
||||
public Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile)
|
||||
public Command ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile)
|
||||
{
|
||||
return new FFmpegProcessBuilder(ffmpegPath, false, _logger)
|
||||
Process process = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
|
||||
.WithThreads(1)
|
||||
.WithQuiet()
|
||||
.WithInput(inputFile)
|
||||
.WithMap($"0:{streamIndex}")
|
||||
.WithOutputFormat("apng", outputFile)
|
||||
.Build();
|
||||
|
||||
return Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.WithEnvironmentVariables(process.StartInfo.Environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
|
||||
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null));
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, string>> GenerateSongImage(
|
||||
@@ -414,7 +438,7 @@ public class FFmpegProcessService
|
||||
path
|
||||
})
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync();
|
||||
.ExecuteBufferedAsync(Encoding.UTF8);
|
||||
|
||||
if (result.ExitCode == 0)
|
||||
{
|
||||
|
||||
@@ -99,7 +99,7 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
|
||||
|
||||
var subtitleStreams = version.Streams
|
||||
.Filter(s => s.MediaStreamKind == MediaStreamKind.Subtitle)
|
||||
// .Filter(s => s.Codec is "hdmv_pgs_subtitle" or "dvd_subtitle")
|
||||
.Filter(s => s.Codec is "hdmv_pgs_subtitle" or "dvd_subtitle")
|
||||
.ToList();
|
||||
|
||||
string language = (channel.PreferredSubtitleLanguageCode ?? string.Empty).ToLowerInvariant();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
@@ -8,7 +8,7 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
|
||||
public interface IFFmpegProcessService
|
||||
{
|
||||
Task<Process> ForPlayoutItem(
|
||||
Task<Command> ForPlayoutItem(
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool saveReports,
|
||||
@@ -31,7 +31,7 @@ public interface IFFmpegProcessService
|
||||
long ptsOffset,
|
||||
Option<int> targetFramerate);
|
||||
|
||||
Task<Process> ForError(
|
||||
Task<Command> ForError(
|
||||
string ffmpegPath,
|
||||
Channel channel,
|
||||
Option<TimeSpan> duration,
|
||||
@@ -39,15 +39,15 @@ public interface IFFmpegProcessService
|
||||
bool hlsRealtime,
|
||||
long ptsOffset);
|
||||
|
||||
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||
Command ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||
|
||||
Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||
Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||
|
||||
Process ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height);
|
||||
Command ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height);
|
||||
|
||||
Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile);
|
||||
Command ConvertToPng(string ffmpegPath, string inputFile, string outputFile);
|
||||
|
||||
Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile);
|
||||
Command ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile);
|
||||
|
||||
Task<Either<BaseError, string>> GenerateSongImage(
|
||||
string ffmpegPath,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using Bugsnag;
|
||||
using Bugsnag;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
@@ -163,16 +162,13 @@ public abstract class LocalFolderScanner
|
||||
{
|
||||
// extract attached pic (and convert to png)
|
||||
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt);
|
||||
using Process process = _ffmpegProcessService.ExtractAttachedPicAsPng(
|
||||
Command process = _ffmpegProcessService.ExtractAttachedPicAsPng(
|
||||
path,
|
||||
artworkFile,
|
||||
picIndex,
|
||||
tempName);
|
||||
|
||||
await Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteAsync(cancellationToken);
|
||||
await process.ExecuteAsync(cancellationToken);
|
||||
|
||||
return tempName;
|
||||
},
|
||||
@@ -180,12 +176,9 @@ public abstract class LocalFolderScanner
|
||||
{
|
||||
// no attached pic index means convert to png
|
||||
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt);
|
||||
using Process process = _ffmpegProcessService.ConvertToPng(path, artworkFile, tempName);
|
||||
Command process = _ffmpegProcessService.ConvertToPng(path, artworkFile, tempName);
|
||||
|
||||
await Cli.Wrap(process.StartInfo.FileName)
|
||||
.WithArguments(process.StartInfo.ArgumentList)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteAsync(cancellationToken);
|
||||
await process.ExecuteAsync(cancellationToken);
|
||||
|
||||
return tempName;
|
||||
});
|
||||
|
||||
@@ -191,11 +191,9 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
.Append(Optional(scheduleItem.PostRollFiller))
|
||||
.ToList();
|
||||
|
||||
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1)
|
||||
// if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1)
|
||||
// multiple pad-to-nearest-minute values are invalid; use no filler
|
||||
if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1)
|
||||
{
|
||||
// multiple pad-to-nearest-minute values are invalid; use no filler
|
||||
// TODO: log error?
|
||||
return itemStartTime + itemDuration;
|
||||
}
|
||||
|
||||
@@ -287,16 +285,17 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
}
|
||||
}
|
||||
|
||||
foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue)))
|
||||
foreach (FillerPreset padFiller in Optional(
|
||||
allFiller.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue)))
|
||||
{
|
||||
int currentMinute = (itemStartTime + totalDuration).Minute;
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
|
||||
padFiller.PadToNearestMinute.Value * padFiller.PadToNearestMinute.Value;
|
||||
|
||||
|
||||
DateTimeOffset targetTime = itemStartTime + totalDuration - TimeSpan.FromMinutes(currentMinute) +
|
||||
TimeSpan.FromMinutes(targetMinute);
|
||||
|
||||
|
||||
return new DateTimeOffset(
|
||||
targetTime.Year,
|
||||
targetTime.Month,
|
||||
@@ -310,7 +309,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
return itemStartTime + totalDuration;
|
||||
}
|
||||
|
||||
internal static List<PlayoutItem> AddFiller(
|
||||
internal List<PlayoutItem> AddFiller(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
@@ -324,11 +323,10 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
.Append(Optional(scheduleItem.PostRollFiller))
|
||||
.ToList();
|
||||
|
||||
if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1)
|
||||
// if (allFiller.Map(f => Optional(f.PadToNearestMinute)).Sequence().Flatten().Distinct().Count() > 1)
|
||||
// multiple pad-to-nearest-minute values are invalid; use no filler
|
||||
if (allFiller.Count(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue) > 1)
|
||||
{
|
||||
// multiple pad-to-nearest-minute values are invalid; use no filler
|
||||
// TODO: log error?
|
||||
_logger.LogError("Multiple pad-to-nearest-minute values are invalid; no filler will be used");
|
||||
return new List<PlayoutItem> { playoutItem };
|
||||
}
|
||||
|
||||
@@ -386,15 +384,18 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
break;
|
||||
case FillerMode.Count when filler.Count.HasValue:
|
||||
IMediaCollectionEnumerator e2 = enumerators[CollectionKey.ForFillerPreset(filler)];
|
||||
for (var i = 0; i < effectiveChapters.Count - 1; i++)
|
||||
for (var i = 0; i < effectiveChapters.Count; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
result.AddRange(
|
||||
AddCountFiller(
|
||||
playoutBuilderState,
|
||||
e2,
|
||||
filler.Count.Value,
|
||||
FillerKind.MidRoll));
|
||||
if (i < effectiveChapters.Count - 1)
|
||||
{
|
||||
result.AddRange(
|
||||
AddCountFiller(
|
||||
playoutBuilderState,
|
||||
e2,
|
||||
filler.Count.Value,
|
||||
FillerKind.MidRoll));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -421,13 +422,14 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
}
|
||||
|
||||
// after all non-padded filler has been added, figure out padding
|
||||
foreach (FillerPreset padFiller in Optional(allFiller.FirstOrDefault(f => f.PadToNearestMinute.HasValue)))
|
||||
foreach (FillerPreset padFiller in Optional(
|
||||
allFiller.FirstOrDefault(f => f.FillerMode == FillerMode.Pad && f.PadToNearestMinute.HasValue)))
|
||||
{
|
||||
var totalDuration =
|
||||
TimeSpan.FromMilliseconds(
|
||||
result.Sum(pi => (pi.Finish - pi.Start).TotalMilliseconds) +
|
||||
effectiveChapters.Sum(c => (c.EndTime - c.StartTime).TotalMilliseconds));
|
||||
|
||||
|
||||
int currentMinute = (playoutItem.StartOffset + totalDuration).Minute;
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
int targetMinute = (currentMinute + padFiller.PadToNearestMinute.Value - 1) /
|
||||
@@ -437,7 +439,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
TimeSpan.FromMinutes(currentMinute) +
|
||||
TimeSpan.FromMinutes(targetMinute);
|
||||
|
||||
|
||||
|
||||
var targetTime = new DateTimeOffset(
|
||||
almostTargetTime.Year,
|
||||
almostTargetTime.Month,
|
||||
@@ -532,7 +534,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
case FillerKind.PostRoll:
|
||||
IMediaCollectionEnumerator post1 = enumerators[CollectionKey.ForFillerPreset(padFiller)];
|
||||
|
||||
@@ -15,15 +15,18 @@ public static class AvailableEncoders
|
||||
FrameState currentState,
|
||||
FrameState desiredState,
|
||||
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
||||
Option<SubtitleInputFile> maybeSubtitleInputFile,
|
||||
ILogger logger) =>
|
||||
(ffmpegState.HardwareAccelerationMode, desiredState.VideoFormat) switch
|
||||
{
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new EncoderHevcNvenc(
|
||||
currentState,
|
||||
maybeWatermarkInputFile),
|
||||
maybeWatermarkInputFile,
|
||||
maybeSubtitleInputFile),
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc(
|
||||
currentState,
|
||||
maybeWatermarkInputFile),
|
||||
maybeWatermarkInputFile,
|
||||
maybeSubtitleInputFile),
|
||||
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Hevc) => new EncoderHevcQsv(
|
||||
currentState,
|
||||
@@ -32,10 +35,12 @@ public static class AvailableEncoders
|
||||
|
||||
(HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi(
|
||||
currentState,
|
||||
maybeWatermarkInputFile),
|
||||
maybeWatermarkInputFile,
|
||||
maybeSubtitleInputFile),
|
||||
(HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi(
|
||||
currentState,
|
||||
maybeWatermarkInputFile),
|
||||
maybeWatermarkInputFile,
|
||||
maybeSubtitleInputFile),
|
||||
|
||||
(HardwareAccelerationMode.VideoToolbox, VideoFormat.Hevc) => new EncoderHevcVideoToolbox(),
|
||||
(HardwareAccelerationMode.VideoToolbox, VideoFormat.H264) => new EncoderH264VideoToolbox(),
|
||||
|
||||
@@ -6,11 +6,16 @@ public class EncoderH264Nvenc : EncoderBase
|
||||
{
|
||||
private readonly FrameState _currentState;
|
||||
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
|
||||
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
|
||||
|
||||
public EncoderH264Nvenc(FrameState currentState, Option<WatermarkInputFile> maybeWatermarkInputFile)
|
||||
public EncoderH264Nvenc(
|
||||
FrameState currentState,
|
||||
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
||||
Option<SubtitleInputFile> maybeSubtitleInputFile)
|
||||
{
|
||||
_currentState = currentState;
|
||||
_maybeWatermarkInputFile = maybeWatermarkInputFile;
|
||||
_maybeSubtitleInputFile = maybeSubtitleInputFile;
|
||||
}
|
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with
|
||||
@@ -27,10 +32,15 @@ public class EncoderH264Nvenc : EncoderBase
|
||||
{
|
||||
get
|
||||
{
|
||||
// only upload to hw if we need to overlay a watermark
|
||||
if (_maybeWatermarkInputFile.IsSome && _currentState.FrameDataLocation == FrameDataLocation.Software)
|
||||
// only upload to hw if we need to overlay (watermark or subtitle)
|
||||
if (_currentState.FrameDataLocation == FrameDataLocation.Software)
|
||||
{
|
||||
return "hwupload_cuda";
|
||||
bool isPictureSubtitle = _maybeSubtitleInputFile.Map(s => s.IsImageBased).IfNone(false);
|
||||
|
||||
if (isPictureSubtitle || _maybeWatermarkInputFile.IsSome)
|
||||
{
|
||||
return "hwupload_cuda";
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
||||
@@ -6,11 +6,16 @@ public class EncoderHevcNvenc : EncoderBase
|
||||
{
|
||||
private readonly FrameState _currentState;
|
||||
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
|
||||
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
|
||||
|
||||
public EncoderHevcNvenc(FrameState currentState, Option<WatermarkInputFile> maybeWatermarkInputFile)
|
||||
public EncoderHevcNvenc(
|
||||
FrameState currentState,
|
||||
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
||||
Option<SubtitleInputFile> maybeSubtitleInputFile)
|
||||
{
|
||||
_currentState = currentState;
|
||||
_maybeWatermarkInputFile = maybeWatermarkInputFile;
|
||||
_maybeSubtitleInputFile = maybeSubtitleInputFile;
|
||||
}
|
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with
|
||||
@@ -27,10 +32,15 @@ public class EncoderHevcNvenc : EncoderBase
|
||||
{
|
||||
get
|
||||
{
|
||||
// only upload to hw if we need to overlay a watermark
|
||||
if (_maybeWatermarkInputFile.IsSome && _currentState.FrameDataLocation == FrameDataLocation.Software)
|
||||
// only upload to hw if we need to overlay (watermark or subtitle)
|
||||
if (_currentState.FrameDataLocation == FrameDataLocation.Software)
|
||||
{
|
||||
return "hwupload_cuda";
|
||||
bool isPictureSubtitle = _maybeSubtitleInputFile.Map(s => s.IsImageBased).IfNone(false);
|
||||
|
||||
if (isPictureSubtitle || _maybeWatermarkInputFile.IsSome)
|
||||
{
|
||||
return "hwupload_cuda";
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
||||
@@ -6,11 +6,16 @@ public class EncoderH264Vaapi : EncoderBase
|
||||
{
|
||||
private readonly FrameState _currentState;
|
||||
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
|
||||
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
|
||||
|
||||
public EncoderH264Vaapi(FrameState currentState, Option<WatermarkInputFile> maybeWatermarkInputFile)
|
||||
public EncoderH264Vaapi(
|
||||
FrameState currentState,
|
||||
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
||||
Option<SubtitleInputFile> maybeSubtitleInputFile)
|
||||
{
|
||||
_currentState = currentState;
|
||||
_maybeWatermarkInputFile = maybeWatermarkInputFile;
|
||||
_maybeSubtitleInputFile = maybeSubtitleInputFile;
|
||||
}
|
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with
|
||||
@@ -22,14 +27,19 @@ public class EncoderH264Vaapi : EncoderBase
|
||||
public override string Name => "h264_vaapi";
|
||||
public override StreamKind Kind => StreamKind.Video;
|
||||
|
||||
// need to upload if we're still in software unless a watermark is used
|
||||
// need to upload if we're still in software unless a watermark or picture subtitle is used
|
||||
public override string Filter
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_maybeWatermarkInputFile.IsNone && _currentState.FrameDataLocation == FrameDataLocation.Software)
|
||||
if (_currentState.FrameDataLocation == FrameDataLocation.Software)
|
||||
{
|
||||
return "format=nv12|vaapi,hwupload";
|
||||
bool isNotImageSubtitle = _maybeSubtitleInputFile.Map(s => s.IsImageBased).IfNone(false) == false;
|
||||
|
||||
if (_maybeWatermarkInputFile.IsNone && isNotImageSubtitle)
|
||||
{
|
||||
return "format=nv12|vaapi,hwupload";
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
||||
@@ -6,11 +6,16 @@ public class EncoderHevcVaapi : EncoderBase
|
||||
{
|
||||
private readonly FrameState _currentState;
|
||||
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
|
||||
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
|
||||
|
||||
public EncoderHevcVaapi(FrameState currentState, Option<WatermarkInputFile> maybeWatermarkInputFile)
|
||||
public EncoderHevcVaapi(
|
||||
FrameState currentState,
|
||||
Option<WatermarkInputFile> maybeWatermarkInputFile,
|
||||
Option<SubtitleInputFile> maybeSubtitleInputFile)
|
||||
{
|
||||
_currentState = currentState;
|
||||
_maybeWatermarkInputFile = maybeWatermarkInputFile;
|
||||
_maybeSubtitleInputFile = maybeSubtitleInputFile;
|
||||
}
|
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with
|
||||
@@ -22,14 +27,19 @@ public class EncoderHevcVaapi : EncoderBase
|
||||
public override string Name => "hevc_vaapi";
|
||||
public override StreamKind Kind => StreamKind.Video;
|
||||
|
||||
// need to upload if we're still in software unless a watermark is used
|
||||
// need to upload if we're still in software unless a watermark or picture subtitle is used
|
||||
public override string Filter
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_maybeWatermarkInputFile.IsNone && _currentState.FrameDataLocation == FrameDataLocation.Software)
|
||||
if (_currentState.FrameDataLocation == FrameDataLocation.Software)
|
||||
{
|
||||
return "format=nv12|vaapi,hwupload";
|
||||
bool isNotImageSubtitle = _maybeSubtitleInputFile.Map(s => s.IsImageBased).IfNone(false) == false;
|
||||
|
||||
if (_maybeWatermarkInputFile.IsNone && isNotImageSubtitle)
|
||||
{
|
||||
return "format=nv12|vaapi,hwupload";
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.FFmpeg.Environment;
|
||||
using ErsatzTV.FFmpeg.Format;
|
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter;
|
||||
|
||||
@@ -144,8 +145,13 @@ public class ComplexFilter : IPipelineStep
|
||||
: videoLabel;
|
||||
|
||||
// vaapi uses software overlay and needs to upload
|
||||
// videotoolbox seems to require a hwupload for hevc
|
||||
// also wait to upload videotoolbox if a subtitle overlay is coming
|
||||
string uploadFilter = string.Empty;
|
||||
if (_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi)
|
||||
if (_maybeSubtitleInputFile.Map(s => s.IsImageBased).IfNone(false) == false &&
|
||||
(_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi ||
|
||||
_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox &&
|
||||
_currentState.VideoFormat == VideoFormat.Hevc))
|
||||
{
|
||||
uploadFilter = new HardwareUploadFilter(_ffmpegState).Filter;
|
||||
}
|
||||
@@ -178,6 +184,10 @@ public class ComplexFilter : IPipelineStep
|
||||
subtitleLabel = "[st]";
|
||||
subtitleFilterComplex += subtitleLabel;
|
||||
}
|
||||
else
|
||||
{
|
||||
subtitleLabel = $"[{subtitleLabel}]";
|
||||
}
|
||||
|
||||
IPipelineFilterStep overlayFilter =
|
||||
AvailableSubtitleOverlayFilters.ForAcceleration(_ffmpegState.HardwareAccelerationMode);
|
||||
@@ -190,8 +200,11 @@ public class ComplexFilter : IPipelineStep
|
||||
: videoLabel;
|
||||
|
||||
// vaapi uses software overlay and needs to upload
|
||||
// videotoolbox seems to require a hwupload for hevc
|
||||
string uploadFilter = string.Empty;
|
||||
if (_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi)
|
||||
if (_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi
|
||||
|| _ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox &&
|
||||
_currentState.VideoFormat == VideoFormat.Hevc)
|
||||
{
|
||||
uploadFilter = new HardwareUploadFilter(_ffmpegState).Filter;
|
||||
}
|
||||
|
||||
@@ -474,6 +474,7 @@ public class PipelineBuilder
|
||||
currentState,
|
||||
desiredState,
|
||||
_watermarkInputFile,
|
||||
_subtitleInputFile,
|
||||
_logger))
|
||||
{
|
||||
encoder = e;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CliWrap;
|
||||
using System.Text;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using ErsatzTV.Core.Health;
|
||||
|
||||
@@ -37,7 +38,7 @@ public abstract class BaseHealthCheck
|
||||
BufferedCommandResult result = await Cli.Wrap(path)
|
||||
.WithArguments(arguments)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync(cancellationToken);
|
||||
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
|
||||
|
||||
return result.StandardOutput;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Application.Streaming;
|
||||
using ErsatzTV.Extensions;
|
||||
using MediatR;
|
||||
@@ -43,11 +44,27 @@ public class InternalController : ControllerBase
|
||||
result.Match<IActionResult>(
|
||||
processModel =>
|
||||
{
|
||||
Process process = processModel.Process;
|
||||
Command command = processModel.Process;
|
||||
|
||||
_logger.LogInformation("ffmpeg arguments {FFmpegArguments}", command.Arguments);
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = command.TargetFilePath,
|
||||
Arguments = command.Arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = false,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
foreach ((string key, string value) in command.EnvironmentVariables)
|
||||
{
|
||||
process.StartInfo.Environment[key] = value;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"ffmpeg arguments {FFmpegArguments}",
|
||||
string.Join(" ", process.StartInfo.ArgumentList));
|
||||
process.Start();
|
||||
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t");
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
using ErsatzTV.Application.Channels;
|
||||
using ErsatzTV.Application.Images;
|
||||
using ErsatzTV.Application.Streaming;
|
||||
@@ -92,15 +93,30 @@ public class IptvController : ControllerBase
|
||||
result => result.Match<IActionResult>(
|
||||
processModel =>
|
||||
{
|
||||
Process process = processModel.Process;
|
||||
Command command = processModel.Process;
|
||||
|
||||
_logger.LogInformation("Starting ts stream for channel {ChannelNumber}", channelNumber);
|
||||
_logger.LogDebug(
|
||||
"ffmpeg ts arguments {FFmpegArguments}",
|
||||
string.Join(" ", process.StartInfo.ArgumentList));
|
||||
_logger.LogInformation("ffmpeg arguments {FFmpegArguments}", command.Arguments);
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = command.TargetFilePath,
|
||||
Arguments = command.Arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = false,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
foreach ((string key, string value) in command.EnvironmentVariables)
|
||||
{
|
||||
process.StartInfo.Environment[key] = value;
|
||||
}
|
||||
|
||||
process.Start();
|
||||
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t");
|
||||
},
|
||||
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t"); },
|
||||
error => BadRequest(error.Value)));
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,17 @@
|
||||
{
|
||||
<MudText>Nothing to see here...</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="margin-left: auto">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Error"
|
||||
StartIcon="@Icons.Material.Filled.DeleteForever"
|
||||
OnClick="@(_ => EmptyTrash())">
|
||||
Empty Trash
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</MudPaper>
|
||||
@@ -560,4 +571,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EmptyTrash()
|
||||
{
|
||||
int count = _movies.Count + _shows.Count + _seasons.Count + _episodes.Count + _artists.Count +
|
||||
_musicVideos.Count + _otherVideos.Count + _songs.Count;
|
||||
|
||||
var parameters = new DialogParameters { { "EntityType", count.ToString() }, { "EntityName", "missing items" } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = Dialog.Show<DeleteFromDatabaseDialog>("Delete From Database", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled)
|
||||
{
|
||||
await Mediator.Send(new EmptyTrash(), CancellationToken);
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class FillerPresetEditViewModel
|
||||
private FillerMode _fillerMode;
|
||||
private int? _count;
|
||||
private ProgramScheduleItemCollectionType _collectionType;
|
||||
private int? _padToNearestMinute;
|
||||
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
@@ -50,7 +51,11 @@ public class FillerPresetEditViewModel
|
||||
set => _count = value;
|
||||
}
|
||||
|
||||
public int? PadToNearestMinute { get; set; }
|
||||
public int? PadToNearestMinute
|
||||
{
|
||||
get => FillerMode == FillerMode.Pad ? _padToNearestMinute : null;
|
||||
set => _padToNearestMinute = value;
|
||||
}
|
||||
|
||||
public ProgramScheduleItemCollectionType CollectionType
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user