BaseStreamingService.cs 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. using MediaBrowser.Common.Configuration;
  11. using MediaBrowser.Common.Extensions;
  12. using MediaBrowser.Controller.Configuration;
  13. using MediaBrowser.Controller.Devices;
  14. using MediaBrowser.Controller.Dlna;
  15. using MediaBrowser.Controller.Library;
  16. using MediaBrowser.Controller.MediaEncoding;
  17. using MediaBrowser.Controller.Net;
  18. using MediaBrowser.Model.Configuration;
  19. using MediaBrowser.Model.Dlna;
  20. using MediaBrowser.Model.Dto;
  21. using MediaBrowser.Model.Entities;
  22. using MediaBrowser.Model.IO;
  23. using MediaBrowser.Model.MediaInfo;
  24. using MediaBrowser.Model.Serialization;
  25. using Microsoft.Extensions.Logging;
  26. namespace MediaBrowser.Api.Playback
  27. {
  28. /// <summary>
  29. /// Class BaseStreamingService
  30. /// </summary>
  31. public abstract class BaseStreamingService : BaseApiService
  32. {
  33. protected virtual bool EnableOutputInSubFolder => false;
  34. /// <summary>
  35. /// Gets or sets the user manager.
  36. /// </summary>
  37. /// <value>The user manager.</value>
  38. protected IUserManager UserManager { get; private set; }
  39. /// <summary>
  40. /// Gets or sets the library manager.
  41. /// </summary>
  42. /// <value>The library manager.</value>
  43. protected ILibraryManager LibraryManager { get; private set; }
  44. /// <summary>
  45. /// Gets or sets the iso manager.
  46. /// </summary>
  47. /// <value>The iso manager.</value>
  48. protected IIsoManager IsoManager { get; private set; }
  49. /// <summary>
  50. /// Gets or sets the media encoder.
  51. /// </summary>
  52. /// <value>The media encoder.</value>
  53. protected IMediaEncoder MediaEncoder { get; private set; }
  54. protected IFileSystem FileSystem { get; private set; }
  55. protected IDlnaManager DlnaManager { get; private set; }
  56. protected IDeviceManager DeviceManager { get; private set; }
  57. protected IMediaSourceManager MediaSourceManager { get; private set; }
  58. protected IJsonSerializer JsonSerializer { get; private set; }
  59. protected IAuthorizationContext AuthorizationContext { get; private set; }
  60. protected EncodingHelper EncodingHelper { get; set; }
  61. /// <summary>
  62. /// Gets the type of the transcoding job.
  63. /// </summary>
  64. /// <value>The type of the transcoding job.</value>
  65. protected abstract TranscodingJobType TranscodingJobType { get; }
  66. /// <summary>
  67. /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
  68. /// </summary>
  69. protected BaseStreamingService(
  70. ILogger logger,
  71. IServerConfigurationManager serverConfigurationManager,
  72. IHttpResultFactory httpResultFactory,
  73. IUserManager userManager,
  74. ILibraryManager libraryManager,
  75. IIsoManager isoManager,
  76. IMediaEncoder mediaEncoder,
  77. IFileSystem fileSystem,
  78. IDlnaManager dlnaManager,
  79. IDeviceManager deviceManager,
  80. IMediaSourceManager mediaSourceManager,
  81. IJsonSerializer jsonSerializer,
  82. IAuthorizationContext authorizationContext,
  83. EncodingHelper encodingHelper)
  84. : base(logger, serverConfigurationManager, httpResultFactory)
  85. {
  86. UserManager = userManager;
  87. LibraryManager = libraryManager;
  88. IsoManager = isoManager;
  89. MediaEncoder = mediaEncoder;
  90. FileSystem = fileSystem;
  91. DlnaManager = dlnaManager;
  92. DeviceManager = deviceManager;
  93. MediaSourceManager = mediaSourceManager;
  94. JsonSerializer = jsonSerializer;
  95. AuthorizationContext = authorizationContext;
  96. EncodingHelper = encodingHelper;
  97. }
  98. /// <summary>
  99. /// Gets the command line arguments.
  100. /// </summary>
  101. protected abstract string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding);
  102. /// <summary>
  103. /// Gets the output file extension.
  104. /// </summary>
  105. /// <param name="state">The state.</param>
  106. /// <returns>System.String.</returns>
  107. protected virtual string GetOutputFileExtension(StreamState state)
  108. {
  109. return Path.GetExtension(state.RequestedUrl);
  110. }
  111. /// <summary>
  112. /// Gets the output file path.
  113. /// </summary>
  114. private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension)
  115. {
  116. var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}";
  117. var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
  118. var ext = outputFileExtension?.ToLowerInvariant();
  119. var folder = ServerConfigurationManager.GetTranscodePath();
  120. return EnableOutputInSubFolder
  121. ? Path.Combine(folder, filename, filename + ext)
  122. : Path.Combine(folder, filename + ext);
  123. }
  124. protected virtual string GetDefaultEncoderPreset()
  125. {
  126. return "superfast";
  127. }
  128. private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
  129. {
  130. if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
  131. {
  132. state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
  133. }
  134. if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
  135. {
  136. var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
  137. {
  138. OpenToken = state.MediaSource.OpenToken
  139. }, cancellationTokenSource.Token).ConfigureAwait(false);
  140. EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
  141. if (state.VideoRequest != null)
  142. {
  143. EncodingHelper.TryStreamCopy(state);
  144. }
  145. }
  146. if (state.MediaSource.BufferMs.HasValue)
  147. {
  148. await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
  149. }
  150. }
  151. /// <summary>
  152. /// Starts the FFMPEG.
  153. /// </summary>
  154. /// <param name="state">The state.</param>
  155. /// <param name="outputPath">The output path.</param>
  156. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  157. /// <param name="workingDirectory">The working directory.</param>
  158. /// <returns>Task.</returns>
  159. protected async Task<TranscodingJob> StartFfMpeg(
  160. StreamState state,
  161. string outputPath,
  162. CancellationTokenSource cancellationTokenSource,
  163. string workingDirectory = null)
  164. {
  165. Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
  166. await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
  167. if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
  168. {
  169. var auth = AuthorizationContext.GetAuthorizationInfo(Request);
  170. if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding)
  171. {
  172. ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
  173. throw new ArgumentException("User does not have access to video transcoding");
  174. }
  175. }
  176. var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
  177. var process = new Process()
  178. {
  179. StartInfo = new ProcessStartInfo()
  180. {
  181. WindowStyle = ProcessWindowStyle.Hidden,
  182. CreateNoWindow = true,
  183. UseShellExecute = false,
  184. // Must consume both stdout and stderr or deadlocks may occur
  185. //RedirectStandardOutput = true,
  186. RedirectStandardError = true,
  187. RedirectStandardInput = true,
  188. FileName = MediaEncoder.EncoderPath,
  189. Arguments = GetCommandLineArguments(outputPath, encodingOptions, state, true),
  190. WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
  191. ErrorDialog = false
  192. },
  193. EnableRaisingEvents = true
  194. };
  195. var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
  196. state.Request.PlaySessionId,
  197. state.MediaSource.LiveStreamId,
  198. Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
  199. TranscodingJobType,
  200. process,
  201. state.Request.DeviceId,
  202. state,
  203. cancellationTokenSource);
  204. var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
  205. Logger.LogInformation(commandLineLogMessage);
  206. var logFilePrefix = "ffmpeg-transcode";
  207. if (state.VideoRequest != null
  208. && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
  209. {
  210. logFilePrefix = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)
  211. ? "ffmpeg-remux" : "ffmpeg-directstream";
  212. }
  213. var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
  214. // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
  215. Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
  216. var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
  217. await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
  218. process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
  219. try
  220. {
  221. process.Start();
  222. }
  223. catch (Exception ex)
  224. {
  225. Logger.LogError(ex, "Error starting ffmpeg");
  226. ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
  227. throw;
  228. }
  229. Logger.LogDebug("Launched ffmpeg process");
  230. state.TranscodingJob = transcodingJob;
  231. // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
  232. _ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
  233. // Wait for the file to exist before proceeeding
  234. var ffmpegTargetFile = state.WaitForPath ?? outputPath;
  235. Logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
  236. while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
  237. {
  238. await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
  239. }
  240. Logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
  241. if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
  242. {
  243. await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
  244. if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
  245. {
  246. await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
  247. }
  248. }
  249. if (!transcodingJob.HasExited)
  250. {
  251. StartThrottler(state, transcodingJob);
  252. }
  253. Logger.LogDebug("StartFfMpeg() finished successfully");
  254. return transcodingJob;
  255. }
  256. private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
  257. {
  258. if (EnableThrottling(state))
  259. {
  260. transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager, FileSystem);
  261. state.TranscodingThrottler.Start();
  262. }
  263. }
  264. private bool EnableThrottling(StreamState state)
  265. {
  266. var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
  267. // enable throttling when NOT using hardware acceleration
  268. if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
  269. {
  270. return state.InputProtocol == MediaProtocol.File &&
  271. state.RunTimeTicks.HasValue &&
  272. state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
  273. state.IsInputVideo &&
  274. state.VideoType == VideoType.VideoFile &&
  275. !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase);
  276. }
  277. return false;
  278. }
  279. /// <summary>
  280. /// Processes the exited.
  281. /// </summary>
  282. /// <param name="process">The process.</param>
  283. /// <param name="job">The job.</param>
  284. /// <param name="state">The state.</param>
  285. private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
  286. {
  287. if (job != null)
  288. {
  289. job.HasExited = true;
  290. }
  291. Logger.LogDebug("Disposing stream resources");
  292. state.Dispose();
  293. if (process.ExitCode == 0)
  294. {
  295. Logger.LogInformation("FFMpeg exited with code 0");
  296. }
  297. else
  298. {
  299. Logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
  300. }
  301. process.Dispose();
  302. }
  303. /// <summary>
  304. /// Parses the parameters.
  305. /// </summary>
  306. /// <param name="request">The request.</param>
  307. private void ParseParams(StreamRequest request)
  308. {
  309. var vals = request.Params.Split(';');
  310. var videoRequest = request as VideoStreamRequest;
  311. for (var i = 0; i < vals.Length; i++)
  312. {
  313. var val = vals[i];
  314. if (string.IsNullOrWhiteSpace(val))
  315. {
  316. continue;
  317. }
  318. switch (i)
  319. {
  320. case 0:
  321. request.DeviceProfileId = val;
  322. break;
  323. case 1:
  324. request.DeviceId = val;
  325. break;
  326. case 2:
  327. request.MediaSourceId = val;
  328. break;
  329. case 3:
  330. request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  331. break;
  332. case 4:
  333. if (videoRequest != null)
  334. {
  335. videoRequest.VideoCodec = val;
  336. }
  337. break;
  338. case 5:
  339. request.AudioCodec = val;
  340. break;
  341. case 6:
  342. if (videoRequest != null)
  343. {
  344. videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
  345. }
  346. break;
  347. case 7:
  348. if (videoRequest != null)
  349. {
  350. videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
  351. }
  352. break;
  353. case 8:
  354. if (videoRequest != null)
  355. {
  356. videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
  357. }
  358. break;
  359. case 9:
  360. request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
  361. break;
  362. case 10:
  363. request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
  364. break;
  365. case 11:
  366. if (videoRequest != null)
  367. {
  368. videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
  369. }
  370. break;
  371. case 12:
  372. if (videoRequest != null)
  373. {
  374. videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
  375. }
  376. break;
  377. case 13:
  378. if (videoRequest != null)
  379. {
  380. videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
  381. }
  382. break;
  383. case 14:
  384. request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
  385. break;
  386. case 15:
  387. if (videoRequest != null)
  388. {
  389. videoRequest.Level = val;
  390. }
  391. break;
  392. case 16:
  393. if (videoRequest != null)
  394. {
  395. videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
  396. }
  397. break;
  398. case 17:
  399. if (videoRequest != null)
  400. {
  401. videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
  402. }
  403. break;
  404. case 18:
  405. if (videoRequest != null)
  406. {
  407. videoRequest.Profile = val;
  408. }
  409. break;
  410. case 19:
  411. // cabac no longer used
  412. break;
  413. case 20:
  414. request.PlaySessionId = val;
  415. break;
  416. case 21:
  417. // api_key
  418. break;
  419. case 22:
  420. request.LiveStreamId = val;
  421. break;
  422. case 23:
  423. // Duplicating ItemId because of MediaMonkey
  424. break;
  425. case 24:
  426. if (videoRequest != null)
  427. {
  428. videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  429. }
  430. break;
  431. case 25:
  432. if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
  433. {
  434. if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
  435. {
  436. videoRequest.SubtitleMethod = method;
  437. }
  438. }
  439. break;
  440. case 26:
  441. request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
  442. break;
  443. case 27:
  444. if (videoRequest != null)
  445. {
  446. videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  447. }
  448. break;
  449. case 28:
  450. request.Tag = val;
  451. break;
  452. case 29:
  453. if (videoRequest != null)
  454. {
  455. videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  456. }
  457. break;
  458. case 30:
  459. request.SubtitleCodec = val;
  460. break;
  461. case 31:
  462. if (videoRequest != null)
  463. {
  464. videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  465. }
  466. break;
  467. case 32:
  468. if (videoRequest != null)
  469. {
  470. videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  471. }
  472. break;
  473. case 33:
  474. request.TranscodeReasons = val;
  475. break;
  476. }
  477. }
  478. }
  479. /// <summary>
  480. /// Parses query parameters as StreamOptions.
  481. /// </summary>
  482. /// <param name="request">The stream request.</param>
  483. private void ParseStreamOptions(StreamRequest request)
  484. {
  485. foreach (var param in Request.QueryString)
  486. {
  487. if (char.IsLower(param.Key[0]))
  488. {
  489. // This was probably not parsed initially and should be a StreamOptions
  490. // TODO: This should be incorporated either in the lower framework for parsing requests
  491. // or the generated URL should correctly serialize it
  492. request.StreamOptions[param.Key] = param.Value;
  493. }
  494. }
  495. }
  496. /// <summary>
  497. /// Parses the dlna headers.
  498. /// </summary>
  499. /// <param name="request">The request.</param>
  500. private void ParseDlnaHeaders(StreamRequest request)
  501. {
  502. if (!request.StartTimeTicks.HasValue)
  503. {
  504. var timeSeek = GetHeader("TimeSeekRange.dlna.org");
  505. request.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
  506. }
  507. }
  508. /// <summary>
  509. /// Parses the time seek header.
  510. /// </summary>
  511. private long? ParseTimeSeekHeader(string value)
  512. {
  513. if (string.IsNullOrWhiteSpace(value))
  514. {
  515. return null;
  516. }
  517. const string Npt = "npt=";
  518. if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase))
  519. {
  520. throw new ArgumentException("Invalid timeseek header");
  521. }
  522. int index = value.IndexOf('-');
  523. value = index == -1
  524. ? value.Substring(Npt.Length)
  525. : value.Substring(Npt.Length, index - Npt.Length);
  526. if (value.IndexOf(':') == -1)
  527. {
  528. // Parses npt times in the format of '417.33'
  529. if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
  530. {
  531. return TimeSpan.FromSeconds(seconds).Ticks;
  532. }
  533. throw new ArgumentException("Invalid timeseek header");
  534. }
  535. // Parses npt times in the format of '10:19:25.7'
  536. var tokens = value.Split(new[] { ':' }, 3);
  537. double secondsSum = 0;
  538. var timeFactor = 3600;
  539. foreach (var time in tokens)
  540. {
  541. if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit))
  542. {
  543. secondsSum += digit * timeFactor;
  544. }
  545. else
  546. {
  547. throw new ArgumentException("Invalid timeseek header");
  548. }
  549. timeFactor /= 60;
  550. }
  551. return TimeSpan.FromSeconds(secondsSum).Ticks;
  552. }
  553. /// <summary>
  554. /// Gets the state.
  555. /// </summary>
  556. /// <param name="request">The request.</param>
  557. /// <param name="cancellationToken">The cancellation token.</param>
  558. /// <returns>StreamState.</returns>
  559. protected async Task<StreamState> GetState(StreamRequest request, CancellationToken cancellationToken)
  560. {
  561. ParseDlnaHeaders(request);
  562. if (!string.IsNullOrWhiteSpace(request.Params))
  563. {
  564. ParseParams(request);
  565. }
  566. ParseStreamOptions(request);
  567. var url = Request.PathInfo;
  568. if (string.IsNullOrEmpty(request.AudioCodec))
  569. {
  570. request.AudioCodec = EncodingHelper.InferAudioCodec(url);
  571. }
  572. var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) ||
  573. string.Equals(GetHeader("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase);
  574. var state = new StreamState(MediaSourceManager, TranscodingJobType)
  575. {
  576. Request = request,
  577. RequestedUrl = url,
  578. UserAgent = Request.UserAgent,
  579. EnableDlnaHeaders = enableDlnaHeaders
  580. };
  581. var auth = AuthorizationContext.GetAuthorizationInfo(Request);
  582. if (!auth.UserId.Equals(Guid.Empty))
  583. {
  584. state.User = UserManager.GetUserById(auth.UserId);
  585. }
  586. //if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
  587. // (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
  588. // (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
  589. //{
  590. // state.SegmentLength = 6;
  591. //}
  592. if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
  593. {
  594. state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
  595. state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
  596. }
  597. if (!string.IsNullOrWhiteSpace(request.AudioCodec))
  598. {
  599. state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
  600. state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToAudioCodec(i))
  601. ?? state.SupportedAudioCodecs.FirstOrDefault();
  602. }
  603. if (!string.IsNullOrWhiteSpace(request.SubtitleCodec))
  604. {
  605. state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
  606. state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToSubtitleCodec(i))
  607. ?? state.SupportedSubtitleCodecs.FirstOrDefault();
  608. }
  609. var item = LibraryManager.GetItemById(request.Id);
  610. state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
  611. //var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
  612. // item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
  613. //if (primaryImage != null)
  614. //{
  615. // state.AlbumCoverPath = primaryImage.Path;
  616. //}
  617. MediaSourceInfo mediaSource = null;
  618. if (string.IsNullOrWhiteSpace(request.LiveStreamId))
  619. {
  620. var currentJob = !string.IsNullOrWhiteSpace(request.PlaySessionId) ?
  621. ApiEntryPoint.Instance.GetTranscodingJob(request.PlaySessionId)
  622. : null;
  623. if (currentJob != null)
  624. {
  625. mediaSource = currentJob.MediaSource;
  626. }
  627. if (mediaSource == null)
  628. {
  629. var mediaSources = await MediaSourceManager.GetPlaybackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false);
  630. mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
  631. ? mediaSources[0]
  632. : mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId));
  633. if (mediaSource == null && Guid.Parse(request.MediaSourceId) == request.Id)
  634. {
  635. mediaSource = mediaSources[0];
  636. }
  637. }
  638. }
  639. else
  640. {
  641. var liveStreamInfo = await MediaSourceManager.GetLiveStreamWithDirectStreamProvider(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
  642. mediaSource = liveStreamInfo.Item1;
  643. state.DirectStreamProvider = liveStreamInfo.Item2;
  644. }
  645. var videoRequest = request as VideoStreamRequest;
  646. EncodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
  647. var container = Path.GetExtension(state.RequestedUrl);
  648. if (string.IsNullOrEmpty(container))
  649. {
  650. container = request.Container;
  651. }
  652. if (string.IsNullOrEmpty(container))
  653. {
  654. container = request.Static ?
  655. StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) :
  656. GetOutputFileExtension(state);
  657. }
  658. state.OutputContainer = (container ?? string.Empty).TrimStart('.');
  659. state.OutputAudioBitrate = EncodingHelper.GetAudioBitrateParam(state.Request, state.AudioStream);
  660. state.OutputAudioCodec = state.Request.AudioCodec;
  661. state.OutputAudioChannels = EncodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
  662. if (videoRequest != null)
  663. {
  664. state.OutputVideoCodec = state.VideoRequest.VideoCodec;
  665. state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
  666. if (videoRequest != null)
  667. {
  668. EncodingHelper.TryStreamCopy(state);
  669. }
  670. if (state.OutputVideoBitrate.HasValue && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
  671. {
  672. var resolution = ResolutionNormalizer.Normalize(
  673. state.VideoStream?.BitRate,
  674. state.VideoStream?.Width,
  675. state.VideoStream?.Height,
  676. state.OutputVideoBitrate.Value,
  677. state.VideoStream?.Codec,
  678. state.OutputVideoCodec,
  679. videoRequest.MaxWidth,
  680. videoRequest.MaxHeight);
  681. videoRequest.MaxWidth = resolution.MaxWidth;
  682. videoRequest.MaxHeight = resolution.MaxHeight;
  683. }
  684. }
  685. ApplyDeviceProfileSettings(state);
  686. var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
  687. ? GetOutputFileExtension(state)
  688. : ('.' + state.OutputContainer);
  689. var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
  690. state.OutputFilePath = GetOutputFilePath(state, encodingOptions, ext);
  691. return state;
  692. }
  693. private void ApplyDeviceProfileSettings(StreamState state)
  694. {
  695. var headers = Request.Headers;
  696. if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId))
  697. {
  698. state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId);
  699. }
  700. else if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
  701. {
  702. var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
  703. state.DeviceProfile = caps == null ? DlnaManager.GetProfile(headers) : caps.DeviceProfile;
  704. }
  705. var profile = state.DeviceProfile;
  706. if (profile == null)
  707. {
  708. // Don't use settings from the default profile.
  709. // Only use a specific profile if it was requested.
  710. return;
  711. }
  712. var audioCodec = state.ActualOutputAudioCodec;
  713. var videoCodec = state.ActualOutputVideoCodec;
  714. var mediaProfile = state.VideoRequest == null ?
  715. profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth) :
  716. profile.GetVideoMediaProfile(state.OutputContainer,
  717. audioCodec,
  718. videoCodec,
  719. state.OutputWidth,
  720. state.OutputHeight,
  721. state.TargetVideoBitDepth,
  722. state.OutputVideoBitrate,
  723. state.TargetVideoProfile,
  724. state.TargetVideoLevel,
  725. state.TargetFramerate,
  726. state.TargetPacketLength,
  727. state.TargetTimestamp,
  728. state.IsTargetAnamorphic,
  729. state.IsTargetInterlaced,
  730. state.TargetRefFrames,
  731. state.TargetVideoStreamCount,
  732. state.TargetAudioStreamCount,
  733. state.TargetVideoCodecTag,
  734. state.IsTargetAVC);
  735. if (mediaProfile != null)
  736. {
  737. state.MimeType = mediaProfile.MimeType;
  738. }
  739. if (!state.Request.Static)
  740. {
  741. var transcodingProfile = state.VideoRequest == null ?
  742. profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) :
  743. profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
  744. if (transcodingProfile != null)
  745. {
  746. state.EstimateContentLength = transcodingProfile.EstimateContentLength;
  747. //state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
  748. state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  749. if (state.VideoRequest != null)
  750. {
  751. state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
  752. state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
  753. }
  754. }
  755. }
  756. }
  757. /// <summary>
  758. /// Adds the dlna headers.
  759. /// </summary>
  760. /// <param name="state">The state.</param>
  761. /// <param name="responseHeaders">The response headers.</param>
  762. /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
  763. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  764. protected void AddDlnaHeaders(StreamState state, IDictionary<string, string> responseHeaders, bool isStaticallyStreamed)
  765. {
  766. if (!state.EnableDlnaHeaders)
  767. {
  768. return;
  769. }
  770. var profile = state.DeviceProfile;
  771. var transferMode = GetHeader("transferMode.dlna.org");
  772. responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode;
  773. responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*";
  774. if (state.RunTimeTicks.HasValue)
  775. {
  776. if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase))
  777. {
  778. var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
  779. responseHeaders["MediaInfo.sec"] = string.Format(
  780. CultureInfo.InvariantCulture,
  781. "SEC_Duration={0};",
  782. Convert.ToInt32(ms));
  783. }
  784. if (!isStaticallyStreamed && profile != null)
  785. {
  786. AddTimeSeekResponseHeaders(state, responseHeaders);
  787. }
  788. }
  789. if (profile == null)
  790. {
  791. profile = DlnaManager.GetDefaultProfile();
  792. }
  793. var audioCodec = state.ActualOutputAudioCodec;
  794. if (state.VideoRequest == null)
  795. {
  796. responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildAudioHeader(
  797. state.OutputContainer,
  798. audioCodec,
  799. state.OutputAudioBitrate,
  800. state.OutputAudioSampleRate,
  801. state.OutputAudioChannels,
  802. state.OutputAudioBitDepth,
  803. isStaticallyStreamed,
  804. state.RunTimeTicks,
  805. state.TranscodeSeekInfo);
  806. }
  807. else
  808. {
  809. var videoCodec = state.ActualOutputVideoCodec;
  810. responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildVideoHeader(
  811. state.OutputContainer,
  812. videoCodec,
  813. audioCodec,
  814. state.OutputWidth,
  815. state.OutputHeight,
  816. state.TargetVideoBitDepth,
  817. state.OutputVideoBitrate,
  818. state.TargetTimestamp,
  819. isStaticallyStreamed,
  820. state.RunTimeTicks,
  821. state.TargetVideoProfile,
  822. state.TargetVideoLevel,
  823. state.TargetFramerate,
  824. state.TargetPacketLength,
  825. state.TranscodeSeekInfo,
  826. state.IsTargetAnamorphic,
  827. state.IsTargetInterlaced,
  828. state.TargetRefFrames,
  829. state.TargetVideoStreamCount,
  830. state.TargetAudioStreamCount,
  831. state.TargetVideoCodecTag,
  832. state.IsTargetAVC).FirstOrDefault() ?? string.Empty;
  833. }
  834. }
  835. private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders)
  836. {
  837. var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
  838. var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
  839. responseHeaders["TimeSeekRange.dlna.org"] = string.Format(
  840. CultureInfo.InvariantCulture,
  841. "npt={0}-{1}/{1}",
  842. startSeconds,
  843. runtimeSeconds);
  844. responseHeaders["X-AvailableSeekRange"] = string.Format(
  845. CultureInfo.InvariantCulture,
  846. "1 npt={0}-{1}",
  847. startSeconds,
  848. runtimeSeconds);
  849. }
  850. }
  851. }