Write subtitles using SubtitleEdit

We've been using SubtitleEdit to parse since 2021
https://github.com/jellyfin/jellyfin/pull/4984

I think it's time we start using it to write too
This commit is contained in:
Bond_009
2026-05-08 21:29:13 +02:00
parent 99e9b2310f
commit 941298ee81
9 changed files with 35 additions and 652 deletions

View File

@@ -26,7 +26,10 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.Common;
using Nikse.SubtitleEdit.Core.SubtitleFormats;
using UtfUnknown;
using SubtitleFormat = MediaBrowser.Model.MediaInfo.SubtitleFormat;
namespace MediaBrowser.MediaEncoding.Subtitles
{
@@ -72,7 +75,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private MemoryStream ConvertSubtitles(
Stream stream,
string inputFormat,
SubtitleInfo inputInfo,
string outputFormat,
long startTimeTicks,
long endTimeTicks,
@@ -83,13 +86,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
var trackInfo = _subtitleParser.Parse(stream, inputFormat);
var subtitle = Subtitle.Parse(stream, Path.GetExtension(inputInfo.Path));
FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
var writer = GetWriter(outputFormat);
var formatter = GetWriter(outputFormat);
var text = formatter.ToText(subtitle, "untitled");
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.Write(text);
}
writer.Write(trackInfo, ms, cancellationToken);
ms.Position = 0;
}
catch
@@ -101,26 +109,24 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return ms;
}
internal void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
internal void FilterEvents(Subtitle track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
{
// Drop subs that have fully elapsed before the requested start position
track.TrackEvents = track.TrackEvents
.SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 && (i.EndPositionTicks - startPositionTicks) < 0)
.ToArray();
track.Paragraphs
.RemoveAll(i => (i.StartTime.TimeSpan.Ticks - startPositionTicks) < 0 && (i.EndTime.TimeSpan.Ticks - startPositionTicks) < 0);
if (endTimeTicks > 0)
{
track.TrackEvents = track.TrackEvents
.TakeWhile(i => i.StartPositionTicks <= endTimeTicks)
.ToArray();
track.Paragraphs
.RemoveAll(i => i.StartTime.TimeSpan.Ticks > endTimeTicks);
}
if (!preserveTimestamps)
{
foreach (var trackEvent in track.TrackEvents)
foreach (var trackEvent in track.Paragraphs)
{
trackEvent.EndPositionTicks = Math.Max(0, trackEvent.EndPositionTicks - startPositionTicks);
trackEvent.StartPositionTicks = Math.Max(0, trackEvent.StartPositionTicks - startPositionTicks);
trackEvent.StartTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.StartTime.TimeSpan.Ticks - startPositionTicks)));
trackEvent.EndTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.EndTime.TimeSpan.Ticks - startPositionTicks)));
}
}
}
@@ -142,14 +148,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
var (stream, info) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
.ConfigureAwait(false);
// Return the original if the same format is being requested
// Character encoding was already handled in GetSubtitleStream
// ASS is a superset of SSA, skipping the conversion and preserving the styles
if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)
|| (string.Equals(inputFormat, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)
if (string.Equals(info.Format, outputFormat, StringComparison.OrdinalIgnoreCase)
|| (string.Equals(info.Format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)
&& string.Equals(outputFormat, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)))
{
return stream;
@@ -157,11 +163,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
using (stream)
{
return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
return ConvertSubtitles(stream, info, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
}
}
private async Task<(Stream Stream, string Format)> GetSubtitleStream(
private async Task<(Stream Stream, SubtitleInfo Info)> GetSubtitleStream(
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
CancellationToken cancellationToken)
@@ -170,7 +176,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false);
return (stream, fileInfo.Format);
return (stream, fileInfo);
}
private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
@@ -267,43 +273,42 @@ namespace MediaBrowser.MediaEncoding.Subtitles
};
}
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
private bool TryGetWriter(string format, [NotNullWhen(true)] out Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat? value)
{
ArgumentException.ThrowIfNullOrEmpty(format);
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
{
value = new AssWriter();
value = new AdvancedSubStationAlpha();
return true;
}
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
value = new JsonWriter();
return true;
throw new NotImplementedException();
}
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
{
value = new SrtWriter();
value = new SubRip();
return true;
}
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
{
value = new SsaWriter();
value = new SubStationAlpha();
return true;
}
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase))
{
value = new VttWriter();
value = new WebVTT();
return true;
}
if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
{
value = new TtmlWriter();
value = new TimedText10();
return true;
}
@@ -311,7 +316,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return false;
}
private ISubtitleWriter GetWriter(string format)
private Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat GetWriter(string format)
{
if (TryGetWriter(format, out var writer))
{