StreamBuilder.cs 25 KB

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