< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
Line coverage
17%
Covered lines: 81
Uncovered lines: 387
Coverable lines: 468
Total lines: 1057
Line coverage: 17.3%
Branch coverage
14%
Covered branches: 32
Total branches: 224
Branch coverage: 14.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/1/2026 - 12:13:05 AM Line coverage: 8.3% (41/489) Branch coverage: 6.4% (12/186) Total lines: 10535/4/2026 - 12:15:16 AM Line coverage: 8.3% (41/491) Branch coverage: 6.3% (12/190) Total lines: 10565/13/2026 - 12:15:27 AM Line coverage: 8.2% (41/498) Branch coverage: 5.9% (12/202) Total lines: 10766/1/2026 - 12:16:05 AM Line coverage: 7.9% (40/506) Branch coverage: 5.6% (12/212) Total lines: 10906/2/2026 - 12:15:49 AM Line coverage: 5.5% (28/501) Branch coverage: 2.8% (6/212) Total lines: 10826/6/2026 - 12:15:50 AM Line coverage: 5.5% (28/501) Branch coverage: 2.3% (5/212) Total lines: 10826/8/2026 - 12:16:15 AM Line coverage: 5.8% (32/544) Branch coverage: 3% (7/232) Total lines: 11747/14/2026 - 12:13:58 AM Line coverage: 5.8% (32/545) Branch coverage: 3% (7/232) Total lines: 11757/18/2026 - 12:15:19 AM Line coverage: 6.9% (32/463) Branch coverage: 3.2% (7/214) Total lines: 10497/21/2026 - 12:16:33 AM Line coverage: 12.5% (58/463) Branch coverage: 9.3% (20/214) Total lines: 10497/22/2026 - 12:16:22 AM Line coverage: 17.3% (81/468) Branch coverage: 14.2% (32/224) Total lines: 1057 5/1/2026 - 12:13:05 AM Line coverage: 8.3% (41/489) Branch coverage: 6.4% (12/186) Total lines: 10535/4/2026 - 12:15:16 AM Line coverage: 8.3% (41/491) Branch coverage: 6.3% (12/190) Total lines: 10565/13/2026 - 12:15:27 AM Line coverage: 8.2% (41/498) Branch coverage: 5.9% (12/202) Total lines: 10766/1/2026 - 12:16:05 AM Line coverage: 7.9% (40/506) Branch coverage: 5.6% (12/212) Total lines: 10906/2/2026 - 12:15:49 AM Line coverage: 5.5% (28/501) Branch coverage: 2.8% (6/212) Total lines: 10826/6/2026 - 12:15:50 AM Line coverage: 5.5% (28/501) Branch coverage: 2.3% (5/212) Total lines: 10826/8/2026 - 12:16:15 AM Line coverage: 5.8% (32/544) Branch coverage: 3% (7/232) Total lines: 11747/14/2026 - 12:13:58 AM Line coverage: 5.8% (32/545) Branch coverage: 3% (7/232) Total lines: 11757/18/2026 - 12:15:19 AM Line coverage: 6.9% (32/463) Branch coverage: 3.2% (7/214) Total lines: 10497/21/2026 - 12:16:33 AM Line coverage: 12.5% (58/463) Branch coverage: 9.3% (20/214) Total lines: 10497/22/2026 - 12:16:22 AM Line coverage: 17.3% (81/468) Branch coverage: 14.2% (32/224) Total lines: 1057

Coverage delta

Coverage delta 7 -7

Metrics

File(s)

/srv/git/jellyfin/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.Diagnostics.CodeAnalysis;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Net.Http;
 11using System.Text;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using AsyncKeyedLock;
 15using MediaBrowser.Common;
 16using MediaBrowser.Common.Configuration;
 17using MediaBrowser.Common.Extensions;
 18using MediaBrowser.Common.Net;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Entities;
 21using MediaBrowser.Controller.IO;
 22using MediaBrowser.Controller.Library;
 23using MediaBrowser.Controller.MediaEncoding;
 24using MediaBrowser.Model.Dto;
 25using MediaBrowser.Model.Entities;
 26using MediaBrowser.Model.IO;
 27using MediaBrowser.Model.MediaInfo;
 28using Microsoft.Extensions.Logging;
 29using Nikse.SubtitleEdit.Core.Common;
 30using Nikse.SubtitleEdit.Core.SubtitleFormats;
 31using UtfUnknown;
 32using SubtitleFormat = MediaBrowser.Model.MediaInfo.SubtitleFormat;
 33
 34namespace MediaBrowser.MediaEncoding.Subtitles
 35{
 36    public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
 37    {
 38        private readonly ILogger<SubtitleEncoder> _logger;
 39        private readonly IFileSystem _fileSystem;
 40        private readonly IMediaEncoder _mediaEncoder;
 41        private readonly IHttpClientFactory _httpClientFactory;
 42        private readonly IMediaSourceManager _mediaSourceManager;
 43        private readonly ISubtitleParser _subtitleParser;
 44        private readonly IPathManager _pathManager;
 45        private readonly IServerConfigurationManager _serverConfigurationManager;
 46
 47        /// <summary>
 48        /// The _semaphoreLocks.
 49        /// </summary>
 3250        private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
 3251        {
 3252            o.PoolSize = 20;
 3253            o.PoolInitialFill = 1;
 3254        });
 55
 56        public SubtitleEncoder(
 57            ILogger<SubtitleEncoder> logger,
 58            IFileSystem fileSystem,
 59            IMediaEncoder mediaEncoder,
 60            IHttpClientFactory httpClientFactory,
 61            IMediaSourceManager mediaSourceManager,
 62            ISubtitleParser subtitleParser,
 63            IPathManager pathManager,
 64            IServerConfigurationManager serverConfigurationManager)
 65        {
 3266            _logger = logger;
 3267            _fileSystem = fileSystem;
 3268            _mediaEncoder = mediaEncoder;
 3269            _httpClientFactory = httpClientFactory;
 3270            _mediaSourceManager = mediaSourceManager;
 3271            _subtitleParser = subtitleParser;
 3272            _pathManager = pathManager;
 3273            _serverConfigurationManager = serverConfigurationManager;
 3274        }
 75
 76        internal MemoryStream ConvertSubtitles(
 77            Stream stream,
 78            SubtitleInfo inputInfo,
 79            string outputFormat,
 80            long startTimeTicks,
 81            long endTimeTicks,
 82            bool preserveOriginalTimestamps)
 83        {
 10484            var subtitle = _subtitleParser.Parse(stream, inputInfo.Format);
 85
 10486            FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
 87
 10488            var formatter = GetWriter(outputFormat);
 89
 10490            var text = formatter.ToText(subtitle, "untitled");
 10491            var bytes = Encoding.UTF8.GetBytes(text);
 92
 10493            return new MemoryStream(bytes, 0, bytes.Length, false, true);
 94        }
 95
 96        internal void FilterEvents(Subtitle track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
 97        {
 98            // Drop subs that have fully elapsed before the requested start position
 10499            track.Paragraphs
 104100                .RemoveAll(i => (i.StartTime.TimeSpan.Ticks - startPositionTicks) < 0 && (i.EndTime.TimeSpan.Ticks - sta
 101
 104102            if (endTimeTicks > 0)
 103            {
 0104                track.Paragraphs
 0105                    .RemoveAll(i => i.StartTime.TimeSpan.Ticks > endTimeTicks);
 106            }
 107
 104108            if (!preserveTimestamps)
 109            {
 104208110                foreach (var trackEvent in track.Paragraphs)
 111                {
 52000112                    trackEvent.StartTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.StartTime.TimeSpan.Tic
 52000113                    trackEvent.EndTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.EndTime.TimeSpan.Ticks -
 114                }
 115            }
 104116        }
 117
 118        async Task<Stream> ISubtitleEncoder.GetSubtitles(BaseItem item, string mediaSourceId, int subtitleStreamIndex, s
 119        {
 0120            ArgumentNullException.ThrowIfNull(item);
 121
 0122            if (string.IsNullOrWhiteSpace(mediaSourceId))
 123            {
 0124                throw new ArgumentNullException(nameof(mediaSourceId));
 125            }
 126
 0127            var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationTo
 128
 0129            var mediaSource = mediaSources
 0130                .First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
 131
 0132            var subtitleStream = mediaSource.MediaStreams
 0133               .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
 134
 0135            var (stream, info) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
 0136                        .ConfigureAwait(false);
 137
 138            // Return the original if the same format is being requested
 139            // Character encoding was already handled in GetSubtitleStream
 140            // ASS is a superset of SSA, skipping the conversion and preserving the styles
 0141            if (string.Equals(info.Format, outputFormat, StringComparison.OrdinalIgnoreCase)
 0142                || (string.Equals(info.Format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)
 0143                    && string.Equals(outputFormat, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)))
 144            {
 0145                return stream;
 146            }
 147
 0148            using (stream)
 149            {
 0150                return ConvertSubtitles(stream, info, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimest
 151            }
 0152        }
 153
 154        private async Task<(Stream Stream, SubtitleInfo Info)> GetSubtitleStream(
 155            MediaSourceInfo mediaSource,
 156            MediaStream subtitleStream,
 157            CancellationToken cancellationToken)
 158        {
 0159            var fileInfo = await GetReadableFile(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
 160
 0161            var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false);
 162
 0163            return (stream, fileInfo);
 0164        }
 165
 166        internal async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
 167        {
 4168            if (fileInfo.IsExternal && MediaStream.IsTextFormat(fileInfo.Format))
 169            {
 4170                var result = await DetectCharset(fileInfo.Path, cancellationToken).ConfigureAwait(false);
 4171                var detected = result.Detected;
 172
 4173                var stream = fileInfo.Protocol == MediaProtocol.Http
 4174                    ? await _httpClientFactory.CreateClient(NamedClient.Default)
 4175                        .GetStreamAsync(new Uri(fileInfo.Path), cancellationToken)
 4176                        .ConfigureAwait(false)
 4177                    : AsyncFile.OpenRead(fileInfo.Path);
 178
 179                // Short-circuit when the file is already UTF-8/ASCII.
 4180                if (detected is null
 4181                    || string.Equals(detected.EncodingName, "utf-8", StringComparison.OrdinalIgnoreCase)
 4182                    || string.Equals(detected.EncodingName, "ascii", StringComparison.OrdinalIgnoreCase)
 4183                    || string.Equals(detected.EncodingName, "us-ascii", StringComparison.OrdinalIgnoreCase))
 184                {
 1185                    return stream;
 186                }
 187
 3188                _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path);
 189
 3190                await using (stream.ConfigureAwait(false))
 191                {
 3192                    using var reader = new StreamReader(stream, detected.Encoding);
 3193                    var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
 194
 3195                    return new MemoryStream(Encoding.UTF8.GetBytes(text));
 196                }
 0197            }
 198
 0199            return AsyncFile.OpenRead(fileInfo.Path);
 4200        }
 201
 202        internal async Task<SubtitleInfo> GetReadableFile(
 203            MediaSourceInfo mediaSource,
 204            MediaStream subtitleStream,
 205            CancellationToken cancellationToken)
 206        {
 4207            if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 208            {
 0209                await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false);
 210
 0211                var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream);
 0212                var outputFormat = GetExtractableSubtitleFormat(subtitleStream);
 0213                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension)
 0214                    ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUI
 215
 0216                return new SubtitleInfo()
 0217                {
 0218                    Path = outputPath,
 0219                    Protocol = MediaProtocol.File,
 0220                    Format = outputFormat,
 0221                    IsExternal = MediaStream.IsVobSubFormat(outputFormat)
 0222                };
 223            }
 224
 225            // Normalize ffmpeg codec names to the file extensions the parser is keyed on
 4226            var currentFormat = NormalizeCodecToParserExtension((Path.GetExtension(subtitleStream.Path) ?? subtitleStrea
 227
 228            // Handle PGS subtitles as raw streams for the client to render
 4229            if (MediaStream.IsPgsFormat(currentFormat))
 230            {
 0231                return new SubtitleInfo()
 0232                {
 0233                    Path = subtitleStream.Path,
 0234                    Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path),
 0235                    Format = "pgssub",
 0236                    IsExternal = true
 0237                };
 238            }
 239
 240            // Fallback to ffmpeg conversion
 4241            if (!_subtitleParser.SupportsFileExtension(currentFormat))
 242            {
 243                // Convert
 0244                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt")
 0245                    ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUI
 246
 0247                await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwai
 248
 0249                return new SubtitleInfo()
 0250                {
 0251                    Path = outputPath,
 0252                    Protocol = MediaProtocol.File,
 0253                    Format = "srt",
 0254                    IsExternal = true
 0255                };
 256            }
 257
 258            // It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with
 4259            return new SubtitleInfo()
 4260            {
 4261                Path = subtitleStream.Path,
 4262                Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path),
 4263                Format = currentFormat,
 4264                IsExternal = true
 4265            };
 4266        }
 267
 268        private bool TryGetWriter(string format, [NotNullWhen(true)] out Nikse.SubtitleEdit.Core.SubtitleFormats.Subtitl
 269        {
 104270            ArgumentException.ThrowIfNullOrEmpty(format);
 271
 104272            if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
 273            {
 0274                value = new AdvancedSubStationAlpha();
 0275                return true;
 276            }
 277
 104278            if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
 279            {
 0280                value = new JsonWriter();
 0281                return true;
 282            }
 283
 104284            if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)
 104285                || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
 286            {
 0287                value = new SubRip();
 0288                return true;
 289            }
 290
 104291            if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
 292            {
 0293                value = new SubStationAlpha();
 0294                return true;
 295            }
 296
 104297            if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)
 104298                || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase))
 299            {
 104300                value = new WebVTT();
 104301                return true;
 302            }
 303
 0304            if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
 305            {
 0306                value = new TimedText10();
 0307                return true;
 308            }
 309
 0310            value = null;
 0311            return false;
 312        }
 313
 314        private Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat GetWriter(string format)
 315        {
 104316            if (TryGetWriter(format, out var writer))
 317            {
 104318                return writer;
 319            }
 320
 0321            throw new ArgumentException("Unsupported format: " + format);
 322        }
 323
 324        /// <summary>
 325        /// Converts the text subtitle to SRT.
 326        /// </summary>
 327        /// <param name="subtitleStream">The subtitle stream.</param>
 328        /// <param name="mediaSource">The input mediaSource.</param>
 329        /// <param name="outputPath">The output path.</param>
 330        /// <param name="cancellationToken">The cancellation token.</param>
 331        /// <returns>Task.</returns>
 332        private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outp
 333        {
 0334            using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
 335            {
 0336                if (!IsCachedSubtitleFresh(outputPath, subtitleStream.Path))
 337                {
 0338                    await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).C
 339                }
 0340            }
 0341        }
 342
 343        // ffmpeg codec names don't always match the file extensions the subtitle parser is keyed on.
 344        private static string NormalizeCodecToParserExtension(string codecOrExtension)
 345        {
 4346            return codecOrExtension switch
 4347            {
 0348                "subrip" => "srt",
 0349                "webvtt" => "vtt",
 4350                _ => codecOrExtension
 4351            };
 352        }
 353
 354        // Records "this cache was built from this exact source revision" in a sidecar file next to the cache: "<sizeByt
 0355        private static string GetCacheMetaPath(string cachePath) => cachePath + ".meta";
 356
 357        private static string FormatCacheMeta(long length, DateTime lastWriteUtc)
 0358            => string.Create(CultureInfo.InvariantCulture, $"{length}:{lastWriteUtc.Ticks}");
 359
 360        private bool IsCachedSubtitleFresh(string cachePath, string? sourcePath)
 361        {
 0362            if (!File.Exists(cachePath))
 363            {
 0364                return false;
 365            }
 366
 0367            var cacheInfo = _fileSystem.GetFileInfo(cachePath);
 0368            if (cacheInfo.Length == 0)
 369            {
 0370                return false;
 371            }
 372
 0373            if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath))
 374            {
 0375                return true;
 376            }
 377
 0378            var metaPath = GetCacheMetaPath(cachePath);
 0379            if (!File.Exists(metaPath))
 380            {
 381                // Pre-existing cache from before metadata tracking - regenerate so we can record the source state.
 0382                return false;
 383            }
 384
 385            try
 386            {
 0387                var sourceInfo = _fileSystem.GetFileInfo(sourcePath);
 0388                var expected = FormatCacheMeta(sourceInfo.Length, sourceInfo.LastWriteTimeUtc);
 0389                var actual = File.ReadAllText(metaPath);
 0390                return string.Equals(expected, actual, StringComparison.Ordinal);
 391            }
 0392            catch (IOException)
 393            {
 0394                return false;
 395            }
 0396        }
 397
 398        private void WriteCacheMeta(string cachePath, string? sourcePath)
 399        {
 0400            if (string.IsNullOrEmpty(sourcePath))
 401            {
 0402                return;
 403            }
 404
 405            try
 406            {
 0407                var sourceInfo = _fileSystem.GetFileInfo(sourcePath);
 0408                if (!sourceInfo.Exists)
 409                {
 0410                    return;
 411                }
 412
 0413                File.WriteAllText(GetCacheMetaPath(cachePath), FormatCacheMeta(sourceInfo.Length, sourceInfo.LastWriteTi
 0414            }
 0415            catch (IOException ex)
 416            {
 0417                _logger.LogWarning(ex, "Failed to record subtitle cache metadata for {CachePath}", cachePath);
 0418            }
 0419        }
 420
 421        /// <summary>
 422        /// Converts the text subtitle to SRT internal.
 423        /// </summary>
 424        /// <param name="subtitleStream">The subtitle stream.</param>
 425        /// <param name="mediaSource">The input mediaSource.</param>
 426        /// <param name="outputPath">The output path.</param>
 427        /// <param name="cancellationToken">The cancellation token.</param>
 428        /// <returns>Task.</returns>
 429        /// <exception cref="ArgumentNullException">
 430        /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
 431        /// </exception>
 432        private async Task ConvertTextSubtitleToSrtInternal(MediaStream subtitleStream, MediaSourceInfo mediaSource, str
 433        {
 0434            var inputPath = subtitleStream.Path;
 0435            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 436
 0437            ArgumentException.ThrowIfNullOrEmpty(outputPath);
 438
 0439            Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path (
 440
 0441            var encodingParam = await GetSubtitleFileCharacterSet(subtitleStream, subtitleStream.Language, mediaSource, 
 442
 443            // FFmpeg automatically convert character encoding when it is UTF-16
 444            // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to re
 0445            if ((inputPath.EndsWith(".smi", StringComparison.Ordinal) || inputPath.EndsWith(".sami", StringComparison.Or
 0446                (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) ||
 0447                 encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase)))
 448            {
 0449                encodingParam = string.Empty;
 450            }
 0451            else if (!string.IsNullOrEmpty(encodingParam))
 452            {
 0453                encodingParam = " -sub_charenc " + encodingParam;
 454            }
 455
 0456            var args = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, 
 457
 0458            await ExtractSubtitlesForFile(
 0459                inputPath,
 0460                args,
 0461                [outputPath],
 0462                cancellationToken).ConfigureAwait(false);
 463
 0464            WriteCacheMeta(outputPath, inputPath);
 0465        }
 466
 467        private string GetExtractableSubtitleFormat(MediaStream subtitleStream)
 468        {
 0469            if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
 0470                || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)
 0471                || string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase))
 472            {
 0473                return subtitleStream.Codec;
 474            }
 0475            else if (MediaStream.IsVobSubFormat(subtitleStream.Codec))
 476            {
 0477                return "mks";
 478            }
 479            else
 480            {
 0481                return "srt";
 482            }
 483        }
 484
 485        private string GetExtractableSubtitleFileExtension(MediaStream subtitleStream)
 486        {
 487            // Using .pgssub as file extension is not allowed by ffmpeg. The file extension for pgs subtitles is .sup.
 0488            if (string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase))
 489            {
 0490                return "sup";
 491            }
 0492            else if (MediaStream.IsVobSubFormat(subtitleStream.Codec))
 493            {
 494                // FFmpeg cannot mux VobSub subtitle streams back into the .idx/.sub pair, so we use .mks container inst
 0495                return "mks";
 496            }
 497            else
 498            {
 0499                return GetExtractableSubtitleFormat(subtitleStream);
 500            }
 501        }
 502
 503        private bool IsCodecCopyable(string codec)
 504        {
 0505            return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase)
 0506                || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase)
 0507                || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase)
 0508                || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase)
 0509                || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase)
 0510                || MediaStream.IsVobSubFormat(codec);
 511        }
 512
 513        /// <inheritdoc />
 514        public async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToke
 515        {
 0516            var locks = new List<IDisposable>();
 0517            var extractableStreams = new List<MediaStream>();
 518
 519            try
 520            {
 0521                var subtitleStreams = mediaSource.MediaStreams
 0522                    .Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true });
 523
 0524                foreach (var subtitleStream in subtitleStreams)
 525                {
 0526                    if (subtitleStream.IsExternal
 0527                        && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 528                    {
 529                        continue;
 530                    }
 531
 0532                    var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitl
 0533                    if (outputPath is null)
 534                    {
 535                        continue;
 536                    }
 537
 0538                    var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
 539
 0540                    var sourcePath = string.IsNullOrEmpty(subtitleStream.Path) ? mediaSource.Path : subtitleStream.Path;
 0541                    if (IsCachedSubtitleFresh(outputPath, sourcePath))
 542                    {
 0543                        releaser.Dispose();
 0544                        continue;
 545                    }
 546
 0547                    locks.Add(releaser);
 0548                    extractableStreams.Add(subtitleStream);
 0549                }
 550
 0551                if (extractableStreams.Count > 0)
 552                {
 0553                    await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).Con
 0554                    await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).Configur
 555                }
 0556            }
 0557            catch (Exception ex)
 558            {
 0559                _logger.LogWarning(ex, "Unable to get streams for File:{File}", mediaSource.Path);
 0560            }
 561            finally
 562            {
 0563                locks.ForEach(x => x.Dispose());
 564            }
 0565        }
 566
 567        private async Task ExtractAllExtractableSubtitlesMKS(
 568           MediaSourceInfo mediaSource,
 569           List<MediaStream> subtitleStreams,
 570           CancellationToken cancellationToken)
 571        {
 0572            var mksFiles = new List<string>();
 573
 0574            foreach (var subtitleStream in subtitleStreams)
 575            {
 0576                if (string.IsNullOrEmpty(subtitleStream.Path) || !subtitleStream.Path.EndsWith(".mks", StringComparison.
 577                {
 578                    continue;
 579                }
 580
 0581                if (!mksFiles.Contains(subtitleStream.Path))
 582                {
 0583                    mksFiles.Add(subtitleStream.Path);
 584                }
 585            }
 586
 0587            if (mksFiles.Count == 0)
 588            {
 0589                return;
 590            }
 591
 0592            foreach (string mksFile in mksFiles)
 593            {
 0594                var inputPath = _mediaEncoder.GetInputArgument(mksFile, mediaSource);
 0595                var outputPaths = new List<string>();
 0596                var args = string.Format(
 0597                    CultureInfo.InvariantCulture,
 0598                    "-y -i {0}",
 0599                    inputPath);
 600
 0601                foreach (var subtitleStream in subtitleStreams)
 602                {
 0603                    if (!subtitleStream.Path.Equals(mksFile, StringComparison.OrdinalIgnoreCase))
 604                    {
 605                        continue;
 606                    }
 607
 0608                    var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitl
 0609                    if (outputPath is null)
 610                    {
 611                        continue;
 612                    }
 613
 0614                    var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
 615                    // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files.
 0616                    var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.
 0617                    var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
 618
 0619                    if (streamIndex == -1)
 620                    {
 0621                        _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this str
 0622                        continue;
 623                    }
 624
 0625                    Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Cal
 626
 0627                    outputPaths.Add(outputPath);
 0628                    args += string.Format(
 0629                        CultureInfo.InvariantCulture,
 0630                        " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"",
 0631                        streamIndex,
 0632                        outputCodec,
 0633                        outputFormatOption,
 0634                        outputPath);
 635                }
 636
 0637                await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
 638
 0639                foreach (var outputPath in outputPaths)
 640                {
 0641                    WriteCacheMeta(outputPath, mksFile);
 642                }
 0643            }
 0644        }
 645
 646        private async Task ExtractAllExtractableSubtitlesInternal(
 647            MediaSourceInfo mediaSource,
 648            List<MediaStream> subtitleStreams,
 649            CancellationToken cancellationToken)
 650        {
 0651            var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
 0652            var outputPaths = new List<string>();
 0653            var args = string.Format(
 0654                CultureInfo.InvariantCulture,
 0655                "-y -i {0}",
 0656                inputPath);
 657
 0658            foreach (var subtitleStream in subtitleStreams)
 659            {
 0660                if (!string.IsNullOrEmpty(subtitleStream.Path) && subtitleStream.Path.EndsWith(".mks", StringComparison.
 661                {
 0662                    _logger.LogDebug("Subtitle {Index} for file {InputPath} is part in an MKS file. Skipping", inputPath
 0663                    continue;
 664                }
 665
 0666                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFil
 0667                if (outputPath is null)
 668                {
 669                    continue;
 670                }
 671
 0672                var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
 673                // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files.
 0674                var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empt
 0675                var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
 676
 0677                if (streamIndex == -1)
 678                {
 0679                    _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream"
 0680                    continue;
 681                }
 682
 0683                Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calcula
 684
 0685                outputPaths.Add(outputPath);
 0686                args += string.Format(
 0687                    CultureInfo.InvariantCulture,
 0688                    " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"",
 0689                    streamIndex,
 0690                    outputCodec,
 0691                    outputFormatOption,
 0692                    outputPath);
 693            }
 694
 0695            if (outputPaths.Count > 0)
 696            {
 0697                await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
 698
 0699                foreach (var outputPath in outputPaths)
 700                {
 0701                    WriteCacheMeta(outputPath, mediaSource.Path);
 702                }
 703            }
 0704        }
 705
 706        private async Task ExtractSubtitlesForFile(
 707            string inputPath,
 708            string args,
 709            IReadOnlyList<string> outputPaths,
 710            CancellationToken cancellationToken)
 711        {
 0712            var (exitCode, ffmpegError) = await RunSubtitleExtractionProcess(args, cancellationToken).ConfigureAwait(fal
 713
 0714            var failed = false;
 715
 0716            if (exitCode == -1)
 717            {
 0718                failed = true;
 719
 0720                foreach (var outputPath in outputPaths)
 721                {
 722                    try
 723                    {
 0724                        _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
 0725                        _fileSystem.DeleteFile(outputPath);
 0726                    }
 0727                    catch (FileNotFoundException)
 728                    {
 0729                    }
 0730                    catch (IOException ex)
 731                    {
 0732                        _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
 0733                    }
 734                }
 735            }
 736            else
 737            {
 0738                foreach (var outputPath in outputPaths)
 739                {
 0740                    if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
 741                    {
 0742                        _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath,
 0743                        failed = true;
 744
 745                        try
 746                        {
 0747                            _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
 0748                            _fileSystem.DeleteFile(outputPath);
 0749                        }
 0750                        catch (FileNotFoundException)
 751                        {
 0752                        }
 0753                        catch (IOException ex)
 754                        {
 0755                            _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
 0756                        }
 757
 758                        continue;
 759                    }
 760
 0761                    if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase))
 762                    {
 0763                        await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
 764                    }
 765
 0766                    _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", input
 0767                }
 768            }
 769
 0770            if (failed)
 771            {
 0772                cancellationToken.ThrowIfCancellationRequested();
 773
 0774                if (!string.IsNullOrWhiteSpace(ffmpegError))
 775                {
 0776                    _logger.LogError("ffmpeg subtitle extraction failed for {InputPath}: {FfmpegOutput}", inputPath, ffm
 777                }
 778
 0779                throw new FfmpegException(
 0780                    string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath))
 781            }
 0782        }
 783
 784        /// <summary>
 785        /// Extracts the text subtitle.
 786        /// </summary>
 787        /// <param name="mediaSource">The mediaSource.</param>
 788        /// <param name="subtitleStream">The subtitle stream.</param>
 789        /// <param name="outputCodec">The output codec.</param>
 790        /// <param name="outputPath">The output path.</param>
 791        /// <param name="cancellationToken">The cancellation token.</param>
 792        /// <returns>Task.</returns>
 793        /// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
 794        private async Task ExtractTextSubtitle(
 795            MediaSourceInfo mediaSource,
 796            MediaStream subtitleStream,
 797            string outputCodec,
 798            string outputPath,
 799            CancellationToken cancellationToken)
 800        {
 0801            using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
 802            {
 0803                if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
 804                {
 0805                    var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
 806
 0807                    var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
 808
 0809                    if (subtitleStream.IsExternal)
 810                    {
 0811                        args = _mediaEncoder.GetExternalSubtitleInputArgument(subtitleStream.Path);
 812                    }
 813
 0814                    await ExtractTextSubtitleInternal(
 0815                        args,
 0816                        subtitleStreamIndex,
 0817                        outputCodec,
 0818                        outputPath,
 0819                        cancellationToken).ConfigureAwait(false);
 820                }
 0821            }
 0822        }
 823
 824        private async Task ExtractTextSubtitleInternal(
 825            string inputPath,
 826            int subtitleStreamIndex,
 827            string outputCodec,
 828            string outputPath,
 829            CancellationToken cancellationToken)
 830        {
 0831            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 832
 0833            ArgumentException.ThrowIfNullOrEmpty(outputPath);
 834
 0835            Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path (
 0836            var processArgs = string.Format(
 0837                CultureInfo.InvariantCulture,
 0838                "-y -i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
 0839                inputPath,
 0840                subtitleStreamIndex,
 0841                outputCodec,
 0842                outputPath);
 843
 0844            await ExtractSubtitlesForFile(
 0845                inputPath,
 0846                processArgs,
 0847                [outputPath],
 0848                cancellationToken).ConfigureAwait(false);
 0849        }
 850
 851        /// <summary>
 852        /// Runs ffmpeg to extract or convert subtitles, capturing its exit code and stderr output.
 853        /// </summary>
 854        /// <remarks>
 855        /// stdin is redirected and closed, and <c>-nostdin</c> is prepended to the arguments, so ffmpeg can never
 856        /// block reading an inherited stdin handle (which happens when Jellyfin runs as a service, e.g. under NSSM,
 857        /// and stalls subtitle extraction until the timeout). stderr is redirected and drained so a full pipe buffer
 858        /// cannot deadlock ffmpeg and so its output can be surfaced on failure; stdout is left un-redirected as it is
 859        /// unused for subtitle extraction.
 860        /// </remarks>
 861        /// <param name="arguments">The ffmpeg command line arguments.</param>
 862        /// <param name="cancellationToken">The cancellation token.</param>
 863        /// <returns>The ffmpeg exit code (-1 on timeout) and its captured stderr output.</returns>
 864        private async Task<(int ExitCode, string StandardError)> RunSubtitleExtractionProcess(string arguments, Cancella
 865        {
 866            int exitCode;
 0867            var standardError = string.Empty;
 868
 0869            using (var process = new Process
 0870            {
 0871                StartInfo = new ProcessStartInfo
 0872                {
 0873                    CreateNoWindow = true,
 0874                    UseShellExecute = false,
 0875                    RedirectStandardInput = true,
 0876                    RedirectStandardError = true,
 0877                    FileName = _mediaEncoder.EncoderPath,
 0878                    Arguments = "-nostdin " + arguments,
 0879                    WindowStyle = ProcessWindowStyle.Hidden,
 0880                    ErrorDialog = false
 0881                },
 0882                EnableRaisingEvents = true
 0883            })
 884            {
 0885                _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 886
 887                try
 888                {
 0889                    process.Start();
 0890                }
 0891                catch (Exception ex)
 892                {
 0893                    _logger.LogError(ex, "Error starting ffmpeg");
 0894                    throw;
 895                }
 896
 897                // Close stdin so ffmpeg observes EOF instead of blocking on an inherited handle.
 0898                process.StandardInput.Close();
 899
 900                // Begin draining stderr before waiting for exit; a full stderr pipe buffer would otherwise deadlock ffm
 0901                var standardErrorTask = process.StandardError.ReadToEndAsync(CancellationToken.None);
 0902                var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
 0903                using var waitSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 0904                waitSource.CancelAfter(TimeSpan.FromMinutes(timeoutMinutes));
 905
 906                try
 907                {
 0908                    await process.WaitForExitAsync(waitSource.Token).ConfigureAwait(false);
 0909                    exitCode = process.ExitCode;
 0910                }
 0911                catch (OperationCanceledException)
 912                {
 0913                    process.Kill(true);
 0914                    exitCode = -1;
 0915                }
 916
 917                try
 918                {
 0919                    standardError = await standardErrorTask.ConfigureAwait(false);
 0920                }
 0921                catch (OperationCanceledException)
 922                {
 923                    // Reading ffmpeg output was cancelled; nothing more to capture.
 0924                }
 0925            }
 926
 0927            return (exitCode, standardError);
 0928        }
 929
 930        /// <summary>
 931        /// Sets the ass font.
 932        /// </summary>
 933        /// <param name="file">The file.</param>
 934        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <c>Syst
 935        /// <returns>Task.</returns>
 936        private async Task SetAssFont(string file, CancellationToken cancellationToken = default)
 937        {
 0938            _logger.LogInformation("Setting ass font within {File}", file);
 939
 940            string text;
 941            Encoding encoding;
 942
 0943            using (var fileStream = AsyncFile.OpenRead(file))
 0944            using (var reader = new StreamReader(fileStream, true))
 945            {
 0946                encoding = reader.CurrentEncoding;
 947
 0948                text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
 0949            }
 950
 0951            var newText = text.Replace(",Arial,", ",Arial Unicode MS,", StringComparison.Ordinal);
 952
 0953            if (!string.Equals(text, newText, StringComparison.Ordinal))
 954            {
 0955                var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.File
 0956                await using (fileStream.ConfigureAwait(false))
 957                {
 0958                    var writer = new StreamWriter(fileStream, encoding);
 0959                    await using (writer.ConfigureAwait(false))
 960                    {
 0961                        await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
 962                    }
 963                }
 964            }
 0965        }
 966
 967        private string? GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitle
 968        {
 0969            return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension);
 970        }
 971
 972        /// <inheritdoc />
 973        public async Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceIn
 974        {
 0975            var subtitleCodec = subtitleStream.Codec;
 0976            var path = subtitleStream.Path;
 977
 0978            if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 979            {
 0980                var cachePath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
 0981                if (cachePath is not null)
 982                {
 0983                    path = cachePath;
 0984                    await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
 0985                        .ConfigureAwait(false);
 986                }
 987            }
 988
 0989            var result = await DetectCharset(path, cancellationToken).ConfigureAwait(false);
 0990            var charset = result.Detected?.EncodingName ?? string.Empty;
 991
 992            // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
 0993            if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || p
 0994                && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase)
 0995                    || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase)))
 996            {
 0997                charset = string.Empty;
 998            }
 999
 01000            _logger.LogDebug("charset {0} detected for {Path}", charset, path);
 1001
 01002            return charset;
 01003        }
 1004
 1005        private async Task<DetectionResult> DetectCharset(string path, CancellationToken cancellationToken)
 1006        {
 41007            var protocol = _mediaSourceManager.GetPathProtocol(path);
 1008            switch (protocol)
 1009            {
 1010                case MediaProtocol.Http:
 1011                    {
 01012                        using var stream = await _httpClientFactory
 01013                          .CreateClient(NamedClient.Default)
 01014                          .GetStreamAsync(new Uri(path), cancellationToken)
 01015                          .ConfigureAwait(false);
 1016
 01017                        return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(fal
 1018                    }
 1019
 1020                case MediaProtocol.File:
 1021                    {
 41022                        return await CharsetDetector.DetectFromFileAsync(path, cancellationToken)
 41023                                              .ConfigureAwait(false);
 1024                    }
 1025
 1026                default:
 01027                    throw new NotSupportedException($"Unsupported protocol: {protocol}");
 1028            }
 41029        }
 1030
 1031        public async Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, Cancellat
 1032        {
 01033            var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken)
 01034                .ConfigureAwait(false);
 01035            return info.Path;
 01036        }
 1037
 1038        /// <inheritdoc />
 1039        public void Dispose()
 1040        {
 241041            _semaphoreLocks.Dispose();
 241042        }
 1043
 1044#pragma warning disable CA1034 // Nested types should not be visible
 1045        // Only public for the unit tests
 1046        public readonly record struct SubtitleInfo
 1047        {
 1048            public string Path { get; init; }
 1049
 1050            public MediaProtocol Protocol { get; init; }
 1051
 1052            public string Format { get; init; }
 1053
 1054            public bool IsExternal { get; init; }
 1055        }
 1056    }
 1057}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILogger`1<MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Controller.MediaEncoding.IMediaEncoder,System.Net.Http.IHttpClientFactory,MediaBrowser.Controller.Library.IMediaSourceManager,MediaBrowser.MediaEncoding.Subtitles.ISubtitleParser,MediaBrowser.Controller.IO.IPathManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager)
ConvertSubtitles(System.IO.Stream,MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder/SubtitleInfo,System.String,System.Int64,System.Int64,System.Boolean)
FilterEvents(Nikse.SubtitleEdit.Core.Common.Subtitle,System.Int64,System.Int64,System.Boolean)
MediaBrowser-Controller-MediaEncoding-ISubtitleEncoder-GetSubtitles()
GetSubtitleStream()
GetSubtitleStream()
GetReadableFile()
TryGetWriter(System.String,Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat&)
GetWriter(System.String)
ConvertTextSubtitleToSrt()
NormalizeCodecToParserExtension(System.String)
GetCacheMetaPath(System.String)
FormatCacheMeta(System.Int64,System.DateTime)
IsCachedSubtitleFresh(System.String,System.String)
WriteCacheMeta(System.String,System.String)
ConvertTextSubtitleToSrtInternal()
GetExtractableSubtitleFormat(MediaBrowser.Model.Entities.MediaStream)
GetExtractableSubtitleFileExtension(MediaBrowser.Model.Entities.MediaStream)
IsCodecCopyable(System.String)
ExtractAllExtractableSubtitles()
ExtractAllExtractableSubtitlesMKS()
ExtractAllExtractableSubtitlesInternal()
ExtractSubtitlesForFile()
ExtractTextSubtitle()
ExtractTextSubtitleInternal()
RunSubtitleExtractionProcess()
SetAssFont()
GetSubtitleCachePath(MediaBrowser.Model.Dto.MediaSourceInfo,System.Int32,System.String)
GetSubtitleFileCharacterSet()
DetectCharset()
GetSubtitleFilePath()
Dispose()