mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
215 lines
9.6 KiB
215 lines
9.6 KiB
using System; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.Threading.Tasks; |
|
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Interfaces.FFmpeg; |
|
using ErsatzTV.Core.Interfaces.Images; |
|
using LanguageExt; |
|
using static LanguageExt.Prelude; |
|
|
|
namespace ErsatzTV.Core.FFmpeg |
|
{ |
|
public class FFmpegProcessService |
|
{ |
|
private readonly IFFmpegStreamSelector _ffmpegStreamSelector; |
|
private readonly IImageCache _imageCache; |
|
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator; |
|
|
|
public FFmpegProcessService( |
|
FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService, |
|
IFFmpegStreamSelector ffmpegStreamSelector, |
|
IImageCache imageCache) |
|
{ |
|
_playbackSettingsCalculator = ffmpegPlaybackSettingsService; |
|
_ffmpegStreamSelector = ffmpegStreamSelector; |
|
_imageCache = imageCache; |
|
} |
|
|
|
public async Task<Process> ForPlayoutItem( |
|
string ffmpegPath, |
|
bool saveReports, |
|
Channel channel, |
|
MediaVersion version, |
|
string path, |
|
DateTimeOffset start, |
|
DateTimeOffset now, |
|
Option<ChannelWatermark> globalWatermark) |
|
{ |
|
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, version); |
|
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, version); |
|
|
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings( |
|
channel.StreamingMode, |
|
channel.FFmpegProfile, |
|
version, |
|
videoStream, |
|
maybeAudioStream, |
|
start, |
|
now); |
|
|
|
(Option<ChannelWatermark> maybeWatermark, Option<string> maybeWatermarkPath) = |
|
GetWatermarkOptions(channel, globalWatermark); |
|
|
|
bool isAnimated = await maybeWatermarkPath.Match( |
|
p => _imageCache.IsAnimated(p), |
|
() => Task.FromResult(false)); |
|
|
|
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports) |
|
.WithThreads(playbackSettings.ThreadCount) |
|
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration) |
|
.WithQuiet() |
|
.WithFormatFlags(playbackSettings.FormatFlags) |
|
.WithRealtimeOutput(playbackSettings.RealtimeOutput) |
|
.WithSeek(playbackSettings.StreamSeek) |
|
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec, videoStream.PixelFormat) |
|
.WithWatermark(maybeWatermark, maybeWatermarkPath, channel.FFmpegProfile.Resolution, isAnimated) |
|
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale) |
|
.WithAlignedAudio(playbackSettings.AudioDuration) |
|
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness); |
|
|
|
playbackSettings.ScaledSize.Match( |
|
scaledSize => |
|
{ |
|
builder = builder.WithDeinterlace(playbackSettings.Deinterlace) |
|
.WithScaling(scaledSize); |
|
|
|
if (NeedToPad(channel.FFmpegProfile.Resolution, scaledSize)) |
|
{ |
|
builder = builder.WithBlackBars(channel.FFmpegProfile.Resolution); |
|
} |
|
|
|
builder = builder |
|
.WithFilterComplex(videoStream, maybeAudioStream); |
|
}, |
|
() => |
|
{ |
|
if (playbackSettings.PadToDesiredResolution) |
|
{ |
|
builder = builder |
|
.WithDeinterlace(playbackSettings.Deinterlace) |
|
.WithBlackBars(channel.FFmpegProfile.Resolution) |
|
.WithFilterComplex(videoStream, maybeAudioStream); |
|
} |
|
else if (playbackSettings.Deinterlace) |
|
{ |
|
builder = builder.WithDeinterlace(playbackSettings.Deinterlace) |
|
.WithAlignedAudio(playbackSettings.AudioDuration) |
|
.WithFilterComplex(videoStream, maybeAudioStream); |
|
} |
|
else |
|
{ |
|
builder = builder |
|
.WithFilterComplex(videoStream, maybeAudioStream); |
|
} |
|
}); |
|
|
|
return builder.WithPlaybackArgs(playbackSettings) |
|
.WithMetadata(channel, maybeAudioStream) |
|
.WithFormat("mpegts") |
|
.WithDuration(start + version.Duration - now) |
|
.WithPipe() |
|
.Build(); |
|
} |
|
|
|
public Process ForError(string ffmpegPath, Channel channel, Option<TimeSpan> duration, string errorMessage) |
|
{ |
|
FFmpegPlaybackSettings playbackSettings = |
|
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile); |
|
|
|
IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution; |
|
|
|
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false) |
|
.WithThreads(1) |
|
.WithQuiet() |
|
.WithFormatFlags(playbackSettings.FormatFlags) |
|
.WithRealtimeOutput(playbackSettings.RealtimeOutput) |
|
.WithLoopedImage(Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png")) |
|
.WithLibavfilter() |
|
.WithInput("anullsrc") |
|
.WithErrorText(desiredResolution, errorMessage) |
|
.WithPixfmt("yuv420p") |
|
.WithPlaybackArgs(playbackSettings) |
|
.WithMetadata(channel, None) |
|
.WithFormat("mpegts"); |
|
|
|
duration.IfSome(d => builder = builder.WithDuration(d)); |
|
|
|
return builder.WithPipe().Build(); |
|
} |
|
|
|
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) |
|
{ |
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings; |
|
|
|
return new FFmpegProcessBuilder(ffmpegPath, saveReports) |
|
.WithThreads(1) |
|
.WithQuiet() |
|
.WithFormatFlags(playbackSettings.FormatFlags) |
|
.WithRealtimeOutput(playbackSettings.RealtimeOutput) |
|
.WithInfiniteLoop() |
|
.WithConcat($"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}") |
|
.WithMetadata(channel, None) |
|
.WithFormat("mpegts") |
|
.WithPipe() |
|
.Build(); |
|
} |
|
|
|
private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) => |
|
displaySize.Width != target.Width || displaySize.Height != target.Height; |
|
|
|
private WatermarkOptions GetWatermarkOptions(Channel channel, Option<ChannelWatermark> globalWatermark) |
|
{ |
|
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect) |
|
{ |
|
// check for channel watermark |
|
if (channel.Watermark != null) |
|
{ |
|
switch (channel.Watermark.ImageSource) |
|
{ |
|
case ChannelWatermarkImageSource.Custom: |
|
string customPath = _imageCache.GetPathForImage( |
|
channel.Watermark.Image, |
|
ArtworkKind.Watermark, |
|
Option<int>.None); |
|
return new WatermarkOptions(channel.Watermark, customPath); |
|
case ChannelWatermarkImageSource.ChannelLogo: |
|
Option<string> maybeChannelPath = channel.Artwork |
|
.Filter(a => a.ArtworkKind == ArtworkKind.Logo) |
|
.HeadOrNone() |
|
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None)); |
|
return new WatermarkOptions(channel.Watermark, maybeChannelPath); |
|
default: |
|
throw new NotSupportedException("Unsupported watermark image source"); |
|
} |
|
} |
|
|
|
// check for global watermark |
|
foreach (ChannelWatermark watermark in globalWatermark) |
|
{ |
|
switch (watermark.ImageSource) |
|
{ |
|
case ChannelWatermarkImageSource.Custom: |
|
string customPath = _imageCache.GetPathForImage( |
|
watermark.Image, |
|
ArtworkKind.Watermark, |
|
Option<int>.None); |
|
return new WatermarkOptions(watermark, customPath); |
|
case ChannelWatermarkImageSource.ChannelLogo: |
|
Option<string> maybeChannelPath = channel.Artwork |
|
.Filter(a => a.ArtworkKind == ArtworkKind.Logo) |
|
.HeadOrNone() |
|
.Map(a => _imageCache.GetPathForImage(a.Path, ArtworkKind.Logo, Option<int>.None)); |
|
return new WatermarkOptions(watermark, maybeChannelPath); |
|
default: |
|
throw new NotSupportedException("Unsupported watermark image source"); |
|
} |
|
} |
|
} |
|
|
|
return new WatermarkOptions(None, None); |
|
} |
|
|
|
private record WatermarkOptions(Option<ChannelWatermark> Watermark, Option<string> ImagePath); |
|
} |
|
}
|
|
|