SubtitleEncoder.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Extensions;
  3. using MediaBrowser.Common.IO;
  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. namespace MediaBrowser.MediaEncoding.Subtitles
  21. {
  22. public class SubtitleEncoder : ISubtitleEncoder
  23. {
  24. private readonly ILibraryManager _libraryManager;
  25. private readonly ILogger _logger;
  26. private readonly IApplicationPaths _appPaths;
  27. private readonly IFileSystem _fileSystem;
  28. private readonly IMediaEncoder _mediaEncoder;
  29. private readonly IJsonSerializer _json;
  30. public SubtitleEncoder(ILibraryManager libraryManager, ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IJsonSerializer json)
  31. {
  32. _libraryManager = libraryManager;
  33. _logger = logger;
  34. _appPaths = appPaths;
  35. _fileSystem = fileSystem;
  36. _mediaEncoder = mediaEncoder;
  37. _json = json;
  38. }
  39. private string SubtitleCachePath
  40. {
  41. get
  42. {
  43. return Path.Combine(_appPaths.CachePath, "subtitles");
  44. }
  45. }
  46. public async Task<Stream> ConvertSubtitles(Stream stream,
  47. string inputFormat,
  48. string outputFormat,
  49. long startTimeTicks,
  50. long? endTimeTicks,
  51. CancellationToken cancellationToken)
  52. {
  53. var ms = new MemoryStream();
  54. try
  55. {
  56. // Return the original without any conversions, if possible
  57. if (startTimeTicks == 0 &&
  58. !endTimeTicks.HasValue &&
  59. string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase))
  60. {
  61. await stream.CopyToAsync(ms, 81920, cancellationToken).ConfigureAwait(false);
  62. }
  63. else
  64. {
  65. var trackInfo = await GetTrackInfo(stream, inputFormat, cancellationToken).ConfigureAwait(false);
  66. FilterEvents(trackInfo, startTimeTicks, endTimeTicks, false);
  67. var writer = GetWriter(outputFormat);
  68. writer.Write(trackInfo, ms, cancellationToken);
  69. }
  70. ms.Position = 0;
  71. }
  72. catch
  73. {
  74. ms.Dispose();
  75. throw;
  76. }
  77. return ms;
  78. }
  79. private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long? endTimeTicks, bool preserveTimestamps)
  80. {
  81. // Drop subs that are earlier than what we're looking for
  82. track.TrackEvents = track.TrackEvents
  83. .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0)
  84. .ToList();
  85. if (endTimeTicks.HasValue)
  86. {
  87. var endTime = endTimeTicks.Value;
  88. track.TrackEvents = track.TrackEvents
  89. .TakeWhile(i => i.StartPositionTicks <= endTime)
  90. .ToList();
  91. }
  92. if (!preserveTimestamps)
  93. {
  94. foreach (var trackEvent in track.TrackEvents)
  95. {
  96. trackEvent.EndPositionTicks -= startPositionTicks;
  97. trackEvent.StartPositionTicks -= startPositionTicks;
  98. }
  99. }
  100. }
  101. public async Task<Stream> GetSubtitles(string itemId,
  102. string mediaSourceId,
  103. int subtitleStreamIndex,
  104. string outputFormat,
  105. long startTimeTicks,
  106. long? endTimeTicks,
  107. CancellationToken cancellationToken)
  108. {
  109. var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken)
  110. .ConfigureAwait(false);
  111. using (var stream = subtitle.Item1)
  112. {
  113. var inputFormat = subtitle.Item2;
  114. return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, cancellationToken).ConfigureAwait(false);
  115. }
  116. }
  117. private async Task<Tuple<Stream, string>> GetSubtitleStream(string itemId,
  118. string mediaSourceId,
  119. int subtitleStreamIndex,
  120. CancellationToken cancellationToken)
  121. {
  122. var item = (Video)_libraryManager.GetItemById(new Guid(itemId));
  123. var mediaSource = item.GetMediaSources(false)
  124. .First(i => string.Equals(i.Id, mediaSourceId));
  125. var subtitleStream = mediaSource.MediaStreams
  126. .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
  127. var inputFiles = new[] { mediaSource.Path };
  128. if (mediaSource.VideoType.HasValue)
  129. {
  130. if (mediaSource.VideoType.Value == VideoType.BluRay ||
  131. mediaSource.VideoType.Value == VideoType.Dvd)
  132. {
  133. var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSourceId));
  134. inputFiles = mediaSourceItem.GetPlayableStreamFiles().ToArray();
  135. }
  136. }
  137. var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
  138. var stream = await GetSubtitleStream(fileInfo.Item1, subtitleStream.Language).ConfigureAwait(false);
  139. return new Tuple<Stream, string>(stream, fileInfo.Item2);
  140. }
  141. private async Task<Stream> GetSubtitleStream(string path, string language)
  142. {
  143. if (!string.IsNullOrEmpty(language))
  144. {
  145. var charset = GetSubtitleFileCharacterSet(path, language);
  146. if (!string.IsNullOrEmpty(charset))
  147. {
  148. using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
  149. {
  150. using (var reader = new StreamReader(fs, Encoding.GetEncoding(charset)))
  151. {
  152. var text = await reader.ReadToEndAsync().ConfigureAwait(false);
  153. var bytes = Encoding.UTF8.GetBytes(text);
  154. return new MemoryStream(bytes);
  155. }
  156. }
  157. }
  158. }
  159. return File.OpenRead(path);
  160. }
  161. private async Task<Tuple<string, string>> GetReadableFile(string mediaPath,
  162. string[] inputFiles,
  163. MediaProtocol protocol,
  164. MediaStream subtitleStream,
  165. CancellationToken cancellationToken)
  166. {
  167. if (!subtitleStream.IsExternal)
  168. {
  169. // Extract
  170. var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, ".ass");
  171. await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, false, outputPath, cancellationToken)
  172. .ConfigureAwait(false);
  173. return new Tuple<string, string>(outputPath, "ass");
  174. }
  175. var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
  176. .TrimStart('.');
  177. if (GetReader(currentFormat, false) == null)
  178. {
  179. // Convert
  180. var outputPath = GetSubtitleCachePath(mediaPath, subtitleStream.Index, ".ass");
  181. await ConvertTextSubtitleToAss(subtitleStream.Path, outputPath, subtitleStream.Language, cancellationToken)
  182. .ConfigureAwait(false);
  183. return new Tuple<string, string>(outputPath, "ass");
  184. }
  185. return new Tuple<string, string>(subtitleStream.Path, currentFormat);
  186. }
  187. private async Task<SubtitleTrackInfo> GetTrackInfo(Stream stream,
  188. string inputFormat,
  189. CancellationToken cancellationToken)
  190. {
  191. var reader = GetReader(inputFormat, true);
  192. return reader.Parse(stream, cancellationToken);
  193. }
  194. private ISubtitleParser GetReader(string format, bool throwIfMissing)
  195. {
  196. if (string.IsNullOrEmpty(format))
  197. {
  198. throw new ArgumentNullException("format");
  199. }
  200. if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
  201. {
  202. return new SrtParser();
  203. }
  204. if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
  205. {
  206. return new SsaParser();
  207. }
  208. if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
  209. {
  210. return new AssParser();
  211. }
  212. if (throwIfMissing)
  213. {
  214. throw new ArgumentException("Unsupported format: " + format);
  215. }
  216. return null;
  217. }
  218. private ISubtitleWriter GetWriter(string format)
  219. {
  220. if (string.IsNullOrEmpty(format))
  221. {
  222. throw new ArgumentNullException("format");
  223. }
  224. if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
  225. {
  226. return new JsonWriter(_json);
  227. }
  228. if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
  229. {
  230. return new SrtWriter();
  231. }
  232. if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
  233. {
  234. return new VttWriter();
  235. }
  236. if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
  237. {
  238. return new TtmlWriter();
  239. }
  240. throw new ArgumentException("Unsupported format: " + format);
  241. }
  242. /// <summary>
  243. /// The _semaphoreLocks
  244. /// </summary>
  245. private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
  246. new ConcurrentDictionary<string, SemaphoreSlim>();
  247. /// <summary>
  248. /// Gets the lock.
  249. /// </summary>
  250. /// <param name="filename">The filename.</param>
  251. /// <returns>System.Object.</returns>
  252. private SemaphoreSlim GetLock(string filename)
  253. {
  254. return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
  255. }
  256. /// <summary>
  257. /// Converts the text subtitle to ass.
  258. /// </summary>
  259. /// <param name="inputPath">The input path.</param>
  260. /// <param name="outputPath">The output path.</param>
  261. /// <param name="language">The language.</param>
  262. /// <param name="cancellationToken">The cancellation token.</param>
  263. /// <returns>Task.</returns>
  264. public async Task ConvertTextSubtitleToAss(string inputPath, string outputPath, string language,
  265. CancellationToken cancellationToken)
  266. {
  267. var semaphore = GetLock(outputPath);
  268. await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  269. try
  270. {
  271. if (!File.Exists(outputPath))
  272. {
  273. await ConvertTextSubtitleToAssInternal(inputPath, outputPath, language).ConfigureAwait(false);
  274. }
  275. }
  276. finally
  277. {
  278. semaphore.Release();
  279. }
  280. }
  281. /// <summary>
  282. /// Converts the text subtitle to ass.
  283. /// </summary>
  284. /// <param name="inputPath">The input path.</param>
  285. /// <param name="outputPath">The output path.</param>
  286. /// <param name="language">The language.</param>
  287. /// <returns>Task.</returns>
  288. /// <exception cref="System.ArgumentNullException">inputPath
  289. /// or
  290. /// outputPath</exception>
  291. /// <exception cref="System.ApplicationException"></exception>
  292. private async Task ConvertTextSubtitleToAssInternal(string inputPath, string outputPath, string language)
  293. {
  294. if (string.IsNullOrEmpty(inputPath))
  295. {
  296. throw new ArgumentNullException("inputPath");
  297. }
  298. if (string.IsNullOrEmpty(outputPath))
  299. {
  300. throw new ArgumentNullException("outputPath");
  301. }
  302. Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
  303. var encodingParam = string.IsNullOrEmpty(language)
  304. ? string.Empty
  305. : GetSubtitleFileCharacterSet(inputPath, language);
  306. if (!string.IsNullOrEmpty(encodingParam))
  307. {
  308. encodingParam = " -sub_charenc " + encodingParam;
  309. }
  310. var process = new Process
  311. {
  312. StartInfo = new ProcessStartInfo
  313. {
  314. RedirectStandardOutput = false,
  315. RedirectStandardError = true,
  316. RedirectStandardInput = true,
  317. CreateNoWindow = true,
  318. UseShellExecute = false,
  319. FileName = _mediaEncoder.EncoderPath,
  320. Arguments = string.Format("{0} -i \"{1}\" -c:s ass \"{2}\"", encodingParam, inputPath, outputPath),
  321. WindowStyle = ProcessWindowStyle.Hidden,
  322. ErrorDialog = false
  323. }
  324. };
  325. _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
  326. var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-convert-" + Guid.NewGuid() + ".txt");
  327. Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
  328. var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read,
  329. true);
  330. try
  331. {
  332. process.Start();
  333. }
  334. catch (Exception ex)
  335. {
  336. logFileStream.Dispose();
  337. _logger.ErrorException("Error starting ffmpeg", ex);
  338. throw;
  339. }
  340. var logTask = process.StandardError.BaseStream.CopyToAsync(logFileStream);
  341. var ranToCompletion = process.WaitForExit(60000);
  342. if (!ranToCompletion)
  343. {
  344. try
  345. {
  346. _logger.Info("Killing ffmpeg subtitle conversion process");
  347. process.StandardInput.WriteLine("q");
  348. process.WaitForExit(1000);
  349. await logTask.ConfigureAwait(false);
  350. }
  351. catch (Exception ex)
  352. {
  353. _logger.ErrorException("Error killing subtitle conversion process", ex);
  354. }
  355. finally
  356. {
  357. logFileStream.Dispose();
  358. }
  359. }
  360. var exitCode = ranToCompletion ? process.ExitCode : -1;
  361. process.Dispose();
  362. var failed = false;
  363. if (exitCode == -1)
  364. {
  365. failed = true;
  366. if (File.Exists(outputPath))
  367. {
  368. try
  369. {
  370. _logger.Info("Deleting converted subtitle due to failure: ", outputPath);
  371. File.Delete(outputPath);
  372. }
  373. catch (IOException ex)
  374. {
  375. _logger.ErrorException("Error deleting converted subtitle {0}", ex, outputPath);
  376. }
  377. }
  378. }
  379. else if (!File.Exists(outputPath))
  380. {
  381. failed = true;
  382. }
  383. if (failed)
  384. {
  385. var msg = string.Format("ffmpeg subtitle converted failed for {0}", inputPath);
  386. _logger.Error(msg);
  387. throw new ApplicationException(msg);
  388. }
  389. await SetAssFont(outputPath).ConfigureAwait(false);
  390. }
  391. /// <summary>
  392. /// Extracts the text subtitle.
  393. /// </summary>
  394. /// <param name="inputFiles">The input files.</param>
  395. /// <param name="protocol">The protocol.</param>
  396. /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
  397. /// <param name="copySubtitleStream">if set to true, copy stream instead of converting.</param>
  398. /// <param name="outputPath">The output path.</param>
  399. /// <param name="cancellationToken">The cancellation token.</param>
  400. /// <returns>Task.</returns>
  401. /// <exception cref="System.ArgumentException">Must use inputPath list overload</exception>
  402. private async Task ExtractTextSubtitle(string[] inputFiles, MediaProtocol protocol, int subtitleStreamIndex,
  403. bool copySubtitleStream, string outputPath, CancellationToken cancellationToken)
  404. {
  405. var semaphore = GetLock(outputPath);
  406. await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  407. try
  408. {
  409. if (!File.Exists(outputPath))
  410. {
  411. await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), subtitleStreamIndex,
  412. copySubtitleStream, outputPath, cancellationToken).ConfigureAwait(false);
  413. }
  414. }
  415. finally
  416. {
  417. semaphore.Release();
  418. }
  419. }
  420. /// <summary>
  421. /// Extracts the text subtitle.
  422. /// </summary>
  423. /// <param name="inputPath">The input path.</param>
  424. /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
  425. /// <param name="copySubtitleStream">if set to true, copy stream instead of converting.</param>
  426. /// <param name="outputPath">The output path.</param>
  427. /// <param name="cancellationToken">The cancellation token.</param>
  428. /// <returns>Task.</returns>
  429. /// <exception cref="System.ArgumentNullException">inputPath
  430. /// or
  431. /// outputPath
  432. /// or
  433. /// cancellationToken</exception>
  434. /// <exception cref="System.ApplicationException"></exception>
  435. private async Task ExtractTextSubtitleInternal(string inputPath, int subtitleStreamIndex,
  436. bool copySubtitleStream, string outputPath, CancellationToken cancellationToken)
  437. {
  438. if (string.IsNullOrEmpty(inputPath))
  439. {
  440. throw new ArgumentNullException("inputPath");
  441. }
  442. if (string.IsNullOrEmpty(outputPath))
  443. {
  444. throw new ArgumentNullException("outputPath");
  445. }
  446. Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
  447. var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s ass \"{2}\"", inputPath,
  448. subtitleStreamIndex, outputPath);
  449. if (copySubtitleStream)
  450. {
  451. processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s copy \"{2}\"", inputPath,
  452. subtitleStreamIndex, outputPath);
  453. }
  454. var process = new Process
  455. {
  456. StartInfo = new ProcessStartInfo
  457. {
  458. CreateNoWindow = true,
  459. UseShellExecute = false,
  460. RedirectStandardOutput = false,
  461. RedirectStandardError = true,
  462. RedirectStandardInput = true,
  463. FileName = _mediaEncoder.EncoderPath,
  464. Arguments = processArgs,
  465. WindowStyle = ProcessWindowStyle.Hidden,
  466. ErrorDialog = false
  467. }
  468. };
  469. _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
  470. var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-extract-" + Guid.NewGuid() + ".txt");
  471. Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
  472. var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read,
  473. true);
  474. try
  475. {
  476. process.Start();
  477. }
  478. catch (Exception ex)
  479. {
  480. logFileStream.Dispose();
  481. _logger.ErrorException("Error starting ffmpeg", ex);
  482. throw;
  483. }
  484. process.StandardError.BaseStream.CopyToAsync(logFileStream);
  485. var ranToCompletion = process.WaitForExit(60000);
  486. if (!ranToCompletion)
  487. {
  488. try
  489. {
  490. _logger.Info("Killing ffmpeg subtitle extraction process");
  491. process.StandardInput.WriteLine("q");
  492. process.WaitForExit(1000);
  493. }
  494. catch (Exception ex)
  495. {
  496. _logger.ErrorException("Error killing subtitle extraction process", ex);
  497. }
  498. finally
  499. {
  500. logFileStream.Dispose();
  501. }
  502. }
  503. var exitCode = ranToCompletion ? process.ExitCode : -1;
  504. process.Dispose();
  505. var failed = false;
  506. if (exitCode == -1)
  507. {
  508. failed = true;
  509. if (File.Exists(outputPath))
  510. {
  511. try
  512. {
  513. _logger.Info("Deleting extracted subtitle due to failure: ", outputPath);
  514. File.Delete(outputPath);
  515. }
  516. catch (IOException ex)
  517. {
  518. _logger.ErrorException("Error deleting extracted subtitle {0}", ex, outputPath);
  519. }
  520. }
  521. }
  522. else if (!File.Exists(outputPath))
  523. {
  524. failed = true;
  525. }
  526. if (failed)
  527. {
  528. var msg = string.Format("ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath);
  529. _logger.Error(msg);
  530. throw new ApplicationException(msg);
  531. }
  532. else
  533. {
  534. var msg = string.Format("ffmpeg subtitle extraction completed for {0} to {1}", inputPath, outputPath);
  535. _logger.Info(msg);
  536. }
  537. await SetAssFont(outputPath).ConfigureAwait(false);
  538. }
  539. /// <summary>
  540. /// Sets the ass font.
  541. /// </summary>
  542. /// <param name="file">The file.</param>
  543. /// <returns>Task.</returns>
  544. private async Task SetAssFont(string file)
  545. {
  546. _logger.Info("Setting ass font within {0}", file);
  547. string text;
  548. Encoding encoding;
  549. using (var reader = new StreamReader(file, true))
  550. {
  551. encoding = reader.CurrentEncoding;
  552. text = await reader.ReadToEndAsync().ConfigureAwait(false);
  553. }
  554. var newText = text.Replace(",Arial,", ",Arial Unicode MS,");
  555. if (!string.Equals(text, newText))
  556. {
  557. using (var writer = new StreamWriter(file, false, encoding))
  558. {
  559. writer.Write(newText);
  560. }
  561. }
  562. }
  563. private string GetSubtitleCachePath(string mediaPath, int subtitleStreamIndex, string outputSubtitleExtension)
  564. {
  565. var ticksParam = string.Empty;
  566. var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
  567. var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
  568. var prefix = filename.Substring(0, 1);
  569. return Path.Combine(SubtitleCachePath, prefix, filename);
  570. }
  571. /// <summary>
  572. /// Gets the subtitle language encoding param.
  573. /// </summary>
  574. /// <param name="path">The path.</param>
  575. /// <param name="language">The language.</param>
  576. /// <returns>System.String.</returns>
  577. public string GetSubtitleFileCharacterSet(string path, string language)
  578. {
  579. if (GetFileEncoding(path).Equals(Encoding.UTF8))
  580. {
  581. return string.Empty;
  582. }
  583. switch (language.ToLower())
  584. {
  585. case "pol":
  586. case "cze":
  587. case "ces":
  588. case "slo":
  589. case "slk":
  590. case "hun":
  591. case "slv":
  592. case "srp":
  593. case "hrv":
  594. case "rum":
  595. case "ron":
  596. case "rup":
  597. case "alb":
  598. case "sqi":
  599. return "windows-1250";
  600. case "ara":
  601. return "windows-1256";
  602. case "heb":
  603. return "windows-1255";
  604. case "grc":
  605. case "gre":
  606. return "windows-1253";
  607. case "crh":
  608. case "ota":
  609. case "tur":
  610. return "windows-1254";
  611. case "rus":
  612. return "windows-1251";
  613. case "vie":
  614. return "windows-1258";
  615. case "kor":
  616. return "cp949";
  617. default:
  618. return "windows-1252";
  619. }
  620. }
  621. private static Encoding GetFileEncoding(string srcFile)
  622. {
  623. // *** Detect byte order mark if any - otherwise assume default
  624. var buffer = new byte[5];
  625. using (var file = new FileStream(srcFile, FileMode.Open))
  626. {
  627. file.Read(buffer, 0, 5);
  628. }
  629. if (buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf)
  630. return Encoding.UTF8;
  631. if (buffer[0] == 0xfe && buffer[1] == 0xff)
  632. return Encoding.Unicode;
  633. if (buffer[0] == 0 && buffer[1] == 0 && buffer[2] == 0xfe && buffer[3] == 0xff)
  634. return Encoding.UTF32;
  635. if (buffer[0] == 0x2b && buffer[1] == 0x2f && buffer[2] == 0x76)
  636. return Encoding.UTF7;
  637. // It's ok - anything aside from utf is ok since that's what we're looking for
  638. return Encoding.Default;
  639. }
  640. }
  641. }