BaseStreamingService.cs 87 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382
  1. using MediaBrowser.Controller.Devices;
  2. using MediaBrowser.Model.Extensions;
  3. using MediaBrowser.Common.Extensions;
  4. using MediaBrowser.Common.IO;
  5. using MediaBrowser.Controller.Channels;
  6. using MediaBrowser.Controller.Configuration;
  7. using MediaBrowser.Controller.Dlna;
  8. using MediaBrowser.Controller.Entities;
  9. using MediaBrowser.Controller.Library;
  10. using MediaBrowser.Controller.LiveTv;
  11. using MediaBrowser.Controller.MediaEncoding;
  12. using MediaBrowser.Model.Configuration;
  13. using MediaBrowser.Model.Dlna;
  14. using MediaBrowser.Model.Drawing;
  15. using MediaBrowser.Model.Dto;
  16. using MediaBrowser.Model.Entities;
  17. using MediaBrowser.Model.IO;
  18. using MediaBrowser.Model.MediaInfo;
  19. using System;
  20. using System.Collections.Generic;
  21. using System.Diagnostics;
  22. using System.Globalization;
  23. using System.IO;
  24. using System.Linq;
  25. using System.Text;
  26. using System.Threading;
  27. using System.Threading.Tasks;
  28. namespace MediaBrowser.Api.Playback
  29. {
  30. /// <summary>
  31. /// Class BaseStreamingService
  32. /// </summary>
  33. public abstract class BaseStreamingService : BaseApiService
  34. {
  35. /// <summary>
  36. /// Gets or sets the application paths.
  37. /// </summary>
  38. /// <value>The application paths.</value>
  39. protected IServerConfigurationManager ServerConfigurationManager { get; private set; }
  40. /// <summary>
  41. /// Gets or sets the user manager.
  42. /// </summary>
  43. /// <value>The user manager.</value>
  44. protected IUserManager UserManager { get; private set; }
  45. /// <summary>
  46. /// Gets or sets the library manager.
  47. /// </summary>
  48. /// <value>The library manager.</value>
  49. protected ILibraryManager LibraryManager { get; private set; }
  50. /// <summary>
  51. /// Gets or sets the iso manager.
  52. /// </summary>
  53. /// <value>The iso manager.</value>
  54. protected IIsoManager IsoManager { get; private set; }
  55. /// <summary>
  56. /// Gets or sets the media encoder.
  57. /// </summary>
  58. /// <value>The media encoder.</value>
  59. protected IMediaEncoder MediaEncoder { get; private set; }
  60. protected IFileSystem FileSystem { get; private set; }
  61. protected ILiveTvManager LiveTvManager { get; private set; }
  62. protected IDlnaManager DlnaManager { get; private set; }
  63. protected IDeviceManager DeviceManager { get; private set; }
  64. protected IChannelManager ChannelManager { get; private set; }
  65. protected ISubtitleEncoder SubtitleEncoder { get; private set; }
  66. /// <summary>
  67. /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
  68. /// </summary>
  69. protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager)
  70. {
  71. DeviceManager = deviceManager;
  72. SubtitleEncoder = subtitleEncoder;
  73. ChannelManager = channelManager;
  74. DlnaManager = dlnaManager;
  75. LiveTvManager = liveTvManager;
  76. FileSystem = fileSystem;
  77. ServerConfigurationManager = serverConfig;
  78. UserManager = userManager;
  79. LibraryManager = libraryManager;
  80. IsoManager = isoManager;
  81. MediaEncoder = mediaEncoder;
  82. }
  83. /// <summary>
  84. /// Gets the command line arguments.
  85. /// </summary>
  86. /// <param name="outputPath">The output path.</param>
  87. /// <param name="transcodingJobId">The transcoding job identifier.</param>
  88. /// <param name="state">The state.</param>
  89. /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param>
  90. /// <returns>System.String.</returns>
  91. protected abstract string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding);
  92. /// <summary>
  93. /// Gets the type of the transcoding job.
  94. /// </summary>
  95. /// <value>The type of the transcoding job.</value>
  96. protected abstract TranscodingJobType TranscodingJobType { get; }
  97. /// <summary>
  98. /// Gets the output file extension.
  99. /// </summary>
  100. /// <param name="state">The state.</param>
  101. /// <returns>System.String.</returns>
  102. protected virtual string GetOutputFileExtension(StreamState state)
  103. {
  104. return Path.GetExtension(state.RequestedUrl);
  105. }
  106. /// <summary>
  107. /// Gets the output file path.
  108. /// </summary>
  109. /// <param name="state">The state.</param>
  110. /// <returns>System.String.</returns>
  111. private string GetOutputFilePath(StreamState state)
  112. {
  113. var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath;
  114. var outputFileExtension = GetOutputFileExtension(state);
  115. var data = GetCommandLineArguments("dummy\\dummy", "dummyTranscodingId", state, false);
  116. data += "-" + (state.Request.DeviceId ?? string.Empty);
  117. data += "-" + (state.Request.ClientTime ?? string.Empty);
  118. return Path.Combine(folder, data.GetMD5().ToString("N") + (outputFileExtension ?? string.Empty).ToLower());
  119. }
  120. protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
  121. /// <summary>
  122. /// Gets the fast seek command line parameter.
  123. /// </summary>
  124. /// <param name="request">The request.</param>
  125. /// <returns>System.String.</returns>
  126. /// <value>The fast seek command line parameter.</value>
  127. protected string GetFastSeekCommandLineParameter(StreamRequest request)
  128. {
  129. var time = request.StartTimeTicks;
  130. if (time.HasValue && time.Value > 0)
  131. {
  132. return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value));
  133. }
  134. return string.Empty;
  135. }
  136. /// <summary>
  137. /// Gets the map args.
  138. /// </summary>
  139. /// <param name="state">The state.</param>
  140. /// <returns>System.String.</returns>
  141. protected virtual string GetMapArgs(StreamState state)
  142. {
  143. // If we don't have known media info
  144. // If input is video, use -sn to drop subtitles
  145. // Otherwise just return empty
  146. if (state.VideoStream == null && state.AudioStream == null)
  147. {
  148. return state.IsInputVideo ? "-sn" : string.Empty;
  149. }
  150. // We have media info, but we don't know the stream indexes
  151. if (state.VideoStream != null && state.VideoStream.Index == -1)
  152. {
  153. return "-sn";
  154. }
  155. // We have media info, but we don't know the stream indexes
  156. if (state.AudioStream != null && state.AudioStream.Index == -1)
  157. {
  158. return state.IsInputVideo ? "-sn" : string.Empty;
  159. }
  160. var args = string.Empty;
  161. if (state.VideoStream != null)
  162. {
  163. args += string.Format("-map 0:{0}", state.VideoStream.Index);
  164. }
  165. else
  166. {
  167. args += "-map -0:v";
  168. }
  169. if (state.AudioStream != null)
  170. {
  171. args += string.Format(" -map 0:{0}", state.AudioStream.Index);
  172. }
  173. else
  174. {
  175. args += " -map -0:a";
  176. }
  177. if (state.SubtitleStream == null)
  178. {
  179. args += " -map -0:s";
  180. }
  181. else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
  182. {
  183. args += " -map 1:0 -sn";
  184. }
  185. return args;
  186. }
  187. /// <summary>
  188. /// Determines which stream will be used for playback
  189. /// </summary>
  190. /// <param name="allStream">All stream.</param>
  191. /// <param name="desiredIndex">Index of the desired.</param>
  192. /// <param name="type">The type.</param>
  193. /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param>
  194. /// <returns>MediaStream.</returns>
  195. private MediaStream GetMediaStream(IEnumerable<MediaStream> allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true)
  196. {
  197. var streams = allStream.Where(s => s.Type == type).OrderBy(i => i.Index).ToList();
  198. if (desiredIndex.HasValue)
  199. {
  200. var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value);
  201. if (stream != null)
  202. {
  203. return stream;
  204. }
  205. }
  206. if (type == MediaStreamType.Video)
  207. {
  208. streams = streams.Where(i => !string.Equals(i.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)).ToList();
  209. }
  210. if (returnFirstIfNoIndex && type == MediaStreamType.Audio)
  211. {
  212. return streams.FirstOrDefault(i => i.Channels.HasValue && i.Channels.Value > 0) ??
  213. streams.FirstOrDefault();
  214. }
  215. // Just return the first one
  216. return returnFirstIfNoIndex ? streams.FirstOrDefault() : null;
  217. }
  218. protected EncodingQuality GetQualitySetting()
  219. {
  220. var quality = ApiEntryPoint.Instance.GetEncodingOptions().EncodingQuality;
  221. if (quality == EncodingQuality.Auto)
  222. {
  223. var cpuCount = Environment.ProcessorCount;
  224. if (cpuCount >= 4)
  225. {
  226. //return EncodingQuality.HighQuality;
  227. }
  228. return EncodingQuality.HighSpeed;
  229. }
  230. return quality;
  231. }
  232. /// <summary>
  233. /// Gets the number of threads.
  234. /// </summary>
  235. /// <returns>System.Int32.</returns>
  236. protected int GetNumberOfThreads(StreamState state, bool isWebm)
  237. {
  238. if (isWebm)
  239. {
  240. // Recommended per docs
  241. return Math.Max(Environment.ProcessorCount - 1, 2);
  242. }
  243. return 0;
  244. }
  245. protected string H264Encoder
  246. {
  247. get
  248. {
  249. var lib = ApiEntryPoint.Instance.GetEncodingOptions().H264Encoder;
  250. if (!string.IsNullOrWhiteSpace(lib))
  251. {
  252. return lib;
  253. }
  254. return "libx264";
  255. }
  256. }
  257. /// <summary>
  258. /// Gets the video bitrate to specify on the command line
  259. /// </summary>
  260. /// <param name="state">The state.</param>
  261. /// <param name="videoCodec">The video codec.</param>
  262. /// <param name="isHls">if set to <c>true</c> [is HLS].</param>
  263. /// <returns>System.String.</returns>
  264. protected string GetVideoQualityParam(StreamState state, string videoCodec, bool isHls)
  265. {
  266. var param = string.Empty;
  267. var isVc1 = state.VideoStream != null &&
  268. string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
  269. var qualitySetting = GetQualitySetting();
  270. if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
  271. {
  272. param = "-preset superfast";
  273. switch (qualitySetting)
  274. {
  275. case EncodingQuality.HighSpeed:
  276. param += " -crf 23";
  277. break;
  278. case EncodingQuality.HighQuality:
  279. param += " -crf 20";
  280. break;
  281. case EncodingQuality.MaxQuality:
  282. param += " -crf 18";
  283. break;
  284. }
  285. }
  286. else if (string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
  287. {
  288. param = "-preset fast";
  289. switch (qualitySetting)
  290. {
  291. case EncodingQuality.HighSpeed:
  292. param += " -crf 28";
  293. break;
  294. case EncodingQuality.HighQuality:
  295. param += " -crf 25";
  296. break;
  297. case EncodingQuality.MaxQuality:
  298. param += " -crf 21";
  299. break;
  300. }
  301. }
  302. // h264 (h264_qsv)
  303. else if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
  304. {
  305. switch (qualitySetting)
  306. {
  307. case EncodingQuality.HighSpeed:
  308. param = "-preset 7";
  309. break;
  310. case EncodingQuality.HighQuality:
  311. param = "-preset 4";
  312. break;
  313. case EncodingQuality.MaxQuality:
  314. param = "-preset 1";
  315. break;
  316. }
  317. }
  318. // h264 (libnvenc)
  319. else if (string.Equals(videoCodec, "libnvenc", StringComparison.OrdinalIgnoreCase))
  320. {
  321. switch (qualitySetting)
  322. {
  323. case EncodingQuality.HighSpeed:
  324. param = "-preset high-performance";
  325. break;
  326. case EncodingQuality.HighQuality:
  327. param = "";
  328. break;
  329. case EncodingQuality.MaxQuality:
  330. param = "-preset high-quality";
  331. break;
  332. }
  333. }
  334. // webm
  335. else if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
  336. {
  337. // Values 0-3, 0 being highest quality but slower
  338. var profileScore = 0;
  339. string crf;
  340. var qmin = "0";
  341. var qmax = "50";
  342. switch (qualitySetting)
  343. {
  344. case EncodingQuality.HighSpeed:
  345. crf = "10";
  346. break;
  347. case EncodingQuality.HighQuality:
  348. crf = "6";
  349. break;
  350. case EncodingQuality.MaxQuality:
  351. crf = "4";
  352. break;
  353. default:
  354. throw new ArgumentException("Unrecognized quality setting");
  355. }
  356. if (isVc1)
  357. {
  358. profileScore++;
  359. }
  360. // Max of 2
  361. profileScore = Math.Min(profileScore, 2);
  362. // http://www.webmproject.org/docs/encoder-parameters/
  363. param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
  364. profileScore.ToString(UsCulture),
  365. crf,
  366. qmin,
  367. qmax);
  368. }
  369. else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase))
  370. {
  371. param = "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
  372. }
  373. // asf/wmv
  374. else if (string.Equals(videoCodec, "wmv2", StringComparison.OrdinalIgnoreCase))
  375. {
  376. param = "-qmin 2";
  377. }
  378. else if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
  379. {
  380. param = "-mbd 2";
  381. }
  382. param += GetVideoBitrateParam(state, videoCodec, isHls);
  383. var framerate = GetFramerateParam(state);
  384. if (framerate.HasValue)
  385. {
  386. param += string.Format(" -r {0}", framerate.Value.ToString(UsCulture));
  387. }
  388. if (!string.IsNullOrEmpty(state.OutputVideoSync))
  389. {
  390. param += " -vsync " + state.OutputVideoSync;
  391. }
  392. if (!string.IsNullOrEmpty(state.VideoRequest.Profile))
  393. {
  394. param += " -profile:v " + state.VideoRequest.Profile;
  395. }
  396. if (!string.IsNullOrEmpty(state.VideoRequest.Level))
  397. {
  398. // h264_qsv and libnvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
  399. if (String.Equals(H264Encoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || String.Equals(H264Encoder, "libnvenc", StringComparison.OrdinalIgnoreCase))
  400. {
  401. switch (state.VideoRequest.Level)
  402. {
  403. case "30":
  404. param += " -level 3";
  405. break;
  406. case "31":
  407. param += " -level 3.1";
  408. break;
  409. case "32":
  410. param += " -level 3.2";
  411. break;
  412. case "40":
  413. param += " -level 4";
  414. break;
  415. case "41":
  416. param += " -level 4.1";
  417. break;
  418. case "42":
  419. param += " -level 4.2";
  420. break;
  421. case "50":
  422. param += " -level 5";
  423. break;
  424. case "51":
  425. param += " -level 5.1";
  426. break;
  427. case "52":
  428. param += " -level 5.2";
  429. break;
  430. default:
  431. param += " -level " + state.VideoRequest.Level;
  432. break;
  433. }
  434. }
  435. else
  436. {
  437. param += " -level " + state.VideoRequest.Level;
  438. }
  439. }
  440. return "-pix_fmt yuv420p " + param;
  441. }
  442. protected string GetAudioFilterParam(StreamState state, bool isHls)
  443. {
  444. var volParam = string.Empty;
  445. var audioSampleRate = string.Empty;
  446. var channels = state.OutputAudioChannels;
  447. // Boost volume to 200% when downsampling from 6ch to 2ch
  448. if (channels.HasValue && channels.Value <= 2)
  449. {
  450. if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
  451. {
  452. volParam = ",volume=" + ApiEntryPoint.Instance.GetEncodingOptions().DownMixAudioBoost.ToString(UsCulture);
  453. }
  454. }
  455. if (state.OutputAudioSampleRate.HasValue)
  456. {
  457. audioSampleRate = state.OutputAudioSampleRate.Value + ":";
  458. }
  459. var adelay = isHls ? "adelay=1," : string.Empty;
  460. var pts = string.Empty;
  461. if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
  462. {
  463. var seconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds;
  464. pts = string.Format(",asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(UsCulture));
  465. }
  466. return string.Format("-af \"{0}aresample={1}async={4}{2}{3}\"",
  467. adelay,
  468. audioSampleRate,
  469. volParam,
  470. pts,
  471. state.OutputAudioSync);
  472. }
  473. /// <summary>
  474. /// If we're going to put a fixed size on the command line, this will calculate it
  475. /// </summary>
  476. /// <param name="state">The state.</param>
  477. /// <param name="outputVideoCodec">The output video codec.</param>
  478. /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param>
  479. /// <returns>System.String.</returns>
  480. protected string GetOutputSizeParam(StreamState state,
  481. string outputVideoCodec,
  482. bool allowTimeStampCopy = true)
  483. {
  484. // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
  485. var request = state.VideoRequest;
  486. var filters = new List<string>();
  487. if (state.DeInterlace)
  488. {
  489. filters.Add("yadif=0:-1:0");
  490. }
  491. // If fixed dimensions were supplied
  492. if (request.Width.HasValue && request.Height.HasValue)
  493. {
  494. var widthParam = request.Width.Value.ToString(UsCulture);
  495. var heightParam = request.Height.Value.ToString(UsCulture);
  496. filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam));
  497. }
  498. // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
  499. else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
  500. {
  501. var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
  502. var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
  503. filters.Add(string.Format("scale=trunc(min(iw\\,{0})/2)*2:trunc(min((iw/dar)\\,{1})/2)*2", maxWidthParam, maxHeightParam));
  504. }
  505. // If a fixed width was requested
  506. else if (request.Width.HasValue)
  507. {
  508. var widthParam = request.Width.Value.ToString(UsCulture);
  509. filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam));
  510. }
  511. // If a fixed height was requested
  512. else if (request.Height.HasValue)
  513. {
  514. var heightParam = request.Height.Value.ToString(UsCulture);
  515. filters.Add(string.Format("scale=trunc(oh*a*2)/2:{0}", heightParam));
  516. }
  517. // If a max width was requested
  518. else if (request.MaxWidth.HasValue && (!request.MaxHeight.HasValue || state.VideoStream == null))
  519. {
  520. var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
  521. filters.Add(string.Format("scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam));
  522. }
  523. // If a max height was requested
  524. else if (request.MaxHeight.HasValue && (!request.MaxWidth.HasValue || state.VideoStream == null))
  525. {
  526. var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
  527. filters.Add(string.Format("scale=trunc(oh*a*2)/2:min(ih\\,{0})", maxHeightParam));
  528. }
  529. else if (request.MaxWidth.HasValue ||
  530. request.MaxHeight.HasValue ||
  531. request.Width.HasValue ||
  532. request.Height.HasValue)
  533. {
  534. if (state.VideoStream != null)
  535. {
  536. // Need to perform calculations manually
  537. // Try to account for bad media info
  538. var currentHeight = state.VideoStream.Height ?? request.MaxHeight ?? request.Height ?? 0;
  539. var currentWidth = state.VideoStream.Width ?? request.MaxWidth ?? request.Width ?? 0;
  540. var outputSize = DrawingUtils.Resize(currentWidth, currentHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
  541. var manualWidthParam = outputSize.Width.ToString(UsCulture);
  542. var manualHeightParam = outputSize.Height.ToString(UsCulture);
  543. filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", manualWidthParam, manualHeightParam));
  544. }
  545. }
  546. if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
  547. {
  548. filters[filters.Count - 1] += ":flags=fast_bilinear";
  549. }
  550. var output = string.Empty;
  551. if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
  552. {
  553. var subParam = GetTextSubtitleParam(state);
  554. filters.Add(subParam);
  555. if (allowTimeStampCopy)
  556. {
  557. output += " -copyts";
  558. }
  559. }
  560. if (filters.Count > 0)
  561. {
  562. output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
  563. }
  564. return output;
  565. }
  566. /// <summary>
  567. /// Gets the text subtitle param.
  568. /// </summary>
  569. /// <param name="state">The state.</param>
  570. /// <returns>System.String.</returns>
  571. protected string GetTextSubtitleParam(StreamState state)
  572. {
  573. var seconds = Math.Round(TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds);
  574. if (state.SubtitleStream.IsExternal)
  575. {
  576. var subtitlePath = state.SubtitleStream.Path;
  577. var charsetParam = string.Empty;
  578. if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
  579. {
  580. var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath);
  581. if (!string.IsNullOrEmpty(charenc))
  582. {
  583. charsetParam = ":charenc=" + charenc;
  584. }
  585. }
  586. // TODO: Perhaps also use original_size=1920x800 ??
  587. return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB",
  588. subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"),
  589. charsetParam,
  590. seconds.ToString(UsCulture));
  591. }
  592. return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
  593. state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
  594. state.InternalSubtitleStreamOffset.ToString(UsCulture),
  595. seconds.ToString(UsCulture));
  596. }
  597. /// <summary>
  598. /// Gets the internal graphical subtitle param.
  599. /// </summary>
  600. /// <param name="state">The state.</param>
  601. /// <param name="outputVideoCodec">The output video codec.</param>
  602. /// <returns>System.String.</returns>
  603. protected string GetGraphicalSubtitleParam(StreamState state, string outputVideoCodec)
  604. {
  605. var outputSizeParam = string.Empty;
  606. var request = state.VideoRequest;
  607. // Add resolution params, if specified
  608. if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
  609. {
  610. outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"');
  611. outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
  612. }
  613. var videoSizeParam = string.Empty;
  614. if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
  615. {
  616. videoSizeParam = string.Format(",scale={0}:{1}", state.VideoStream.Width.Value.ToString(UsCulture), state.VideoStream.Height.Value.ToString(UsCulture));
  617. }
  618. var mapPrefix = state.SubtitleStream.IsExternal ?
  619. 1 :
  620. 0;
  621. var subtitleStreamIndex = state.SubtitleStream.IsExternal
  622. ? 0
  623. : state.SubtitleStream.Index;
  624. return string.Format(" -filter_complex \"[{0}:{1}]format=yuva444p{4},lut=u=128:v=128:y=gammaval(.3)[sub] ; [0:{2}] [sub] overlay{3}\"",
  625. mapPrefix.ToString(UsCulture),
  626. subtitleStreamIndex.ToString(UsCulture),
  627. state.VideoStream.Index.ToString(UsCulture),
  628. outputSizeParam,
  629. videoSizeParam);
  630. }
  631. /// <summary>
  632. /// Gets the probe size argument.
  633. /// </summary>
  634. /// <param name="state">The state.</param>
  635. /// <returns>System.String.</returns>
  636. private string GetProbeSizeArgument(StreamState state)
  637. {
  638. if (state.PlayableStreamFileNames.Count > 0)
  639. {
  640. return MediaEncoder.GetProbeSizeArgument(state.PlayableStreamFileNames.ToArray(), state.InputProtocol);
  641. }
  642. return MediaEncoder.GetProbeSizeArgument(new[] { state.MediaPath }, state.InputProtocol);
  643. }
  644. /// <summary>
  645. /// Gets the number of audio channels to specify on the command line
  646. /// </summary>
  647. /// <param name="request">The request.</param>
  648. /// <param name="audioStream">The audio stream.</param>
  649. /// <param name="outputAudioCodec">The output audio codec.</param>
  650. /// <returns>System.Nullable{System.Int32}.</returns>
  651. private int? GetNumAudioChannelsParam(StreamRequest request, MediaStream audioStream, string outputAudioCodec)
  652. {
  653. if (audioStream != null)
  654. {
  655. var codec = outputAudioCodec ?? string.Empty;
  656. if (audioStream.Channels > 2 && codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
  657. {
  658. // wmav2 currently only supports two channel output
  659. return 2;
  660. }
  661. }
  662. if (request.MaxAudioChannels.HasValue)
  663. {
  664. if (audioStream != null && audioStream.Channels.HasValue)
  665. {
  666. return Math.Min(request.MaxAudioChannels.Value, audioStream.Channels.Value);
  667. }
  668. // If we don't have any media info then limit it to 5 to prevent encoding errors due to asking for too many channels
  669. return Math.Min(request.MaxAudioChannels.Value, 5);
  670. }
  671. return request.AudioChannels;
  672. }
  673. /// <summary>
  674. /// Determines whether the specified stream is H264.
  675. /// </summary>
  676. /// <param name="stream">The stream.</param>
  677. /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
  678. protected bool IsH264(MediaStream stream)
  679. {
  680. var codec = stream.Codec ?? string.Empty;
  681. return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
  682. codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
  683. }
  684. /// <summary>
  685. /// Gets the name of the output audio codec
  686. /// </summary>
  687. /// <param name="request">The request.</param>
  688. /// <returns>System.String.</returns>
  689. private string GetAudioCodec(StreamRequest request)
  690. {
  691. var codec = request.AudioCodec;
  692. if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
  693. {
  694. return "aac -strict experimental";
  695. }
  696. if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
  697. {
  698. return "libmp3lame";
  699. }
  700. if (string.Equals(codec, "vorbis", StringComparison.OrdinalIgnoreCase))
  701. {
  702. return "libvorbis";
  703. }
  704. if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase))
  705. {
  706. return "wmav2";
  707. }
  708. return codec.ToLower();
  709. }
  710. /// <summary>
  711. /// Gets the name of the output video codec
  712. /// </summary>
  713. /// <param name="request">The request.</param>
  714. /// <returns>System.String.</returns>
  715. private string GetVideoCodec(VideoStreamRequest request)
  716. {
  717. var codec = request.VideoCodec;
  718. if (!string.IsNullOrEmpty(codec))
  719. {
  720. if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
  721. {
  722. return H264Encoder;
  723. }
  724. if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
  725. {
  726. return "libvpx";
  727. }
  728. if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase))
  729. {
  730. return "wmv2";
  731. }
  732. if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase))
  733. {
  734. return "libtheora";
  735. }
  736. return codec.ToLower();
  737. }
  738. return "copy";
  739. }
  740. private bool SupportsThrottleWithStream
  741. {
  742. get
  743. {
  744. return false;
  745. }
  746. }
  747. /// <summary>
  748. /// Gets the input argument.
  749. /// </summary>
  750. /// <param name="transcodingJobId">The transcoding job identifier.</param>
  751. /// <param name="state">The state.</param>
  752. /// <returns>System.String.</returns>
  753. protected string GetInputArgument(string transcodingJobId, StreamState state)
  754. {
  755. var arg = "-i " + GetInputPathArgument(transcodingJobId, state);
  756. if (state.SubtitleStream != null)
  757. {
  758. if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
  759. {
  760. arg += " -i \"" + state.SubtitleStream.Path + "\"";
  761. }
  762. }
  763. return arg;
  764. }
  765. private string GetInputPathArgument(string transcodingJobId, StreamState state)
  766. {
  767. if (state.InputProtocol == MediaProtocol.File &&
  768. state.RunTimeTicks.HasValue &&
  769. state.VideoType == VideoType.VideoFile &&
  770. !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
  771. {
  772. if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
  773. {
  774. if (SupportsThrottleWithStream)
  775. {
  776. var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId;
  777. url += "&transcodingJobId=" + transcodingJobId;
  778. return string.Format("\"{0}\"", url);
  779. }
  780. }
  781. }
  782. var protocol = state.InputProtocol;
  783. var inputPath = new[] { state.MediaPath };
  784. if (state.IsInputVideo)
  785. {
  786. if (!(state.VideoType == VideoType.Iso && state.IsoMount == null))
  787. {
  788. inputPath = MediaEncoderHelpers.GetInputArgument(state.MediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames);
  789. }
  790. }
  791. return MediaEncoder.GetInputArgument(inputPath, protocol);
  792. }
  793. private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
  794. {
  795. if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
  796. {
  797. state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
  798. }
  799. if (string.IsNullOrEmpty(state.MediaPath))
  800. {
  801. var checkCodecs = false;
  802. if (string.Equals(state.ItemType, typeof(LiveTvChannel).Name))
  803. {
  804. var streamInfo = await LiveTvManager.GetChannelStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false);
  805. state.LiveTvStreamId = streamInfo.Id;
  806. state.MediaPath = streamInfo.Path;
  807. state.InputProtocol = streamInfo.Protocol;
  808. await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
  809. AttachMediaStreamInfo(state, streamInfo, state.VideoRequest, state.RequestedUrl);
  810. checkCodecs = true;
  811. }
  812. else if (string.Equals(state.ItemType, typeof(LiveTvVideoRecording).Name) ||
  813. string.Equals(state.ItemType, typeof(LiveTvAudioRecording).Name))
  814. {
  815. var streamInfo = await LiveTvManager.GetRecordingStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false);
  816. state.LiveTvStreamId = streamInfo.Id;
  817. state.MediaPath = streamInfo.Path;
  818. state.InputProtocol = streamInfo.Protocol;
  819. await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
  820. AttachMediaStreamInfo(state, streamInfo, state.VideoRequest, state.RequestedUrl);
  821. checkCodecs = true;
  822. }
  823. var videoRequest = state.VideoRequest;
  824. if (videoRequest != null && checkCodecs)
  825. {
  826. if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
  827. {
  828. state.OutputVideoCodec = "copy";
  829. }
  830. if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
  831. {
  832. state.OutputAudioCodec = "copy";
  833. }
  834. }
  835. }
  836. }
  837. /// <summary>
  838. /// Starts the FFMPEG.
  839. /// </summary>
  840. /// <param name="state">The state.</param>
  841. /// <param name="outputPath">The output path.</param>
  842. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  843. /// <param name="workingDirectory">The working directory.</param>
  844. /// <returns>Task.</returns>
  845. protected async Task<TranscodingJob> StartFfMpeg(StreamState state,
  846. string outputPath,
  847. CancellationTokenSource cancellationTokenSource,
  848. string workingDirectory = null)
  849. {
  850. Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
  851. await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
  852. var transcodingId = Guid.NewGuid().ToString("N");
  853. var commandLineArgs = GetCommandLineArguments(outputPath, transcodingId, state, true);
  854. if (ApiEntryPoint.Instance.GetEncodingOptions().EnableDebugLogging)
  855. {
  856. commandLineArgs = "-loglevel debug " + commandLineArgs;
  857. }
  858. var process = new Process
  859. {
  860. StartInfo = new ProcessStartInfo
  861. {
  862. CreateNoWindow = true,
  863. UseShellExecute = false,
  864. // Must consume both stdout and stderr or deadlocks may occur
  865. RedirectStandardOutput = true,
  866. RedirectStandardError = true,
  867. RedirectStandardInput = true,
  868. FileName = MediaEncoder.EncoderPath,
  869. Arguments = commandLineArgs,
  870. WindowStyle = ProcessWindowStyle.Hidden,
  871. ErrorDialog = false
  872. },
  873. EnableRaisingEvents = true
  874. };
  875. if (!string.IsNullOrWhiteSpace(workingDirectory))
  876. {
  877. process.StartInfo.WorkingDirectory = workingDirectory;
  878. }
  879. var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
  880. transcodingId,
  881. TranscodingJobType,
  882. process,
  883. state.Request.DeviceId,
  884. state,
  885. cancellationTokenSource);
  886. var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
  887. Logger.Info(commandLineLogMessage);
  888. var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, "transcode-" + Guid.NewGuid() + ".txt");
  889. Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
  890. // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
  891. state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
  892. var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
  893. await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
  894. process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
  895. try
  896. {
  897. process.Start();
  898. }
  899. catch (Exception ex)
  900. {
  901. Logger.ErrorException("Error starting ffmpeg", ex);
  902. ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
  903. throw;
  904. }
  905. // MUST read both stdout and stderr asynchronously or a deadlock may occurr
  906. process.BeginOutputReadLine();
  907. // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
  908. StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream);
  909. // Wait for the file to exist before proceeeding
  910. while (!File.Exists(outputPath) && !transcodingJob.HasExited)
  911. {
  912. await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
  913. }
  914. if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive)
  915. {
  916. await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
  917. if (state.ReadInputAtNativeFramerate)
  918. {
  919. await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
  920. }
  921. }
  922. return transcodingJob;
  923. }
  924. private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
  925. {
  926. try
  927. {
  928. using (var reader = new StreamReader(source))
  929. {
  930. while (!reader.EndOfStream)
  931. {
  932. var line = await reader.ReadLineAsync().ConfigureAwait(false);
  933. ParseLogLine(line, transcodingJob, state);
  934. var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
  935. await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
  936. }
  937. }
  938. }
  939. catch (Exception ex)
  940. {
  941. Logger.ErrorException("Error reading ffmpeg log", ex);
  942. }
  943. }
  944. private void ParseLogLine(string line, TranscodingJob transcodingJob, StreamState state)
  945. {
  946. float? framerate = null;
  947. double? percent = null;
  948. TimeSpan? transcodingPosition = null;
  949. long? bytesTranscoded = null;
  950. var parts = line.Split(' ');
  951. var totalMs = state.RunTimeTicks.HasValue
  952. ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
  953. : 0;
  954. var startMs = state.Request.StartTimeTicks.HasValue
  955. ? TimeSpan.FromTicks(state.Request.StartTimeTicks.Value).TotalMilliseconds
  956. : 0;
  957. for (var i = 0; i < parts.Length; i++)
  958. {
  959. var part = parts[i];
  960. if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) &&
  961. (i + 1 < parts.Length))
  962. {
  963. var rate = parts[i + 1];
  964. float val;
  965. if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
  966. {
  967. framerate = val;
  968. }
  969. }
  970. else if (state.RunTimeTicks.HasValue &&
  971. part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
  972. {
  973. var time = part.Split(new[] { '=' }, 2).Last();
  974. TimeSpan val;
  975. if (TimeSpan.TryParse(time, UsCulture, out val))
  976. {
  977. var currentMs = startMs + val.TotalMilliseconds;
  978. var percentVal = currentMs / totalMs;
  979. percent = 100 * percentVal;
  980. transcodingPosition = val;
  981. }
  982. }
  983. else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
  984. {
  985. var size = part.Split(new[] { '=' }, 2).Last();
  986. int? scale = null;
  987. if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
  988. {
  989. scale = 1024;
  990. size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
  991. }
  992. if (scale.HasValue)
  993. {
  994. long val;
  995. if (long.TryParse(size, NumberStyles.Any, UsCulture, out val))
  996. {
  997. bytesTranscoded = val * scale.Value;
  998. }
  999. }
  1000. }
  1001. }
  1002. if (framerate.HasValue || percent.HasValue)
  1003. {
  1004. ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded);
  1005. }
  1006. }
  1007. private int? GetVideoBitrateParamValue(VideoStreamRequest request, MediaStream videoStream)
  1008. {
  1009. var bitrate = request.VideoBitRate;
  1010. if (videoStream != null)
  1011. {
  1012. var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue &&
  1013. request.Height.Value > videoStream.Height.Value;
  1014. if (request.Width.HasValue && videoStream.Width.HasValue &&
  1015. request.Width.Value > videoStream.Width.Value)
  1016. {
  1017. isUpscaling = true;
  1018. }
  1019. // Don't allow bitrate increases unless upscaling
  1020. if (!isUpscaling)
  1021. {
  1022. if (bitrate.HasValue && videoStream.BitRate.HasValue)
  1023. {
  1024. bitrate = Math.Min(bitrate.Value, videoStream.BitRate.Value);
  1025. }
  1026. }
  1027. }
  1028. return bitrate;
  1029. }
  1030. protected string GetVideoBitrateParam(StreamState state, string videoCodec, bool isHls)
  1031. {
  1032. var bitrate = state.OutputVideoBitrate;
  1033. if (bitrate.HasValue)
  1034. {
  1035. var hasFixedResolution = state.VideoRequest.HasFixedResolution;
  1036. if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
  1037. {
  1038. if (hasFixedResolution)
  1039. {
  1040. return string.Format(" -minrate:v ({0}*.90) -maxrate:v ({0}*1.10) -bufsize:v {0} -b:v {0}", bitrate.Value.ToString(UsCulture));
  1041. }
  1042. // With vpx when crf is used, b:v becomes a max rate
  1043. // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up.
  1044. return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture));
  1045. }
  1046. if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
  1047. {
  1048. return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
  1049. }
  1050. // h264_qsv
  1051. if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "libnvenc", StringComparison.OrdinalIgnoreCase))
  1052. {
  1053. if (hasFixedResolution)
  1054. {
  1055. if (isHls)
  1056. {
  1057. return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
  1058. }
  1059. return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
  1060. }
  1061. return string.Format(" -b:v {0} -maxrate ({0}*1.2) -bufsize ({0}*2)", bitrate.Value.ToString(UsCulture));
  1062. }
  1063. // H264
  1064. if (hasFixedResolution)
  1065. {
  1066. if (isHls)
  1067. {
  1068. return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
  1069. }
  1070. return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
  1071. }
  1072. return string.Format(" -maxrate {0} -bufsize {1}",
  1073. bitrate.Value.ToString(UsCulture),
  1074. (bitrate.Value * 2).ToString(UsCulture));
  1075. }
  1076. return string.Empty;
  1077. }
  1078. private int? GetAudioBitrateParam(StreamRequest request, MediaStream audioStream)
  1079. {
  1080. if (request.AudioBitRate.HasValue)
  1081. {
  1082. // Make sure we don't request a bitrate higher than the source
  1083. var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value;
  1084. return request.AudioBitRate.Value;
  1085. //return Math.Min(currentBitrate, request.AudioBitRate.Value);
  1086. }
  1087. return null;
  1088. }
  1089. /// <summary>
  1090. /// Gets the user agent param.
  1091. /// </summary>
  1092. /// <param name="state">The state.</param>
  1093. /// <returns>System.String.</returns>
  1094. private string GetUserAgentParam(StreamState state)
  1095. {
  1096. string useragent = null;
  1097. state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
  1098. if (!string.IsNullOrWhiteSpace(useragent))
  1099. {
  1100. return "-user-agent \"" + useragent + "\"";
  1101. }
  1102. return string.Empty;
  1103. }
  1104. /// <summary>
  1105. /// Processes the exited.
  1106. /// </summary>
  1107. /// <param name="process">The process.</param>
  1108. /// <param name="job">The job.</param>
  1109. /// <param name="state">The state.</param>
  1110. private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
  1111. {
  1112. if (job != null)
  1113. {
  1114. job.HasExited = true;
  1115. }
  1116. Logger.Debug("Disposing stream resources");
  1117. state.Dispose();
  1118. try
  1119. {
  1120. Logger.Info("FFMpeg exited with code {0}", process.ExitCode);
  1121. }
  1122. catch
  1123. {
  1124. Logger.Error("FFMpeg exited with an error.");
  1125. }
  1126. // This causes on exited to be called twice:
  1127. //try
  1128. //{
  1129. // // Dispose the process
  1130. // process.Dispose();
  1131. //}
  1132. //catch (Exception ex)
  1133. //{
  1134. // Logger.ErrorException("Error disposing ffmpeg.", ex);
  1135. //}
  1136. }
  1137. protected double? GetFramerateParam(StreamState state)
  1138. {
  1139. if (state.VideoRequest != null)
  1140. {
  1141. if (state.VideoRequest.Framerate.HasValue)
  1142. {
  1143. return state.VideoRequest.Framerate.Value;
  1144. }
  1145. var maxrate = state.VideoRequest.MaxFramerate;
  1146. if (maxrate.HasValue && state.VideoStream != null)
  1147. {
  1148. var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
  1149. if (contentRate.HasValue && contentRate.Value > maxrate.Value)
  1150. {
  1151. return maxrate;
  1152. }
  1153. }
  1154. }
  1155. return null;
  1156. }
  1157. /// <summary>
  1158. /// Parses the parameters.
  1159. /// </summary>
  1160. /// <param name="request">The request.</param>
  1161. private void ParseParams(StreamRequest request)
  1162. {
  1163. var vals = request.Params.Split(';');
  1164. var videoRequest = request as VideoStreamRequest;
  1165. for (var i = 0; i < vals.Length; i++)
  1166. {
  1167. var val = vals[i];
  1168. if (string.IsNullOrWhiteSpace(val))
  1169. {
  1170. continue;
  1171. }
  1172. if (i == 0)
  1173. {
  1174. request.DeviceProfileId = val;
  1175. }
  1176. else if (i == 1)
  1177. {
  1178. request.DeviceId = val;
  1179. }
  1180. else if (i == 2)
  1181. {
  1182. request.MediaSourceId = val;
  1183. }
  1184. else if (i == 3)
  1185. {
  1186. request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  1187. }
  1188. else if (i == 4)
  1189. {
  1190. if (videoRequest != null)
  1191. {
  1192. videoRequest.VideoCodec = val;
  1193. }
  1194. }
  1195. else if (i == 5)
  1196. {
  1197. request.AudioCodec = val;
  1198. }
  1199. else if (i == 6)
  1200. {
  1201. if (videoRequest != null)
  1202. {
  1203. videoRequest.AudioStreamIndex = int.Parse(val, UsCulture);
  1204. }
  1205. }
  1206. else if (i == 7)
  1207. {
  1208. if (videoRequest != null)
  1209. {
  1210. videoRequest.SubtitleStreamIndex = int.Parse(val, UsCulture);
  1211. }
  1212. }
  1213. else if (i == 8)
  1214. {
  1215. if (videoRequest != null)
  1216. {
  1217. videoRequest.VideoBitRate = int.Parse(val, UsCulture);
  1218. }
  1219. }
  1220. else if (i == 9)
  1221. {
  1222. request.AudioBitRate = int.Parse(val, UsCulture);
  1223. }
  1224. else if (i == 10)
  1225. {
  1226. request.MaxAudioChannels = int.Parse(val, UsCulture);
  1227. }
  1228. else if (i == 11)
  1229. {
  1230. if (videoRequest != null)
  1231. {
  1232. videoRequest.MaxFramerate = float.Parse(val, UsCulture);
  1233. }
  1234. }
  1235. else if (i == 12)
  1236. {
  1237. if (videoRequest != null)
  1238. {
  1239. videoRequest.MaxWidth = int.Parse(val, UsCulture);
  1240. }
  1241. }
  1242. else if (i == 13)
  1243. {
  1244. if (videoRequest != null)
  1245. {
  1246. videoRequest.MaxHeight = int.Parse(val, UsCulture);
  1247. }
  1248. }
  1249. else if (i == 14)
  1250. {
  1251. request.StartTimeTicks = long.Parse(val, UsCulture);
  1252. }
  1253. else if (i == 15)
  1254. {
  1255. if (videoRequest != null)
  1256. {
  1257. videoRequest.Level = val;
  1258. }
  1259. }
  1260. else if (i == 16)
  1261. {
  1262. request.ClientTime = val;
  1263. }
  1264. else if (i == 17)
  1265. {
  1266. if (videoRequest != null)
  1267. {
  1268. videoRequest.MaxRefFrames = int.Parse(val, UsCulture);
  1269. }
  1270. }
  1271. else if (i == 18)
  1272. {
  1273. if (videoRequest != null)
  1274. {
  1275. videoRequest.MaxVideoBitDepth = int.Parse(val, UsCulture);
  1276. }
  1277. }
  1278. else if (i == 19)
  1279. {
  1280. if (videoRequest != null)
  1281. {
  1282. videoRequest.Profile = val;
  1283. }
  1284. }
  1285. else if (i == 20)
  1286. {
  1287. if (videoRequest != null)
  1288. {
  1289. videoRequest.Cabac = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  1290. }
  1291. }
  1292. }
  1293. }
  1294. /// <summary>
  1295. /// Parses the dlna headers.
  1296. /// </summary>
  1297. /// <param name="request">The request.</param>
  1298. private void ParseDlnaHeaders(StreamRequest request)
  1299. {
  1300. if (!request.StartTimeTicks.HasValue)
  1301. {
  1302. var timeSeek = GetHeader("TimeSeekRange.dlna.org");
  1303. request.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
  1304. }
  1305. }
  1306. /// <summary>
  1307. /// Parses the time seek header.
  1308. /// </summary>
  1309. private long? ParseTimeSeekHeader(string value)
  1310. {
  1311. if (string.IsNullOrWhiteSpace(value))
  1312. {
  1313. return null;
  1314. }
  1315. if (value.IndexOf("npt=", StringComparison.OrdinalIgnoreCase) != 0)
  1316. {
  1317. throw new ArgumentException("Invalid timeseek header");
  1318. }
  1319. value = value.Substring(4).Split(new[] { '-' }, 2)[0];
  1320. if (value.IndexOf(':') == -1)
  1321. {
  1322. // Parses npt times in the format of '417.33'
  1323. double seconds;
  1324. if (double.TryParse(value, NumberStyles.Any, UsCulture, out seconds))
  1325. {
  1326. return TimeSpan.FromSeconds(seconds).Ticks;
  1327. }
  1328. throw new ArgumentException("Invalid timeseek header");
  1329. }
  1330. // Parses npt times in the format of '10:19:25.7'
  1331. var tokens = value.Split(new[] { ':' }, 3);
  1332. double secondsSum = 0;
  1333. var timeFactor = 3600;
  1334. foreach (var time in tokens)
  1335. {
  1336. double digit;
  1337. if (double.TryParse(time, NumberStyles.Any, UsCulture, out digit))
  1338. {
  1339. secondsSum += (digit * timeFactor);
  1340. }
  1341. else
  1342. {
  1343. throw new ArgumentException("Invalid timeseek header");
  1344. }
  1345. timeFactor /= 60;
  1346. }
  1347. return TimeSpan.FromSeconds(secondsSum).Ticks;
  1348. }
  1349. /// <summary>
  1350. /// Gets the state.
  1351. /// </summary>
  1352. /// <param name="request">The request.</param>
  1353. /// <param name="cancellationToken">The cancellation token.</param>
  1354. /// <returns>StreamState.</returns>
  1355. protected async Task<StreamState> GetState(StreamRequest request, CancellationToken cancellationToken)
  1356. {
  1357. ParseDlnaHeaders(request);
  1358. if (!string.IsNullOrWhiteSpace(request.Params))
  1359. {
  1360. ParseParams(request);
  1361. }
  1362. var url = Request.PathInfo;
  1363. if (string.IsNullOrEmpty(request.AudioCodec))
  1364. {
  1365. request.AudioCodec = InferAudioCodec(url);
  1366. }
  1367. var state = new StreamState(LiveTvManager, Logger)
  1368. {
  1369. Request = request,
  1370. RequestedUrl = url
  1371. };
  1372. if (!string.IsNullOrWhiteSpace(request.AudioCodec))
  1373. {
  1374. state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
  1375. state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
  1376. }
  1377. var item = LibraryManager.GetItemById(request.Id);
  1378. List<MediaStream> mediaStreams = null;
  1379. state.ItemType = item.GetType().Name;
  1380. if (item is ILiveTvRecording)
  1381. {
  1382. var recording = await LiveTvManager.GetInternalRecording(request.Id, cancellationToken).ConfigureAwait(false);
  1383. state.VideoType = VideoType.VideoFile;
  1384. state.IsInputVideo = string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
  1385. var path = recording.RecordingInfo.Path;
  1386. var mediaUrl = recording.RecordingInfo.Url;
  1387. var source = string.IsNullOrEmpty(request.MediaSourceId)
  1388. ? recording.GetMediaSources(false).First()
  1389. : recording.GetMediaSources(false).First(i => string.Equals(i.Id, request.MediaSourceId));
  1390. mediaStreams = source.MediaStreams;
  1391. // Just to prevent this from being null and causing other methods to fail
  1392. state.MediaPath = string.Empty;
  1393. if (!string.IsNullOrEmpty(path))
  1394. {
  1395. state.MediaPath = path;
  1396. state.InputProtocol = MediaProtocol.File;
  1397. }
  1398. else if (!string.IsNullOrEmpty(mediaUrl))
  1399. {
  1400. state.MediaPath = mediaUrl;
  1401. state.InputProtocol = MediaProtocol.Http;
  1402. }
  1403. state.RunTimeTicks = recording.RunTimeTicks;
  1404. state.DeInterlace = true;
  1405. state.OutputAudioSync = "1000";
  1406. state.InputVideoSync = "-1";
  1407. state.InputAudioSync = "1";
  1408. state.InputContainer = recording.Container;
  1409. state.ReadInputAtNativeFramerate = source.ReadAtNativeFramerate;
  1410. }
  1411. else if (item is LiveTvChannel)
  1412. {
  1413. var channel = LiveTvManager.GetInternalChannel(request.Id);
  1414. state.VideoType = VideoType.VideoFile;
  1415. state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
  1416. mediaStreams = new List<MediaStream>();
  1417. state.DeInterlace = true;
  1418. // Just to prevent this from being null and causing other methods to fail
  1419. state.MediaPath = string.Empty;
  1420. }
  1421. else if (item is IChannelMediaItem)
  1422. {
  1423. var mediaSource = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false);
  1424. state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
  1425. state.InputProtocol = mediaSource.Protocol;
  1426. state.MediaPath = mediaSource.Path;
  1427. state.RunTimeTicks = item.RunTimeTicks;
  1428. state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
  1429. state.InputBitrate = mediaSource.Bitrate;
  1430. state.InputFileSize = mediaSource.Size;
  1431. state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
  1432. mediaStreams = mediaSource.MediaStreams;
  1433. }
  1434. else
  1435. {
  1436. var hasMediaSources = (IHasMediaSources)item;
  1437. var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
  1438. ? hasMediaSources.GetMediaSources(false).First()
  1439. : hasMediaSources.GetMediaSources(false).First(i => string.Equals(i.Id, request.MediaSourceId));
  1440. mediaStreams = mediaSource.MediaStreams;
  1441. state.MediaPath = mediaSource.Path;
  1442. state.InputProtocol = mediaSource.Protocol;
  1443. state.InputContainer = mediaSource.Container;
  1444. state.InputFileSize = mediaSource.Size;
  1445. state.InputBitrate = mediaSource.Bitrate;
  1446. state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
  1447. var video = item as Video;
  1448. if (video != null)
  1449. {
  1450. state.IsInputVideo = true;
  1451. if (mediaSource.VideoType.HasValue)
  1452. {
  1453. state.VideoType = mediaSource.VideoType.Value;
  1454. }
  1455. state.IsoType = mediaSource.IsoType;
  1456. state.PlayableStreamFileNames = mediaSource.PlayableStreamFileNames.ToList();
  1457. if (mediaSource.Timestamp.HasValue)
  1458. {
  1459. state.InputTimestamp = mediaSource.Timestamp.Value;
  1460. }
  1461. }
  1462. state.RunTimeTicks = mediaSource.RunTimeTicks;
  1463. }
  1464. var videoRequest = request as VideoStreamRequest;
  1465. AttachMediaStreamInfo(state, mediaStreams, videoRequest, url);
  1466. var container = Path.GetExtension(state.RequestedUrl);
  1467. if (string.IsNullOrEmpty(container))
  1468. {
  1469. container = request.Static ?
  1470. state.InputContainer :
  1471. (Path.GetExtension(GetOutputFilePath(state)) ?? string.Empty).TrimStart('.');
  1472. }
  1473. state.OutputContainer = (container ?? string.Empty).TrimStart('.');
  1474. state.OutputAudioBitrate = GetAudioBitrateParam(state.Request, state.AudioStream);
  1475. state.OutputAudioSampleRate = request.AudioSampleRate;
  1476. state.OutputAudioCodec = GetAudioCodec(state.Request);
  1477. state.OutputAudioChannels = GetNumAudioChannelsParam(state.Request, state.AudioStream, state.OutputAudioCodec);
  1478. if (videoRequest != null)
  1479. {
  1480. state.OutputVideoCodec = GetVideoCodec(videoRequest);
  1481. state.OutputVideoBitrate = GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream);
  1482. if (state.OutputVideoBitrate.HasValue)
  1483. {
  1484. var resolution = ResolutionNormalizer.Normalize(state.OutputVideoBitrate.Value,
  1485. state.OutputVideoCodec,
  1486. videoRequest.MaxWidth,
  1487. videoRequest.MaxHeight);
  1488. videoRequest.MaxWidth = resolution.MaxWidth;
  1489. videoRequest.MaxHeight = resolution.MaxHeight;
  1490. }
  1491. }
  1492. ApplyDeviceProfileSettings(state);
  1493. if (videoRequest != null)
  1494. {
  1495. if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
  1496. {
  1497. state.OutputVideoCodec = "copy";
  1498. }
  1499. if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs))
  1500. {
  1501. state.OutputAudioCodec = "copy";
  1502. }
  1503. }
  1504. state.OutputFilePath = GetOutputFilePath(state);
  1505. return state;
  1506. }
  1507. private void AttachMediaStreamInfo(StreamState state,
  1508. ChannelMediaInfo mediaInfo,
  1509. VideoStreamRequest videoRequest,
  1510. string requestedUrl)
  1511. {
  1512. var mediaSource = mediaInfo.ToMediaSource();
  1513. state.InputProtocol = mediaSource.Protocol;
  1514. state.MediaPath = mediaSource.Path;
  1515. state.RunTimeTicks = mediaSource.RunTimeTicks;
  1516. state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
  1517. state.InputBitrate = mediaSource.Bitrate;
  1518. state.InputFileSize = mediaSource.Size;
  1519. state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
  1520. if (state.ReadInputAtNativeFramerate)
  1521. {
  1522. state.OutputAudioSync = "1000";
  1523. state.InputVideoSync = "-1";
  1524. state.InputAudioSync = "1";
  1525. }
  1526. AttachMediaStreamInfo(state, mediaSource.MediaStreams, videoRequest, requestedUrl);
  1527. }
  1528. private void AttachMediaStreamInfo(StreamState state,
  1529. List<MediaStream> mediaStreams,
  1530. VideoStreamRequest videoRequest,
  1531. string requestedUrl)
  1532. {
  1533. if (videoRequest != null)
  1534. {
  1535. if (string.IsNullOrEmpty(videoRequest.VideoCodec))
  1536. {
  1537. videoRequest.VideoCodec = InferVideoCodec(requestedUrl);
  1538. }
  1539. state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video);
  1540. state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false);
  1541. state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio);
  1542. if (state.SubtitleStream != null && !state.SubtitleStream.IsExternal)
  1543. {
  1544. state.InternalSubtitleStreamOffset = mediaStreams.Where(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal).ToList().IndexOf(state.SubtitleStream);
  1545. }
  1546. if (state.VideoStream != null && state.VideoStream.IsInterlaced)
  1547. {
  1548. state.DeInterlace = true;
  1549. }
  1550. EnforceResolutionLimit(state, videoRequest);
  1551. }
  1552. else
  1553. {
  1554. state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
  1555. }
  1556. state.AllMediaStreams = mediaStreams;
  1557. }
  1558. private async Task<MediaSourceInfo> GetChannelMediaInfo(string id,
  1559. string mediaSourceId,
  1560. CancellationToken cancellationToken)
  1561. {
  1562. var channelMediaSources = await ChannelManager.GetChannelItemMediaSources(id, true, cancellationToken)
  1563. .ConfigureAwait(false);
  1564. var list = channelMediaSources.ToList();
  1565. if (!string.IsNullOrWhiteSpace(mediaSourceId))
  1566. {
  1567. var source = list
  1568. .FirstOrDefault(i => string.Equals(mediaSourceId, i.Id));
  1569. if (source != null)
  1570. {
  1571. return source;
  1572. }
  1573. }
  1574. return list.First();
  1575. }
  1576. private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream)
  1577. {
  1578. if (videoStream.IsInterlaced)
  1579. {
  1580. return false;
  1581. }
  1582. // Can't stream copy if we're burning in subtitles
  1583. if (request.SubtitleStreamIndex.HasValue)
  1584. {
  1585. if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode)
  1586. {
  1587. return false;
  1588. }
  1589. }
  1590. // Source and target codecs must match
  1591. if (!string.Equals(request.VideoCodec, videoStream.Codec, StringComparison.OrdinalIgnoreCase))
  1592. {
  1593. return false;
  1594. }
  1595. // If client is requesting a specific video profile, it must match the source
  1596. if (!string.IsNullOrEmpty(request.Profile))
  1597. {
  1598. if (string.IsNullOrEmpty(videoStream.Profile))
  1599. {
  1600. return false;
  1601. }
  1602. if (!string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase))
  1603. {
  1604. var currentScore = GetVideoProfileScore(videoStream.Profile);
  1605. var requestedScore = GetVideoProfileScore(request.Profile);
  1606. if (currentScore == -1 || currentScore > requestedScore)
  1607. {
  1608. return false;
  1609. }
  1610. }
  1611. }
  1612. // Video width must fall within requested value
  1613. if (request.MaxWidth.HasValue)
  1614. {
  1615. if (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value)
  1616. {
  1617. return false;
  1618. }
  1619. }
  1620. // Video height must fall within requested value
  1621. if (request.MaxHeight.HasValue)
  1622. {
  1623. if (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value)
  1624. {
  1625. return false;
  1626. }
  1627. }
  1628. // Video framerate must fall within requested value
  1629. var requestedFramerate = request.MaxFramerate ?? request.Framerate;
  1630. if (requestedFramerate.HasValue)
  1631. {
  1632. var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate;
  1633. if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
  1634. {
  1635. return false;
  1636. }
  1637. }
  1638. // Video bitrate must fall within requested value
  1639. if (request.VideoBitRate.HasValue)
  1640. {
  1641. if (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value)
  1642. {
  1643. return false;
  1644. }
  1645. }
  1646. if (request.MaxVideoBitDepth.HasValue)
  1647. {
  1648. if (videoStream.BitDepth.HasValue && videoStream.BitDepth.Value > request.MaxVideoBitDepth.Value)
  1649. {
  1650. return false;
  1651. }
  1652. }
  1653. if (request.MaxRefFrames.HasValue)
  1654. {
  1655. if (videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > request.MaxRefFrames.Value)
  1656. {
  1657. return false;
  1658. }
  1659. }
  1660. // If a specific level was requested, the source must match or be less than
  1661. if (!string.IsNullOrEmpty(request.Level))
  1662. {
  1663. double requestLevel;
  1664. if (double.TryParse(request.Level, NumberStyles.Any, UsCulture, out requestLevel))
  1665. {
  1666. if (!videoStream.Level.HasValue)
  1667. {
  1668. return false;
  1669. }
  1670. if (videoStream.Level.Value > requestLevel)
  1671. {
  1672. return false;
  1673. }
  1674. }
  1675. }
  1676. if (request.Cabac.HasValue && request.Cabac.Value)
  1677. {
  1678. if (videoStream.IsCabac.HasValue && !videoStream.IsCabac.Value)
  1679. {
  1680. return false;
  1681. }
  1682. }
  1683. return request.EnableAutoStreamCopy;
  1684. }
  1685. private int GetVideoProfileScore(string profile)
  1686. {
  1687. var list = new List<string>
  1688. {
  1689. "Constrained Baseline",
  1690. "Baseline",
  1691. "Extended",
  1692. "Main",
  1693. "High",
  1694. "Progressive High",
  1695. "Constrained High"
  1696. };
  1697. return Array.FindIndex(list.ToArray(), t => string.Equals(t, profile, StringComparison.OrdinalIgnoreCase));
  1698. }
  1699. private bool CanStreamCopyAudio(VideoStreamRequest request, MediaStream audioStream, List<string> supportedAudioCodecs)
  1700. {
  1701. // Source and target codecs must match
  1702. if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
  1703. {
  1704. return false;
  1705. }
  1706. // Video bitrate must fall within requested value
  1707. if (request.AudioBitRate.HasValue)
  1708. {
  1709. if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0)
  1710. {
  1711. return false;
  1712. }
  1713. if (audioStream.BitRate.Value > request.AudioBitRate.Value)
  1714. {
  1715. return false;
  1716. }
  1717. }
  1718. // Channels must fall within requested value
  1719. var channels = request.AudioChannels ?? request.MaxAudioChannels;
  1720. if (channels.HasValue)
  1721. {
  1722. if (!audioStream.Channels.HasValue || audioStream.Channels.Value <= 0)
  1723. {
  1724. return false;
  1725. }
  1726. if (audioStream.Channels.Value > channels.Value)
  1727. {
  1728. return false;
  1729. }
  1730. }
  1731. // Sample rate must fall within requested value
  1732. if (request.AudioSampleRate.HasValue)
  1733. {
  1734. if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value <= 0)
  1735. {
  1736. return false;
  1737. }
  1738. if (audioStream.SampleRate.Value > request.AudioSampleRate.Value)
  1739. {
  1740. return false;
  1741. }
  1742. }
  1743. return request.EnableAutoStreamCopy;
  1744. }
  1745. private void ApplyDeviceProfileSettings(StreamState state)
  1746. {
  1747. var headers = new Dictionary<string, string>();
  1748. foreach (var key in Request.Headers.AllKeys)
  1749. {
  1750. headers[key] = Request.Headers[key];
  1751. }
  1752. if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId))
  1753. {
  1754. state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId);
  1755. }
  1756. else
  1757. {
  1758. if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
  1759. {
  1760. var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
  1761. if (caps != null)
  1762. {
  1763. state.DeviceProfile = caps.DeviceProfile;
  1764. }
  1765. else
  1766. {
  1767. state.DeviceProfile = DlnaManager.GetProfile(headers);
  1768. }
  1769. }
  1770. }
  1771. var profile = state.DeviceProfile;
  1772. if (profile == null)
  1773. {
  1774. // Don't use settings from the default profile.
  1775. // Only use a specific profile if it was requested.
  1776. return;
  1777. }
  1778. var audioCodec = state.ActualOutputAudioCodec;
  1779. var videoCodec = state.ActualOutputVideoCodec;
  1780. var mediaProfile = state.VideoRequest == null ?
  1781. profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate) :
  1782. profile.GetVideoMediaProfile(state.OutputContainer,
  1783. audioCodec,
  1784. videoCodec,
  1785. state.OutputAudioBitrate,
  1786. state.OutputAudioChannels,
  1787. state.OutputWidth,
  1788. state.OutputHeight,
  1789. state.TargetVideoBitDepth,
  1790. state.OutputVideoBitrate,
  1791. state.TargetVideoProfile,
  1792. state.TargetVideoLevel,
  1793. state.TargetFramerate,
  1794. state.TargetPacketLength,
  1795. state.TargetTimestamp,
  1796. state.IsTargetAnamorphic,
  1797. state.IsTargetCabac,
  1798. state.TargetRefFrames);
  1799. if (mediaProfile != null)
  1800. {
  1801. state.MimeType = mediaProfile.MimeType;
  1802. }
  1803. var transcodingProfile = state.VideoRequest == null ?
  1804. profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) :
  1805. profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
  1806. if (transcodingProfile != null)
  1807. {
  1808. state.EstimateContentLength = transcodingProfile.EstimateContentLength;
  1809. state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
  1810. state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  1811. }
  1812. }
  1813. /// <summary>
  1814. /// Adds the dlna headers.
  1815. /// </summary>
  1816. /// <param name="state">The state.</param>
  1817. /// <param name="responseHeaders">The response headers.</param>
  1818. /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
  1819. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  1820. protected void AddDlnaHeaders(StreamState state, IDictionary<string, string> responseHeaders, bool isStaticallyStreamed)
  1821. {
  1822. var profile = state.DeviceProfile;
  1823. var transferMode = GetHeader("transferMode.dlna.org");
  1824. responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode;
  1825. responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*";
  1826. if (state.RunTimeTicks.HasValue && !isStaticallyStreamed && profile != null)
  1827. {
  1828. AddTimeSeekResponseHeaders(state, responseHeaders);
  1829. }
  1830. if (profile == null)
  1831. {
  1832. profile = DlnaManager.GetDefaultProfile();
  1833. }
  1834. var audioCodec = state.ActualOutputAudioCodec;
  1835. if (state.VideoRequest == null)
  1836. {
  1837. responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile)
  1838. .BuildAudioHeader(
  1839. state.OutputContainer,
  1840. audioCodec,
  1841. state.OutputAudioBitrate,
  1842. state.OutputAudioSampleRate,
  1843. state.OutputAudioChannels,
  1844. isStaticallyStreamed,
  1845. state.RunTimeTicks,
  1846. state.TranscodeSeekInfo
  1847. );
  1848. }
  1849. else
  1850. {
  1851. var videoCodec = state.ActualOutputVideoCodec;
  1852. responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile)
  1853. .BuildVideoHeader(
  1854. state.OutputContainer,
  1855. videoCodec,
  1856. audioCodec,
  1857. state.OutputWidth,
  1858. state.OutputHeight,
  1859. state.TargetVideoBitDepth,
  1860. state.OutputVideoBitrate,
  1861. state.OutputAudioBitrate,
  1862. state.OutputAudioChannels,
  1863. state.TargetTimestamp,
  1864. isStaticallyStreamed,
  1865. state.RunTimeTicks,
  1866. state.TargetVideoProfile,
  1867. state.TargetVideoLevel,
  1868. state.TargetFramerate,
  1869. state.TargetPacketLength,
  1870. state.TranscodeSeekInfo,
  1871. state.IsTargetAnamorphic,
  1872. state.IsTargetCabac,
  1873. state.TargetRefFrames
  1874. ).FirstOrDefault() ?? string.Empty;
  1875. }
  1876. foreach (var item in responseHeaders)
  1877. {
  1878. Request.Response.AddHeader(item.Key, item.Value);
  1879. }
  1880. }
  1881. private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders)
  1882. {
  1883. var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(UsCulture);
  1884. var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(UsCulture);
  1885. responseHeaders["TimeSeekRange.dlna.org"] = string.Format("npt={0}-{1}/{1}", startSeconds, runtimeSeconds);
  1886. responseHeaders["X-AvailableSeekRange"] = string.Format("1 npt={0}-{1}", startSeconds, runtimeSeconds);
  1887. }
  1888. /// <summary>
  1889. /// Enforces the resolution limit.
  1890. /// </summary>
  1891. /// <param name="state">The state.</param>
  1892. /// <param name="videoRequest">The video request.</param>
  1893. private void EnforceResolutionLimit(StreamState state, VideoStreamRequest videoRequest)
  1894. {
  1895. // Switch the incoming params to be ceilings rather than fixed values
  1896. videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width;
  1897. videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height;
  1898. videoRequest.Width = null;
  1899. videoRequest.Height = null;
  1900. }
  1901. protected string GetInputModifier(StreamState state, bool genPts = true)
  1902. {
  1903. var inputModifier = string.Empty;
  1904. var probeSize = GetProbeSizeArgument(state);
  1905. inputModifier += " " + probeSize;
  1906. inputModifier = inputModifier.Trim();
  1907. var userAgentParam = GetUserAgentParam(state);
  1908. if (!string.IsNullOrWhiteSpace(userAgentParam))
  1909. {
  1910. inputModifier += " " + userAgentParam;
  1911. }
  1912. inputModifier = inputModifier.Trim();
  1913. inputModifier += " " + GetFastSeekCommandLineParameter(state.Request);
  1914. inputModifier = inputModifier.Trim();
  1915. if (state.VideoRequest != null && genPts)
  1916. {
  1917. inputModifier += " -fflags +genpts";
  1918. }
  1919. if (!string.IsNullOrEmpty(state.InputAudioSync))
  1920. {
  1921. inputModifier += " -async " + state.InputAudioSync;
  1922. }
  1923. if (!string.IsNullOrEmpty(state.InputVideoSync))
  1924. {
  1925. inputModifier += " -vsync " + state.InputVideoSync;
  1926. }
  1927. if (state.ReadInputAtNativeFramerate)
  1928. {
  1929. inputModifier += " -re";
  1930. }
  1931. return inputModifier;
  1932. }
  1933. /// <summary>
  1934. /// Infers the audio codec based on the url
  1935. /// </summary>
  1936. /// <param name="url">The URL.</param>
  1937. /// <returns>System.Nullable{AudioCodecs}.</returns>
  1938. private string InferAudioCodec(string url)
  1939. {
  1940. var ext = Path.GetExtension(url);
  1941. if (string.Equals(ext, ".mp3", StringComparison.OrdinalIgnoreCase))
  1942. {
  1943. return "mp3";
  1944. }
  1945. if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase))
  1946. {
  1947. return "aac";
  1948. }
  1949. if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase))
  1950. {
  1951. return "wma";
  1952. }
  1953. if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase))
  1954. {
  1955. return "vorbis";
  1956. }
  1957. if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase))
  1958. {
  1959. return "vorbis";
  1960. }
  1961. if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
  1962. {
  1963. return "vorbis";
  1964. }
  1965. if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
  1966. {
  1967. return "vorbis";
  1968. }
  1969. if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase))
  1970. {
  1971. return "vorbis";
  1972. }
  1973. return "copy";
  1974. }
  1975. /// <summary>
  1976. /// Infers the video codec.
  1977. /// </summary>
  1978. /// <param name="url">The URL.</param>
  1979. /// <returns>System.Nullable{VideoCodecs}.</returns>
  1980. private string InferVideoCodec(string url)
  1981. {
  1982. var ext = Path.GetExtension(url);
  1983. if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
  1984. {
  1985. return "wmv";
  1986. }
  1987. if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
  1988. {
  1989. return "vpx";
  1990. }
  1991. if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
  1992. {
  1993. return "theora";
  1994. }
  1995. if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
  1996. {
  1997. return "h264";
  1998. }
  1999. return "copy";
  2000. }
  2001. }
  2002. }