SubtitleEncoder.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using MediaBrowser.Common.Configuration;
  10. using MediaBrowser.Common.Extensions;
  11. using MediaBrowser.Common.Net;
  12. using MediaBrowser.Controller.Entities;
  13. using MediaBrowser.Controller.Library;
  14. using MediaBrowser.Controller.MediaEncoding;
  15. using MediaBrowser.Model.Diagnostics;
  16. using MediaBrowser.Model.Dto;
  17. using MediaBrowser.Model.Entities;
  18. using MediaBrowser.Model.IO;
  19. using MediaBrowser.Model.MediaInfo;
  20. using MediaBrowser.Model.Serialization;
  21. using MediaBrowser.Model.Text;
  22. using Microsoft.Extensions.Logging;
  23. namespace MediaBrowser.MediaEncoding.Subtitles
  24. {
  25. public class SubtitleEncoder : ISubtitleEncoder
  26. {
  27. private readonly ILibraryManager _libraryManager;
  28. private readonly ILogger _logger;
  29. private readonly IApplicationPaths _appPaths;
  30. private readonly IFileSystem _fileSystem;
  31. private readonly IMediaEncoder _mediaEncoder;
  32. private readonly IJsonSerializer _json;
  33. private readonly IHttpClient _httpClient;
  34. private readonly IMediaSourceManager _mediaSourceManager;
  35. private readonly IProcessFactory _processFactory;
  36. private readonly ITextEncoding _textEncoding;
  37. public SubtitleEncoder(
  38. ILibraryManager libraryManager,
  39. ILogger logger,
  40. IApplicationPaths appPaths,
  41. IFileSystem fileSystem,
  42. IMediaEncoder mediaEncoder,
  43. IJsonSerializer json,
  44. IHttpClient httpClient,
  45. IMediaSourceManager mediaSourceManager,
  46. IProcessFactory processFactory,
  47. ITextEncoding textEncoding)
  48. {
  49. _libraryManager = libraryManager;
  50. _logger = logger;
  51. _appPaths = appPaths;
  52. _fileSystem = fileSystem;
  53. _mediaEncoder = mediaEncoder;
  54. _json = json;
  55. _httpClient = httpClient;
  56. _mediaSourceManager = mediaSourceManager;
  57. _processFactory = processFactory;
  58. _textEncoding = textEncoding;
  59. }
  60. private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
  61. private Stream ConvertSubtitles(Stream stream,
  62. string inputFormat,
  63. string outputFormat,
  64. long startTimeTicks,
  65. long endTimeTicks,
  66. bool preserveOriginalTimestamps,
  67. CancellationToken cancellationToken)
  68. {
  69. var ms = new MemoryStream();
  70. try
  71. {
  72. var reader = GetReader(inputFormat, true);
  73. var trackInfo = reader.Parse(stream, cancellationToken);
  74. FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
  75. var writer = GetWriter(outputFormat);
  76. writer.Write(trackInfo, ms, cancellationToken);
  77. ms.Position = 0;
  78. }
  79. catch
  80. {
  81. ms.Dispose();
  82. throw;
  83. }
  84. return ms;
  85. }
  86. private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
  87. {
  88. // Drop subs that are earlier than what we're looking for
  89. track.TrackEvents = track.TrackEvents
  90. .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0)
  91. .ToArray();
  92. if (endTimeTicks > 0)
  93. {
  94. long endTime = endTimeTicks;
  95. track.TrackEvents = track.TrackEvents
  96. .TakeWhile(i => i.StartPositionTicks <= endTime)
  97. .ToArray();
  98. }
  99. if (!preserveTimestamps)
  100. {
  101. foreach (var trackEvent in track.TrackEvents)
  102. {
  103. trackEvent.EndPositionTicks -= startPositionTicks;
  104. trackEvent.StartPositionTicks -= startPositionTicks;
  105. }
  106. }
  107. }
  108. async Task<Stream> ISubtitleEncoder.GetSubtitles(BaseItem item, string mediaSourceId, int subtitleStreamIndex, string outputFormat, long startTimeTicks, long endTimeTicks, bool preserveOriginalTimestamps, CancellationToken cancellationToken)
  109. {
  110. if (item == null)
  111. {
  112. throw new ArgumentNullException(nameof(item));
  113. }
  114. if (string.IsNullOrWhiteSpace(mediaSourceId))
  115. {
  116. throw new ArgumentNullException(nameof(mediaSourceId));
  117. }
  118. // TODO network path substition useful ?
  119. var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, true, cancellationToken).ConfigureAwait(false);
  120. var mediaSource = mediaSources
  121. .First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
  122. var subtitleStream = mediaSource.MediaStreams
  123. .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
  124. var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
  125. .ConfigureAwait(false);
  126. var inputFormat = subtitle.format;
  127. var writer = TryGetWriter(outputFormat);
  128. // Return the original if we don't have any way of converting it
  129. if (writer == null)
  130. {
  131. return subtitle.stream;
  132. }
  133. // Return the original if the same format is being requested
  134. // Character encoding was already handled in GetSubtitleStream
  135. if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
  136. {
  137. return subtitle.stream;
  138. }
  139. using (var stream = subtitle.stream)
  140. {
  141. return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
  142. }
  143. }
  144. private async Task<(Stream stream, string format)> GetSubtitleStream(
  145. MediaSourceInfo mediaSource,
  146. MediaStream subtitleStream,
  147. CancellationToken cancellationToken)
  148. {
  149. string[] inputFiles;
  150. if (mediaSource.VideoType.HasValue
  151. && (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd))
  152. {
  153. var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSource.Id));
  154. inputFiles = mediaSourceItem.GetPlayableStreamFileNames(_mediaEncoder);
  155. }
  156. else
  157. {
  158. inputFiles = new[] { mediaSource.Path };
  159. }
  160. var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
  161. var stream = await GetSubtitleStream(fileInfo.Path, subtitleStream.Language, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
  162. return (stream, fileInfo.Format);
  163. }
  164. private async Task<Stream> GetSubtitleStream(string path, string language, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
  165. {
  166. if (requiresCharset)
  167. {
  168. var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false);
  169. var charset = _textEncoding.GetDetectedEncodingName(bytes, bytes.Length, language, true);
  170. _logger.LogDebug("charset {0} detected for {1}", charset ?? "null", path);
  171. if (!string.IsNullOrEmpty(charset))
  172. {
  173. using (var inputStream = new MemoryStream(bytes))
  174. using (var reader = new StreamReader(inputStream, _textEncoding.GetEncodingFromCharset(charset)))
  175. {
  176. var text = await reader.ReadToEndAsync().ConfigureAwait(false);
  177. bytes = Encoding.UTF8.GetBytes(text);
  178. return new MemoryStream(bytes);
  179. }
  180. }
  181. }
  182. return _fileSystem.OpenRead(path);
  183. }
  184. private async Task<SubtitleInfo> GetReadableFile(
  185. string mediaPath,
  186. string[] inputFiles,
  187. MediaProtocol protocol,
  188. MediaStream subtitleStream,
  189. CancellationToken cancellationToken)
  190. {
  191. if (!subtitleStream.IsExternal)
  192. {
  193. string outputFormat;
  194. string outputCodec;
  195. if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) ||
  196. string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) ||
  197. string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
  198. {
  199. // Extract
  200. outputCodec = "copy";
  201. outputFormat = subtitleStream.Codec;
  202. }
  203. else if (string.Equals(subtitleStream.Codec, "subrip", StringComparison.OrdinalIgnoreCase))
  204. {
  205. // Extract
  206. outputCodec = "copy";
  207. outputFormat = "srt";
  208. }
  209. else
  210. {
  211. // Extract
  212. outputCodec = "srt";
  213. outputFormat = "srt";
  214. }
  215. // Extract
  216. var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, "." + outputFormat);
  217. await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
  218. .ConfigureAwait(false);
  219. return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false);
  220. }
  221. var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
  222. .TrimStart('.');
  223. if (GetReader(currentFormat, false) == null)
  224. {
  225. // Convert
  226. var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, ".srt");
  227. await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, protocol, outputPath, cancellationToken).ConfigureAwait(false);
  228. return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
  229. }
  230. return new SubtitleInfo(subtitleStream.Path, protocol, currentFormat, true);
  231. }
  232. private struct SubtitleInfo
  233. {
  234. public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
  235. {
  236. Path = path;
  237. Protocol = protocol;
  238. Format = format;
  239. IsExternal = isExternal;
  240. }
  241. public string Path { get; set; }
  242. public MediaProtocol Protocol { get; set; }
  243. public string Format { get; set; }
  244. public bool IsExternal { get; set; }
  245. }
  246. private ISubtitleParser GetReader(string format, bool throwIfMissing)
  247. {
  248. if (string.IsNullOrEmpty(format))
  249. {
  250. throw new ArgumentNullException(nameof(format));
  251. }
  252. if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
  253. {
  254. return new SrtParser(_logger);
  255. }
  256. if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
  257. {
  258. return new SsaParser();
  259. }
  260. if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
  261. {
  262. return new AssParser();
  263. }
  264. if (throwIfMissing)
  265. {
  266. throw new ArgumentException("Unsupported format: " + format);
  267. }
  268. return null;
  269. }
  270. private ISubtitleWriter TryGetWriter(string format)
  271. {
  272. if (string.IsNullOrEmpty(format))
  273. {
  274. throw new ArgumentNullException(nameof(format));
  275. }
  276. if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
  277. {
  278. return new JsonWriter(_json);
  279. }
  280. if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
  281. {
  282. return new SrtWriter();
  283. }
  284. if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
  285. {
  286. return new VttWriter();
  287. }
  288. if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
  289. {
  290. return new TtmlWriter();
  291. }
  292. return null;
  293. }
  294. private ISubtitleWriter GetWriter(string format)
  295. {
  296. var writer = TryGetWriter(format);
  297. if (writer != null)
  298. {
  299. return writer;
  300. }
  301. throw new ArgumentException("Unsupported format: " + format);
  302. }
  303. /// <summary>
  304. /// The _semaphoreLocks
  305. /// </summary>
  306. private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
  307. new ConcurrentDictionary<string, SemaphoreSlim>();
  308. /// <summary>
  309. /// Gets the lock.
  310. /// </summary>
  311. /// <param name="filename">The filename.</param>
  312. /// <returns>System.Object.</returns>
  313. private SemaphoreSlim GetLock(string filename)
  314. {
  315. return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
  316. }
  317. /// <summary>
  318. /// Converts the text subtitle to SRT.
  319. /// </summary>
  320. /// <param name="inputPath">The input path.</param>
  321. /// <param name="inputProtocol">The input protocol.</param>
  322. /// <param name="outputPath">The output path.</param>
  323. /// <param name="cancellationToken">The cancellation token.</param>
  324. /// <returns>Task.</returns>
  325. private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
  326. {
  327. var semaphore = GetLock(outputPath);
  328. await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  329. try
  330. {
  331. if (!_fileSystem.FileExists(outputPath))
  332. {
  333. await ConvertTextSubtitleToSrtInternal(inputPath, language, inputProtocol, outputPath, cancellationToken).ConfigureAwait(false);
  334. }
  335. }
  336. finally
  337. {
  338. semaphore.Release();
  339. }
  340. }
  341. /// <summary>
  342. /// Converts the text subtitle to SRT internal.
  343. /// </summary>
  344. /// <param name="inputPath">The input path.</param>
  345. /// <param name="inputProtocol">The input protocol.</param>
  346. /// <param name="outputPath">The output path.</param>
  347. /// <param name="cancellationToken">The cancellation token.</param>
  348. /// <returns>Task.</returns>
  349. /// <exception cref="ArgumentNullException">
  350. /// inputPath
  351. /// or
  352. /// outputPath
  353. /// </exception>
  354. private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken)
  355. {
  356. if (string.IsNullOrEmpty(inputPath))
  357. {
  358. throw new ArgumentNullException(nameof(inputPath));
  359. }
  360. if (string.IsNullOrEmpty(outputPath))
  361. {
  362. throw new ArgumentNullException(nameof(outputPath));
  363. }
  364. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
  365. var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, inputProtocol, cancellationToken).ConfigureAwait(false);
  366. if (!string.IsNullOrEmpty(encodingParam))
  367. {
  368. encodingParam = " -sub_charenc " + encodingParam;
  369. }
  370. var process = _processFactory.Create(new ProcessOptions
  371. {
  372. CreateNoWindow = true,
  373. UseShellExecute = false,
  374. FileName = _mediaEncoder.EncoderPath,
  375. Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
  376. IsHidden = true,
  377. ErrorDialog = false
  378. });
  379. _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
  380. try
  381. {
  382. process.Start();
  383. }
  384. catch (Exception ex)
  385. {
  386. _logger.LogError(ex, "Error starting ffmpeg");
  387. throw;
  388. }
  389. var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
  390. if (!ranToCompletion)
  391. {
  392. try
  393. {
  394. _logger.LogInformation("Killing ffmpeg subtitle conversion process");
  395. process.Kill();
  396. }
  397. catch (Exception ex)
  398. {
  399. _logger.LogError(ex, "Error killing subtitle conversion process");
  400. }
  401. }
  402. var exitCode = ranToCompletion ? process.ExitCode : -1;
  403. process.Dispose();
  404. var failed = false;
  405. if (exitCode == -1)
  406. {
  407. failed = true;
  408. if (_fileSystem.FileExists(outputPath))
  409. {
  410. try
  411. {
  412. _logger.LogInformation("Deleting converted subtitle due to failure: ", outputPath);
  413. _fileSystem.DeleteFile(outputPath);
  414. }
  415. catch (IOException ex)
  416. {
  417. _logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
  418. }
  419. }
  420. }
  421. else if (!_fileSystem.FileExists(outputPath))
  422. {
  423. failed = true;
  424. }
  425. if (failed)
  426. {
  427. var msg = string.Format("ffmpeg subtitle conversion failed for {Path}", inputPath);
  428. _logger.LogError(msg);
  429. throw new Exception(msg);
  430. }
  431. await SetAssFont(outputPath).ConfigureAwait(false);
  432. _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath);
  433. }
  434. /// <summary>
  435. /// Extracts the text subtitle.
  436. /// </summary>
  437. /// <param name="inputFiles">The input files.</param>
  438. /// <param name="protocol">The protocol.</param>
  439. /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
  440. /// <param name="outputCodec">The output codec.</param>
  441. /// <param name="outputPath">The output path.</param>
  442. /// <param name="cancellationToken">The cancellation token.</param>
  443. /// <returns>Task.</returns>
  444. /// <exception cref="ArgumentException">Must use inputPath list overload</exception>
  445. private async Task ExtractTextSubtitle(
  446. string[] inputFiles,
  447. MediaProtocol protocol,
  448. int subtitleStreamIndex,
  449. string outputCodec,
  450. string outputPath,
  451. CancellationToken cancellationToken)
  452. {
  453. var semaphore = GetLock(outputPath);
  454. await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  455. try
  456. {
  457. if (!_fileSystem.FileExists(outputPath))
  458. {
  459. await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), subtitleStreamIndex, outputCodec, outputPath, cancellationToken).ConfigureAwait(false);
  460. }
  461. }
  462. finally
  463. {
  464. semaphore.Release();
  465. }
  466. }
  467. private async Task ExtractTextSubtitleInternal(
  468. string inputPath,
  469. int subtitleStreamIndex,
  470. string outputCodec,
  471. string outputPath,
  472. CancellationToken cancellationToken)
  473. {
  474. if (string.IsNullOrEmpty(inputPath))
  475. {
  476. throw new ArgumentNullException(nameof(inputPath));
  477. }
  478. if (string.IsNullOrEmpty(outputPath))
  479. {
  480. throw new ArgumentNullException(nameof(outputPath));
  481. }
  482. _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath));
  483. var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath,
  484. subtitleStreamIndex, outputCodec, outputPath);
  485. var process = _processFactory.Create(new ProcessOptions
  486. {
  487. CreateNoWindow = true,
  488. UseShellExecute = false,
  489. FileName = _mediaEncoder.EncoderPath,
  490. Arguments = processArgs,
  491. IsHidden = true,
  492. ErrorDialog = false
  493. });
  494. _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
  495. try
  496. {
  497. process.Start();
  498. }
  499. catch (Exception ex)
  500. {
  501. _logger.LogError(ex, "Error starting ffmpeg");
  502. throw;
  503. }
  504. var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
  505. if (!ranToCompletion)
  506. {
  507. try
  508. {
  509. _logger.LogWarning("Killing ffmpeg subtitle extraction process");
  510. process.Kill();
  511. }
  512. catch (Exception ex)
  513. {
  514. _logger.LogError(ex, "Error killing subtitle extraction process");
  515. }
  516. }
  517. var exitCode = ranToCompletion ? process.ExitCode : -1;
  518. process.Dispose();
  519. var failed = false;
  520. if (exitCode == -1)
  521. {
  522. failed = true;
  523. try
  524. {
  525. _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
  526. _fileSystem.DeleteFile(outputPath);
  527. }
  528. catch (FileNotFoundException)
  529. {
  530. }
  531. catch (IOException ex)
  532. {
  533. _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
  534. }
  535. }
  536. else if (!_fileSystem.FileExists(outputPath))
  537. {
  538. failed = true;
  539. }
  540. if (failed)
  541. {
  542. var msg = $"ffmpeg subtitle extraction failed for {inputPath} to {outputPath}";
  543. _logger.LogError(msg);
  544. throw new Exception(msg);
  545. }
  546. else
  547. {
  548. var msg = $"ffmpeg subtitle extraction completed for {inputPath} to {outputPath}";
  549. _logger.LogInformation(msg);
  550. }
  551. if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase))
  552. {
  553. await SetAssFont(outputPath).ConfigureAwait(false);
  554. }
  555. }
  556. /// <summary>
  557. /// Sets the ass font.
  558. /// </summary>
  559. /// <param name="file">The file.</param>
  560. /// <returns>Task.</returns>
  561. private async Task SetAssFont(string file)
  562. {
  563. _logger.LogInformation("Setting ass font within {File}", file);
  564. string text;
  565. Encoding encoding;
  566. using (var fileStream = _fileSystem.OpenRead(file))
  567. using (var reader = new StreamReader(fileStream, true))
  568. {
  569. encoding = reader.CurrentEncoding;
  570. text = await reader.ReadToEndAsync().ConfigureAwait(false);
  571. }
  572. var newText = text.Replace(",Arial,", ",Arial Unicode MS,");
  573. if (!string.Equals(text, newText))
  574. {
  575. using (var fileStream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
  576. using (var writer = new StreamWriter(fileStream, encoding))
  577. {
  578. writer.Write(newText);
  579. }
  580. }
  581. }
  582. private string GetSubtitleCachePath(string mediaPath, MediaProtocol protocol, int subtitleStreamIndex, string outputSubtitleExtension)
  583. {
  584. if (protocol == MediaProtocol.File)
  585. {
  586. var ticksParam = string.Empty;
  587. var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
  588. var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
  589. var prefix = filename.Substring(0, 1);
  590. return Path.Combine(SubtitleCachePath, prefix, filename);
  591. }
  592. else
  593. {
  594. var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
  595. var prefix = filename.Substring(0, 1);
  596. return Path.Combine(SubtitleCachePath, prefix, filename);
  597. }
  598. }
  599. public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken)
  600. {
  601. var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false);
  602. var charset = _textEncoding.GetDetectedEncodingName(bytes, bytes.Length, language, true);
  603. _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path);
  604. return charset;
  605. }
  606. private async Task<byte[]> GetBytes(string path, MediaProtocol protocol, CancellationToken cancellationToken)
  607. {
  608. if (protocol == MediaProtocol.Http)
  609. {
  610. var opts = new HttpRequestOptions()
  611. {
  612. Url = path,
  613. CancellationToken = cancellationToken
  614. };
  615. using (var file = await _httpClient.Get(opts).ConfigureAwait(false))
  616. using (var memoryStream = new MemoryStream())
  617. {
  618. await file.CopyToAsync(memoryStream).ConfigureAwait(false);
  619. memoryStream.Position = 0;
  620. return memoryStream.ToArray();
  621. }
  622. }
  623. if (protocol == MediaProtocol.File)
  624. {
  625. return _fileSystem.ReadAllBytes(path);
  626. }
  627. throw new ArgumentOutOfRangeException(nameof(protocol));
  628. }
  629. }
  630. }