From 231a214223176d11997dada3386c20497056fcda Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:45:22 -0600 Subject: [PATCH] add new subtitle settings (#1590) --- CHANGELOG.md | 7 ++ .../Commands/UpdateFFmpegSettingsHandler.cs | 27 +++++++- .../FFmpegProfiles/FFmpegSettingsViewModel.cs | 2 + .../Queries/GetFFmpegSettingsHandler.cs | 6 ++ .../Commands/ExtractEmbeddedSubtitles.cs | 3 +- .../ExtractEmbeddedSubtitlesHandler.cs | 46 ++++++++++--- .../Queries/GetTroubleshootingInfoHandler.cs | 12 +++- ErsatzTV.Core/Domain/ConfigElementKey.cs | 2 + ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs | 68 +++++++++++++------ ErsatzTV/Pages/Settings.razor | 13 ++++ 10 files changed, 152 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a5bd44e2..fdb5d792f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added - Add `show_studio` to search index for seasons and episodes +- Add two new global subtitle settings: + - `Use embedded subtitles` + - Default value: `true` + - When disabled, embedded subtitles will not be considered for extraction (text subtitles), or playback (all embedded subtitles) + - `Extract and use embedded (text) subtitles` + - Default value: `false` + - When enabled, embedded text subtitles will be periodically extracted, and considered for playback ### Fixed - Fix antiforgery error caused by reusing existing browser tabs across docker container restarts diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs index 08e8ef754..b57cd41f5 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs @@ -1,5 +1,7 @@ using System.Diagnostics; using System.Globalization; +using System.Threading.Channels; +using ErsatzTV.Application.Subtitles; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; @@ -11,13 +13,16 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler _workerChannel; public UpdateFFmpegSettingsHandler( IConfigElementRepository configElementRepository, - ILocalFileSystem localFileSystem) + ILocalFileSystem localFileSystem, + ChannelWriter workerChannel) { _configElementRepository = configElementRepository; _localFileSystem = localFileSystem; + _workerChannel = workerChannel; } public Task> Handle( @@ -87,6 +92,26 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler.None)); + } + if (request.Settings.GlobalWatermarkId is not null) { await _configElementRepository.Upsert( diff --git a/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs b/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs index ec2cd2326..989113385 100644 --- a/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs +++ b/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs @@ -8,6 +8,8 @@ public class FFmpegSettingsViewModel public string FFprobePath { get; set; } public int DefaultFFmpegProfileId { get; set; } public string PreferredAudioLanguageCode { get; set; } + public bool UseEmbeddedSubtitles { get; set; } + public bool ExtractEmbeddedSubtitles { get; set; } public bool SaveReports { get; set; } public int? GlobalWatermarkId { get; set; } public int? GlobalFallbackFillerId { get; set; } diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs index a5a04717a..8ab4e1ea9 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs @@ -23,6 +23,10 @@ public class GetFFmpegSettingsHandler : IRequestHandler(ConfigElementKey.FFmpegSaveReports); Option preferredAudioLanguageCode = await _configElementRepository.GetValue(ConfigElementKey.FFmpegPreferredLanguageCode); + Option useEmbeddedSubtitles = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegUseEmbeddedSubtitles); + Option extractEmbeddedSubtitles = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegExtractEmbeddedSubtitles); Option watermark = await _configElementRepository.GetValue(ConfigElementKey.FFmpegGlobalWatermarkId); Option fallbackFiller = @@ -42,6 +46,8 @@ public class GetFFmpegSettingsHandler : IRequestHandler PlayoutId) : IRequest>, - IBackgroundServiceRequest; +public record ExtractEmbeddedSubtitles(Option PlayoutId) : IRequest>, IBackgroundServiceRequest; diff --git a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs index 5317d55aa..3ee1018a4 100644 --- a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs +++ b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs @@ -12,6 +12,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -20,10 +21,11 @@ using Microsoft.Extensions.Logging; namespace ErsatzTV.Application.Subtitles; [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")] -public class ExtractEmbeddedSubtitlesHandler : IRequestHandler> +public class ExtractEmbeddedSubtitlesHandler : IRequestHandler> { private readonly IDbContextFactory _dbContextFactory; private readonly IEntityLocker _entityLocker; + private readonly IConfigElementRepository _configElementRepository; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly ChannelWriter _workerChannel; @@ -32,17 +34,19 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler dbContextFactory, ILocalFileSystem localFileSystem, IEntityLocker entityLocker, + IConfigElementRepository configElementRepository, ChannelWriter workerChannel, ILogger logger) { _dbContextFactory = dbContextFactory; _localFileSystem = localFileSystem; _entityLocker = entityLocker; + _configElementRepository = configElementRepository; _workerChannel = workerChannel; _logger = logger; } - public async Task> Handle( + public async Task> Handle( ExtractEmbeddedSubtitles request, CancellationToken cancellationToken) { @@ -51,14 +55,14 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler { - Either result = await ExtractAll(dbContext, request, ffmpegPath, cancellationToken); + Option result = await ExtractAll(dbContext, request, ffmpegPath, cancellationToken); await _workerChannel.WriteAsync(new ReleaseMemory(false), cancellationToken); return result; }, - error => Task.FromResult>(error.Join())); + error => Task.FromResult>(error.Join())); } - private async Task> ExtractAll( + private async Task> ExtractAll( TvContext dbContext, ExtractEmbeddedSubtitles request, string ffmpegPath, @@ -66,6 +70,26 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler(ConfigElementKey.FFmpegUseEmbeddedSubtitles) + .IfNoneAsync(true); + + if (!useEmbeddedSubtitles) + { + _logger.LogDebug("Embedded subtitles are NOT enabled; nothing to extract"); + return Option.None; + } + + bool extractEmbeddedSubtitles = await _configElementRepository + .GetValue(ConfigElementKey.FFmpegExtractEmbeddedSubtitles) + .IfNoneAsync(false); + + if (!extractEmbeddedSubtitles) + { + _logger.LogDebug("Embedded subtitle extraction is NOT enabled"); + return Option.None; + } + DateTime now = DateTime.UtcNow; DateTime until = now.AddHours(1); @@ -102,11 +126,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler.None; } _logger.LogDebug("No playouts have subtitles enabled; nothing to extract"); - return Unit.Default; + return Option.None; } foreach (int playoutId in playoutIdsToCheck) @@ -157,7 +181,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler.None; } // extract subtitles and fonts for each item and update db @@ -171,13 +195,13 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler.None; } private static async Task> GetMediaItemIdsWithTextSubtitles( diff --git a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs index 36950021e..a60bd7fab 100644 --- a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs @@ -8,6 +8,7 @@ using ErsatzTV.Core.Health; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Capabilities.Qsv; +using ErsatzTV.FFmpeg.OutputFormat; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; @@ -149,6 +150,10 @@ public class GetTroubleshootingInfoHandler : IRequestHandler(ConfigElementKey.FFmpegSaveReports); Option preferredAudioLanguageCode = await _configElementRepository.GetValue(ConfigElementKey.FFmpegPreferredLanguageCode); + Option useEmbeddedSubtitles = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegUseEmbeddedSubtitles); + Option extractEmbeddedSubtitles = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegExtractEmbeddedSubtitles); Option watermark = await _configElementRepository.GetValue(ConfigElementKey.FFmpegGlobalWatermarkId); Option fallbackFiller = @@ -159,6 +164,8 @@ public class GetTroubleshootingInfoHandler : IRequestHandler(ConfigElementKey.FFmpegWorkAheadSegmenters); Option initialSegmentCount = await _configElementRepository.GetValue(ConfigElementKey.FFmpegInitialSegmentCount); + Option outputFormatKind = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegHlsDirectOutputFormat); var result = new FFmpegSettingsViewModel { @@ -166,10 +173,13 @@ public class GetTroubleshootingInfoHandler : IRequestHandler new("ffmpeg.default_profile_id"); public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id"); public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports"); + public static ConfigElementKey FFmpegUseEmbeddedSubtitles => new("ffmpeg.use_embedded_subtitles"); + public static ConfigElementKey FFmpegExtractEmbeddedSubtitles => new("ffmpeg.extract_embedded_subtitles"); public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code"); public static ConfigElementKey FFmpegGlobalWatermarkId => new("ffmpeg.global_watermark_id"); public static ConfigElementKey FFmpegGlobalFallbackFillerId => new("ffmpeg.global_fallback_filler_id"); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs index b8de8f9c3..edf223721 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs @@ -138,6 +138,36 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector return None; } + bool useEmbeddedSubtitles = await _configElementRepository + .GetValue(ConfigElementKey.FFmpegUseEmbeddedSubtitles) + .IfNoneAsync(true); + + if (!useEmbeddedSubtitles) + { + _logger.LogDebug("Ignoring embedded subtitles for channel {Number}", channel.Number); + subtitles = subtitles.Filter(s => s.SubtitleKind is not SubtitleKind.Embedded).ToList(); + } + + foreach (Subtitle subtitle in subtitles.Filter(s => s.SubtitleKind is SubtitleKind.Embedded && !s.IsImage).ToList()) + { + if (subtitle.IsExtracted == false) + { + _logger.LogDebug( + "Ignoring embedded subtitle with index {Index} that has not been extracted", + subtitle.StreamIndex); + + subtitles.Remove(subtitle); + } + else if (string.IsNullOrWhiteSpace(subtitle.Path)) + { + _logger.LogDebug( + "BUG: ignoring embedded subtitle with index {Index} that is missing a path", + subtitle.StreamIndex); + + subtitles.Remove(subtitle); + } + } + var allCodes = new List(); string language = (preferredSubtitleLanguage ?? string.Empty).ToLowerInvariant(); if (string.IsNullOrWhiteSpace(language)) @@ -160,29 +190,29 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector if (subtitles.Count > 0) { - switch (subtitleMode) + Option maybeSelectedSubtitle = subtitleMode switch { - case ChannelSubtitleMode.Forced: - foreach (Subtitle subtitle in subtitles.OrderBy(s => s.StreamIndex).Find(s => s.Forced)) - { - return subtitle; - } + ChannelSubtitleMode.Forced => subtitles + .OrderBy(s => s.StreamIndex) + .Find(s => s.Forced) + .HeadOrNone(), - break; - case ChannelSubtitleMode.Default: - foreach (Subtitle subtitle in subtitles.OrderBy(s => s.Default ? 0 : 1).ThenBy(s => s.StreamIndex)) - { - return subtitle; - } + ChannelSubtitleMode.Default => subtitles + .OrderBy(s => s.Default ? 0 : 1) + .ThenBy(s => s.StreamIndex) + .HeadOrNone(), - break; - case ChannelSubtitleMode.Any: - foreach (Subtitle subtitle in subtitles.OrderBy(s => s.StreamIndex).HeadOrNone()) - { - return subtitle; - } + ChannelSubtitleMode.Any => subtitles + .OrderBy(s => s.StreamIndex) + .HeadOrNone(), - break; + _ => Option.None + }; + + foreach (Subtitle subtitle in maybeSelectedSubtitle) + { + _logger.LogDebug("Selecting subtitle {@Subtitle}", subtitle); + return subtitle; } } diff --git a/ErsatzTV/Pages/Settings.razor b/ErsatzTV/Pages/Settings.razor index 0bc8c115b..ac2a12888 100644 --- a/ErsatzTV/Pages/Settings.razor +++ b/ErsatzTV/Pages/Settings.razor @@ -43,6 +43,19 @@ @culture.EnglishName } + + + + + +