BaseEncoder.cs 38 KB

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