MpegDashService.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. using MediaBrowser.Api.Playback.Hls;
  2. using MediaBrowser.Common.IO;
  3. using MediaBrowser.Common.Net;
  4. using MediaBrowser.Controller.Configuration;
  5. using MediaBrowser.Controller.Devices;
  6. using MediaBrowser.Controller.Diagnostics;
  7. using MediaBrowser.Controller.Dlna;
  8. using MediaBrowser.Controller.Library;
  9. using MediaBrowser.Controller.LiveTv;
  10. using MediaBrowser.Controller.MediaEncoding;
  11. using MediaBrowser.Controller.Net;
  12. using MediaBrowser.Model.IO;
  13. using ServiceStack;
  14. using System;
  15. using System.Collections.Generic;
  16. using System.Globalization;
  17. using System.IO;
  18. using System.Linq;
  19. using System.Threading;
  20. using System.Threading.Tasks;
  21. using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
  22. namespace MediaBrowser.Api.Playback.Dash
  23. {
  24. /// <summary>
  25. /// Options is needed for chromecast. Threw Head in there since it's related
  26. /// </summary>
  27. [Route("/Videos/{Id}/master.mpd", "GET", Summary = "Gets a video stream using Mpeg dash.")]
  28. [Route("/Videos/{Id}/master.mpd", "HEAD", Summary = "Gets a video stream using Mpeg dash.")]
  29. public class GetMasterManifest : VideoStreamRequest
  30. {
  31. public bool EnableAdaptiveBitrateStreaming { get; set; }
  32. public GetMasterManifest()
  33. {
  34. EnableAdaptiveBitrateStreaming = true;
  35. }
  36. }
  37. [Route("/Videos/{Id}/dash/{RepresentationId}/{SegmentId}.m4s", "GET")]
  38. public class GetDashSegment : VideoStreamRequest
  39. {
  40. /// <summary>
  41. /// Gets or sets the segment id.
  42. /// </summary>
  43. /// <value>The segment id.</value>
  44. public string SegmentId { get; set; }
  45. /// <summary>
  46. /// Gets or sets the representation identifier.
  47. /// </summary>
  48. /// <value>The representation identifier.</value>
  49. public string RepresentationId { get; set; }
  50. }
  51. public class MpegDashService : BaseHlsService
  52. {
  53. public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, INetworkManager networkManager)
  54. : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager)
  55. {
  56. NetworkManager = networkManager;
  57. }
  58. protected INetworkManager NetworkManager { get; private set; }
  59. public object Get(GetMasterManifest request)
  60. {
  61. var result = GetAsync(request, "GET").Result;
  62. return result;
  63. }
  64. public object Head(GetMasterManifest request)
  65. {
  66. var result = GetAsync(request, "HEAD").Result;
  67. return result;
  68. }
  69. protected override bool EnableOutputInSubFolder
  70. {
  71. get
  72. {
  73. return true;
  74. }
  75. }
  76. private async Task<object> GetAsync(GetMasterManifest request, string method)
  77. {
  78. if (string.IsNullOrEmpty(request.MediaSourceId))
  79. {
  80. throw new ArgumentException("MediaSourceId is required");
  81. }
  82. var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
  83. var playlistText = string.Empty;
  84. if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
  85. {
  86. playlistText = new ManifestBuilder().GetManifestText(state, Request.RawUrl);
  87. }
  88. return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.mpd"), new Dictionary<string, string>());
  89. }
  90. public object Get(GetDashSegment request)
  91. {
  92. return GetDynamicSegment(request, request.SegmentId, request.RepresentationId).Result;
  93. }
  94. private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId, string representationId)
  95. {
  96. if ((request.StartTimeTicks ?? 0) > 0)
  97. {
  98. throw new ArgumentException("StartTimeTicks is not allowed.");
  99. }
  100. var cancellationTokenSource = new CancellationTokenSource();
  101. var cancellationToken = cancellationTokenSource.Token;
  102. var index = string.Equals(segmentId, "init", StringComparison.OrdinalIgnoreCase) ?
  103. -1 :
  104. int.Parse(segmentId, NumberStyles.Integer, UsCulture);
  105. var state = await GetState(request, cancellationToken).ConfigureAwait(false);
  106. var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".mpd");
  107. var segmentExtension = GetSegmentFileExtension(state);
  108. var segmentPath = GetSegmentPath(playlistPath, representationId, segmentExtension, index);
  109. var segmentLength = state.SegmentLength;
  110. TranscodingJob job = null;
  111. if (File.Exists(segmentPath))
  112. {
  113. job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
  114. return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
  115. }
  116. await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
  117. try
  118. {
  119. if (File.Exists(segmentPath))
  120. {
  121. job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
  122. return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
  123. }
  124. else
  125. {
  126. if (string.Equals(representationId, "0", StringComparison.OrdinalIgnoreCase))
  127. {
  128. var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
  129. Logger.Debug("Current transcoding index is {0}", currentTranscodingIndex ?? -2);
  130. if (currentTranscodingIndex == null || index < currentTranscodingIndex.Value || (index - currentTranscodingIndex.Value) > 4)
  131. {
  132. // If the playlist doesn't already exist, startup ffmpeg
  133. try
  134. {
  135. KillTranscodingJobs(request.DeviceId, playlistPath);
  136. if (currentTranscodingIndex.HasValue)
  137. {
  138. DeleteTranscodedFiles(playlistPath, 0);
  139. }
  140. var positionTicks = GetPositionTicks(state, index);
  141. request.StartTimeTicks = positionTicks;
  142. job = await StartFfMpeg(state, playlistPath, cancellationTokenSource, Path.GetDirectoryName(playlistPath)).ConfigureAwait(false);
  143. Task.Run(() => MonitorDashProcess(playlistPath, positionTicks == 0, job, cancellationToken));
  144. }
  145. catch
  146. {
  147. state.Dispose();
  148. throw;
  149. }
  150. await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
  151. }
  152. }
  153. }
  154. }
  155. finally
  156. {
  157. ApiEntryPoint.Instance.TranscodingStartLock.Release();
  158. }
  159. Logger.Info("waiting for {0}", segmentPath);
  160. while (!File.Exists(segmentPath))
  161. {
  162. await Task.Delay(50, cancellationToken).ConfigureAwait(false);
  163. }
  164. Logger.Info("returning {0}", segmentPath);
  165. return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType), cancellationToken).ConfigureAwait(false);
  166. }
  167. private void KillTranscodingJobs(string deviceId, string playlistPath)
  168. {
  169. ApiEntryPoint.Instance.KillTranscodingJobs(j => j.Type == TranscodingJobType && string.Equals(j.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase), p => !string.Equals(p, playlistPath, StringComparison.OrdinalIgnoreCase));
  170. }
  171. private long GetPositionTicks(StreamState state, int segmentIndex)
  172. {
  173. if (segmentIndex <= 1)
  174. {
  175. return 0;
  176. }
  177. var startSeconds = segmentIndex * state.SegmentLength;
  178. return TimeSpan.FromSeconds(startSeconds).Ticks;
  179. }
  180. protected override async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
  181. {
  182. var tmpPath = playlist + ".tmp";
  183. Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist);
  184. while (true)
  185. {
  186. FileStream fileStream;
  187. try
  188. {
  189. fileStream = FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
  190. }
  191. catch (IOException)
  192. {
  193. fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
  194. }
  195. // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
  196. using (fileStream)
  197. {
  198. using (var reader = new StreamReader(fileStream))
  199. {
  200. while (!reader.EndOfStream)
  201. {
  202. var line = await reader.ReadLineAsync().ConfigureAwait(false);
  203. if (line.IndexOf("stream0-" + segmentCount.ToString("00000", CultureInfo.InvariantCulture) + ".m4s", StringComparison.OrdinalIgnoreCase) != -1)
  204. {
  205. Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
  206. return;
  207. }
  208. }
  209. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  210. }
  211. }
  212. }
  213. }
  214. private async Task<object> GetSegmentResult(string playlistPath,
  215. string segmentPath,
  216. int segmentIndex,
  217. int segmentLength,
  218. TranscodingJob transcodingJob,
  219. CancellationToken cancellationToken)
  220. {
  221. // If all transcoding has completed, just return immediately
  222. if (transcodingJob != null && transcodingJob.HasExited)
  223. {
  224. return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
  225. }
  226. // Wait for the file to stop being written to, then stream it
  227. var length = new FileInfo(segmentPath).Length;
  228. var eofCount = 0;
  229. while (eofCount < 10)
  230. {
  231. var info = new FileInfo(segmentPath);
  232. if (!info.Exists)
  233. {
  234. break;
  235. }
  236. var newLength = info.Length;
  237. if (newLength == length)
  238. {
  239. eofCount++;
  240. }
  241. else
  242. {
  243. eofCount = 0;
  244. }
  245. length = newLength;
  246. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  247. }
  248. return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
  249. }
  250. private object GetSegmentResult(string segmentPath, int index, int segmentLength, TranscodingJob transcodingJob)
  251. {
  252. var segmentEndingSeconds = (1 + index) * segmentLength;
  253. var segmentEndingPositionTicks = TimeSpan.FromSeconds(segmentEndingSeconds).Ticks;
  254. return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
  255. {
  256. Path = segmentPath,
  257. FileShare = FileShare.ReadWrite,
  258. OnComplete = () =>
  259. {
  260. if (transcodingJob != null)
  261. {
  262. transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
  263. }
  264. }
  265. });
  266. }
  267. public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
  268. {
  269. var file = GetLastTranscodingFiles(playlist, segmentExtension, FileSystem, 1).FirstOrDefault();
  270. if (file == null)
  271. {
  272. return null;
  273. }
  274. return GetIndex(file.Name);
  275. }
  276. public int GetIndex(string segmentFile)
  277. {
  278. var indexString = Path.GetFileNameWithoutExtension(segmentFile).Split('-').LastOrDefault();
  279. if (string.Equals(indexString, "init", StringComparison.OrdinalIgnoreCase))
  280. {
  281. return -1;
  282. }
  283. return int.Parse(indexString, NumberStyles.Integer, UsCulture) - 1;
  284. }
  285. private void DeleteTranscodedFiles(string path, int retryCount)
  286. {
  287. if (retryCount >= 5)
  288. {
  289. return;
  290. }
  291. }
  292. private static List<FileInfo> GetLastTranscodingFiles(string playlist, string segmentExtension, IFileSystem fileSystem, int count)
  293. {
  294. var folder = Path.GetDirectoryName(playlist);
  295. try
  296. {
  297. return new DirectoryInfo(folder)
  298. .EnumerateFiles("*", SearchOption.TopDirectoryOnly)
  299. .Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase))
  300. .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
  301. .Take(count)
  302. .ToList();
  303. }
  304. catch (DirectoryNotFoundException)
  305. {
  306. return new List<FileInfo>();
  307. }
  308. }
  309. private string GetSegmentPath(string playlist, string representationId, string segmentExtension, int index)
  310. {
  311. var folder = Path.GetDirectoryName(playlist);
  312. var number = index == -1 ?
  313. "init" :
  314. index.ToString("00000", CultureInfo.InvariantCulture);
  315. var filename = "stream" + representationId + "-" + number + segmentExtension;
  316. return Path.Combine(folder, "completed", filename);
  317. }
  318. protected override string GetAudioArguments(StreamState state)
  319. {
  320. var codec = state.OutputAudioCodec;
  321. if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
  322. {
  323. return "-codec:a:0 copy";
  324. }
  325. var args = "-codec:a:0 " + codec;
  326. var channels = state.OutputAudioChannels;
  327. if (channels.HasValue)
  328. {
  329. args += " -ac " + channels.Value;
  330. }
  331. var bitrate = state.OutputAudioBitrate;
  332. if (bitrate.HasValue)
  333. {
  334. args += " -ab " + bitrate.Value.ToString(UsCulture);
  335. }
  336. args += " " + GetAudioFilterParam(state, true);
  337. return args;
  338. }
  339. protected override string GetVideoArguments(StreamState state)
  340. {
  341. var codec = state.OutputVideoCodec;
  342. var args = "-codec:v:0 " + codec;
  343. if (state.EnableMpegtsM2TsMode)
  344. {
  345. args += " -mpegts_m2ts_mode 1";
  346. }
  347. // See if we can save come cpu cycles by avoiding encoding
  348. if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
  349. {
  350. return state.VideoStream != null && IsH264(state.VideoStream) ?
  351. args + " -bsf:v h264_mp4toannexb" :
  352. args;
  353. }
  354. var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
  355. state.SegmentLength.ToString(UsCulture));
  356. var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
  357. args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg;
  358. // Add resolution params, if specified
  359. if (!hasGraphicalSubs)
  360. {
  361. args += GetOutputSizeParam(state, codec, false);
  362. }
  363. // This is for internal graphical subs
  364. if (hasGraphicalSubs)
  365. {
  366. args += GetGraphicalSubtitleParam(state, codec);
  367. }
  368. return args;
  369. }
  370. protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding)
  371. {
  372. // test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3
  373. // Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/
  374. var threads = GetNumberOfThreads(state, false);
  375. var inputModifier = GetInputModifier(state);
  376. //var startNumber = GetStartNumber(state);
  377. var initSegmentName = "stream$RepresentationID$-init.m4s";
  378. var segmentName = "stream$RepresentationID$-$Number%05d$.m4s";
  379. var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts {5} -f dash -init_seg_name \"{6}\" -media_seg_name \"{7}\" -use_template 0 -use_timeline 1 -min_seg_duration {8} -y \"{9}\"",
  380. inputModifier,
  381. GetInputArgument(transcodingJobId, state),
  382. threads,
  383. GetMapArgs(state),
  384. GetVideoArguments(state),
  385. GetAudioArguments(state),
  386. initSegmentName,
  387. segmentName,
  388. (state.SegmentLength * 1000000).ToString(CultureInfo.InvariantCulture),
  389. outputPath
  390. ).Trim();
  391. return args;
  392. }
  393. protected override int GetStartNumber(StreamState state)
  394. {
  395. return GetStartNumber(state.VideoRequest);
  396. }
  397. private int GetStartNumber(VideoStreamRequest request)
  398. {
  399. var segmentId = "0";
  400. var segmentRequest = request as GetDashSegment;
  401. if (segmentRequest != null)
  402. {
  403. segmentId = segmentRequest.SegmentId;
  404. }
  405. if (string.Equals(segmentId, "init", StringComparison.OrdinalIgnoreCase))
  406. {
  407. return -1;
  408. }
  409. return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
  410. }
  411. /// <summary>
  412. /// Gets the segment file extension.
  413. /// </summary>
  414. /// <param name="state">The state.</param>
  415. /// <returns>System.String.</returns>
  416. protected override string GetSegmentFileExtension(StreamState state)
  417. {
  418. return ".m4s";
  419. }
  420. protected override TranscodingJobType TranscodingJobType
  421. {
  422. get
  423. {
  424. return TranscodingJobType.Dash;
  425. }
  426. }
  427. private async void MonitorDashProcess(string playlist, bool moveInitSegment, TranscodingJob transcodingJob, CancellationToken cancellationToken)
  428. {
  429. var directory = new DirectoryInfo(Path.GetDirectoryName(playlist));
  430. var completedDirectory = Path.Combine(Path.GetDirectoryName(playlist), "completed");
  431. Directory.CreateDirectory(completedDirectory);
  432. while (!cancellationToken.IsCancellationRequested)
  433. {
  434. try
  435. {
  436. var files = directory.EnumerateFiles("*.m4s", SearchOption.TopDirectoryOnly)
  437. .OrderBy(FileSystem.GetCreationTimeUtc)
  438. .ToList();
  439. foreach (var file in files)
  440. {
  441. var fileIndex = GetIndex(file.Name);
  442. if (fileIndex == -1 && !moveInitSegment)
  443. {
  444. continue;
  445. }
  446. await WaitForFileToBeComplete(file.FullName, playlist, transcodingJob, cancellationToken).ConfigureAwait(false);
  447. var newName = fileIndex == -1
  448. ? "init.m4s"
  449. : fileIndex.ToString("00000", CultureInfo.InvariantCulture) + ".m4s";
  450. var representationId = file.FullName.IndexOf("stream0", StringComparison.OrdinalIgnoreCase) != -1 ?
  451. "0" :
  452. "1";
  453. newName = "stream" + representationId + "-" + newName;
  454. File.Copy(file.FullName, Path.Combine(completedDirectory, newName), true);
  455. cancellationToken.ThrowIfCancellationRequested();
  456. }
  457. await Task.Delay(250, cancellationToken).ConfigureAwait(false);
  458. }
  459. catch (OperationCanceledException)
  460. {
  461. break;
  462. }
  463. catch (IOException)
  464. {
  465. }
  466. }
  467. }
  468. private async Task WaitForFileToBeComplete(string segmentPath, string playlistPath, TranscodingJob transcodingJob, CancellationToken cancellationToken)
  469. {
  470. // If all transcoding has completed, just return immediately
  471. if (transcodingJob != null && transcodingJob.HasExited)
  472. {
  473. return;
  474. }
  475. var segmentFilename = Path.GetFileName(segmentPath);
  476. using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
  477. {
  478. using (var reader = new StreamReader(fileStream))
  479. {
  480. var text = await reader.ReadToEndAsync().ConfigureAwait(false);
  481. // If it appears in the playlist, it's done
  482. if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
  483. {
  484. return;
  485. }
  486. }
  487. }
  488. // Wait for the file to stop being written to, then stream it
  489. var length = new FileInfo(segmentPath).Length;
  490. var eofCount = 0;
  491. while (eofCount < 10)
  492. {
  493. var info = new FileInfo(segmentPath);
  494. if (!info.Exists)
  495. {
  496. break;
  497. }
  498. var newLength = info.Length;
  499. if (newLength == length)
  500. {
  501. eofCount++;
  502. }
  503. else
  504. {
  505. eofCount = 0;
  506. }
  507. length = newLength;
  508. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  509. }
  510. }
  511. }
  512. }