StreamBuilder.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. using MediaBrowser.Model.Dto;
  2. using MediaBrowser.Model.Entities;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using System.Linq;
  7. namespace MediaBrowser.Model.Dlna
  8. {
  9. public class StreamBuilder
  10. {
  11. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  12. public StreamInfo BuildAudioItem(AudioOptions options)
  13. {
  14. ValidateAudioInput(options);
  15. var mediaSources = options.MediaSources;
  16. // If the client wants a specific media soure, filter now
  17. if (!string.IsNullOrEmpty(options.MediaSourceId))
  18. {
  19. // Avoid implicitly captured closure
  20. var mediaSourceId = options.MediaSourceId;
  21. mediaSources = mediaSources
  22. .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
  23. .ToList();
  24. }
  25. var streams = mediaSources.Select(i => BuildAudioItem(i, options)).ToList();
  26. foreach (var stream in streams)
  27. {
  28. stream.DeviceId = options.DeviceId;
  29. stream.DeviceProfileId = options.Profile.Id;
  30. }
  31. return GetOptimalStream(streams);
  32. }
  33. public StreamInfo BuildVideoItem(VideoOptions options)
  34. {
  35. ValidateInput(options);
  36. var mediaSources = options.MediaSources;
  37. // If the client wants a specific media soure, filter now
  38. if (!string.IsNullOrEmpty(options.MediaSourceId))
  39. {
  40. // Avoid implicitly captured closure
  41. var mediaSourceId = options.MediaSourceId;
  42. mediaSources = mediaSources
  43. .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
  44. .ToList();
  45. }
  46. var streams = mediaSources.Select(i => BuildVideoItem(i, options)).ToList();
  47. foreach (var stream in streams)
  48. {
  49. stream.DeviceId = options.DeviceId;
  50. stream.DeviceProfileId = options.Profile.Id;
  51. }
  52. return GetOptimalStream(streams);
  53. }
  54. private StreamInfo GetOptimalStream(List<StreamInfo> streams)
  55. {
  56. // Grab the first one that can be direct streamed
  57. // If that doesn't produce anything, just take the first
  58. return streams.FirstOrDefault(i => i.IsDirectStream) ??
  59. streams.FirstOrDefault();
  60. }
  61. private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
  62. {
  63. var playlistItem = new StreamInfo
  64. {
  65. ItemId = options.ItemId,
  66. MediaType = DlnaProfileType.Audio,
  67. MediaSource = item,
  68. RunTimeTicks = item.RunTimeTicks
  69. };
  70. var maxBitrateSetting = options.MaxBitrate ?? options.Profile.MaxBitrate;
  71. var audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
  72. // Honor the max bitrate setting
  73. if (IsAudioEligibleForDirectPlay(item, maxBitrateSetting))
  74. {
  75. var directPlay = options.Profile.DirectPlayProfiles
  76. .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsAudioDirectPlaySupported(i, item, audioStream));
  77. if (directPlay != null)
  78. {
  79. var audioCodec = audioStream == null ? null : audioStream.Codec;
  80. // Make sure audio codec profiles are satisfied
  81. if (!string.IsNullOrEmpty(audioCodec) && options.Profile.CodecProfiles.Where(i => i.Type == CodecType.Audio && i.ContainsCodec(audioCodec))
  82. .All(i => AreConditionsSatisfied(i.Conditions, item.Path, null, audioStream)))
  83. {
  84. playlistItem.IsDirectStream = true;
  85. playlistItem.Container = item.Container;
  86. return playlistItem;
  87. }
  88. }
  89. }
  90. var transcodingProfile = options.Profile.TranscodingProfiles
  91. .FirstOrDefault(i => i.Type == playlistItem.MediaType);
  92. if (transcodingProfile != null)
  93. {
  94. playlistItem.IsDirectStream = false;
  95. playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  96. playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
  97. playlistItem.Container = transcodingProfile.Container;
  98. playlistItem.AudioCodec = transcodingProfile.AudioCodec;
  99. playlistItem.Protocol = transcodingProfile.Protocol;
  100. var audioTranscodingConditions = options.Profile.CodecProfiles
  101. .Where(i => i.Type == CodecType.Audio && i.ContainsCodec(transcodingProfile.AudioCodec))
  102. .Take(1)
  103. .SelectMany(i => i.Conditions);
  104. ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
  105. // Honor requested max channels
  106. if (options.MaxAudioChannels.HasValue)
  107. {
  108. var currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
  109. playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
  110. }
  111. // Honor requested max bitrate
  112. if (maxBitrateSetting.HasValue)
  113. {
  114. var currentValue = playlistItem.AudioBitrate ?? maxBitrateSetting.Value;
  115. playlistItem.AudioBitrate = Math.Min(maxBitrateSetting.Value, currentValue);
  116. }
  117. }
  118. return playlistItem;
  119. }
  120. private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
  121. {
  122. var playlistItem = new StreamInfo
  123. {
  124. ItemId = options.ItemId,
  125. MediaType = DlnaProfileType.Video,
  126. MediaSource = item,
  127. RunTimeTicks = item.RunTimeTicks
  128. };
  129. var audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
  130. var videoStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
  131. var maxBitrateSetting = options.MaxBitrate ?? options.Profile.MaxBitrate;
  132. if (IsEligibleForDirectPlay(item, options, maxBitrateSetting))
  133. {
  134. // See if it can be direct played
  135. var directPlay = options.Profile.DirectPlayProfiles
  136. .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsVideoDirectPlaySupported(i, item, videoStream, audioStream));
  137. if (directPlay != null)
  138. {
  139. var videoCodec = videoStream == null ? null : videoStream.Codec;
  140. // Make sure video codec profiles are satisfied
  141. if (!string.IsNullOrEmpty(videoCodec) && options.Profile.CodecProfiles.Where(i => i.Type == CodecType.Video && i.ContainsCodec(videoCodec))
  142. .All(i => AreConditionsSatisfied(i.Conditions, item.Path, videoStream, audioStream)))
  143. {
  144. var audioCodec = audioStream == null ? null : audioStream.Codec;
  145. // Make sure audio codec profiles are satisfied
  146. if (string.IsNullOrEmpty(audioCodec) || options.Profile.CodecProfiles.Where(i => i.Type == CodecType.VideoAudio && i.ContainsCodec(audioCodec))
  147. .All(i => AreConditionsSatisfied(i.Conditions, item.Path, videoStream, audioStream)))
  148. {
  149. playlistItem.IsDirectStream = true;
  150. playlistItem.Container = item.Container;
  151. return playlistItem;
  152. }
  153. }
  154. }
  155. }
  156. // Can't direct play, find the transcoding profile
  157. var transcodingProfile = options.Profile.TranscodingProfiles
  158. .FirstOrDefault(i => i.Type == playlistItem.MediaType);
  159. if (transcodingProfile != null)
  160. {
  161. playlistItem.IsDirectStream = false;
  162. playlistItem.Container = transcodingProfile.Container;
  163. playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
  164. playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  165. playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',').FirstOrDefault();
  166. playlistItem.VideoCodec = transcodingProfile.VideoCodec;
  167. playlistItem.Protocol = transcodingProfile.Protocol;
  168. var videoTranscodingConditions = options.Profile.CodecProfiles
  169. .Where(i => i.Type == CodecType.Video && i.ContainsCodec(transcodingProfile.VideoCodec))
  170. .Take(1)
  171. .SelectMany(i => i.Conditions);
  172. ApplyTranscodingConditions(playlistItem, videoTranscodingConditions);
  173. var audioTranscodingConditions = options.Profile.CodecProfiles
  174. .Where(i => i.Type == CodecType.VideoAudio && i.ContainsCodec(transcodingProfile.AudioCodec))
  175. .Take(1)
  176. .SelectMany(i => i.Conditions);
  177. ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
  178. // Honor requested max channels
  179. if (options.MaxAudioChannels.HasValue)
  180. {
  181. var currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
  182. playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
  183. }
  184. // Honor requested max bitrate
  185. if (options.MaxAudioTranscodingBitrate.HasValue)
  186. {
  187. var currentValue = playlistItem.AudioBitrate ?? options.MaxAudioTranscodingBitrate.Value;
  188. playlistItem.AudioBitrate = Math.Min(options.MaxAudioTranscodingBitrate.Value, currentValue);
  189. }
  190. // Honor max rate
  191. if (maxBitrateSetting.HasValue)
  192. {
  193. var videoBitrate = maxBitrateSetting.Value;
  194. if (playlistItem.AudioBitrate.HasValue)
  195. {
  196. videoBitrate -= playlistItem.AudioBitrate.Value;
  197. }
  198. var currentValue = playlistItem.VideoBitrate ?? videoBitrate;
  199. playlistItem.VideoBitrate = Math.Min(videoBitrate, currentValue);
  200. }
  201. }
  202. return playlistItem;
  203. }
  204. private bool IsEligibleForDirectPlay(MediaSourceInfo item, VideoOptions options, int? maxBitrate)
  205. {
  206. if (options.SubtitleStreamIndex.HasValue)
  207. {
  208. return false;
  209. }
  210. if (options.AudioStreamIndex.HasValue &&
  211. item.MediaStreams.Count(i => i.Type == MediaStreamType.Audio) > 1)
  212. {
  213. return false;
  214. }
  215. return IsAudioEligibleForDirectPlay(item, maxBitrate);
  216. }
  217. private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, int? maxBitrate)
  218. {
  219. // Honor the max bitrate setting
  220. return !maxBitrate.HasValue || (item.Bitrate.HasValue && item.Bitrate.Value <= maxBitrate.Value);
  221. }
  222. private void ValidateInput(VideoOptions options)
  223. {
  224. ValidateAudioInput(options);
  225. if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
  226. {
  227. throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
  228. }
  229. if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
  230. {
  231. throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
  232. }
  233. }
  234. private void ValidateAudioInput(AudioOptions options)
  235. {
  236. if (string.IsNullOrEmpty(options.ItemId))
  237. {
  238. throw new ArgumentException("ItemId is required");
  239. }
  240. if (string.IsNullOrEmpty(options.DeviceId))
  241. {
  242. throw new ArgumentException("DeviceId is required");
  243. }
  244. if (options.Profile == null)
  245. {
  246. throw new ArgumentException("Profile is required");
  247. }
  248. if (options.MediaSources == null)
  249. {
  250. throw new ArgumentException("MediaSources is required");
  251. }
  252. }
  253. private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions)
  254. {
  255. foreach (var condition in conditions
  256. .Where(i => !string.IsNullOrEmpty(i.Value)))
  257. {
  258. var value = condition.Value;
  259. switch (condition.Property)
  260. {
  261. case ProfileConditionValue.AudioBitrate:
  262. {
  263. int num;
  264. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  265. {
  266. item.AudioBitrate = num;
  267. }
  268. break;
  269. }
  270. case ProfileConditionValue.AudioChannels:
  271. {
  272. int num;
  273. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  274. {
  275. item.MaxAudioChannels = num;
  276. }
  277. break;
  278. }
  279. case ProfileConditionValue.AudioProfile:
  280. case ProfileConditionValue.Has64BitOffsets:
  281. case ProfileConditionValue.VideoBitDepth:
  282. case ProfileConditionValue.VideoProfile:
  283. {
  284. // Not supported yet
  285. break;
  286. }
  287. case ProfileConditionValue.Height:
  288. {
  289. int num;
  290. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  291. {
  292. item.MaxHeight = num;
  293. }
  294. break;
  295. }
  296. case ProfileConditionValue.VideoBitrate:
  297. {
  298. int num;
  299. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  300. {
  301. item.VideoBitrate = num;
  302. }
  303. break;
  304. }
  305. case ProfileConditionValue.VideoFramerate:
  306. {
  307. int num;
  308. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  309. {
  310. item.MaxFramerate = num;
  311. }
  312. break;
  313. }
  314. case ProfileConditionValue.VideoLevel:
  315. {
  316. int num;
  317. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  318. {
  319. item.VideoLevel = num;
  320. }
  321. break;
  322. }
  323. case ProfileConditionValue.Width:
  324. {
  325. int num;
  326. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  327. {
  328. item.MaxWidth = num;
  329. }
  330. break;
  331. }
  332. default:
  333. throw new ArgumentException("Unrecognized ProfileConditionValue");
  334. }
  335. }
  336. }
  337. private bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
  338. {
  339. if (profile.Container.Length > 0)
  340. {
  341. // Check container type
  342. var mediaContainer = item.Container ?? string.Empty;
  343. if (!profile.GetContainers().Any(i => string.Equals(i, mediaContainer, StringComparison.OrdinalIgnoreCase)))
  344. {
  345. return false;
  346. }
  347. }
  348. return true;
  349. }
  350. private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
  351. {
  352. // Only plain video files can be direct played
  353. if (item.VideoType != VideoType.VideoFile)
  354. {
  355. return false;
  356. }
  357. if (profile.Container.Length > 0)
  358. {
  359. // Check container type
  360. var mediaContainer = item.Container ?? string.Empty;
  361. if (!profile.GetContainers().Any(i => string.Equals(i, mediaContainer, StringComparison.OrdinalIgnoreCase)))
  362. {
  363. return false;
  364. }
  365. }
  366. // Check video codec
  367. var videoCodecs = profile.GetVideoCodecs();
  368. if (videoCodecs.Count > 0)
  369. {
  370. var videoCodec = videoStream == null ? null : videoStream.Codec;
  371. if (string.IsNullOrEmpty(videoCodec) || !videoCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
  372. {
  373. return false;
  374. }
  375. }
  376. var audioCodecs = profile.GetAudioCodecs();
  377. if (audioCodecs.Count > 0)
  378. {
  379. // Check audio codecs
  380. var audioCodec = audioStream == null ? null : audioStream.Codec;
  381. if (string.IsNullOrEmpty(audioCodec) || !audioCodecs.Contains(audioCodec, StringComparer.OrdinalIgnoreCase))
  382. {
  383. return false;
  384. }
  385. }
  386. return true;
  387. }
  388. private bool AreConditionsSatisfied(IEnumerable<ProfileCondition> conditions, string mediaPath, MediaStream videoStream, MediaStream audioStream)
  389. {
  390. return conditions.All(i => IsConditionSatisfied(i, mediaPath, videoStream, audioStream));
  391. }
  392. /// <summary>
  393. /// Determines whether [is condition satisfied] [the specified condition].
  394. /// </summary>
  395. /// <param name="condition">The condition.</param>
  396. /// <param name="mediaPath">The media path.</param>
  397. /// <param name="videoStream">The video stream.</param>
  398. /// <param name="audioStream">The audio stream.</param>
  399. /// <returns><c>true</c> if [is condition satisfied] [the specified condition]; otherwise, <c>false</c>.</returns>
  400. /// <exception cref="System.InvalidOperationException">Unexpected ProfileConditionType</exception>
  401. private bool IsConditionSatisfied(ProfileCondition condition, string mediaPath, MediaStream videoStream, MediaStream audioStream)
  402. {
  403. if (condition.Property == ProfileConditionValue.Has64BitOffsets)
  404. {
  405. // TODO: Determine how to evaluate this
  406. }
  407. if (condition.Property == ProfileConditionValue.VideoProfile)
  408. {
  409. var profile = videoStream == null ? null : videoStream.Profile;
  410. if (!string.IsNullOrEmpty(profile))
  411. {
  412. switch (condition.Condition)
  413. {
  414. case ProfileConditionType.Equals:
  415. return string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase);
  416. case ProfileConditionType.NotEquals:
  417. return !string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase);
  418. default:
  419. throw new InvalidOperationException("Unexpected ProfileConditionType");
  420. }
  421. }
  422. }
  423. else if (condition.Property == ProfileConditionValue.AudioProfile)
  424. {
  425. var profile = audioStream == null ? null : audioStream.Profile;
  426. if (!string.IsNullOrEmpty(profile))
  427. {
  428. switch (condition.Condition)
  429. {
  430. case ProfileConditionType.Equals:
  431. return string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase);
  432. case ProfileConditionType.NotEquals:
  433. return !string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase);
  434. default:
  435. throw new InvalidOperationException("Unexpected ProfileConditionType");
  436. }
  437. }
  438. }
  439. else
  440. {
  441. var actualValue = GetConditionValue(condition, mediaPath, videoStream, audioStream);
  442. if (actualValue.HasValue)
  443. {
  444. double expected;
  445. if (double.TryParse(condition.Value, NumberStyles.Any, _usCulture, out expected))
  446. {
  447. switch (condition.Condition)
  448. {
  449. case ProfileConditionType.Equals:
  450. return actualValue.Value.Equals(expected);
  451. case ProfileConditionType.GreaterThanEqual:
  452. return actualValue.Value >= expected;
  453. case ProfileConditionType.LessThanEqual:
  454. return actualValue.Value <= expected;
  455. case ProfileConditionType.NotEquals:
  456. return !actualValue.Value.Equals(expected);
  457. default:
  458. throw new InvalidOperationException("Unexpected ProfileConditionType");
  459. }
  460. }
  461. }
  462. }
  463. // Value doesn't exist in metadata. Fail it if required.
  464. return !condition.IsRequired;
  465. }
  466. /// <summary>
  467. /// Gets the condition value.
  468. /// </summary>
  469. /// <param name="condition">The condition.</param>
  470. /// <param name="mediaPath">The media path.</param>
  471. /// <param name="videoStream">The video stream.</param>
  472. /// <param name="audioStream">The audio stream.</param>
  473. /// <returns>System.Nullable{System.Int64}.</returns>
  474. /// <exception cref="System.InvalidOperationException">Unexpected Property</exception>
  475. private double? GetConditionValue(ProfileCondition condition, string mediaPath, MediaStream videoStream, MediaStream audioStream)
  476. {
  477. switch (condition.Property)
  478. {
  479. case ProfileConditionValue.AudioBitrate:
  480. return audioStream == null ? null : audioStream.BitRate;
  481. case ProfileConditionValue.AudioChannels:
  482. return audioStream == null ? null : audioStream.Channels;
  483. case ProfileConditionValue.VideoBitrate:
  484. return videoStream == null ? null : videoStream.BitRate;
  485. case ProfileConditionValue.VideoFramerate:
  486. return videoStream == null ? null : (videoStream.AverageFrameRate ?? videoStream.RealFrameRate);
  487. case ProfileConditionValue.Height:
  488. return videoStream == null ? null : videoStream.Height;
  489. case ProfileConditionValue.Width:
  490. return videoStream == null ? null : videoStream.Width;
  491. case ProfileConditionValue.VideoLevel:
  492. return videoStream == null ? null : videoStream.Level;
  493. case ProfileConditionValue.VideoBitDepth:
  494. return videoStream == null ? null : GetBitDepth(videoStream);
  495. default:
  496. throw new InvalidOperationException("Unexpected Property");
  497. }
  498. }
  499. private int? GetBitDepth(MediaStream videoStream)
  500. {
  501. var eightBit = new List<string>
  502. {
  503. "yuv420p",
  504. "yuv411p",
  505. "yuvj420p",
  506. "uyyvyy411",
  507. "nv12",
  508. "nv21",
  509. "rgb444le",
  510. "rgb444be",
  511. "bgr444le",
  512. "bgr444be",
  513. "yuvj411p"
  514. };
  515. if (!string.IsNullOrEmpty(videoStream.PixelFormat))
  516. {
  517. if (eightBit.Contains(videoStream.PixelFormat, StringComparer.OrdinalIgnoreCase))
  518. {
  519. return 8;
  520. }
  521. }
  522. return null;
  523. }
  524. }
  525. }