BaseEncoder.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979
  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(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/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)
  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)
  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. var output = string.Empty;
  716. if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
  717. {
  718. var subParam = GetTextSubtitleParam(state);
  719. filters.Add(subParam);
  720. if (allowTimeStampCopy)
  721. {
  722. output += " -copyts";
  723. }
  724. }
  725. if (filters.Count > 0)
  726. {
  727. output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
  728. }
  729. return output;
  730. }
  731. /// <summary>
  732. /// Gets the text subtitle param.
  733. /// </summary>
  734. /// <param name="state">The state.</param>
  735. /// <returns>System.String.</returns>
  736. protected string GetTextSubtitleParam(EncodingJob state)
  737. {
  738. var seconds = Math.Round(TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds);
  739. if (state.SubtitleStream.IsExternal)
  740. {
  741. var subtitlePath = state.SubtitleStream.Path;
  742. var charsetParam = string.Empty;
  743. if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
  744. {
  745. var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.MediaSource.Protocol, CancellationToken.None).Result;
  746. if (!string.IsNullOrEmpty(charenc))
  747. {
  748. charsetParam = ":charenc=" + charenc;
  749. }
  750. }
  751. // TODO: Perhaps also use original_size=1920x800 ??
  752. return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB",
  753. subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"),
  754. charsetParam,
  755. seconds.ToString(UsCulture));
  756. }
  757. return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
  758. state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
  759. state.InternalSubtitleStreamOffset.ToString(UsCulture),
  760. seconds.ToString(UsCulture));
  761. }
  762. protected string GetAudioFilterParam(EncodingJob state, bool isHls)
  763. {
  764. var volParam = string.Empty;
  765. var audioSampleRate = string.Empty;
  766. var channels = state.OutputAudioChannels;
  767. // Boost volume to 200% when downsampling from 6ch to 2ch
  768. if (channels.HasValue && channels.Value <= 2)
  769. {
  770. if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
  771. {
  772. volParam = ",volume=" + GetEncodingOptions().DownMixAudioBoost.ToString(UsCulture);
  773. }
  774. }
  775. if (state.OutputAudioSampleRate.HasValue)
  776. {
  777. audioSampleRate = state.OutputAudioSampleRate.Value + ":";
  778. }
  779. var adelay = isHls ? "adelay=1," : string.Empty;
  780. var pts = string.Empty;
  781. if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream)
  782. {
  783. var seconds = TimeSpan.FromTicks(state.Options.StartTimeTicks ?? 0).TotalSeconds;
  784. pts = string.Format(",asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(UsCulture));
  785. }
  786. return string.Format("-af \"{0}aresample={1}async={4}{2}{3}\"",
  787. adelay,
  788. audioSampleRate,
  789. volParam,
  790. pts,
  791. state.OutputAudioSync);
  792. }
  793. }
  794. }