BaseStreamingService.cs 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Devices;
  4. using MediaBrowser.Controller.Dlna;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.MediaEncoding;
  7. using MediaBrowser.Model.Dlna;
  8. using MediaBrowser.Model.Dto;
  9. using MediaBrowser.Model.Entities;
  10. using MediaBrowser.Model.Extensions;
  11. using MediaBrowser.Model.IO;
  12. using MediaBrowser.Model.MediaInfo;
  13. using MediaBrowser.Model.Serialization;
  14. using System;
  15. using System.Collections.Generic;
  16. using System.Globalization;
  17. using System.IO;
  18. using System.Linq;
  19. using System.Text;
  20. using System.Threading;
  21. using System.Threading.Tasks;
  22. using MediaBrowser.Common.Net;
  23. using MediaBrowser.Controller;
  24. using MediaBrowser.Controller.Net;
  25. using MediaBrowser.Model.Diagnostics;
  26. namespace MediaBrowser.Api.Playback
  27. {
  28. /// <summary>
  29. /// Class BaseStreamingService
  30. /// </summary>
  31. public abstract class BaseStreamingService : BaseApiService
  32. {
  33. /// <summary>
  34. /// Gets or sets the application paths.
  35. /// </summary>
  36. /// <value>The application paths.</value>
  37. protected IServerConfigurationManager ServerConfigurationManager { get; private set; }
  38. /// <summary>
  39. /// Gets or sets the user manager.
  40. /// </summary>
  41. /// <value>The user manager.</value>
  42. protected IUserManager UserManager { get; private set; }
  43. /// <summary>
  44. /// Gets or sets the library manager.
  45. /// </summary>
  46. /// <value>The library manager.</value>
  47. protected ILibraryManager LibraryManager { get; private set; }
  48. /// <summary>
  49. /// Gets or sets the iso manager.
  50. /// </summary>
  51. /// <value>The iso manager.</value>
  52. protected IIsoManager IsoManager { get; private set; }
  53. /// <summary>
  54. /// Gets or sets the media encoder.
  55. /// </summary>
  56. /// <value>The media encoder.</value>
  57. protected IMediaEncoder MediaEncoder { get; private set; }
  58. protected IFileSystem FileSystem { get; private set; }
  59. protected IDlnaManager DlnaManager { get; private set; }
  60. protected IDeviceManager DeviceManager { get; private set; }
  61. protected ISubtitleEncoder SubtitleEncoder { get; private set; }
  62. protected IMediaSourceManager MediaSourceManager { get; private set; }
  63. protected IZipClient ZipClient { get; private set; }
  64. protected IJsonSerializer JsonSerializer { get; private set; }
  65. public static IServerApplicationHost AppHost;
  66. public static IHttpClient HttpClient;
  67. protected IAuthorizationContext AuthorizationContext { get; private set; }
  68. protected EncodingHelper EncodingHelper { get; set; }
  69. /// <summary>
  70. /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
  71. /// </summary>
  72. protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext)
  73. {
  74. JsonSerializer = jsonSerializer;
  75. AuthorizationContext = authorizationContext;
  76. ZipClient = zipClient;
  77. MediaSourceManager = mediaSourceManager;
  78. DeviceManager = deviceManager;
  79. SubtitleEncoder = subtitleEncoder;
  80. DlnaManager = dlnaManager;
  81. FileSystem = fileSystem;
  82. ServerConfigurationManager = serverConfig;
  83. UserManager = userManager;
  84. LibraryManager = libraryManager;
  85. IsoManager = isoManager;
  86. MediaEncoder = mediaEncoder;
  87. EncodingHelper = new EncodingHelper(MediaEncoder, serverConfig, FileSystem, SubtitleEncoder);
  88. }
  89. /// <summary>
  90. /// Gets the command line arguments.
  91. /// </summary>
  92. /// <param name="outputPath">The output path.</param>
  93. /// <param name="state">The state.</param>
  94. /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param>
  95. /// <returns>System.String.</returns>
  96. protected abstract string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding);
  97. /// <summary>
  98. /// Gets the type of the transcoding job.
  99. /// </summary>
  100. /// <value>The type of the transcoding job.</value>
  101. protected abstract TranscodingJobType TranscodingJobType { get; }
  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, string outputFileExtension)
  115. {
  116. var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath;
  117. var data = GetCommandLineArguments("dummy\\dummy", state, false);
  118. data += "-" + (state.Request.DeviceId ?? string.Empty);
  119. data += "-" + (state.Request.PlaySessionId ?? string.Empty);
  120. var dataHash = data.GetMD5().ToString("N");
  121. if (EnableOutputInSubFolder)
  122. {
  123. return Path.Combine(folder, dataHash, dataHash + (outputFileExtension ?? string.Empty).ToLower());
  124. }
  125. return Path.Combine(folder, dataHash + (outputFileExtension ?? string.Empty).ToLower());
  126. }
  127. protected virtual bool EnableOutputInSubFolder
  128. {
  129. get { return false; }
  130. }
  131. protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
  132. protected virtual string GetDefaultH264Preset()
  133. {
  134. return "superfast";
  135. }
  136. private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
  137. {
  138. if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
  139. {
  140. state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
  141. }
  142. if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
  143. {
  144. var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
  145. {
  146. OpenToken = state.MediaSource.OpenToken
  147. }, false, cancellationTokenSource.Token).ConfigureAwait(false);
  148. EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
  149. if (state.VideoRequest != null)
  150. {
  151. EncodingHelper.TryStreamCopy(state);
  152. }
  153. }
  154. if (state.MediaSource.BufferMs.HasValue)
  155. {
  156. await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
  157. }
  158. }
  159. /// <summary>
  160. /// Starts the FFMPEG.
  161. /// </summary>
  162. /// <param name="state">The state.</param>
  163. /// <param name="outputPath">The output path.</param>
  164. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  165. /// <param name="workingDirectory">The working directory.</param>
  166. /// <returns>Task.</returns>
  167. protected async Task<TranscodingJob> StartFfMpeg(StreamState state,
  168. string outputPath,
  169. CancellationTokenSource cancellationTokenSource,
  170. string workingDirectory = null)
  171. {
  172. FileSystem.CreateDirectory(Path.GetDirectoryName(outputPath));
  173. await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
  174. if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
  175. {
  176. var auth = AuthorizationContext.GetAuthorizationInfo(Request);
  177. if (!string.IsNullOrWhiteSpace(auth.UserId))
  178. {
  179. var user = UserManager.GetUserById(auth.UserId);
  180. if (!user.Policy.EnableVideoPlaybackTranscoding)
  181. {
  182. ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
  183. throw new ArgumentException("User does not have access to video transcoding");
  184. }
  185. }
  186. }
  187. var transcodingId = Guid.NewGuid().ToString("N");
  188. var commandLineArgs = GetCommandLineArguments(outputPath, state, true);
  189. var process = ApiEntryPoint.Instance.ProcessFactory.Create(new ProcessOptions
  190. {
  191. CreateNoWindow = true,
  192. UseShellExecute = false,
  193. // Must consume both stdout and stderr or deadlocks may occur
  194. //RedirectStandardOutput = true,
  195. RedirectStandardError = true,
  196. RedirectStandardInput = true,
  197. FileName = MediaEncoder.EncoderPath,
  198. Arguments = commandLineArgs,
  199. IsHidden = true,
  200. ErrorDialog = false,
  201. EnableRaisingEvents = true,
  202. WorkingDirectory = !string.IsNullOrWhiteSpace(workingDirectory) ? workingDirectory : null
  203. });
  204. var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
  205. state.Request.PlaySessionId,
  206. state.MediaSource.LiveStreamId,
  207. transcodingId,
  208. TranscodingJobType,
  209. process,
  210. state.Request.DeviceId,
  211. state,
  212. cancellationTokenSource);
  213. var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
  214. Logger.Info(commandLineLogMessage);
  215. var logFilePrefix = "ffmpeg-transcode";
  216. if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
  217. {
  218. logFilePrefix = "ffmpeg-directstream";
  219. }
  220. else if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
  221. {
  222. logFilePrefix = "ffmpeg-remux";
  223. }
  224. var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
  225. FileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath));
  226. // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
  227. state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true);
  228. var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
  229. await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
  230. process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
  231. try
  232. {
  233. process.Start();
  234. }
  235. catch (Exception ex)
  236. {
  237. Logger.ErrorException("Error starting ffmpeg", ex);
  238. ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
  239. throw;
  240. }
  241. // MUST read both stdout and stderr asynchronously or a deadlock may occurr
  242. //process.BeginOutputReadLine();
  243. // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
  244. var task = Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream));
  245. // Wait for the file to exist before proceeeding
  246. while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
  247. {
  248. await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
  249. }
  250. if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
  251. {
  252. await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
  253. if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
  254. {
  255. await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
  256. }
  257. }
  258. if (!transcodingJob.HasExited)
  259. {
  260. StartThrottler(state, transcodingJob);
  261. }
  262. ReportUsage(state);
  263. return transcodingJob;
  264. }
  265. private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
  266. {
  267. if (EnableThrottling(state))
  268. {
  269. transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager, ApiEntryPoint.Instance.TimerFactory, FileSystem);
  270. state.TranscodingThrottler.Start();
  271. }
  272. }
  273. private bool EnableThrottling(StreamState state)
  274. {
  275. return false;
  276. //// do not use throttling with hardware encoders
  277. //return state.InputProtocol == MediaProtocol.File &&
  278. // state.RunTimeTicks.HasValue &&
  279. // state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
  280. // state.IsInputVideo &&
  281. // state.VideoType == VideoType.VideoFile &&
  282. // !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) &&
  283. // string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase);
  284. }
  285. private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
  286. {
  287. try
  288. {
  289. using (var reader = new StreamReader(source))
  290. {
  291. while (!reader.EndOfStream)
  292. {
  293. var line = await reader.ReadLineAsync().ConfigureAwait(false);
  294. ParseLogLine(line, transcodingJob, state);
  295. var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
  296. await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
  297. await target.FlushAsync().ConfigureAwait(false);
  298. }
  299. }
  300. }
  301. catch (ObjectDisposedException)
  302. {
  303. // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
  304. }
  305. catch (Exception ex)
  306. {
  307. Logger.ErrorException("Error reading ffmpeg log", ex);
  308. }
  309. }
  310. private void ParseLogLine(string line, TranscodingJob transcodingJob, StreamState state)
  311. {
  312. float? framerate = null;
  313. double? percent = null;
  314. TimeSpan? transcodingPosition = null;
  315. long? bytesTranscoded = null;
  316. int? bitRate = null;
  317. var parts = line.Split(' ');
  318. var totalMs = state.RunTimeTicks.HasValue
  319. ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
  320. : 0;
  321. var startMs = state.Request.StartTimeTicks.HasValue
  322. ? TimeSpan.FromTicks(state.Request.StartTimeTicks.Value).TotalMilliseconds
  323. : 0;
  324. for (var i = 0; i < parts.Length; i++)
  325. {
  326. var part = parts[i];
  327. if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) &&
  328. (i + 1 < parts.Length))
  329. {
  330. var rate = parts[i + 1];
  331. float val;
  332. if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
  333. {
  334. framerate = val;
  335. }
  336. }
  337. else if (state.RunTimeTicks.HasValue &&
  338. part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
  339. {
  340. var time = part.Split(new[] { '=' }, 2).Last();
  341. TimeSpan val;
  342. if (TimeSpan.TryParse(time, UsCulture, out val))
  343. {
  344. var currentMs = startMs + val.TotalMilliseconds;
  345. var percentVal = currentMs / totalMs;
  346. percent = 100 * percentVal;
  347. transcodingPosition = val;
  348. }
  349. }
  350. else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
  351. {
  352. var size = part.Split(new[] { '=' }, 2).Last();
  353. int? scale = null;
  354. if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
  355. {
  356. scale = 1024;
  357. size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
  358. }
  359. if (scale.HasValue)
  360. {
  361. long val;
  362. if (long.TryParse(size, NumberStyles.Any, UsCulture, out val))
  363. {
  364. bytesTranscoded = val * scale.Value;
  365. }
  366. }
  367. }
  368. else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
  369. {
  370. var rate = part.Split(new[] { '=' }, 2).Last();
  371. int? scale = null;
  372. if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
  373. {
  374. scale = 1024;
  375. rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
  376. }
  377. if (scale.HasValue)
  378. {
  379. float val;
  380. if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
  381. {
  382. bitRate = (int)Math.Ceiling(val * scale.Value);
  383. }
  384. }
  385. }
  386. }
  387. if (framerate.HasValue || percent.HasValue)
  388. {
  389. ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded, bitRate);
  390. }
  391. }
  392. /// <summary>
  393. /// Processes the exited.
  394. /// </summary>
  395. /// <param name="process">The process.</param>
  396. /// <param name="job">The job.</param>
  397. /// <param name="state">The state.</param>
  398. private void OnFfMpegProcessExited(IProcess process, TranscodingJob job, StreamState state)
  399. {
  400. if (job != null)
  401. {
  402. job.HasExited = true;
  403. }
  404. Logger.Debug("Disposing stream resources");
  405. state.Dispose();
  406. try
  407. {
  408. Logger.Info("FFMpeg exited with code {0}", process.ExitCode);
  409. }
  410. catch
  411. {
  412. Logger.Error("FFMpeg exited with an error.");
  413. }
  414. // This causes on exited to be called twice:
  415. //try
  416. //{
  417. // // Dispose the process
  418. // process.Dispose();
  419. //}
  420. //catch (Exception ex)
  421. //{
  422. // Logger.ErrorException("Error disposing ffmpeg.", ex);
  423. //}
  424. }
  425. /// <summary>
  426. /// Parses the parameters.
  427. /// </summary>
  428. /// <param name="request">The request.</param>
  429. private void ParseParams(StreamRequest request)
  430. {
  431. var vals = request.Params.Split(';');
  432. var videoRequest = request as VideoStreamRequest;
  433. for (var i = 0; i < vals.Length; i++)
  434. {
  435. var val = vals[i];
  436. if (string.IsNullOrWhiteSpace(val))
  437. {
  438. continue;
  439. }
  440. if (i == 0)
  441. {
  442. request.DeviceProfileId = val;
  443. }
  444. else if (i == 1)
  445. {
  446. request.DeviceId = val;
  447. }
  448. else if (i == 2)
  449. {
  450. request.MediaSourceId = val;
  451. }
  452. else if (i == 3)
  453. {
  454. request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  455. }
  456. else if (i == 4)
  457. {
  458. if (videoRequest != null)
  459. {
  460. videoRequest.VideoCodec = val;
  461. }
  462. }
  463. else if (i == 5)
  464. {
  465. request.AudioCodec = val;
  466. }
  467. else if (i == 6)
  468. {
  469. if (videoRequest != null)
  470. {
  471. videoRequest.AudioStreamIndex = int.Parse(val, UsCulture);
  472. }
  473. }
  474. else if (i == 7)
  475. {
  476. if (videoRequest != null)
  477. {
  478. videoRequest.SubtitleStreamIndex = int.Parse(val, UsCulture);
  479. }
  480. }
  481. else if (i == 8)
  482. {
  483. if (videoRequest != null)
  484. {
  485. videoRequest.VideoBitRate = int.Parse(val, UsCulture);
  486. }
  487. }
  488. else if (i == 9)
  489. {
  490. request.AudioBitRate = int.Parse(val, UsCulture);
  491. }
  492. else if (i == 10)
  493. {
  494. request.MaxAudioChannels = int.Parse(val, UsCulture);
  495. }
  496. else if (i == 11)
  497. {
  498. if (videoRequest != null)
  499. {
  500. videoRequest.MaxFramerate = float.Parse(val, UsCulture);
  501. }
  502. }
  503. else if (i == 12)
  504. {
  505. if (videoRequest != null)
  506. {
  507. videoRequest.MaxWidth = int.Parse(val, UsCulture);
  508. }
  509. }
  510. else if (i == 13)
  511. {
  512. if (videoRequest != null)
  513. {
  514. videoRequest.MaxHeight = int.Parse(val, UsCulture);
  515. }
  516. }
  517. else if (i == 14)
  518. {
  519. request.StartTimeTicks = long.Parse(val, UsCulture);
  520. }
  521. else if (i == 15)
  522. {
  523. if (videoRequest != null)
  524. {
  525. videoRequest.Level = val;
  526. }
  527. }
  528. else if (i == 16)
  529. {
  530. if (videoRequest != null)
  531. {
  532. videoRequest.MaxRefFrames = int.Parse(val, UsCulture);
  533. }
  534. }
  535. else if (i == 17)
  536. {
  537. if (videoRequest != null)
  538. {
  539. videoRequest.MaxVideoBitDepth = int.Parse(val, UsCulture);
  540. }
  541. }
  542. else if (i == 18)
  543. {
  544. if (videoRequest != null)
  545. {
  546. videoRequest.Profile = val;
  547. }
  548. }
  549. else if (i == 19)
  550. {
  551. // cabac no longer used
  552. }
  553. else if (i == 20)
  554. {
  555. request.PlaySessionId = val;
  556. }
  557. else if (i == 21)
  558. {
  559. // api_key
  560. }
  561. else if (i == 22)
  562. {
  563. request.LiveStreamId = val;
  564. }
  565. else if (i == 23)
  566. {
  567. // Duplicating ItemId because of MediaMonkey
  568. }
  569. else if (i == 24)
  570. {
  571. if (videoRequest != null)
  572. {
  573. videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  574. }
  575. }
  576. else if (i == 25)
  577. {
  578. if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
  579. {
  580. SubtitleDeliveryMethod method;
  581. if (Enum.TryParse(val, out method))
  582. {
  583. videoRequest.SubtitleMethod = method;
  584. }
  585. }
  586. }
  587. else if (i == 26)
  588. {
  589. request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture);
  590. }
  591. else if (i == 27)
  592. {
  593. if (videoRequest != null)
  594. {
  595. videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  596. }
  597. }
  598. else if (i == 28)
  599. {
  600. request.Tag = val;
  601. }
  602. else if (i == 29)
  603. {
  604. if (videoRequest != null)
  605. {
  606. videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  607. }
  608. }
  609. else if (i == 30)
  610. {
  611. request.SubtitleCodec = val;
  612. }
  613. }
  614. }
  615. /// <summary>
  616. /// Parses the dlna headers.
  617. /// </summary>
  618. /// <param name="request">The request.</param>
  619. private void ParseDlnaHeaders(StreamRequest request)
  620. {
  621. if (!request.StartTimeTicks.HasValue)
  622. {
  623. var timeSeek = GetHeader("TimeSeekRange.dlna.org");
  624. request.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
  625. }
  626. }
  627. /// <summary>
  628. /// Parses the time seek header.
  629. /// </summary>
  630. private long? ParseTimeSeekHeader(string value)
  631. {
  632. if (string.IsNullOrWhiteSpace(value))
  633. {
  634. return null;
  635. }
  636. if (value.IndexOf("npt=", StringComparison.OrdinalIgnoreCase) != 0)
  637. {
  638. throw new ArgumentException("Invalid timeseek header");
  639. }
  640. value = value.Substring(4).Split(new[] { '-' }, 2)[0];
  641. if (value.IndexOf(':') == -1)
  642. {
  643. // Parses npt times in the format of '417.33'
  644. double seconds;
  645. if (double.TryParse(value, NumberStyles.Any, UsCulture, out seconds))
  646. {
  647. return TimeSpan.FromSeconds(seconds).Ticks;
  648. }
  649. throw new ArgumentException("Invalid timeseek header");
  650. }
  651. // Parses npt times in the format of '10:19:25.7'
  652. var tokens = value.Split(new[] { ':' }, 3);
  653. double secondsSum = 0;
  654. var timeFactor = 3600;
  655. foreach (var time in tokens)
  656. {
  657. double digit;
  658. if (double.TryParse(time, NumberStyles.Any, UsCulture, out digit))
  659. {
  660. secondsSum += digit * timeFactor;
  661. }
  662. else
  663. {
  664. throw new ArgumentException("Invalid timeseek header");
  665. }
  666. timeFactor /= 60;
  667. }
  668. return TimeSpan.FromSeconds(secondsSum).Ticks;
  669. }
  670. /// <summary>
  671. /// Gets the state.
  672. /// </summary>
  673. /// <param name="request">The request.</param>
  674. /// <param name="cancellationToken">The cancellation token.</param>
  675. /// <returns>StreamState.</returns>
  676. protected async Task<StreamState> GetState(StreamRequest request, CancellationToken cancellationToken)
  677. {
  678. ParseDlnaHeaders(request);
  679. if (!string.IsNullOrWhiteSpace(request.Params))
  680. {
  681. ParseParams(request);
  682. }
  683. var url = Request.PathInfo;
  684. if (string.IsNullOrEmpty(request.AudioCodec))
  685. {
  686. request.AudioCodec = EncodingHelper.InferAudioCodec(url);
  687. }
  688. var state = new StreamState(MediaSourceManager, Logger, TranscodingJobType)
  689. {
  690. Request = request,
  691. RequestedUrl = url,
  692. UserAgent = Request.UserAgent
  693. };
  694. var auth = AuthorizationContext.GetAuthorizationInfo(Request);
  695. if (!string.IsNullOrWhiteSpace(auth.UserId))
  696. {
  697. state.User = UserManager.GetUserById(auth.UserId);
  698. }
  699. //if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
  700. // (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
  701. // (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
  702. //{
  703. // state.SegmentLength = 6;
  704. //}
  705. if (state.VideoRequest != null)
  706. {
  707. if (!string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
  708. {
  709. state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
  710. state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
  711. }
  712. }
  713. if (!string.IsNullOrWhiteSpace(request.AudioCodec))
  714. {
  715. state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
  716. state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToAudioCodec(i))
  717. ?? state.SupportedAudioCodecs.FirstOrDefault();
  718. }
  719. if (!string.IsNullOrWhiteSpace(request.SubtitleCodec))
  720. {
  721. state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
  722. state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToSubtitleCodec(i))
  723. ?? state.SupportedSubtitleCodecs.FirstOrDefault();
  724. }
  725. var item = LibraryManager.GetItemById(request.Id);
  726. state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
  727. MediaSourceInfo mediaSource = null;
  728. if (string.IsNullOrWhiteSpace(request.LiveStreamId))
  729. {
  730. TranscodingJob currentJob = !string.IsNullOrWhiteSpace(request.PlaySessionId) ?
  731. ApiEntryPoint.Instance.GetTranscodingJob(request.PlaySessionId)
  732. : null;
  733. if (currentJob != null)
  734. {
  735. mediaSource = currentJob.MediaSource;
  736. }
  737. if (mediaSource == null)
  738. {
  739. var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(request.Id, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false)).ToList();
  740. mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
  741. ? mediaSources.First()
  742. : mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId));
  743. if (mediaSource == null && string.Equals(request.Id, request.MediaSourceId, StringComparison.OrdinalIgnoreCase))
  744. {
  745. mediaSource = mediaSources.First();
  746. }
  747. }
  748. }
  749. else
  750. {
  751. var liveStreamInfo = await MediaSourceManager.GetLiveStreamWithDirectStreamProvider(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
  752. mediaSource = liveStreamInfo.Item1;
  753. state.DirectStreamProvider = liveStreamInfo.Item2;
  754. }
  755. var videoRequest = request as VideoStreamRequest;
  756. EncodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
  757. var container = Path.GetExtension(state.RequestedUrl);
  758. if (string.IsNullOrEmpty(container))
  759. {
  760. container = request.Container;
  761. }
  762. if (string.IsNullOrEmpty(container))
  763. {
  764. container = request.Static ?
  765. state.InputContainer :
  766. GetOutputFileExtension(state);
  767. }
  768. state.OutputContainer = (container ?? string.Empty).TrimStart('.');
  769. state.OutputAudioBitrate = EncodingHelper.GetAudioBitrateParam(state.Request, state.AudioStream);
  770. state.OutputAudioSampleRate = request.AudioSampleRate;
  771. state.OutputAudioCodec = state.Request.AudioCodec;
  772. state.OutputAudioChannels = EncodingHelper.GetNumAudioChannelsParam(state.Request, state.AudioStream, state.OutputAudioCodec);
  773. if (videoRequest != null)
  774. {
  775. state.OutputVideoCodec = state.VideoRequest.VideoCodec;
  776. state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
  777. if (videoRequest != null)
  778. {
  779. EncodingHelper.TryStreamCopy(state);
  780. }
  781. if (state.OutputVideoBitrate.HasValue && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
  782. {
  783. var resolution = ResolutionNormalizer.Normalize(
  784. state.VideoStream == null ? (int?)null : state.VideoStream.BitRate,
  785. state.OutputVideoBitrate.Value,
  786. state.VideoStream == null ? null : state.VideoStream.Codec,
  787. state.OutputVideoCodec,
  788. videoRequest.MaxWidth,
  789. videoRequest.MaxHeight);
  790. videoRequest.MaxWidth = resolution.MaxWidth;
  791. videoRequest.MaxHeight = resolution.MaxHeight;
  792. }
  793. ApplyDeviceProfileSettings(state);
  794. }
  795. else
  796. {
  797. ApplyDeviceProfileSettings(state);
  798. }
  799. var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
  800. ? GetOutputFileExtension(state)
  801. : ("." + state.OutputContainer);
  802. state.OutputFilePath = GetOutputFilePath(state, ext);
  803. return state;
  804. }
  805. private void ApplyDeviceProfileSettings(StreamState state)
  806. {
  807. var headers = Request.Headers.ToDictionary();
  808. if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId))
  809. {
  810. state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId);
  811. }
  812. else
  813. {
  814. if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
  815. {
  816. var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
  817. if (caps != null)
  818. {
  819. state.DeviceProfile = caps.DeviceProfile;
  820. }
  821. else
  822. {
  823. state.DeviceProfile = DlnaManager.GetProfile(headers);
  824. }
  825. }
  826. }
  827. var profile = state.DeviceProfile;
  828. if (profile == null)
  829. {
  830. // Don't use settings from the default profile.
  831. // Only use a specific profile if it was requested.
  832. return;
  833. }
  834. var audioCodec = state.ActualOutputAudioCodec;
  835. var videoCodec = state.ActualOutputVideoCodec;
  836. var mediaProfile = state.VideoRequest == null ?
  837. profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate) :
  838. profile.GetVideoMediaProfile(state.OutputContainer,
  839. audioCodec,
  840. videoCodec,
  841. state.OutputWidth,
  842. state.OutputHeight,
  843. state.TargetVideoBitDepth,
  844. state.OutputVideoBitrate,
  845. state.TargetVideoProfile,
  846. state.TargetVideoLevel,
  847. state.TargetFramerate,
  848. state.TargetPacketLength,
  849. state.TargetTimestamp,
  850. state.IsTargetAnamorphic,
  851. state.TargetRefFrames,
  852. state.TargetVideoStreamCount,
  853. state.TargetAudioStreamCount,
  854. state.TargetVideoCodecTag,
  855. state.IsTargetAVC);
  856. if (mediaProfile != null)
  857. {
  858. state.MimeType = mediaProfile.MimeType;
  859. }
  860. if (!state.Request.Static)
  861. {
  862. var transcodingProfile = state.VideoRequest == null ?
  863. profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) :
  864. profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
  865. if (transcodingProfile != null)
  866. {
  867. state.EstimateContentLength = transcodingProfile.EstimateContentLength;
  868. state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
  869. state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  870. if (state.VideoRequest != null)
  871. {
  872. state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
  873. state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
  874. }
  875. }
  876. }
  877. }
  878. private async void ReportUsage(StreamState state)
  879. {
  880. try
  881. {
  882. await ReportUsageInternal(state).ConfigureAwait(false);
  883. }
  884. catch
  885. {
  886. }
  887. }
  888. private Task ReportUsageInternal(StreamState state)
  889. {
  890. if (!ServerConfigurationManager.Configuration.EnableAnonymousUsageReporting)
  891. {
  892. return Task.FromResult(true);
  893. }
  894. if (!MediaEncoder.IsDefaultEncoderPath)
  895. {
  896. return Task.FromResult(true);
  897. }
  898. return Task.FromResult(true);
  899. //var dict = new Dictionary<string, string>();
  900. //var outputAudio = GetAudioEncoder(state);
  901. //if (!string.IsNullOrWhiteSpace(outputAudio))
  902. //{
  903. // dict["outputAudio"] = outputAudio;
  904. //}
  905. //var outputVideo = GetVideoEncoder(state);
  906. //if (!string.IsNullOrWhiteSpace(outputVideo))
  907. //{
  908. // dict["outputVideo"] = outputVideo;
  909. //}
  910. //if (ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase) &&
  911. // ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase))
  912. //{
  913. // return Task.FromResult(true);
  914. //}
  915. //dict["id"] = AppHost.SystemId;
  916. //dict["type"] = state.VideoRequest == null ? "Audio" : "Video";
  917. //var audioStream = state.AudioStream;
  918. //if (audioStream != null && !string.IsNullOrWhiteSpace(audioStream.Codec))
  919. //{
  920. // dict["inputAudio"] = audioStream.Codec;
  921. //}
  922. //var videoStream = state.VideoStream;
  923. //if (videoStream != null && !string.IsNullOrWhiteSpace(videoStream.Codec))
  924. //{
  925. // dict["inputVideo"] = videoStream.Codec;
  926. //}
  927. //var cert = GetType().Assembly.GetModules().First().GetSignerCertificate();
  928. //if (cert != null)
  929. //{
  930. // dict["assemblySig"] = cert.GetCertHashString();
  931. // dict["certSubject"] = cert.Subject ?? string.Empty;
  932. // dict["certIssuer"] = cert.Issuer ?? string.Empty;
  933. //}
  934. //else
  935. //{
  936. // return Task.FromResult(true);
  937. //}
  938. //if (state.SupportedAudioCodecs.Count > 0)
  939. //{
  940. // dict["supportedAudioCodecs"] = string.Join(",", state.SupportedAudioCodecs.ToArray());
  941. //}
  942. //var auth = AuthorizationContext.GetAuthorizationInfo(Request);
  943. //dict["appName"] = auth.Client ?? string.Empty;
  944. //dict["appVersion"] = auth.Version ?? string.Empty;
  945. //dict["device"] = auth.Device ?? string.Empty;
  946. //dict["deviceId"] = auth.DeviceId ?? string.Empty;
  947. //dict["context"] = "streaming";
  948. ////Logger.Info(JsonSerializer.SerializeToString(dict));
  949. //if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase))
  950. //{
  951. // var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList();
  952. // list.Add(outputAudio);
  953. // ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray();
  954. //}
  955. //if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase))
  956. //{
  957. // var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList();
  958. // list.Add(outputVideo);
  959. // ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray();
  960. //}
  961. //ServerConfigurationManager.SaveConfiguration();
  962. ////Logger.Info(JsonSerializer.SerializeToString(dict));
  963. //var options = new HttpRequestOptions()
  964. //{
  965. // Url = "https://mb3admin.com/admin/service/transcoding/report",
  966. // CancellationToken = CancellationToken.None,
  967. // LogRequest = false,
  968. // LogErrors = false,
  969. // BufferContent = false
  970. //};
  971. //options.RequestContent = JsonSerializer.SerializeToString(dict);
  972. //options.RequestContentType = "application/json";
  973. //return HttpClient.Post(options);
  974. }
  975. /// <summary>
  976. /// Adds the dlna headers.
  977. /// </summary>
  978. /// <param name="state">The state.</param>
  979. /// <param name="responseHeaders">The response headers.</param>
  980. /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
  981. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  982. protected void AddDlnaHeaders(StreamState state, IDictionary<string, string> responseHeaders, bool isStaticallyStreamed)
  983. {
  984. var profile = state.DeviceProfile;
  985. var transferMode = GetHeader("transferMode.dlna.org");
  986. responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode;
  987. responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*";
  988. if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase))
  989. {
  990. if (state.RunTimeTicks.HasValue)
  991. {
  992. var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
  993. responseHeaders["MediaInfo.sec"] = string.Format("SEC_Duration={0};", Convert.ToInt32(ms).ToString(CultureInfo.InvariantCulture));
  994. }
  995. }
  996. if (state.RunTimeTicks.HasValue && !isStaticallyStreamed && profile != null)
  997. {
  998. AddTimeSeekResponseHeaders(state, responseHeaders);
  999. }
  1000. if (profile == null)
  1001. {
  1002. profile = DlnaManager.GetDefaultProfile();
  1003. }
  1004. var audioCodec = state.ActualOutputAudioCodec;
  1005. if (state.VideoRequest == null)
  1006. {
  1007. responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile)
  1008. .BuildAudioHeader(
  1009. state.OutputContainer,
  1010. audioCodec,
  1011. state.OutputAudioBitrate,
  1012. state.OutputAudioSampleRate,
  1013. state.OutputAudioChannels,
  1014. isStaticallyStreamed,
  1015. state.RunTimeTicks,
  1016. state.TranscodeSeekInfo
  1017. );
  1018. }
  1019. else
  1020. {
  1021. var videoCodec = state.ActualOutputVideoCodec;
  1022. responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile)
  1023. .BuildVideoHeader(
  1024. state.OutputContainer,
  1025. videoCodec,
  1026. audioCodec,
  1027. state.OutputWidth,
  1028. state.OutputHeight,
  1029. state.TargetVideoBitDepth,
  1030. state.OutputVideoBitrate,
  1031. state.TargetTimestamp,
  1032. isStaticallyStreamed,
  1033. state.RunTimeTicks,
  1034. state.TargetVideoProfile,
  1035. state.TargetVideoLevel,
  1036. state.TargetFramerate,
  1037. state.TargetPacketLength,
  1038. state.TranscodeSeekInfo,
  1039. state.IsTargetAnamorphic,
  1040. state.TargetRefFrames,
  1041. state.TargetVideoStreamCount,
  1042. state.TargetAudioStreamCount,
  1043. state.TargetVideoCodecTag,
  1044. state.IsTargetAVC
  1045. ).FirstOrDefault() ?? string.Empty;
  1046. }
  1047. foreach (var item in responseHeaders)
  1048. {
  1049. Request.Response.AddHeader(item.Key, item.Value);
  1050. }
  1051. }
  1052. private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders)
  1053. {
  1054. var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(UsCulture);
  1055. var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(UsCulture);
  1056. responseHeaders["TimeSeekRange.dlna.org"] = string.Format("npt={0}-{1}/{1}", startSeconds, runtimeSeconds);
  1057. responseHeaders["X-AvailableSeekRange"] = string.Format("1 npt={0}-{1}", startSeconds, runtimeSeconds);
  1058. }
  1059. }
  1060. }