< 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: 82
Uncovered lines: 389
Coverable lines: 471
Total lines: 1066
Line coverage: 17.4%
Branch coverage
14%
Covered branches: 33
Total branches: 226
Branch coverage: 14.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 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: 105710/29/2026 - 12:17:11 AM Line coverage: 17.4% (82/471) Branch coverage: 14.6% (33/226) Total lines: 1066 5/16/2026 - 12:15:55 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: 105710/29/2026 - 12:17:11 AM Line coverage: 17.4% (82/471) Branch coverage: 14.6% (33/226) Total lines: 1066

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 Jellyfin.Extensions;
 16using MediaBrowser.Common;
 17using MediaBrowser.Common.Configuration;
 18using MediaBrowser.Common.Extensions;
 19using MediaBrowser.Common.Net;
 20using MediaBrowser.Controller.Configuration;
 21using MediaBrowser.Controller.Entities;
 22using MediaBrowser.Controller.IO;
 23using MediaBrowser.Controller.Library;
 24using MediaBrowser.Controller.MediaEncoding;
 25using MediaBrowser.Model.Dto;
 26using MediaBrowser.Model.Entities;
 27using MediaBrowser.Model.IO;
 28using MediaBrowser.Model.MediaInfo;
 29using Microsoft.Extensions.Logging;
 30using Nikse.SubtitleEdit.Core.Common;
 31using Nikse.SubtitleEdit.Core.SubtitleFormats;
 32using UtfUnknown;
 33using SubtitleFormat = MediaBrowser.Model.MediaInfo.SubtitleFormat;
 34
 35namespace MediaBrowser.MediaEncoding.Subtitles
 36{
 37    public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
 38    {
 39        private readonly ILogger<SubtitleEncoder> _logger;
 40        private readonly IFileSystem _fileSystem;
 41        private readonly IMediaEncoder _mediaEncoder;
 42        private readonly IHttpClientFactory _httpClientFactory;
 43        private readonly IMediaSourceManager _mediaSourceManager;
 44        private readonly ISubtitleParser _subtitleParser;
 45        private readonly IPathManager _pathManager;
 46        private readonly IServerConfigurationManager _serverConfigurationManager;
 47
 48        /// <summary>
 49        /// The _semaphoreLocks.
 50        /// </summary>
 3851        private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
 3852        {
 3853            o.PoolSize = 20;
 3854            o.PoolInitialFill = 1;
 3855        });
 56
 57        public SubtitleEncoder(
 58            ILogger<SubtitleEncoder> logger,
 59            IFileSystem fileSystem,
 60            IMediaEncoder mediaEncoder,
 61            IHttpClientFactory httpClientFactory,
 62            IMediaSourceManager mediaSourceManager,
 63            ISubtitleParser subtitleParser,
 64            IPathManager pathManager,
 65            IServerConfigurationManager serverConfigurationManager)
 66        {
 3867            _logger = logger;
 3868            _fileSystem = fileSystem;
 3869            _mediaEncoder = mediaEncoder;
 3870            _httpClientFactory = httpClientFactory;
 3871            _mediaSourceManager = mediaSourceManager;
 3872            _subtitleParser = subtitleParser;
 3873            _pathManager = pathManager;
 3874            _serverConfigurationManager = serverConfigurationManager;
 3875        }
 76
 77        internal MemoryStream ConvertSubtitles(
 78            Stream stream,
 79            SubtitleInfo inputInfo,
 80            string outputFormat,
 81            long startTimeTicks,
 82            long endTimeTicks,
 83            bool preserveOriginalTimestamps)
 84        {
 10485            var subtitle = _subtitleParser.Parse(stream, inputInfo.Format);
 86
 10487            FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
 88
 10489            var formatter = GetWriter(outputFormat);
 90
 10491            var text = formatter.ToText(subtitle, "untitled");
 10492            var bytes = Encoding.UTF8.GetBytes(text);
 93
 10494            return new MemoryStream(bytes, 0, bytes.Length, false, true);
 95        }
 96
 97        internal void FilterEvents(Subtitle track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
 98        {
 99            // Drop subs that have fully elapsed before the requested start position
 104100            track.Paragraphs
 104101                .RemoveAll(i => (i.StartTime.TimeSpan.Ticks - startPositionTicks) < 0 && (i.EndTime.TimeSpan.Ticks - sta
 102
 104103            if (endTimeTicks > 0)
 104            {
 0105                track.Paragraphs
 0106                    .RemoveAll(i => i.StartTime.TimeSpan.Ticks > endTimeTicks);
 107            }
 108
 104109            if (!preserveTimestamps)
 110            {
 104208111                foreach (var trackEvent in track.Paragraphs)
 112                {
 52000113                    trackEvent.StartTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.StartTime.TimeSpan.Tic
 52000114                    trackEvent.EndTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.EndTime.TimeSpan.Ticks -
 115                }
 116            }
 104117        }
 118
 119        async Task<Stream> ISubtitleEncoder.GetSubtitles(BaseItem item, string mediaSourceId, int subtitleStreamIndex, s
 120        {
 0121            ArgumentNullException.ThrowIfNull(item);
 122
 0123            if (string.IsNullOrWhiteSpace(mediaSourceId))
 124            {
 0125                throw new ArgumentNullException(nameof(mediaSourceId));
 126            }
 127
 0128            var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationTo
 129
 0130            var mediaSource = mediaSources
 0131                .First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
 132
 0133            var subtitleStream = mediaSource.MediaStreams
 0134               .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
 135
 0136            var (stream, info) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
 0137                        .ConfigureAwait(false);
 138
 139            // Return the original if the same format is being requested
 140            // Character encoding was already handled in GetSubtitleStream
 141            // ASS is a superset of SSA, skipping the conversion and preserving the styles
 0142            if (string.Equals(info.Format, outputFormat, StringComparison.OrdinalIgnoreCase)
 0143                || (string.Equals(info.Format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)
 0144                    && string.Equals(outputFormat, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)))
 145            {
 0146                return stream;
 147            }
 148
 0149            using (stream)
 150            {
 0151                return ConvertSubtitles(stream, info, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimest
 152            }
 0153        }
 154
 155        private async Task<(Stream Stream, SubtitleInfo Info)> GetSubtitleStream(
 156            MediaSourceInfo mediaSource,
 157            MediaStream subtitleStream,
 158            CancellationToken cancellationToken)
 159        {
 0160            var fileInfo = await GetReadableFile(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
 161
 0162            var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false);
 163
 0164            return (stream, fileInfo);
 0165        }
 166
 167        internal async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
 168        {
 5169            if (fileInfo.IsExternal && MediaStream.IsTextFormat(fileInfo.Format))
 170            {
 5171                var result = await DetectCharset(fileInfo.Path, cancellationToken).ConfigureAwait(false);
 5172                var detected = result.Detected;
 173
 5174                var stream = fileInfo.Protocol == MediaProtocol.Http
 5175                    ? await _httpClientFactory.CreateClient(NamedClient.Default)
 5176                        .GetStreamAsync(new Uri(fileInfo.Path), cancellationToken)
 5177                        .ConfigureAwait(false)
 5178                    : AsyncFile.OpenRead(fileInfo.Path);
 179
 180                // Short-circuit when the file is already UTF-8/ASCII.
 5181                if (detected is null
 5182                    || string.Equals(detected.EncodingName, "utf-8", StringComparison.OrdinalIgnoreCase)
 5183                    || string.Equals(detected.EncodingName, "ascii", StringComparison.OrdinalIgnoreCase)
 5184                    || string.Equals(detected.EncodingName, "us-ascii", StringComparison.OrdinalIgnoreCase))
 185                {
 1186                    return stream;
 187                }
 188
 4189                _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path);
 190
 191                // If encoding is not supported by .NET (e.g., ISO-8859-16), return stream as-is
 192                // The charset name will be passed to ffmpeg via -sub_charenc if needed
 4193                if (detected.Encoding is null)
 194                {
 0195                    _logger.LogWarning("Encoding {Encoding} is not supported by .NET for {Path}, returning stream as-is"
 0196                    return stream;
 197                }
 198
 4199                await using (stream.ConfigureAwait(false))
 200                {
 4201                    using var reader = new StreamReader(stream, detected.Encoding);
 4202                    var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
 203
 4204                    return new MemoryStream(Encoding.UTF8.GetBytes(text));
 205                }
 0206            }
 207
 0208            return AsyncFile.OpenRead(fileInfo.Path);
 5209        }
 210
 211        internal async Task<SubtitleInfo> GetReadableFile(
 212            MediaSourceInfo mediaSource,
 213            MediaStream subtitleStream,
 214            CancellationToken cancellationToken)
 215        {
 4216            if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 217            {
 0218                await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false);
 219
 0220                var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream);
 0221                var outputFormat = GetExtractableSubtitleFormat(subtitleStream);
 0222                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension)
 0223                    ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUI
 224
 0225                return new SubtitleInfo()
 0226                {
 0227                    Path = outputPath,
 0228                    Protocol = MediaProtocol.File,
 0229                    Format = outputFormat,
 0230                    IsExternal = MediaStream.IsVobSubFormat(outputFormat)
 0231                };
 232            }
 233
 234            // Normalize ffmpeg codec names to the file extensions the parser is keyed on
 4235            var currentFormat = NormalizeCodecToParserExtension((Path.GetExtension(subtitleStream.Path) ?? subtitleStrea
 236
 237            // Handle PGS subtitles as raw streams for the client to render
 4238            if (MediaStream.IsPgsFormat(currentFormat))
 239            {
 0240                return new SubtitleInfo()
 0241                {
 0242                    Path = subtitleStream.Path,
 0243                    Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path),
 0244                    Format = "pgssub",
 0245                    IsExternal = true
 0246                };
 247            }
 248
 249            // Fallback to ffmpeg conversion
 4250            if (!_subtitleParser.SupportsFileExtension(currentFormat))
 251            {
 252                // Convert
 0253                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt")
 0254                    ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUI
 255
 0256                await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwai
 257
 0258                return new SubtitleInfo()
 0259                {
 0260                    Path = outputPath,
 0261                    Protocol = MediaProtocol.File,
 0262                    Format = "srt",
 0263                    IsExternal = true
 0264                };
 265            }
 266
 267            // It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with
 4268            return new SubtitleInfo()
 4269            {
 4270                Path = subtitleStream.Path,
 4271                Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path),
 4272                Format = currentFormat,
 4273                IsExternal = true
 4274            };
 4275        }
 276
 277        private bool TryGetWriter(string format, [NotNullWhen(true)] out Nikse.SubtitleEdit.Core.SubtitleFormats.Subtitl
 278        {
 104279            ArgumentException.ThrowIfNullOrEmpty(format);
 280
 104281            if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
 282            {
 0283                value = new AdvancedSubStationAlpha();
 0284                return true;
 285            }
 286
 104287            if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
 288            {
 0289                value = new JsonWriter();
 0290                return true;
 291            }
 292
 104293            if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)
 104294                || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
 295            {
 0296                value = new SubRip();
 0297                return true;
 298            }
 299
 104300            if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
 301            {
 0302                value = new SubStationAlpha();
 0303                return true;
 304            }
 305
 104306            if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)
 104307                || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase))
 308            {
 104309                value = new WebVTT();
 104310                return true;
 311            }
 312
 0313            if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
 314            {
 0315                value = new TimedText10();
 0316                return true;
 317            }
 318
 0319            value = null;
 0320            return false;
 321        }
 322
 323        private Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat GetWriter(string format)
 324        {
 104325            if (TryGetWriter(format, out var writer))
 326            {
 104327                return writer;
 328            }
 329
 0330            throw new ArgumentException("Unsupported format: " + format);
 331        }
 332
 333        /// <summary>
 334        /// Converts the text subtitle to SRT.
 335        /// </summary>
 336        /// <param name="subtitleStream">The subtitle stream.</param>
 337        /// <param name="mediaSource">The input mediaSource.</param>
 338        /// <param name="outputPath">The output path.</param>
 339        /// <param name="cancellationToken">The cancellation token.</param>
 340        /// <returns>Task.</returns>
 341        private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outp
 342        {
 0343            using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
 344            {
 0345                if (!IsCachedSubtitleFresh(outputPath, subtitleStream.Path))
 346                {
 0347                    await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).C
 348                }
 0349            }
 0350        }
 351
 352        // ffmpeg codec names don't always match the file extensions the subtitle parser is keyed on.
 353        private static string NormalizeCodecToParserExtension(string codecOrExtension)
 354        {
 4355            return codecOrExtension switch
 4356            {
 0357                "subrip" => "srt",
 0358                "webvtt" => "vtt",
 4359                _ => codecOrExtension
 4360            };
 361        }
 362
 363        // Records "this cache was built from this exact source revision" in a sidecar file next to the cache: "<sizeByt
 0364        private static string GetCacheMetaPath(string cachePath) => cachePath + ".meta";
 365
 366        private static string FormatCacheMeta(long length, DateTime lastWriteUtc)
 0367            => string.Create(CultureInfo.InvariantCulture, $"{length}:{lastWriteUtc.Ticks}");
 368
 369        private bool IsCachedSubtitleFresh(string cachePath, string? sourcePath)
 370        {
 0371            if (!File.Exists(cachePath))
 372            {
 0373                return false;
 374            }
 375
 0376            var cacheInfo = _fileSystem.GetFileInfo(cachePath);
 0377            if (cacheInfo.Length == 0)
 378            {
 0379                return false;
 380            }
 381
 0382            if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath))
 383            {
 0384                return true;
 385            }
 386
 0387            var metaPath = GetCacheMetaPath(cachePath);
 0388            if (!File.Exists(metaPath))
 389            {
 390                // Pre-existing cache from before metadata tracking - regenerate so we can record the source state.
 0391                return false;
 392            }
 393
 394            try
 395            {
 0396                var sourceInfo = _fileSystem.GetFileInfo(sourcePath);
 0397                var expected = FormatCacheMeta(sourceInfo.Length, sourceInfo.LastWriteTimeUtc);
 0398                var actual = File.ReadAllText(metaPath);
 0399                return string.Equals(expected, actual, StringComparison.Ordinal);
 400            }
 0401            catch (IOException)
 402            {
 0403                return false;
 404            }
 0405        }
 406
 407        private void WriteCacheMeta(string cachePath, string? sourcePath)
 408        {
 0409            if (string.IsNullOrEmpty(sourcePath))
 410            {
 0411                return;
 412            }
 413
 414            try
 415            {
 0416                var sourceInfo = _fileSystem.GetFileInfo(sourcePath);
 0417                if (!sourceInfo.Exists)
 418                {
 0419                    return;
 420                }
 421
 0422                File.WriteAllText(GetCacheMetaPath(cachePath), FormatCacheMeta(sourceInfo.Length, sourceInfo.LastWriteTi
 0423            }
 0424            catch (IOException ex)
 425            {
 0426                _logger.LogWarning(ex, "Failed to record subtitle cache metadata for {CachePath}", cachePath);
 0427            }
 0428        }
 429
 430        /// <summary>
 431        /// Converts the text subtitle to SRT internal.
 432        /// </summary>
 433        /// <param name="subtitleStream">The subtitle stream.</param>
 434        /// <param name="mediaSource">The input mediaSource.</param>
 435        /// <param name="outputPath">The output path.</param>
 436        /// <param name="cancellationToken">The cancellation token.</param>
 437        /// <returns>Task.</returns>
 438        /// <exception cref="ArgumentNullException">
 439        /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
 440        /// </exception>
 441        private async Task ConvertTextSubtitleToSrtInternal(MediaStream subtitleStream, MediaSourceInfo mediaSource, str
 442        {
 0443            var inputPath = subtitleStream.Path;
 0444            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 445
 0446            ArgumentException.ThrowIfNullOrEmpty(outputPath);
 447
 0448            Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path (
 449
 0450            var encodingParam = await GetSubtitleFileCharacterSet(subtitleStream, subtitleStream.Language, mediaSource, 
 451
 452            // FFmpeg automatically convert character encoding when it is UTF-16
 453            // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to re
 0454            if ((inputPath.EndsWith(".smi", StringComparison.Ordinal) || inputPath.EndsWith(".sami", StringComparison.Or
 0455                (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) ||
 0456                 encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase)))
 457            {
 0458                encodingParam = string.Empty;
 459            }
 0460            else if (!string.IsNullOrEmpty(encodingParam))
 461            {
 0462                encodingParam = " -sub_charenc " + encodingParam;
 463            }
 464
 0465            var args = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, 
 466
 0467            await ExtractSubtitlesForFile(
 0468                inputPath,
 0469                args,
 0470                [outputPath],
 0471                cancellationToken).ConfigureAwait(false);
 472
 0473            WriteCacheMeta(outputPath, inputPath);
 0474        }
 475
 476        private string GetExtractableSubtitleFormat(MediaStream subtitleStream)
 477        {
 0478            if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
 0479                || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)
 0480                || string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase))
 481            {
 0482                return subtitleStream.Codec;
 483            }
 0484            else if (MediaStream.IsVobSubFormat(subtitleStream.Codec))
 485            {
 0486                return "mks";
 487            }
 488            else
 489            {
 0490                return "srt";
 491            }
 492        }
 493
 494        private string GetExtractableSubtitleFileExtension(MediaStream subtitleStream)
 495        {
 496            // Using .pgssub as file extension is not allowed by ffmpeg. The file extension for pgs subtitles is .sup.
 0497            if (string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase))
 498            {
 0499                return "sup";
 500            }
 0501            else if (MediaStream.IsVobSubFormat(subtitleStream.Codec))
 502            {
 503                // FFmpeg cannot mux VobSub subtitle streams back into the .idx/.sub pair, so we use .mks container inst
 0504                return "mks";
 505            }
 506            else
 507            {
 0508                return GetExtractableSubtitleFormat(subtitleStream);
 509            }
 510        }
 511
 512        private bool IsCodecCopyable(string codec)
 513        {
 0514            return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase)
 0515                || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase)
 0516                || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase)
 0517                || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase)
 0518                || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase)
 0519                || MediaStream.IsVobSubFormat(codec);
 520        }
 521
 522        /// <inheritdoc />
 523        public async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToke
 524        {
 0525            var locks = new List<IDisposable>();
 0526            var extractableStreams = new List<MediaStream>();
 527
 528            try
 529            {
 0530                var subtitleStreams = mediaSource.MediaStreams
 0531                    .Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true });
 532
 0533                foreach (var subtitleStream in subtitleStreams)
 534                {
 0535                    if (subtitleStream.IsExternal
 0536                        && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 537                    {
 538                        continue;
 539                    }
 540
 0541                    var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitl
 0542                    if (outputPath is null)
 543                    {
 544                        continue;
 545                    }
 546
 0547                    var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
 548
 0549                    var sourcePath = string.IsNullOrEmpty(subtitleStream.Path) ? mediaSource.Path : subtitleStream.Path;
 0550                    if (IsCachedSubtitleFresh(outputPath, sourcePath))
 551                    {
 0552                        releaser.Dispose();
 0553                        continue;
 554                    }
 555
 0556                    locks.Add(releaser);
 0557                    extractableStreams.Add(subtitleStream);
 0558                }
 559
 0560                if (extractableStreams.Count > 0)
 561                {
 0562                    await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).Con
 0563                    await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).Configur
 564                }
 0565            }
 0566            catch (Exception ex)
 567            {
 0568                _logger.LogWarning(ex, "Unable to get streams for File:{File}", mediaSource.Path);
 0569            }
 570            finally
 571            {
 0572                locks.ForEach(x => x.Dispose());
 573            }
 0574        }
 575
 576        private async Task ExtractAllExtractableSubtitlesMKS(
 577           MediaSourceInfo mediaSource,
 578           List<MediaStream> subtitleStreams,
 579           CancellationToken cancellationToken)
 580        {
 0581            var mksFiles = new List<string>();
 582
 0583            foreach (var subtitleStream in subtitleStreams)
 584            {
 0585                if (string.IsNullOrEmpty(subtitleStream.Path) || !subtitleStream.Path.EndsWith(".mks", StringComparison.
 586                {
 587                    continue;
 588                }
 589
 0590                if (!mksFiles.Contains(subtitleStream.Path))
 591                {
 0592                    mksFiles.Add(subtitleStream.Path);
 593                }
 594            }
 595
 0596            if (mksFiles.Count == 0)
 597            {
 0598                return;
 599            }
 600
 0601            foreach (string mksFile in mksFiles)
 602            {
 0603                var inputPath = _mediaEncoder.GetInputArgument(mksFile, mediaSource);
 0604                var outputPaths = new List<string>();
 0605                var args = string.Format(
 0606                    CultureInfo.InvariantCulture,
 0607                    "-y -i {0}",
 0608                    inputPath);
 609
 0610                foreach (var subtitleStream in subtitleStreams)
 611                {
 0612                    if (!subtitleStream.Path.Equals(mksFile, StringComparison.OrdinalIgnoreCase))
 613                    {
 614                        continue;
 615                    }
 616
 0617                    var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitl
 0618                    if (outputPath is null)
 619                    {
 620                        continue;
 621                    }
 622
 0623                    var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
 624                    // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files.
 0625                    var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.
 0626                    var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
 627
 0628                    if (streamIndex == -1)
 629                    {
 0630                        _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this str
 0631                        continue;
 632                    }
 633
 0634                    Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Cal
 635
 0636                    outputPaths.Add(outputPath);
 0637                    args += string.Format(
 0638                        CultureInfo.InvariantCulture,
 0639                        " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"",
 0640                        streamIndex,
 0641                        outputCodec,
 0642                        outputFormatOption,
 0643                        outputPath.EscapeProcessArgument());
 644                }
 645
 0646                await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
 647
 0648                foreach (var outputPath in outputPaths)
 649                {
 0650                    WriteCacheMeta(outputPath, mksFile);
 651                }
 0652            }
 0653        }
 654
 655        private async Task ExtractAllExtractableSubtitlesInternal(
 656            MediaSourceInfo mediaSource,
 657            List<MediaStream> subtitleStreams,
 658            CancellationToken cancellationToken)
 659        {
 0660            var inputPath = _mediaEncoder.GetInputPathArgument(mediaSource.Path, mediaSource);
 0661            var outputPaths = new List<string>();
 0662            var args = string.Format(
 0663                CultureInfo.InvariantCulture,
 0664                "-y -i {0}",
 0665                inputPath);
 666
 0667            foreach (var subtitleStream in subtitleStreams)
 668            {
 0669                if (!string.IsNullOrEmpty(subtitleStream.Path) && subtitleStream.Path.EndsWith(".mks", StringComparison.
 670                {
 0671                    _logger.LogDebug("Subtitle {Index} for file {InputPath} is part in an MKS file. Skipping", inputPath
 0672                    continue;
 673                }
 674
 0675                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFil
 0676                if (outputPath is null)
 677                {
 678                    continue;
 679                }
 680
 0681                var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
 682                // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files.
 0683                var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empt
 0684                var streamIndex = EncodingHelper.GetSubtitleStreamIndexForFfmpeg(mediaSource, subtitleStream);
 685
 0686                if (streamIndex == -1)
 687                {
 0688                    _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream"
 0689                    continue;
 690                }
 691
 0692                Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calcula
 693
 0694                outputPaths.Add(outputPath);
 0695                args += string.Format(
 0696                    CultureInfo.InvariantCulture,
 0697                    " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"",
 0698                    streamIndex,
 0699                    outputCodec,
 0700                    outputFormatOption,
 0701                    outputPath.EscapeProcessArgument());
 702            }
 703
 0704            if (outputPaths.Count > 0)
 705            {
 0706                await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
 707
 0708                foreach (var outputPath in outputPaths)
 709                {
 0710                    WriteCacheMeta(outputPath, mediaSource.Path);
 711                }
 712            }
 0713        }
 714
 715        private async Task ExtractSubtitlesForFile(
 716            string inputPath,
 717            string args,
 718            IReadOnlyList<string> outputPaths,
 719            CancellationToken cancellationToken)
 720        {
 0721            var (exitCode, ffmpegError) = await RunSubtitleExtractionProcess(args, cancellationToken).ConfigureAwait(fal
 722
 0723            var failed = false;
 724
 0725            if (exitCode == -1)
 726            {
 0727                failed = true;
 728
 0729                foreach (var outputPath in outputPaths)
 730                {
 731                    try
 732                    {
 0733                        _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
 0734                        _fileSystem.DeleteFile(outputPath);
 0735                    }
 0736                    catch (FileNotFoundException)
 737                    {
 0738                    }
 0739                    catch (IOException ex)
 740                    {
 0741                        _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
 0742                    }
 743                }
 744            }
 745            else
 746            {
 0747                foreach (var outputPath in outputPaths)
 748                {
 0749                    if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
 750                    {
 0751                        _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath,
 0752                        failed = true;
 753
 754                        try
 755                        {
 0756                            _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
 0757                            _fileSystem.DeleteFile(outputPath);
 0758                        }
 0759                        catch (FileNotFoundException)
 760                        {
 0761                        }
 0762                        catch (IOException ex)
 763                        {
 0764                            _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
 0765                        }
 766
 767                        continue;
 768                    }
 769
 0770                    if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase))
 771                    {
 0772                        await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
 773                    }
 774
 0775                    _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", input
 0776                }
 777            }
 778
 0779            if (failed)
 780            {
 0781                cancellationToken.ThrowIfCancellationRequested();
 782
 0783                if (!string.IsNullOrWhiteSpace(ffmpegError))
 784                {
 0785                    _logger.LogError("ffmpeg subtitle extraction failed for {InputPath}: {FfmpegOutput}", inputPath, ffm
 786                }
 787
 0788                throw new FfmpegException(
 0789                    string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath))
 790            }
 0791        }
 792
 793        /// <summary>
 794        /// Extracts the text subtitle.
 795        /// </summary>
 796        /// <param name="mediaSource">The mediaSource.</param>
 797        /// <param name="subtitleStream">The subtitle stream.</param>
 798        /// <param name="outputCodec">The output codec.</param>
 799        /// <param name="outputPath">The output path.</param>
 800        /// <param name="cancellationToken">The cancellation token.</param>
 801        /// <returns>Task.</returns>
 802        /// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
 803        private async Task ExtractTextSubtitle(
 804            MediaSourceInfo mediaSource,
 805            MediaStream subtitleStream,
 806            string outputCodec,
 807            string outputPath,
 808            CancellationToken cancellationToken)
 809        {
 0810            using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
 811            {
 0812                if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
 813                {
 0814                    var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
 815
 0816                    var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
 817
 0818                    if (subtitleStream.IsExternal)
 819                    {
 0820                        args = _mediaEncoder.GetExternalSubtitleInputArgument(subtitleStream.Path);
 821                    }
 822
 0823                    await ExtractTextSubtitleInternal(
 0824                        args,
 0825                        subtitleStreamIndex,
 0826                        outputCodec,
 0827                        outputPath,
 0828                        cancellationToken).ConfigureAwait(false);
 829                }
 0830            }
 0831        }
 832
 833        private async Task ExtractTextSubtitleInternal(
 834            string inputPath,
 835            int subtitleStreamIndex,
 836            string outputCodec,
 837            string outputPath,
 838            CancellationToken cancellationToken)
 839        {
 0840            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 841
 0842            ArgumentException.ThrowIfNullOrEmpty(outputPath);
 843
 0844            Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path (
 0845            var processArgs = string.Format(
 0846                CultureInfo.InvariantCulture,
 0847                "-y -i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
 0848                inputPath,
 0849                subtitleStreamIndex,
 0850                outputCodec,
 0851                outputPath);
 852
 0853            await ExtractSubtitlesForFile(
 0854                inputPath,
 0855                processArgs,
 0856                [outputPath],
 0857                cancellationToken).ConfigureAwait(false);
 0858        }
 859
 860        /// <summary>
 861        /// Runs ffmpeg to extract or convert subtitles, capturing its exit code and stderr output.
 862        /// </summary>
 863        /// <remarks>
 864        /// stdin is redirected and closed, and <c>-nostdin</c> is prepended to the arguments, so ffmpeg can never
 865        /// block reading an inherited stdin handle (which happens when Jellyfin runs as a service, e.g. under NSSM,
 866        /// and stalls subtitle extraction until the timeout). stderr is redirected and drained so a full pipe buffer
 867        /// cannot deadlock ffmpeg and so its output can be surfaced on failure; stdout is left un-redirected as it is
 868        /// unused for subtitle extraction.
 869        /// </remarks>
 870        /// <param name="arguments">The ffmpeg command line arguments.</param>
 871        /// <param name="cancellationToken">The cancellation token.</param>
 872        /// <returns>The ffmpeg exit code (-1 on timeout) and its captured stderr output.</returns>
 873        private async Task<(int ExitCode, string StandardError)> RunSubtitleExtractionProcess(string arguments, Cancella
 874        {
 875            int exitCode;
 0876            var standardError = string.Empty;
 877
 0878            using (var process = new Process
 0879            {
 0880                StartInfo = new ProcessStartInfo
 0881                {
 0882                    CreateNoWindow = true,
 0883                    UseShellExecute = false,
 0884                    RedirectStandardInput = true,
 0885                    RedirectStandardError = true,
 0886                    FileName = _mediaEncoder.EncoderPath,
 0887                    Arguments = "-nostdin " + arguments,
 0888                    WindowStyle = ProcessWindowStyle.Hidden,
 0889                    ErrorDialog = false
 0890                },
 0891                EnableRaisingEvents = true
 0892            })
 893            {
 0894                _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 895
 896                try
 897                {
 0898                    process.Start();
 0899                }
 0900                catch (Exception ex)
 901                {
 0902                    _logger.LogError(ex, "Error starting ffmpeg");
 0903                    throw;
 904                }
 905
 906                // Close stdin so ffmpeg observes EOF instead of blocking on an inherited handle.
 0907                process.StandardInput.Close();
 908
 909                // Begin draining stderr before waiting for exit; a full stderr pipe buffer would otherwise deadlock ffm
 0910                var standardErrorTask = process.StandardError.ReadToEndAsync(CancellationToken.None);
 0911                var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
 0912                using var waitSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 0913                waitSource.CancelAfter(TimeSpan.FromMinutes(timeoutMinutes));
 914
 915                try
 916                {
 0917                    await process.WaitForExitAsync(waitSource.Token).ConfigureAwait(false);
 0918                    exitCode = process.ExitCode;
 0919                }
 0920                catch (OperationCanceledException)
 921                {
 0922                    process.Kill(true);
 0923                    exitCode = -1;
 0924                }
 925
 926                try
 927                {
 0928                    standardError = await standardErrorTask.ConfigureAwait(false);
 0929                }
 0930                catch (OperationCanceledException)
 931                {
 932                    // Reading ffmpeg output was cancelled; nothing more to capture.
 0933                }
 0934            }
 935
 0936            return (exitCode, standardError);
 0937        }
 938
 939        /// <summary>
 940        /// Sets the ass font.
 941        /// </summary>
 942        /// <param name="file">The file.</param>
 943        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <c>Syst
 944        /// <returns>Task.</returns>
 945        private async Task SetAssFont(string file, CancellationToken cancellationToken = default)
 946        {
 0947            _logger.LogInformation("Setting ass font within {File}", file);
 948
 949            string text;
 950            Encoding encoding;
 951
 0952            using (var fileStream = AsyncFile.OpenRead(file))
 0953            using (var reader = new StreamReader(fileStream, true))
 954            {
 0955                encoding = reader.CurrentEncoding;
 956
 0957                text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
 0958            }
 959
 0960            var newText = text.Replace(",Arial,", ",Arial Unicode MS,", StringComparison.Ordinal);
 961
 0962            if (!string.Equals(text, newText, StringComparison.Ordinal))
 963            {
 0964                var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.File
 0965                await using (fileStream.ConfigureAwait(false))
 966                {
 0967                    var writer = new StreamWriter(fileStream, encoding);
 0968                    await using (writer.ConfigureAwait(false))
 969                    {
 0970                        await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
 971                    }
 972                }
 973            }
 0974        }
 975
 976        private string? GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitle
 977        {
 0978            return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension);
 979        }
 980
 981        /// <inheritdoc />
 982        public async Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceIn
 983        {
 0984            var subtitleCodec = subtitleStream.Codec;
 0985            var path = subtitleStream.Path;
 986
 0987            if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 988            {
 0989                var cachePath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
 0990                if (cachePath is not null)
 991                {
 0992                    path = cachePath;
 0993                    await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
 0994                        .ConfigureAwait(false);
 995                }
 996            }
 997
 0998            var result = await DetectCharset(path, cancellationToken).ConfigureAwait(false);
 0999            var charset = result.Detected?.EncodingName ?? string.Empty;
 1000
 1001            // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
 01002            if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || p
 01003                && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase)
 01004                    || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase)))
 1005            {
 01006                charset = string.Empty;
 1007            }
 1008
 01009            _logger.LogDebug("charset {0} detected for {Path}", charset, path);
 1010
 01011            return charset;
 01012        }
 1013
 1014        private async Task<DetectionResult> DetectCharset(string path, CancellationToken cancellationToken)
 1015        {
 51016            var protocol = _mediaSourceManager.GetPathProtocol(path);
 1017            switch (protocol)
 1018            {
 1019                case MediaProtocol.Http:
 1020                    {
 01021                        using var stream = await _httpClientFactory
 01022                          .CreateClient(NamedClient.Default)
 01023                          .GetStreamAsync(new Uri(path), cancellationToken)
 01024                          .ConfigureAwait(false);
 1025
 01026                        return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(fal
 1027                    }
 1028
 1029                case MediaProtocol.File:
 1030                    {
 51031                        return await CharsetDetector.DetectFromFileAsync(path, cancellationToken)
 51032                                              .ConfigureAwait(false);
 1033                    }
 1034
 1035                default:
 01036                    throw new NotSupportedException($"Unsupported protocol: {protocol}");
 1037            }
 51038        }
 1039
 1040        public async Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, Cancellat
 1041        {
 01042            var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken)
 01043                .ConfigureAwait(false);
 01044            return info.Path;
 01045        }
 1046
 1047        /// <inheritdoc />
 1048        public void Dispose()
 1049        {
 291050            _semaphoreLocks.Dispose();
 291051        }
 1052
 1053#pragma warning disable CA1034 // Nested types should not be visible
 1054        // Only public for the unit tests
 1055        public readonly record struct SubtitleInfo
 1056        {
 1057            public string Path { get; init; }
 1058
 1059            public MediaProtocol Protocol { get; init; }
 1060
 1061            public string Format { get; init; }
 1062
 1063            public bool IsExternal { get; init; }
 1064        }
 1065    }
 1066}

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()