Browse Source

feat: pass extracted subtitle paths to next engine

pull/2960/head
Jason Dove 5 days ago
parent
commit
d5cdd0afdf
No known key found for this signature in database
  1. 2
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs
  3. 29
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  4. 22
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandlerBase.cs
  5. 3
      ErsatzTV.Core/Next/Config/ChannelConfig.cs
  6. 65
      ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

2
CHANGELOG.md

@ -40,6 +40,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix case where block playouts would occasionally get stuck building forever - Fix case where block playouts would occasionally get stuck building forever
- Fix green line sometimes seen with NVIDIA and AMD/VAAPI encoding - Fix green line sometimes seen with NVIDIA and AMD/VAAPI encoding
- Both bugs were in ffmpeg, and ETV's patched ffmpeg 8.1.2 is required for the fixes - Both bugs were in ffmpeg, and ETV's patched ffmpeg 8.1.2 is required for the fixes
- Pass extracted subtitle paths to next engine; this should fix text subtitle burn-in when extraction is enabled
- Embedded text subtitles will otherwise be ignored and unused (when extraction is disabled)
## [26.6.0] - 2026-07-09 ## [26.6.0] - 2026-07-09
### Added ### Added

3
ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs

@ -353,7 +353,8 @@ public class StartFFmpegNextSessionHandler(
{ {
NextEngineTextSubtitleMode.Convert => Mode.Convert, NextEngineTextSubtitleMode.Convert => Mode.Convert,
_ => Mode.Burn _ => Mode.Burn
} },
FontsFolder = FileSystemLayout.FontsCacheFolder
}; };
string playoutFolder = _fileSystem.Path.Combine(FileSystemLayout.NextPlayoutsFolder, channel.Number, "current"); string playoutFolder = _fileSystem.Path.Combine(FileSystemLayout.NextPlayoutsFolder, channel.Number, "current");

29
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs

@ -1,6 +1,7 @@
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Threading.Channels; using System.Threading.Channels;
using ErsatzTV.Application.Maintenance; using ErsatzTV.Application.Maintenance;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
@ -168,6 +169,7 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
.Map(pi => pi.MediaItemId) .Map(pi => pi.MediaItemId)
.ToList(); .ToList();
bool extractedAnything = false;
foreach (int mediaItemId in toUpdate) foreach (int mediaItemId in toUpdate)
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
@ -176,8 +178,16 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
} }
// extract subtitles and fonts for each item and update db // extract subtitles and fonts for each item and update db
await ExtractSubtitles(dbContext, mediaItemId, ffmpegPath, cancellationToken); extractedAnything |= await ExtractSubtitles(
await ExtractFonts(dbContext, mediaItemId, ffmpegPath, cancellationToken); dbContext,
mediaItemId,
ffmpegPath,
cancellationToken);
extractedAnything |= await ExtractFonts(
dbContext,
mediaItemId,
ffmpegPath,
cancellationToken);
} }
_logger.LogDebug("Done checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck); _logger.LogDebug("Done checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck);
@ -186,6 +196,21 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
{ {
await _entityLocker.UnlockPlayout(playoutId); await _entityLocker.UnlockPlayout(playoutId);
} }
if (extractedAnything)
{
List<string> channelNumbers = await dbContext.Channels
.AsNoTracking()
.Where(c => c.StreamingEngine == StreamingEngine.Next &&
c.Playouts.Any(p => playoutIdsToCheck.Contains(p.Id)))
.Select(c => c.Number)
.ToListAsync(cancellationToken);
foreach (string channelNumber in channelNumbers)
{
await _workerChannel.WriteAsync(new SyncNextPlayout(channelNumber), cancellationToken);
}
}
} }
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{ {

22
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandlerBase.cs

@ -89,12 +89,14 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(IFileSystem fileSystem
return result; return result;
} }
protected async Task ExtractSubtitles( protected async Task<bool> ExtractSubtitles(
TvContext dbContext, TvContext dbContext,
int mediaItemId, int mediaItemId,
string ffmpegPath, string ffmpegPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
bool extractedAnything = false;
foreach (MediaItem mediaItem in await GetMediaItem(dbContext, mediaItemId, cancellationToken)) foreach (MediaItem mediaItem in await GetMediaItem(dbContext, mediaItemId, cancellationToken))
{ {
foreach (List<Subtitle> allSubtitles in GetSubtitles(mediaItem)) foreach (List<Subtitle> allSubtitles in GetSubtitles(mediaItem))
@ -133,9 +135,10 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(IFileSystem fileSystem
.Add("-hide_banner") .Add("-hide_banner")
.Add("-i").Add(mediaItemPath); .Add("-i").Add(mediaItemPath);
var subtitleIndexes = subtitlesToExtract var subtitleIndexes = allSubtitles
.OrderBy(s => s.Subtitle.StreamIndex) .Filter(s => s.SubtitleKind is SubtitleKind.Embedded)
.Select(s => s.Subtitle.StreamIndex) .OrderBy(s => s.StreamIndex)
.Select(s => s.StreamIndex)
.ToList(); .ToList();
foreach (SubtitleToExtract subtitle in subtitlesToExtract) foreach (SubtitleToExtract subtitle in subtitlesToExtract)
@ -172,6 +175,8 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(IFileSystem fileSystem
"Successfully extracted {Count} subtitles in {Duration}", "Successfully extracted {Count} subtitles in {Duration}",
subtitlesToExtract.Count, subtitlesToExtract.Count,
sw.Elapsed.Humanize()); sw.Elapsed.Humanize());
extractedAnything = true;
} }
else else
{ {
@ -182,14 +187,18 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(IFileSystem fileSystem
} }
} }
} }
return extractedAnything;
} }
protected async Task ExtractFonts( protected async Task<bool> ExtractFonts(
TvContext dbContext, TvContext dbContext,
int mediaItemId, int mediaItemId,
string ffmpegPath, string ffmpegPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
bool extractedAnything = false;
foreach (MediaItem mediaItem in await GetMediaItem(dbContext, mediaItemId, cancellationToken)) foreach (MediaItem mediaItem in await GetMediaItem(dbContext, mediaItemId, cancellationToken))
{ {
MediaVersion headVersion = mediaItem.GetHeadVersion(); MediaVersion headVersion = mediaItem.GetHeadVersion();
@ -232,6 +241,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(IFileSystem fileSystem
if (fileSystem.File.Exists(fullOutputPath)) if (fileSystem.File.Exists(fullOutputPath))
{ {
logger.LogDebug("Successfully extracted font {Font}", fontStream.FileName); logger.LogDebug("Successfully extracted font {Font}", fontStream.FileName);
extractedAnything = true;
} }
else else
{ {
@ -242,6 +252,8 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(IFileSystem fileSystem
} }
} }
} }
return extractedAnything;
} }

3
ErsatzTV.Core/Next/Config/ChannelConfig.cs

@ -104,6 +104,9 @@ namespace ErsatzTV.Core.Next.Config
public partial class Subtitle public partial class Subtitle
{ {
[JsonPropertyName("fonts_folder")]
public string? FontsFolder { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("mode")] [JsonPropertyName("mode")]
public Mode? Mode { get; set; } public Mode? Mode { get; set; }

65
ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

@ -103,20 +103,7 @@ public class PlayoutItemConverter(
foreach (Core.Next.Source source in maybeSource) foreach (Core.Next.Source source in maybeSource)
{ {
if (playoutItem is not DynamicPlayoutItem) SetInOutPoints(playoutItem, source);
{
if (playoutItem.InPoint > TimeSpan.Zero)
{
source.InPointMs = (long)playoutItem.InPoint.TotalMilliseconds;
}
var duration = playoutItem.MediaItem.GetDurationForPlayout();
if (playoutItem.OutPoint > TimeSpan.Zero && playoutItem.OutPoint < duration)
{
source.OutPointMs = (long)playoutItem.OutPoint.TotalMilliseconds;
}
}
nextPlayoutItem.Source = source; nextPlayoutItem.Source = source;
} }
@ -158,7 +145,7 @@ public class PlayoutItemConverter(
Codec = s.Codec Codec = s.Codec
}).ToList(); }).ToList();
nextPlayoutItem.Source.ProbeHint = new Core.Next.ProbeHint nextPlayoutItem.Source!.ProbeHint = new Core.Next.ProbeHint
{ {
Audio = sourceAudioHints, Audio = sourceAudioHints,
Video = sourceVideoHints, Video = sourceVideoHints,
@ -428,11 +415,30 @@ public class PlayoutItemConverter(
{ {
if (subtitle.SubtitleKind is SubtitleKind.Embedded) if (subtitle.SubtitleKind is SubtitleKind.Embedded)
{ {
if (nextPlayoutItem.Tracks?.Subtitle?.StreamIndex is null) if (subtitle.IsImage)
{ {
nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks(); if (nextPlayoutItem.Tracks?.Subtitle?.StreamIndex is null)
nextPlayoutItem.Tracks.Subtitle ??= new Core.Next.TrackSelection(); {
nextPlayoutItem.Tracks.Subtitle.StreamIndex = subtitle.StreamIndex; nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks();
nextPlayoutItem.Tracks.Subtitle ??= new Core.Next.TrackSelection();
nextPlayoutItem.Tracks.Subtitle.StreamIndex = subtitle.StreamIndex;
}
}
// next only supports sidecar text subtitles at the moment; ignore non-extracted text subs
else if (subtitle.IsExtracted && !string.IsNullOrWhiteSpace(subtitle.Path))
{
if (nextPlayoutItem.Tracks?.Subtitle?.Source is null)
{
nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks();
nextPlayoutItem.Tracks.Subtitle ??= new Core.Next.TrackSelection();
nextPlayoutItem.Tracks.Subtitle.Source = new Core.Next.Source
{
SourceType = Core.Next.SourceType.Local,
Path = Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path),
};
SetInOutPoints(playoutItem, nextPlayoutItem.Tracks.Subtitle.Source);
}
} }
} }
else if (!subtitle.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) else if (!subtitle.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
@ -446,6 +452,8 @@ public class PlayoutItemConverter(
SourceType = Core.Next.SourceType.Local, SourceType = Core.Next.SourceType.Local,
Path = subtitle.Path, Path = subtitle.Path,
}; };
SetInOutPoints(playoutItem, nextPlayoutItem.Tracks.Subtitle.Source);
} }
} }
else if (subtitle.Path.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase)) else if (subtitle.Path.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase))
@ -461,6 +469,8 @@ public class PlayoutItemConverter(
KeepAlive = false, KeepAlive = false,
Reconnect = true Reconnect = true
}; };
SetInOutPoints(playoutItem, nextPlayoutItem.Tracks.Subtitle.Source);
} }
} }
} }
@ -602,4 +612,21 @@ public class PlayoutItemConverter(
} }
]; ];
} }
private static void SetInOutPoints(PlayoutItem playoutItem, Core.Next.Source source)
{
if (playoutItem is not DynamicPlayoutItem)
{
if (playoutItem.InPoint > TimeSpan.Zero)
{
source.InPointMs = (long)playoutItem.InPoint.TotalMilliseconds;
}
var duration = playoutItem.MediaItem.GetDurationForPlayout();
if (playoutItem.OutPoint > TimeSpan.Zero && playoutItem.OutPoint < duration)
{
source.OutPointMs = (long)playoutItem.OutPoint.TotalMilliseconds;
}
}
}
} }

Loading…
Cancel
Save