SubtitleEncoder.cs 26 KB

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