2
0

SubtitleEncoder.cs 27 KB

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