BaseEncoder.cs 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.IO;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Controller.Library;
  5. using MediaBrowser.Controller.LiveTv;
  6. using MediaBrowser.Controller.MediaEncoding;
  7. using MediaBrowser.Controller.Session;
  8. using MediaBrowser.MediaEncoding.Subtitles;
  9. using MediaBrowser.Model.Configuration;
  10. using MediaBrowser.Model.Dlna;
  11. using MediaBrowser.Model.Drawing;
  12. using MediaBrowser.Model.Dto;
  13. using MediaBrowser.Model.Entities;
  14. using MediaBrowser.Model.IO;
  15. using MediaBrowser.Model.Logging;
  16. using MediaBrowser.Model.MediaInfo;
  17. using System;
  18. using System.Collections.Generic;
  19. using System.Diagnostics;
  20. using System.Globalization;
  21. using System.IO;
  22. using System.Text;
  23. using System.Threading;
  24. using System.Threading.Tasks;
  25. namespace MediaBrowser.MediaEncoding.Encoder
  26. {
  27. public abstract class BaseEncoder
  28. {
  29. protected readonly MediaEncoder MediaEncoder;
  30. protected readonly ILogger Logger;
  31. protected readonly IServerConfigurationManager ConfigurationManager;
  32. protected readonly IFileSystem FileSystem;
  33. protected readonly IIsoManager IsoManager;
  34. protected readonly ILibraryManager LibraryManager;
  35. protected readonly ISessionManager SessionManager;
  36. protected readonly ISubtitleEncoder SubtitleEncoder;
  37. protected readonly IMediaSourceManager MediaSourceManager;
  38. protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
  39. public BaseEncoder(MediaEncoder mediaEncoder,
  40. ILogger logger,
  41. IServerConfigurationManager configurationManager,
  42. IFileSystem fileSystem,
  43. IIsoManager isoManager,
  44. ILibraryManager libraryManager,
  45. ISessionManager sessionManager,
  46. ISubtitleEncoder subtitleEncoder,
  47. IMediaSourceManager mediaSourceManager)
  48. {
  49. MediaEncoder = mediaEncoder;
  50. Logger = logger;
  51. ConfigurationManager = configurationManager;
  52. FileSystem = fileSystem;
  53. IsoManager = isoManager;
  54. LibraryManager = libraryManager;
  55. SessionManager = sessionManager;
  56. SubtitleEncoder = subtitleEncoder;
  57. MediaSourceManager = mediaSourceManager;
  58. }
  59. public async Task<EncodingJob> Start(EncodingJobOptions options,
  60. IProgress<double> progress,
  61. CancellationToken cancellationToken)
  62. {
  63. var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager)
  64. .CreateJob(options, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
  65. encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
  66. Directory.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath));
  67. encodingJob.ReadInputAtNativeFramerate = options.ReadInputAtNativeFramerate;
  68. await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
  69. var commandLineArgs = GetCommandLineArguments(encodingJob);
  70. if (GetEncodingOptions().EnableDebugLogging)
  71. {
  72. commandLineArgs = "-loglevel debug " + commandLineArgs;
  73. }
  74. var process = new Process
  75. {
  76. StartInfo = new ProcessStartInfo
  77. {
  78. CreateNoWindow = true,
  79. UseShellExecute = false,
  80. // Must consume both stdout and stderr or deadlocks may occur
  81. RedirectStandardOutput = true,
  82. RedirectStandardError = true,
  83. RedirectStandardInput = true,
  84. FileName = MediaEncoder.EncoderPath,
  85. Arguments = commandLineArgs,
  86. WindowStyle = ProcessWindowStyle.Hidden,
  87. ErrorDialog = false
  88. },
  89. EnableRaisingEvents = true
  90. };
  91. var workingDirectory = GetWorkingDirectory(options);
  92. if (!string.IsNullOrWhiteSpace(workingDirectory))
  93. {
  94. process.StartInfo.WorkingDirectory = workingDirectory;
  95. }
  96. OnTranscodeBeginning(encodingJob);
  97. var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
  98. Logger.Info(commandLineLogMessage);
  99. var logFilePath = Path.Combine(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, "transcode-" + Guid.NewGuid() + ".txt");
  100. Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
  101. // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
  102. encodingJob.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
  103. var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
  104. await encodingJob.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false);
  105. process.Exited += (sender, args) => OnFfMpegProcessExited(process, encodingJob);
  106. try
  107. {
  108. process.Start();
  109. }
  110. catch (Exception ex)
  111. {
  112. Logger.ErrorException("Error starting ffmpeg", ex);
  113. OnTranscodeFailedToStart(encodingJob.OutputFilePath, encodingJob);
  114. throw;
  115. }
  116. cancellationToken.Register(() => Cancel(process, encodingJob));
  117. // MUST read both stdout and stderr asynchronously or a deadlock may occurr
  118. process.BeginOutputReadLine();
  119. // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
  120. new JobLogger(Logger).StartStreamingLog(encodingJob, process.StandardError.BaseStream, encodingJob.LogFileStream);
  121. // Wait for the file to exist before proceeeding
  122. while (!File.Exists(encodingJob.OutputFilePath) && !encodingJob.HasExited)
  123. {
  124. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  125. }
  126. return encodingJob;
  127. }
  128. private void Cancel(Process process, EncodingJob job)
  129. {
  130. Logger.Info("Killing ffmpeg process for {0}", job.OutputFilePath);
  131. //process.Kill();
  132. process.StandardInput.WriteLine("q");
  133. job.IsCancelled = true;
  134. }
  135. /// <summary>
  136. /// Processes the exited.
  137. /// </summary>
  138. /// <param name="process">The process.</param>
  139. /// <param name="job">The job.</param>
  140. private void OnFfMpegProcessExited(Process process, EncodingJob job)
  141. {
  142. job.HasExited = true;
  143. Logger.Debug("Disposing stream resources");
  144. job.Dispose();
  145. var isSuccesful = false;
  146. try
  147. {
  148. var exitCode = process.ExitCode;
  149. Logger.Info("FFMpeg exited with code {0}", exitCode);
  150. isSuccesful = exitCode == 0;
  151. }
  152. catch
  153. {
  154. Logger.Error("FFMpeg exited with an error.");
  155. }
  156. if (isSuccesful && !job.IsCancelled)
  157. {
  158. job.TaskCompletionSource.TrySetResult(true);
  159. }
  160. else if (job.IsCancelled)
  161. {
  162. try
  163. {
  164. DeleteFiles(job);
  165. }
  166. catch
  167. {
  168. }
  169. try
  170. {
  171. job.TaskCompletionSource.TrySetException(new OperationCanceledException());
  172. }
  173. catch
  174. {
  175. }
  176. }
  177. else
  178. {
  179. try
  180. {
  181. DeleteFiles(job);
  182. }
  183. catch
  184. {
  185. }
  186. try
  187. {
  188. job.TaskCompletionSource.TrySetException(new ApplicationException("Encoding failed"));
  189. }
  190. catch
  191. {
  192. }
  193. }
  194. // This causes on exited to be called twice:
  195. //try
  196. //{
  197. // // Dispose the process
  198. // process.Dispose();
  199. //}
  200. //catch (Exception ex)
  201. //{
  202. // Logger.ErrorException("Error disposing ffmpeg.", ex);
  203. //}
  204. }
  205. protected virtual void DeleteFiles(EncodingJob job)
  206. {
  207. FileSystem.DeleteFile(job.OutputFilePath);
  208. }
  209. private void OnTranscodeBeginning(EncodingJob job)
  210. {
  211. job.ReportTranscodingProgress(null, null, null, null);
  212. }
  213. private void OnTranscodeFailedToStart(string path, EncodingJob job)
  214. {
  215. if (!string.IsNullOrWhiteSpace(job.Options.DeviceId))
  216. {
  217. SessionManager.ClearTranscodingInfo(job.Options.DeviceId);
  218. }
  219. }
  220. protected abstract bool IsVideoEncoder { get; }
  221. protected virtual string GetWorkingDirectory(EncodingJobOptions options)
  222. {
  223. return null;
  224. }
  225. protected EncodingOptions GetEncodingOptions()
  226. {
  227. return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
  228. }
  229. protected abstract string GetCommandLineArguments(EncodingJob job);
  230. private string GetOutputFilePath(EncodingJob state)
  231. {
  232. var folder = string.IsNullOrWhiteSpace(state.Options.OutputDirectory) ?
  233. ConfigurationManager.ApplicationPaths.TranscodingTempPath :
  234. state.Options.OutputDirectory;
  235. var outputFileExtension = GetOutputFileExtension(state);
  236. var filename = state.Id + (outputFileExtension ?? string.Empty).ToLower();
  237. return Path.Combine(folder, filename);
  238. }
  239. protected virtual string GetOutputFileExtension(EncodingJob state)
  240. {
  241. if (!string.IsNullOrWhiteSpace(state.Options.OutputContainer))
  242. {
  243. return "." + state.Options.OutputContainer;
  244. }
  245. return null;
  246. }
  247. /// <summary>
  248. /// Gets the number of threads.
  249. /// </summary>
  250. /// <returns>System.Int32.</returns>
  251. protected int GetNumberOfThreads(EncodingJob job, bool isWebm)
  252. {
  253. return job.Options.CpuCoreLimit ?? 0;
  254. }
  255. protected EncodingQuality GetQualitySetting()
  256. {
  257. var quality = GetEncodingOptions().EncodingQuality;
  258. if (quality == EncodingQuality.Auto)
  259. {
  260. var cpuCount = Environment.ProcessorCount;
  261. if (cpuCount >= 4)
  262. {
  263. //return EncodingQuality.HighQuality;
  264. }
  265. return EncodingQuality.HighSpeed;
  266. }
  267. return quality;
  268. }
  269. protected string GetInputModifier(EncodingJob job, bool genPts = true)
  270. {
  271. var inputModifier = string.Empty;
  272. var probeSize = GetProbeSizeArgument(job);
  273. inputModifier += " " + probeSize;
  274. inputModifier = inputModifier.Trim();
  275. var userAgentParam = GetUserAgentParam(job);
  276. if (!string.IsNullOrWhiteSpace(userAgentParam))
  277. {
  278. inputModifier += " " + userAgentParam;
  279. }
  280. inputModifier = inputModifier.Trim();
  281. inputModifier += " " + GetFastSeekCommandLineParameter(job.Options);
  282. inputModifier = inputModifier.Trim();
  283. if (job.IsVideoRequest && genPts)
  284. {
  285. inputModifier += " -fflags +genpts";
  286. }
  287. if (!string.IsNullOrEmpty(job.InputAudioSync))
  288. {
  289. inputModifier += " -async " + job.InputAudioSync;
  290. }
  291. if (!string.IsNullOrEmpty(job.InputVideoSync))
  292. {
  293. inputModifier += " -vsync " + job.InputVideoSync;
  294. }
  295. if (job.ReadInputAtNativeFramerate)
  296. {
  297. inputModifier += " -re";
  298. }
  299. return inputModifier;
  300. }
  301. private string GetUserAgentParam(EncodingJob job)
  302. {
  303. string useragent = null;
  304. job.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
  305. if (!string.IsNullOrWhiteSpace(useragent))
  306. {
  307. return "-user-agent \"" + useragent + "\"";
  308. }
  309. return string.Empty;
  310. }
  311. /// <summary>
  312. /// Gets the probe size argument.
  313. /// </summary>
  314. /// <param name="job">The job.</param>
  315. /// <returns>System.String.</returns>
  316. private string GetProbeSizeArgument(EncodingJob job)
  317. {
  318. if (job.PlayableStreamFileNames.Count > 0)
  319. {
  320. return MediaEncoder.GetProbeSizeArgument(job.PlayableStreamFileNames.ToArray(), job.InputProtocol);
  321. }
  322. return MediaEncoder.GetProbeSizeArgument(new[] { job.MediaPath }, job.InputProtocol);
  323. }
  324. /// <summary>
  325. /// Gets the fast seek command line parameter.
  326. /// </summary>
  327. /// <param name="options">The options.</param>
  328. /// <returns>System.String.</returns>
  329. /// <value>The fast seek command line parameter.</value>
  330. protected string GetFastSeekCommandLineParameter(EncodingJobOptions options)
  331. {
  332. var time = options.StartTimeTicks;
  333. if (time.HasValue && time.Value > 0)
  334. {
  335. return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value));
  336. }
  337. return string.Empty;
  338. }
  339. /// <summary>
  340. /// Gets the input argument.
  341. /// </summary>
  342. /// <param name="job">The job.</param>
  343. /// <returns>System.String.</returns>
  344. protected string GetInputArgument(EncodingJob job)
  345. {
  346. var arg = "-i " + GetInputPathArgument(job);
  347. if (job.SubtitleStream != null)
  348. {
  349. if (job.SubtitleStream.IsExternal && !job.SubtitleStream.IsTextSubtitleStream)
  350. {
  351. arg += " -i \"" + job.SubtitleStream.Path + "\"";
  352. }
  353. }
  354. return arg;
  355. }
  356. private string GetInputPathArgument(EncodingJob job)
  357. {
  358. var protocol = job.InputProtocol;
  359. var inputPath = new[] { job.MediaPath };
  360. if (job.IsInputVideo)
  361. {
  362. if (!(job.VideoType == VideoType.Iso && job.IsoMount == null))
  363. {
  364. inputPath = MediaEncoderHelpers.GetInputArgument(job.MediaPath, job.InputProtocol, job.IsoMount, job.PlayableStreamFileNames);
  365. }
  366. }
  367. return MediaEncoder.GetInputArgument(inputPath, protocol);
  368. }
  369. private async Task AcquireResources(EncodingJob state, CancellationToken cancellationToken)
  370. {
  371. if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
  372. {
  373. state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationToken).ConfigureAwait(false);
  374. }
  375. if (state.MediaSource.RequiresOpening)
  376. {
  377. var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
  378. {
  379. OpenToken = state.MediaSource.OpenToken
  380. }, false, cancellationToken).ConfigureAwait(false);
  381. AttachMediaStreamInfo(state, liveStreamResponse.MediaSource, state.Options);
  382. if (state.IsVideoRequest)
  383. {
  384. EncodingJobFactory.TryStreamCopy(state, state.Options);
  385. }
  386. }
  387. if (state.MediaSource.BufferMs.HasValue)
  388. {
  389. await Task.Delay(state.MediaSource.BufferMs.Value, cancellationToken).ConfigureAwait(false);
  390. }
  391. }
  392. private void AttachMediaStreamInfo(EncodingJob state,
  393. MediaSourceInfo mediaSource,
  394. EncodingJobOptions videoRequest)
  395. {
  396. EncodingJobFactory.AttachMediaStreamInfo(state, mediaSource, videoRequest);
  397. }
  398. /// <summary>
  399. /// Gets the internal graphical subtitle param.
  400. /// </summary>
  401. /// <param name="state">The state.</param>
  402. /// <param name="outputVideoCodec">The output video codec.</param>
  403. /// <returns>System.String.</returns>
  404. protected string GetGraphicalSubtitleParam(EncodingJob state, string outputVideoCodec)
  405. {
  406. var outputSizeParam = string.Empty;
  407. var request = state.Options;
  408. // Add resolution params, if specified
  409. if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
  410. {
  411. outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"');
  412. outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
  413. }
  414. var videoSizeParam = string.Empty;
  415. if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
  416. {
  417. videoSizeParam = string.Format(",scale={0}:{1}", state.VideoStream.Width.Value.ToString(UsCulture), state.VideoStream.Height.Value.ToString(UsCulture));
  418. }
  419. var mapPrefix = state.SubtitleStream.IsExternal ?
  420. 1 :
  421. 0;
  422. var subtitleStreamIndex = state.SubtitleStream.IsExternal
  423. ? 0
  424. : state.SubtitleStream.Index;
  425. return string.Format(" -filter_complex \"[{0}:{1}]format=yuva444p{4},lut=u=128:v=128:y=gammaval(.3)[sub] ; [0:{2}] [sub] overlay{3}\"",
  426. mapPrefix.ToString(UsCulture),
  427. subtitleStreamIndex.ToString(UsCulture),
  428. state.VideoStream.Index.ToString(UsCulture),
  429. outputSizeParam,
  430. videoSizeParam);
  431. }
  432. /// <summary>
  433. /// Gets the video bitrate to specify on the command line
  434. /// </summary>
  435. /// <param name="state">The state.</param>
  436. /// <param name="videoCodec">The video codec.</param>
  437. /// <param name="isHls">if set to <c>true</c> [is HLS].</param>
  438. /// <returns>System.String.</returns>
  439. protected string GetVideoQualityParam(EncodingJob state, string videoCodec, bool isHls)
  440. {
  441. var param = string.Empty;
  442. var isVc1 = state.VideoStream != null &&
  443. string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
  444. var qualitySetting = GetQualitySetting();
  445. if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
  446. {
  447. param = "-preset superfast";
  448. switch (qualitySetting)
  449. {
  450. case EncodingQuality.HighSpeed:
  451. param += " -crf 28";
  452. break;
  453. case EncodingQuality.HighQuality:
  454. param += " -crf 25";
  455. break;
  456. case EncodingQuality.MaxQuality:
  457. param += " -crf 21";
  458. break;
  459. }
  460. }
  461. else if (string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
  462. {
  463. param = "-preset fast";
  464. switch (qualitySetting)
  465. {
  466. case EncodingQuality.HighSpeed:
  467. param += " -crf 28";
  468. break;
  469. case EncodingQuality.HighQuality:
  470. param += " -crf 25";
  471. break;
  472. case EncodingQuality.MaxQuality:
  473. param += " -crf 21";
  474. break;
  475. }
  476. }
  477. // webm
  478. else if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
  479. {
  480. // Values 0-3, 0 being highest quality but slower
  481. var profileScore = 0;
  482. string crf;
  483. var qmin = "0";
  484. var qmax = "50";
  485. switch (qualitySetting)
  486. {
  487. case EncodingQuality.HighSpeed:
  488. crf = "10";
  489. break;
  490. case EncodingQuality.HighQuality:
  491. crf = "6";
  492. break;
  493. case EncodingQuality.MaxQuality:
  494. crf = "4";
  495. break;
  496. default:
  497. throw new ArgumentException("Unrecognized quality setting");
  498. }
  499. if (isVc1)
  500. {
  501. profileScore++;
  502. }
  503. // Max of 2
  504. profileScore = Math.Min(profileScore, 2);
  505. // http://www.webmproject.org/docs/encoder-parameters/
  506. param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
  507. profileScore.ToString(UsCulture),
  508. crf,
  509. qmin,
  510. qmax);
  511. }
  512. else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase))
  513. {
  514. param = "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
  515. }
  516. // asf/wmv
  517. else if (string.Equals(videoCodec, "wmv2", StringComparison.OrdinalIgnoreCase))
  518. {
  519. param = "-qmin 2";
  520. }
  521. else if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
  522. {
  523. param = "-mbd 2";
  524. }
  525. param += GetVideoBitrateParam(state, videoCodec, isHls);
  526. var framerate = GetFramerateParam(state);
  527. if (framerate.HasValue)
  528. {
  529. param += string.Format(" -r {0}", framerate.Value.ToString(UsCulture));
  530. }
  531. if (!string.IsNullOrEmpty(state.OutputVideoSync))
  532. {
  533. param += " -vsync " + state.OutputVideoSync;
  534. }
  535. if (!string.IsNullOrEmpty(state.Options.Profile))
  536. {
  537. param += " -profile:v " + state.Options.Profile;
  538. }
  539. if (state.Options.Level.HasValue)
  540. {
  541. param += " -level " + state.Options.Level.Value.ToString(UsCulture);
  542. }
  543. return "-pix_fmt yuv420p " + param;
  544. }
  545. protected string GetVideoBitrateParam(EncodingJob state, string videoCodec, bool isHls)
  546. {
  547. var bitrate = state.OutputVideoBitrate;
  548. if (bitrate.HasValue)
  549. {
  550. var hasFixedResolution = state.Options.HasFixedResolution;
  551. if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
  552. {
  553. if (hasFixedResolution)
  554. {
  555. return string.Format(" -minrate:v ({0}*.90) -maxrate:v ({0}*1.10) -bufsize:v {0} -b:v {0}", bitrate.Value.ToString(UsCulture));
  556. }
  557. // With vpx when crf is used, b:v becomes a max rate
  558. // 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.
  559. return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture));
  560. }
  561. if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
  562. {
  563. return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
  564. }
  565. // H264
  566. if (hasFixedResolution)
  567. {
  568. if (isHls)
  569. {
  570. return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
  571. }
  572. return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture));
  573. }
  574. return string.Format(" -maxrate {0} -bufsize {1}",
  575. bitrate.Value.ToString(UsCulture),
  576. (bitrate.Value * 2).ToString(UsCulture));
  577. }
  578. return string.Empty;
  579. }
  580. protected double? GetFramerateParam(EncodingJob state)
  581. {
  582. if (state.Options.Framerate.HasValue)
  583. {
  584. return state.Options.Framerate.Value;
  585. }
  586. var maxrate = state.Options.MaxFramerate;
  587. if (maxrate.HasValue && state.VideoStream != null)
  588. {
  589. var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
  590. if (contentRate.HasValue && contentRate.Value > maxrate.Value)
  591. {
  592. return maxrate;
  593. }
  594. }
  595. return null;
  596. }
  597. /// <summary>
  598. /// Gets the map args.
  599. /// </summary>
  600. /// <param name="state">The state.</param>
  601. /// <returns>System.String.</returns>
  602. protected virtual string GetMapArgs(EncodingJob state)
  603. {
  604. // If we don't have known media info
  605. // If input is video, use -sn to drop subtitles
  606. // Otherwise just return empty
  607. if (state.VideoStream == null && state.AudioStream == null)
  608. {
  609. return state.IsInputVideo ? "-sn" : string.Empty;
  610. }
  611. // We have media info, but we don't know the stream indexes
  612. if (state.VideoStream != null && state.VideoStream.Index == -1)
  613. {
  614. return "-sn";
  615. }
  616. // We have media info, but we don't know the stream indexes
  617. if (state.AudioStream != null && state.AudioStream.Index == -1)
  618. {
  619. return state.IsInputVideo ? "-sn" : string.Empty;
  620. }
  621. var args = string.Empty;
  622. if (state.VideoStream != null)
  623. {
  624. args += string.Format("-map 0:{0}", state.VideoStream.Index);
  625. }
  626. else
  627. {
  628. args += "-map -0:v";
  629. }
  630. if (state.AudioStream != null)
  631. {
  632. args += string.Format(" -map 0:{0}", state.AudioStream.Index);
  633. }
  634. else
  635. {
  636. args += " -map -0:a";
  637. }
  638. if (state.SubtitleStream == null)
  639. {
  640. args += " -map -0:s";
  641. }
  642. else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
  643. {
  644. args += " -map 1:0 -sn";
  645. }
  646. return args;
  647. }
  648. /// <summary>
  649. /// Determines whether the specified stream is H264.
  650. /// </summary>
  651. /// <param name="stream">The stream.</param>
  652. /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
  653. protected bool IsH264(MediaStream stream)
  654. {
  655. var codec = stream.Codec ?? string.Empty;
  656. return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
  657. codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
  658. }
  659. /// <summary>
  660. /// If we're going to put a fixed size on the command line, this will calculate it
  661. /// </summary>
  662. /// <param name="state">The state.</param>
  663. /// <param name="outputVideoCodec">The output video codec.</param>
  664. /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param>
  665. /// <returns>System.String.</returns>
  666. protected string GetOutputSizeParam(EncodingJob state,
  667. string outputVideoCodec,
  668. bool allowTimeStampCopy = true)
  669. {
  670. // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
  671. var request = state.Options;
  672. var filters = new List<string>();
  673. if (state.DeInterlace)
  674. {
  675. filters.Add("yadif=0:-1:0");
  676. }
  677. // If fixed dimensions were supplied
  678. if (request.Width.HasValue && request.Height.HasValue)
  679. {
  680. var widthParam = request.Width.Value.ToString(UsCulture);
  681. var heightParam = request.Height.Value.ToString(UsCulture);
  682. filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", widthParam, heightParam));
  683. }
  684. // 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
  685. else if (request.MaxWidth.HasValue && request.MaxHeight.HasValue)
  686. {
  687. var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
  688. var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
  689. filters.Add(string.Format("scale=trunc(min(iw\\,{0})/2)*2:trunc(min((iw/dar)\\,{1})/2)*2", maxWidthParam, maxHeightParam));
  690. }
  691. // If a fixed width was requested
  692. else if (request.Width.HasValue)
  693. {
  694. var widthParam = request.Width.Value.ToString(UsCulture);
  695. filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam));
  696. }
  697. // If a fixed height was requested
  698. else if (request.Height.HasValue)
  699. {
  700. var heightParam = request.Height.Value.ToString(UsCulture);
  701. filters.Add(string.Format("scale=trunc(oh*a*2)/2:{0}", heightParam));
  702. }
  703. // If a max width was requested
  704. else if (request.MaxWidth.HasValue && (!request.MaxHeight.HasValue || state.VideoStream == null))
  705. {
  706. var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture);
  707. filters.Add(string.Format("scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam));
  708. }
  709. // If a max height was requested
  710. else if (request.MaxHeight.HasValue && (!request.MaxWidth.HasValue || state.VideoStream == null))
  711. {
  712. var maxHeightParam = request.MaxHeight.Value.ToString(UsCulture);
  713. filters.Add(string.Format("scale=trunc(oh*a*2)/2:min(ih\\,{0})", maxHeightParam));
  714. }
  715. else if (request.MaxWidth.HasValue ||
  716. request.MaxHeight.HasValue ||
  717. request.Width.HasValue ||
  718. request.Height.HasValue)
  719. {
  720. if (state.VideoStream != null)
  721. {
  722. // Need to perform calculations manually
  723. // Try to account for bad media info
  724. var currentHeight = state.VideoStream.Height ?? request.MaxHeight ?? request.Height ?? 0;
  725. var currentWidth = state.VideoStream.Width ?? request.MaxWidth ?? request.Width ?? 0;
  726. var outputSize = DrawingUtils.Resize(currentWidth, currentHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
  727. var manualWidthParam = outputSize.Width.ToString(UsCulture);
  728. var manualHeightParam = outputSize.Height.ToString(UsCulture);
  729. filters.Add(string.Format("scale=trunc({0}/2)*2:trunc({1}/2)*2", manualWidthParam, manualHeightParam));
  730. }
  731. }
  732. var output = string.Empty;
  733. if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
  734. {
  735. var subParam = GetTextSubtitleParam(state);
  736. filters.Add(subParam);
  737. if (allowTimeStampCopy)
  738. {
  739. output += " -copyts";
  740. }
  741. }
  742. if (filters.Count > 0)
  743. {
  744. output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
  745. }
  746. return output;
  747. }
  748. /// <summary>
  749. /// Gets the text subtitle param.
  750. /// </summary>
  751. /// <param name="state">The state.</param>
  752. /// <returns>System.String.</returns>
  753. protected string GetTextSubtitleParam(EncodingJob state)
  754. {
  755. var seconds = Math.Round(TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds);
  756. if (state.SubtitleStream.IsExternal)
  757. {
  758. var subtitlePath = state.SubtitleStream.Path;
  759. var charsetParam = string.Empty;
  760. if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
  761. {
  762. var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.MediaSource.Protocol, CancellationToken.None).Result;
  763. if (!string.IsNullOrEmpty(charenc))
  764. {
  765. charsetParam = ":charenc=" + charenc;
  766. }
  767. }
  768. // TODO: Perhaps also use original_size=1920x800 ??
  769. return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB",
  770. subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"),
  771. charsetParam,
  772. seconds.ToString(UsCulture));
  773. }
  774. return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
  775. state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
  776. state.InternalSubtitleStreamOffset.ToString(UsCulture),
  777. seconds.ToString(UsCulture));
  778. }
  779. protected string GetAudioFilterParam(EncodingJob state, bool isHls)
  780. {
  781. var volParam = string.Empty;
  782. var audioSampleRate = string.Empty;
  783. var channels = state.OutputAudioChannels;
  784. // Boost volume to 200% when downsampling from 6ch to 2ch
  785. if (channels.HasValue && channels.Value <= 2)
  786. {
  787. if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
  788. {
  789. volParam = ",volume=" + GetEncodingOptions().DownMixAudioBoost.ToString(UsCulture);
  790. }
  791. }
  792. if (state.OutputAudioSampleRate.HasValue)
  793. {
  794. audioSampleRate = state.OutputAudioSampleRate.Value + ":";
  795. }
  796. var adelay = isHls ? "adelay=1," : string.Empty;
  797. var pts = string.Empty;
  798. if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
  799. {
  800. var seconds = TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds;
  801. pts = string.Format(",asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(UsCulture));
  802. }
  803. return string.Format("-af \"{0}aresample={1}async={4}{2}{3}\"",
  804. adelay,
  805. audioSampleRate,
  806. volParam,
  807. pts,
  808. state.OutputAudioSync);
  809. }
  810. }
  811. }