2
0

ApiEntryPoint.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using MediaBrowser.Api.Playback;
  9. using MediaBrowser.Common.Configuration;
  10. using MediaBrowser.Controller.Configuration;
  11. using MediaBrowser.Controller.Library;
  12. using MediaBrowser.Controller.MediaEncoding;
  13. using MediaBrowser.Controller.Net;
  14. using MediaBrowser.Controller.Plugins;
  15. using MediaBrowser.Controller.Session;
  16. using MediaBrowser.Model.Configuration;
  17. using MediaBrowser.Model.Diagnostics;
  18. using MediaBrowser.Model.Dto;
  19. using MediaBrowser.Model.IO;
  20. using MediaBrowser.Model.Session;
  21. using Microsoft.Extensions.Logging;
  22. namespace MediaBrowser.Api
  23. {
  24. /// <summary>
  25. /// Class ServerEntryPoint
  26. /// </summary>
  27. public class ApiEntryPoint : IServerEntryPoint
  28. {
  29. /// <summary>
  30. /// The instance
  31. /// </summary>
  32. public static ApiEntryPoint Instance;
  33. /// <summary>
  34. /// Gets or sets the logger.
  35. /// </summary>
  36. /// <value>The logger.</value>
  37. internal ILogger Logger { get; private set; }
  38. internal IHttpResultFactory ResultFactory { get; private set; }
  39. /// <summary>
  40. /// Gets the configuration manager.
  41. /// </summary>
  42. internal IServerConfigurationManager ConfigurationManager { get; }
  43. private readonly ISessionManager _sessionManager;
  44. private readonly IFileSystem _fileSystem;
  45. private readonly IMediaSourceManager _mediaSourceManager;
  46. /// <summary>
  47. /// The active transcoding jobs
  48. /// </summary>
  49. private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
  50. private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
  51. new Dictionary<string, SemaphoreSlim>();
  52. private bool _disposed = false;
  53. /// <summary>
  54. /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
  55. /// </summary>
  56. /// <param name="logger">The logger.</param>
  57. /// <param name="sessionManager">The session manager.</param>
  58. /// <param name="config">The configuration.</param>
  59. /// <param name="fileSystem">The file system.</param>
  60. /// <param name="mediaSourceManager">The media source manager.</param>
  61. public ApiEntryPoint(
  62. ILogger logger,
  63. ISessionManager sessionManager,
  64. IServerConfigurationManager config,
  65. IFileSystem fileSystem,
  66. IMediaSourceManager mediaSourceManager,
  67. IHttpResultFactory resultFactory)
  68. {
  69. Logger = logger;
  70. _sessionManager = sessionManager;
  71. ConfigurationManager = config;
  72. _fileSystem = fileSystem;
  73. _mediaSourceManager = mediaSourceManager;
  74. ResultFactory = resultFactory;
  75. _sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress;
  76. _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
  77. Instance = this;
  78. }
  79. public static string[] Split(string value, char separator, bool removeEmpty)
  80. {
  81. if (string.IsNullOrWhiteSpace(value))
  82. {
  83. return Array.Empty<string>();
  84. }
  85. if (removeEmpty)
  86. {
  87. return value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries);
  88. }
  89. return value.Split(separator);
  90. }
  91. public SemaphoreSlim GetTranscodingLock(string outputPath)
  92. {
  93. lock (_transcodingLocks)
  94. {
  95. if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
  96. {
  97. result = new SemaphoreSlim(1, 1);
  98. _transcodingLocks[outputPath] = result;
  99. }
  100. return result;
  101. }
  102. }
  103. private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
  104. {
  105. if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
  106. {
  107. PingTranscodingJob(e.PlaySessionId, e.IsPaused);
  108. }
  109. }
  110. void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
  111. {
  112. if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
  113. {
  114. PingTranscodingJob(e.PlaySessionId, e.IsPaused);
  115. }
  116. }
  117. /// <summary>
  118. /// Runs this instance.
  119. /// </summary>
  120. public Task RunAsync()
  121. {
  122. try
  123. {
  124. DeleteEncodedMediaCache();
  125. }
  126. catch (FileNotFoundException)
  127. {
  128. // Don't clutter the log
  129. }
  130. catch (IOException)
  131. {
  132. // Don't clutter the log
  133. }
  134. catch (Exception ex)
  135. {
  136. Logger.LogError(ex, "Error deleting encoded media cache");
  137. }
  138. return Task.CompletedTask;
  139. }
  140. public EncodingOptions GetEncodingOptions()
  141. {
  142. return ConfigurationManagerExtensions.GetConfiguration<EncodingOptions>(ConfigurationManager, "encoding");
  143. }
  144. /// <summary>
  145. /// Deletes the encoded media cache.
  146. /// </summary>
  147. private void DeleteEncodedMediaCache()
  148. {
  149. var path = ConfigurationManager.ApplicationPaths.GetTranscodingTempPath();
  150. if (!Directory.Exists(path))
  151. {
  152. return;
  153. }
  154. foreach (var file in _fileSystem.GetFilePaths(path, true))
  155. {
  156. _fileSystem.DeleteFile(file);
  157. }
  158. }
  159. /// <summary>
  160. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  161. /// </summary>
  162. public void Dispose()
  163. {
  164. Dispose(true);
  165. GC.SuppressFinalize(this);
  166. }
  167. /// <summary>
  168. /// Releases unmanaged and - optionally - managed resources.
  169. /// </summary>
  170. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  171. protected virtual void Dispose(bool dispose)
  172. {
  173. if (_disposed)
  174. {
  175. return;
  176. }
  177. if (dispose)
  178. {
  179. // TODO: dispose
  180. }
  181. var jobs = _activeTranscodingJobs.ToList();
  182. var jobCount = jobs.Count;
  183. IEnumerable<Task> GetKillJobs()
  184. {
  185. foreach (var job in jobs)
  186. {
  187. yield return KillTranscodingJob(job, false, path => true);
  188. }
  189. }
  190. // Wait for all processes to be killed
  191. if (jobCount > 0)
  192. {
  193. Task.WaitAll(GetKillJobs().ToArray());
  194. }
  195. _activeTranscodingJobs.Clear();
  196. _transcodingLocks.Clear();
  197. _sessionManager.PlaybackProgress -= _sessionManager_PlaybackProgress;
  198. _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
  199. _disposed = true;
  200. }
  201. /// <summary>
  202. /// Called when [transcode beginning].
  203. /// </summary>
  204. /// <param name="path">The path.</param>
  205. /// <param name="playSessionId">The play session identifier.</param>
  206. /// <param name="liveStreamId">The live stream identifier.</param>
  207. /// <param name="transcodingJobId">The transcoding job identifier.</param>
  208. /// <param name="type">The type.</param>
  209. /// <param name="process">The process.</param>
  210. /// <param name="deviceId">The device id.</param>
  211. /// <param name="state">The state.</param>
  212. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  213. /// <returns>TranscodingJob.</returns>
  214. public TranscodingJob OnTranscodeBeginning(
  215. string path,
  216. string playSessionId,
  217. string liveStreamId,
  218. string transcodingJobId,
  219. TranscodingJobType type,
  220. Process process,
  221. string deviceId,
  222. StreamState state,
  223. CancellationTokenSource cancellationTokenSource)
  224. {
  225. lock (_activeTranscodingJobs)
  226. {
  227. var job = new TranscodingJob(Logger)
  228. {
  229. Type = type,
  230. Path = path,
  231. Process = process,
  232. ActiveRequestCount = 1,
  233. DeviceId = deviceId,
  234. CancellationTokenSource = cancellationTokenSource,
  235. Id = transcodingJobId,
  236. PlaySessionId = playSessionId,
  237. LiveStreamId = liveStreamId,
  238. MediaSource = state.MediaSource
  239. };
  240. _activeTranscodingJobs.Add(job);
  241. ReportTranscodingProgress(job, state, null, null, null, null, null);
  242. return job;
  243. }
  244. }
  245. public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
  246. {
  247. var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
  248. if (job != null)
  249. {
  250. job.Framerate = framerate;
  251. job.CompletionPercentage = percentComplete;
  252. job.TranscodingPositionTicks = ticks;
  253. job.BytesTranscoded = bytesTranscoded;
  254. job.BitRate = bitRate;
  255. }
  256. var deviceId = state.Request.DeviceId;
  257. if (!string.IsNullOrWhiteSpace(deviceId))
  258. {
  259. var audioCodec = state.ActualOutputAudioCodec;
  260. var videoCodec = state.ActualOutputVideoCodec;
  261. _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
  262. {
  263. Bitrate = bitRate ?? state.TotalOutputBitrate,
  264. AudioCodec = audioCodec,
  265. VideoCodec = videoCodec,
  266. Container = state.OutputContainer,
  267. Framerate = framerate,
  268. CompletionPercentage = percentComplete,
  269. Width = state.OutputWidth,
  270. Height = state.OutputHeight,
  271. AudioChannels = state.OutputAudioChannels,
  272. IsAudioDirect = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase),
  273. IsVideoDirect = string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase),
  274. TranscodeReasons = state.TranscodeReasons
  275. });
  276. }
  277. }
  278. /// <summary>
  279. /// <summary>
  280. /// The progressive
  281. /// </summary>
  282. /// Called when [transcode failed to start].
  283. /// </summary>
  284. /// <param name="path">The path.</param>
  285. /// <param name="type">The type.</param>
  286. /// <param name="state">The state.</param>
  287. public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
  288. {
  289. lock (_activeTranscodingJobs)
  290. {
  291. var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  292. if (job != null)
  293. {
  294. _activeTranscodingJobs.Remove(job);
  295. }
  296. }
  297. lock (_transcodingLocks)
  298. {
  299. _transcodingLocks.Remove(path);
  300. }
  301. if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
  302. {
  303. _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
  304. }
  305. }
  306. /// <summary>
  307. /// Determines whether [has active transcoding job] [the specified path].
  308. /// </summary>
  309. /// <param name="path">The path.</param>
  310. /// <param name="type">The type.</param>
  311. /// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
  312. public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
  313. {
  314. return GetTranscodingJob(path, type) != null;
  315. }
  316. public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
  317. {
  318. lock (_activeTranscodingJobs)
  319. {
  320. return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  321. }
  322. }
  323. public TranscodingJob GetTranscodingJob(string playSessionId)
  324. {
  325. lock (_activeTranscodingJobs)
  326. {
  327. return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
  328. }
  329. }
  330. /// <summary>
  331. /// Called when [transcode begin request].
  332. /// </summary>
  333. /// <param name="path">The path.</param>
  334. /// <param name="type">The type.</param>
  335. public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type)
  336. {
  337. lock (_activeTranscodingJobs)
  338. {
  339. var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  340. if (job == null)
  341. {
  342. return null;
  343. }
  344. OnTranscodeBeginRequest(job);
  345. return job;
  346. }
  347. }
  348. public void OnTranscodeBeginRequest(TranscodingJob job)
  349. {
  350. job.ActiveRequestCount++;
  351. if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
  352. {
  353. job.StopKillTimer();
  354. }
  355. }
  356. public void OnTranscodeEndRequest(TranscodingJob job)
  357. {
  358. job.ActiveRequestCount--;
  359. Logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
  360. if (job.ActiveRequestCount <= 0)
  361. {
  362. PingTimer(job, false);
  363. }
  364. }
  365. internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
  366. {
  367. if (string.IsNullOrEmpty(playSessionId))
  368. {
  369. throw new ArgumentNullException(nameof(playSessionId));
  370. }
  371. Logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
  372. List<TranscodingJob> jobs;
  373. lock (_activeTranscodingJobs)
  374. {
  375. // This is really only needed for HLS.
  376. // Progressive streams can stop on their own reliably
  377. jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
  378. }
  379. foreach (var job in jobs)
  380. {
  381. if (isUserPaused.HasValue)
  382. {
  383. Logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
  384. job.IsUserPaused = isUserPaused.Value;
  385. }
  386. PingTimer(job, true);
  387. }
  388. }
  389. private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
  390. {
  391. if (job.HasExited)
  392. {
  393. job.StopKillTimer();
  394. return;
  395. }
  396. var timerDuration = 10000;
  397. if (job.Type != TranscodingJobType.Progressive)
  398. {
  399. timerDuration = 60000;
  400. }
  401. job.PingTimeout = timerDuration;
  402. job.LastPingDate = DateTime.UtcNow;
  403. // Don't start the timer for playback checkins with progressive streaming
  404. if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
  405. {
  406. job.StartKillTimer(OnTranscodeKillTimerStopped);
  407. }
  408. else
  409. {
  410. job.ChangeKillTimerIfStarted();
  411. }
  412. }
  413. /// <summary>
  414. /// Called when [transcode kill timer stopped].
  415. /// </summary>
  416. /// <param name="state">The state.</param>
  417. private async void OnTranscodeKillTimerStopped(object state)
  418. {
  419. var job = (TranscodingJob)state;
  420. if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
  421. {
  422. var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
  423. if (timeSinceLastPing < job.PingTimeout)
  424. {
  425. job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
  426. return;
  427. }
  428. }
  429. Logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
  430. await KillTranscodingJob(job, true, path => true);
  431. }
  432. /// <summary>
  433. /// Kills the single transcoding job.
  434. /// </summary>
  435. /// <param name="deviceId">The device id.</param>
  436. /// <param name="playSessionId">The play session identifier.</param>
  437. /// <param name="deleteFiles">The delete files.</param>
  438. /// <returns>Task.</returns>
  439. internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
  440. {
  441. return KillTranscodingJobs(j =>
  442. {
  443. if (!string.IsNullOrWhiteSpace(playSessionId))
  444. {
  445. return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
  446. }
  447. return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase);
  448. }, deleteFiles);
  449. }
  450. /// <summary>
  451. /// Kills the transcoding jobs.
  452. /// </summary>
  453. /// <param name="killJob">The kill job.</param>
  454. /// <param name="deleteFiles">The delete files.</param>
  455. /// <returns>Task.</returns>
  456. private Task KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
  457. {
  458. var jobs = new List<TranscodingJob>();
  459. lock (_activeTranscodingJobs)
  460. {
  461. // This is really only needed for HLS.
  462. // Progressive streams can stop on their own reliably
  463. jobs.AddRange(_activeTranscodingJobs.Where(killJob));
  464. }
  465. if (jobs.Count == 0)
  466. {
  467. return Task.CompletedTask;
  468. }
  469. IEnumerable<Task> GetKillJobs()
  470. {
  471. foreach (var job in jobs)
  472. {
  473. yield return KillTranscodingJob(job, false, deleteFiles);
  474. }
  475. }
  476. return Task.WhenAll(GetKillJobs());
  477. }
  478. /// <summary>
  479. /// Kills the transcoding job.
  480. /// </summary>
  481. /// <param name="job">The job.</param>
  482. /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
  483. /// <param name="delete">The delete.</param>
  484. private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
  485. {
  486. job.DisposeKillTimer();
  487. Logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
  488. lock (_activeTranscodingJobs)
  489. {
  490. _activeTranscodingJobs.Remove(job);
  491. if (!job.CancellationTokenSource.IsCancellationRequested)
  492. {
  493. job.CancellationTokenSource.Cancel();
  494. }
  495. }
  496. lock (_transcodingLocks)
  497. {
  498. _transcodingLocks.Remove(job.Path);
  499. }
  500. lock (job.ProcessLock)
  501. {
  502. if (job.TranscodingThrottler != null)
  503. {
  504. job.TranscodingThrottler.Stop().GetAwaiter().GetResult();
  505. }
  506. var process = job.Process;
  507. var hasExited = job.HasExited;
  508. if (!hasExited)
  509. {
  510. try
  511. {
  512. Logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
  513. process.StandardInput.WriteLine("q");
  514. // Need to wait because killing is asynchronous
  515. if (!process.WaitForExit(5000))
  516. {
  517. Logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
  518. process.Kill();
  519. }
  520. }
  521. catch (InvalidOperationException)
  522. {
  523. }
  524. }
  525. }
  526. if (delete(job.Path))
  527. {
  528. await DeletePartialStreamFiles(job.Path, job.Type, 0, 1500).ConfigureAwait(false);
  529. }
  530. if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
  531. {
  532. try
  533. {
  534. await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
  535. }
  536. catch (Exception ex)
  537. {
  538. Logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
  539. }
  540. }
  541. }
  542. private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
  543. {
  544. if (retryCount >= 10)
  545. {
  546. return;
  547. }
  548. Logger.LogInformation("Deleting partial stream file(s) {Path}", path);
  549. await Task.Delay(delayMs).ConfigureAwait(false);
  550. try
  551. {
  552. if (jobType == TranscodingJobType.Progressive)
  553. {
  554. DeleteProgressivePartialStreamFiles(path);
  555. }
  556. else
  557. {
  558. DeleteHlsPartialStreamFiles(path);
  559. }
  560. }
  561. catch (FileNotFoundException)
  562. {
  563. }
  564. catch (IOException ex)
  565. {
  566. Logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
  567. await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
  568. }
  569. catch (Exception ex)
  570. {
  571. Logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
  572. }
  573. }
  574. /// <summary>
  575. /// Deletes the progressive partial stream files.
  576. /// </summary>
  577. /// <param name="outputFilePath">The output file path.</param>
  578. private void DeleteProgressivePartialStreamFiles(string outputFilePath)
  579. {
  580. _fileSystem.DeleteFile(outputFilePath);
  581. }
  582. /// <summary>
  583. /// Deletes the HLS partial stream files.
  584. /// </summary>
  585. /// <param name="outputFilePath">The output file path.</param>
  586. private void DeleteHlsPartialStreamFiles(string outputFilePath)
  587. {
  588. var directory = Path.GetDirectoryName(outputFilePath);
  589. var name = Path.GetFileNameWithoutExtension(outputFilePath);
  590. var filesToDelete = _fileSystem.GetFilePaths(directory)
  591. .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
  592. Exception e = null;
  593. foreach (var file in filesToDelete)
  594. {
  595. try
  596. {
  597. Logger.LogDebug("Deleting HLS file {0}", file);
  598. _fileSystem.DeleteFile(file);
  599. }
  600. catch (FileNotFoundException)
  601. {
  602. }
  603. catch (IOException ex)
  604. {
  605. e = ex;
  606. Logger.LogError(ex, "Error deleting HLS file {Path}", file);
  607. }
  608. }
  609. if (e != null)
  610. {
  611. throw e;
  612. }
  613. }
  614. }
  615. /// <summary>
  616. /// Class TranscodingJob
  617. /// </summary>
  618. public class TranscodingJob
  619. {
  620. /// <summary>
  621. /// Gets or sets the play session identifier.
  622. /// </summary>
  623. /// <value>The play session identifier.</value>
  624. public string PlaySessionId { get; set; }
  625. /// <summary>
  626. /// Gets or sets the live stream identifier.
  627. /// </summary>
  628. /// <value>The live stream identifier.</value>
  629. public string LiveStreamId { get; set; }
  630. public bool IsLiveOutput { get; set; }
  631. /// <summary>
  632. /// Gets or sets the path.
  633. /// </summary>
  634. /// <value>The path.</value>
  635. public MediaSourceInfo MediaSource { get; set; }
  636. public string Path { get; set; }
  637. /// <summary>
  638. /// Gets or sets the type.
  639. /// </summary>
  640. /// <value>The type.</value>
  641. public TranscodingJobType Type { get; set; }
  642. /// <summary>
  643. /// Gets or sets the process.
  644. /// </summary>
  645. /// <value>The process.</value>
  646. public Process Process { get; set; }
  647. public ILogger Logger { get; private set; }
  648. /// <summary>
  649. /// Gets or sets the active request count.
  650. /// </summary>
  651. /// <value>The active request count.</value>
  652. public int ActiveRequestCount { get; set; }
  653. /// <summary>
  654. /// Gets or sets the kill timer.
  655. /// </summary>
  656. /// <value>The kill timer.</value>
  657. private Timer KillTimer { get; set; }
  658. public string DeviceId { get; set; }
  659. public CancellationTokenSource CancellationTokenSource { get; set; }
  660. public object ProcessLock = new object();
  661. public bool HasExited { get; set; }
  662. public bool IsUserPaused { get; set; }
  663. public string Id { get; set; }
  664. public float? Framerate { get; set; }
  665. public double? CompletionPercentage { get; set; }
  666. public long? BytesDownloaded { get; set; }
  667. public long? BytesTranscoded { get; set; }
  668. public int? BitRate { get; set; }
  669. public long? TranscodingPositionTicks { get; set; }
  670. public long? DownloadPositionTicks { get; set; }
  671. public TranscodingThrottler TranscodingThrottler { get; set; }
  672. private readonly object _timerLock = new object();
  673. public DateTime LastPingDate { get; set; }
  674. public int PingTimeout { get; set; }
  675. public TranscodingJob(ILogger logger)
  676. {
  677. Logger = logger;
  678. }
  679. public void StopKillTimer()
  680. {
  681. lock (_timerLock)
  682. {
  683. if (KillTimer != null)
  684. {
  685. KillTimer.Change(Timeout.Infinite, Timeout.Infinite);
  686. }
  687. }
  688. }
  689. public void DisposeKillTimer()
  690. {
  691. lock (_timerLock)
  692. {
  693. if (KillTimer != null)
  694. {
  695. KillTimer.Dispose();
  696. KillTimer = null;
  697. }
  698. }
  699. }
  700. public void StartKillTimer(Action<object> callback)
  701. {
  702. StartKillTimer(callback, PingTimeout);
  703. }
  704. public void StartKillTimer(Action<object> callback, int intervalMs)
  705. {
  706. if (HasExited)
  707. {
  708. return;
  709. }
  710. lock (_timerLock)
  711. {
  712. if (KillTimer == null)
  713. {
  714. Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
  715. KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
  716. }
  717. else
  718. {
  719. Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
  720. KillTimer.Change(intervalMs, Timeout.Infinite);
  721. }
  722. }
  723. }
  724. public void ChangeKillTimerIfStarted()
  725. {
  726. if (HasExited)
  727. {
  728. return;
  729. }
  730. lock (_timerLock)
  731. {
  732. if (KillTimer != null)
  733. {
  734. var intervalMs = PingTimeout;
  735. Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
  736. KillTimer.Change(intervalMs, Timeout.Infinite);
  737. }
  738. }
  739. }
  740. }
  741. }