StreamBuilder.cs 22 KB

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