SubtitleEncoder.cs 27 KB

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