BaseEncoder.cs 36 KB

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