add preferred language (#109)
* add explicit warning for zero/invalid duration media items * set dateadded on plex media versions * add media stream table * save local media streams to db * save plex media streams to db * add preferred language settings (no validation) * use preferred language if possible * code cleanup * proper language code validation * force scan of all libraries to pull in media streams
This commit is contained in:
@@ -54,12 +54,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public Option<FFmpegComplexFilter> Build()
|
||||
public Option<FFmpegComplexFilter> Build(int videoStreamIndex, int audioStreamIndex)
|
||||
{
|
||||
var complexFilter = new StringBuilder();
|
||||
|
||||
var videoLabel = "0:V";
|
||||
var audioLabel = "0:a";
|
||||
var videoLabel = $"0:{videoStreamIndex}";
|
||||
var audioLabel = $"0:{audioStreamIndex}";
|
||||
|
||||
HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None);
|
||||
bool isHardwareDecode = acceleration switch
|
||||
|
||||
@@ -45,6 +45,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
StreamingMode streamingMode,
|
||||
FFmpegProfile ffmpegProfile,
|
||||
MediaVersion version,
|
||||
MediaStream videoStream,
|
||||
MediaStream audioStream,
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
@@ -85,7 +87,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
}
|
||||
|
||||
if (result.ScaledSize.IsSome || result.PadToDesiredResolution ||
|
||||
NeedToNormalizeVideoCodec(ffmpegProfile, version))
|
||||
NeedToNormalizeVideoCodec(ffmpegProfile, videoStream))
|
||||
{
|
||||
result.VideoCodec = ffmpegProfile.VideoCodec;
|
||||
result.VideoBitrate = ffmpegProfile.VideoBitrate;
|
||||
@@ -96,7 +98,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
result.VideoCodec = "copy";
|
||||
}
|
||||
|
||||
if (NeedToNormalizeAudioCodec(ffmpegProfile, version))
|
||||
if (NeedToNormalizeAudioCodec(ffmpegProfile, audioStream))
|
||||
{
|
||||
result.AudioCodec = ffmpegProfile.AudioCodec;
|
||||
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
||||
@@ -104,7 +106,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
if (ffmpegProfile.NormalizeAudio)
|
||||
{
|
||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||
if (audioStream.Channels != ffmpegProfile.AudioChannels)
|
||||
{
|
||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||
}
|
||||
|
||||
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
||||
result.AudioDuration = version.Duration;
|
||||
}
|
||||
@@ -152,11 +158,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
private static bool IsOddSize(MediaVersion version) =>
|
||||
version.Height % 2 == 1 || version.Width % 2 == 1;
|
||||
|
||||
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaVersion version) =>
|
||||
ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != version.VideoCodec;
|
||||
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaStream videoStream) =>
|
||||
ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != videoStream.Codec;
|
||||
|
||||
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaVersion version) =>
|
||||
ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != version.AudioCodec;
|
||||
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaStream audioStream) =>
|
||||
ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != audioStream.Codec;
|
||||
|
||||
private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
|
||||
{
|
||||
|
||||
@@ -329,12 +329,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithFilterComplex()
|
||||
public FFmpegProcessBuilder WithFilterComplex(int videoStreamIndex, int audioStreamIndex)
|
||||
{
|
||||
var videoLabel = "0:V";
|
||||
var audioLabel = "0:a";
|
||||
var videoLabel = $"0:v:{videoStreamIndex}";
|
||||
var audioLabel = $"0:a:{audioStreamIndex}";
|
||||
|
||||
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build();
|
||||
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build(videoStreamIndex, audioStreamIndex);
|
||||
maybeFilter.IfSome(
|
||||
filter =>
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using LanguageExt;
|
||||
@@ -8,12 +9,18 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
public class FFmpegProcessService
|
||||
{
|
||||
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
|
||||
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
|
||||
|
||||
public FFmpegProcessService(FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService) =>
|
||||
public FFmpegProcessService(
|
||||
FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService,
|
||||
IFFmpegStreamSelector ffmpegStreamSelector)
|
||||
{
|
||||
_playbackSettingsCalculator = ffmpegPlaybackSettingsService;
|
||||
_ffmpegStreamSelector = ffmpegStreamSelector;
|
||||
}
|
||||
|
||||
public Process ForPlayoutItem(
|
||||
public async Task<Process> ForPlayoutItem(
|
||||
string ffmpegPath,
|
||||
bool saveReports,
|
||||
Channel channel,
|
||||
@@ -22,10 +29,15 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, version);
|
||||
MediaStream audioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, version);
|
||||
|
||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
|
||||
channel.StreamingMode,
|
||||
channel.FFmpegProfile,
|
||||
version,
|
||||
videoStream,
|
||||
audioStream,
|
||||
start,
|
||||
now);
|
||||
|
||||
@@ -36,7 +48,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||
.WithSeek(playbackSettings.StreamSeek)
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, version.VideoCodec);
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec);
|
||||
|
||||
playbackSettings.ScaledSize.Match(
|
||||
scaledSize =>
|
||||
@@ -51,7 +63,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration).WithFilterComplex();
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
@@ -61,19 +74,19 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithDeinterlace(playbackSettings.Deinterlace)
|
||||
.WithBlackBars(channel.FFmpegProfile.Resolution)
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex();
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
}
|
||||
else if (playbackSettings.Deinterlace)
|
||||
{
|
||||
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex();
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder = builder
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex();
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
public class FFmpegStreamSelector : IFFmpegStreamSelector
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILogger<FFmpegStreamSelector> _logger;
|
||||
|
||||
public FFmpegStreamSelector(
|
||||
ILogger<FFmpegStreamSelector> logger,
|
||||
IConfigElementRepository configElementRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_configElementRepository = configElementRepository;
|
||||
}
|
||||
|
||||
public Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version) =>
|
||||
version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask();
|
||||
|
||||
public async Task<MediaStream> SelectAudioStream(Channel channel, MediaVersion version)
|
||||
{
|
||||
var audioStreams = version.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).ToList();
|
||||
|
||||
string language = channel.PreferredLanguageCode.ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
_logger.LogDebug("Channel {Number} has no preferred language code", channel.Number);
|
||||
Option<string> maybeDefaultLanguage = await _configElementRepository.GetValue<string>(
|
||||
ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||
maybeDefaultLanguage.Match(
|
||||
lang => language = lang.ToLowerInvariant(),
|
||||
() =>
|
||||
{
|
||||
_logger.LogDebug("FFmpeg has no preferred language code; falling back to {Code}", "eng");
|
||||
language = "eng";
|
||||
});
|
||||
}
|
||||
|
||||
var correctLanguage = audioStreams.Filter(
|
||||
s => string.Equals(
|
||||
s.Language,
|
||||
language,
|
||||
StringComparison.InvariantCultureIgnoreCase)).ToList();
|
||||
if (correctLanguage.Any())
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Found {Count} audio streams with preferred language code {Code}; selecting stream with most channels",
|
||||
correctLanguage.Count,
|
||||
language);
|
||||
|
||||
return correctLanguage.OrderByDescending(s => s.Channels).Head();
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Unable to find audio stream with preferred language code {Code}; selecting stream with most channels",
|
||||
language);
|
||||
|
||||
return audioStreams.OrderByDescending(s => s.Channels).Head();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user