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. 57
      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/). @@ -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 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
- 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
### Added

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

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

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

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.Maintenance;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
@ -168,6 +169,7 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa @@ -168,6 +169,7 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
.Map(pi => pi.MediaItemId)
.ToList();
bool extractedAnything = false;
foreach (int mediaItemId in toUpdate)
{
if (cancellationToken.IsCancellationRequested)
@ -176,8 +178,16 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa @@ -176,8 +178,16 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
}
// extract subtitles and fonts for each item and update db
await ExtractSubtitles(dbContext, mediaItemId, ffmpegPath, cancellationToken);
await ExtractFonts(dbContext, mediaItemId, ffmpegPath, cancellationToken);
extractedAnything |= await ExtractSubtitles(
dbContext,
mediaItemId,
ffmpegPath,
cancellationToken);
extractedAnything |= await ExtractFonts(
dbContext,
mediaItemId,
ffmpegPath,
cancellationToken);
}
_logger.LogDebug("Done checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck);
@ -186,6 +196,21 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa @@ -186,6 +196,21 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
{
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)
{

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

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

57
ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

@ -103,20 +103,7 @@ public class PlayoutItemConverter( @@ -103,20 +103,7 @@ public class PlayoutItemConverter(
foreach (Core.Next.Source source in maybeSource)
{
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;
}
}
SetInOutPoints(playoutItem, source);
nextPlayoutItem.Source = source;
}
@ -158,7 +145,7 @@ public class PlayoutItemConverter( @@ -158,7 +145,7 @@ public class PlayoutItemConverter(
Codec = s.Codec
}).ToList();
nextPlayoutItem.Source.ProbeHint = new Core.Next.ProbeHint
nextPlayoutItem.Source!.ProbeHint = new Core.Next.ProbeHint
{
Audio = sourceAudioHints,
Video = sourceVideoHints,
@ -427,6 +414,8 @@ public class PlayoutItemConverter( @@ -427,6 +414,8 @@ public class PlayoutItemConverter(
foreach (Subtitle subtitle in maybeSubtitle)
{
if (subtitle.SubtitleKind is SubtitleKind.Embedded)
{
if (subtitle.IsImage)
{
if (nextPlayoutItem.Tracks?.Subtitle?.StreamIndex is null)
{
@ -435,6 +424,23 @@ public class PlayoutItemConverter( @@ -435,6 +424,23 @@ public class PlayoutItemConverter(
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))
{
if (nextPlayoutItem.Tracks?.Subtitle?.Source is null)
@ -446,6 +452,8 @@ public class PlayoutItemConverter( @@ -446,6 +452,8 @@ public class PlayoutItemConverter(
SourceType = Core.Next.SourceType.Local,
Path = subtitle.Path,
};
SetInOutPoints(playoutItem, nextPlayoutItem.Tracks.Subtitle.Source);
}
}
else if (subtitle.Path.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase))
@ -461,6 +469,8 @@ public class PlayoutItemConverter( @@ -461,6 +469,8 @@ public class PlayoutItemConverter(
KeepAlive = false,
Reconnect = true
};
SetInOutPoints(playoutItem, nextPlayoutItem.Tracks.Subtitle.Source);
}
}
}
@ -602,4 +612,21 @@ public class PlayoutItemConverter( @@ -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