StreamBuilder.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. using MediaBrowser.Model.Dto;
  2. using MediaBrowser.Model.Entities;
  3. using MediaBrowser.Model.MediaInfo;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Globalization;
  7. using System.Linq;
  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. List<MediaSourceInfo> 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. string mediaSourceId = options.MediaSourceId;
  22. mediaSources = mediaSources
  23. .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
  24. .ToList();
  25. }
  26. List<StreamInfo> streams = mediaSources.Select(i => BuildAudioItem(i, options)).ToList();
  27. foreach (StreamInfo 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. List<MediaSourceInfo> 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. string mediaSourceId = options.MediaSourceId;
  43. mediaSources = mediaSources
  44. .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
  45. .ToList();
  46. }
  47. List<StreamInfo> streams = mediaSources.Select(i => BuildVideoItem(i, options)).ToList();
  48. foreach (StreamInfo 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(MediaSourceInfo item, AudioOptions options)
  63. {
  64. StreamInfo playlistItem = new StreamInfo
  65. {
  66. ItemId = options.ItemId,
  67. MediaType = DlnaProfileType.Audio,
  68. MediaSource = item,
  69. RunTimeTicks = item.RunTimeTicks
  70. };
  71. int? maxBitrateSetting = options.MaxBitrate ?? options.Profile.MaxBitrate;
  72. MediaStream audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
  73. // Honor the max bitrate setting
  74. if (IsAudioEligibleForDirectPlay(item, maxBitrateSetting))
  75. {
  76. DirectPlayProfile directPlay = options.Profile.DirectPlayProfiles
  77. .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsAudioDirectPlaySupported(i, item, audioStream));
  78. if (directPlay != null)
  79. {
  80. string audioCodec = audioStream == null ? null : audioStream.Codec;
  81. // Make sure audio codec profiles are satisfied
  82. if (!string.IsNullOrEmpty(audioCodec))
  83. {
  84. ConditionProcessor conditionProcessor = new ConditionProcessor();
  85. IEnumerable<ProfileCondition> conditions = options.Profile.CodecProfiles.Where(i => i.Type == CodecType.Audio && i.ContainsCodec(audioCodec))
  86. .SelectMany(i => i.Conditions);
  87. int? audioChannels = audioStream.Channels;
  88. int? audioBitrate = audioStream.BitRate;
  89. if (conditions.All(c => conditionProcessor.IsAudioConditionSatisfied(c, audioChannels, audioBitrate)))
  90. {
  91. playlistItem.IsDirectStream = true;
  92. playlistItem.Container = item.Container;
  93. return playlistItem;
  94. }
  95. }
  96. }
  97. }
  98. TranscodingProfile transcodingProfile = options.Profile.TranscodingProfiles
  99. .FirstOrDefault(i => i.Type == playlistItem.MediaType);
  100. if (transcodingProfile != null)
  101. {
  102. playlistItem.IsDirectStream = false;
  103. playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  104. playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
  105. playlistItem.Container = transcodingProfile.Container;
  106. playlistItem.AudioCodec = transcodingProfile.AudioCodec;
  107. playlistItem.Protocol = transcodingProfile.Protocol;
  108. IEnumerable<ProfileCondition> audioTranscodingConditions = options.Profile.CodecProfiles
  109. .Where(i => i.Type == CodecType.Audio && i.ContainsCodec(transcodingProfile.AudioCodec))
  110. .Take(1)
  111. .SelectMany(i => i.Conditions);
  112. ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
  113. // Honor requested max channels
  114. if (options.MaxAudioChannels.HasValue)
  115. {
  116. int currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
  117. playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
  118. }
  119. // Honor requested max bitrate
  120. if (maxBitrateSetting.HasValue)
  121. {
  122. int currentValue = playlistItem.AudioBitrate ?? maxBitrateSetting.Value;
  123. playlistItem.AudioBitrate = Math.Min(maxBitrateSetting.Value, currentValue);
  124. }
  125. }
  126. return playlistItem;
  127. }
  128. private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
  129. {
  130. StreamInfo playlistItem = new StreamInfo
  131. {
  132. ItemId = options.ItemId,
  133. MediaType = DlnaProfileType.Video,
  134. MediaSource = item,
  135. RunTimeTicks = item.RunTimeTicks
  136. };
  137. MediaStream audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
  138. MediaStream videoStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
  139. int? maxBitrateSetting = options.MaxBitrate ?? options.Profile.MaxBitrate;
  140. if (IsEligibleForDirectPlay(item, options, maxBitrateSetting))
  141. {
  142. // See if it can be direct played
  143. DirectPlayProfile directPlay = GetVideoDirectPlayProfile(options.Profile, item, videoStream, audioStream);
  144. if (directPlay != null)
  145. {
  146. playlistItem.IsDirectStream = true;
  147. playlistItem.Container = item.Container;
  148. return playlistItem;
  149. }
  150. }
  151. // Can't direct play, find the transcoding profile
  152. TranscodingProfile transcodingProfile = options.Profile.TranscodingProfiles
  153. .FirstOrDefault(i => i.Type == playlistItem.MediaType);
  154. if (transcodingProfile != null)
  155. {
  156. playlistItem.IsDirectStream = false;
  157. playlistItem.Container = transcodingProfile.Container;
  158. playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
  159. playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  160. playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',').FirstOrDefault();
  161. playlistItem.VideoCodec = transcodingProfile.VideoCodec;
  162. playlistItem.Protocol = transcodingProfile.Protocol;
  163. playlistItem.AudioStreamIndex = options.AudioStreamIndex;
  164. playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex;
  165. IEnumerable<ProfileCondition> videoTranscodingConditions = options.Profile.CodecProfiles
  166. .Where(i => i.Type == CodecType.Video && i.ContainsCodec(transcodingProfile.VideoCodec))
  167. .Take(1)
  168. .SelectMany(i => i.Conditions);
  169. ApplyTranscodingConditions(playlistItem, videoTranscodingConditions);
  170. IEnumerable<ProfileCondition> audioTranscodingConditions = options.Profile.CodecProfiles
  171. .Where(i => i.Type == CodecType.VideoAudio && i.ContainsCodec(transcodingProfile.AudioCodec))
  172. .Take(1)
  173. .SelectMany(i => i.Conditions);
  174. ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
  175. // Honor requested max channels
  176. if (options.MaxAudioChannels.HasValue)
  177. {
  178. int currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
  179. playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
  180. }
  181. // Honor requested max bitrate
  182. if (options.MaxAudioTranscodingBitrate.HasValue)
  183. {
  184. int currentValue = playlistItem.AudioBitrate ?? options.MaxAudioTranscodingBitrate.Value;
  185. playlistItem.AudioBitrate = Math.Min(options.MaxAudioTranscodingBitrate.Value, currentValue);
  186. }
  187. // Honor max rate
  188. if (maxBitrateSetting.HasValue)
  189. {
  190. int videoBitrate = maxBitrateSetting.Value;
  191. if (playlistItem.AudioBitrate.HasValue)
  192. {
  193. videoBitrate -= playlistItem.AudioBitrate.Value;
  194. }
  195. int currentValue = playlistItem.VideoBitrate ?? videoBitrate;
  196. playlistItem.VideoBitrate = Math.Min(videoBitrate, currentValue);
  197. }
  198. }
  199. return playlistItem;
  200. }
  201. private DirectPlayProfile GetVideoDirectPlayProfile(DeviceProfile profile,
  202. MediaSourceInfo mediaSource,
  203. MediaStream videoStream,
  204. MediaStream audioStream)
  205. {
  206. // See if it can be direct played
  207. DirectPlayProfile directPlay = profile.DirectPlayProfiles
  208. .FirstOrDefault(i => i.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(i, mediaSource, videoStream, audioStream));
  209. if (directPlay == null)
  210. {
  211. return null;
  212. }
  213. string container = mediaSource.Container;
  214. IEnumerable<ProfileCondition> conditions = profile.ContainerProfiles
  215. .Where(i => i.Type == DlnaProfileType.Video && i.GetContainers().Contains(container, StringComparer.OrdinalIgnoreCase))
  216. .SelectMany(i => i.Conditions);
  217. ConditionProcessor conditionProcessor = new ConditionProcessor();
  218. int? width = videoStream == null ? null : videoStream.Width;
  219. int? height = videoStream == null ? null : videoStream.Height;
  220. int? bitDepth = videoStream == null ? null : videoStream.BitDepth;
  221. int? videoBitrate = videoStream == null ? null : videoStream.BitRate;
  222. double? videoLevel = videoStream == null ? null : videoStream.Level;
  223. string videoProfile = videoStream == null ? null : videoStream.Profile;
  224. float? videoFramerate = videoStream == null ? null : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate;
  225. int? audioBitrate = audioStream == null ? null : audioStream.BitRate;
  226. int? audioChannels = audioStream == null ? null : audioStream.Channels;
  227. string audioProfile = audioStream == null ? null : audioStream.Profile;
  228. TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
  229. int? packetLength = videoStream == null ? null : videoStream.PacketLength;
  230. // Check container conditions
  231. if (!conditions.All(i => conditionProcessor.IsVideoConditionSatisfied(i,
  232. audioBitrate,
  233. audioChannels,
  234. width,
  235. height,
  236. bitDepth,
  237. videoBitrate,
  238. videoProfile,
  239. videoLevel,
  240. videoFramerate,
  241. packetLength,
  242. timestamp)))
  243. {
  244. return null;
  245. }
  246. string videoCodec = videoStream == null ? null : videoStream.Codec;
  247. if (string.IsNullOrEmpty(videoCodec))
  248. {
  249. return null;
  250. }
  251. conditions = profile.CodecProfiles
  252. .Where(i => i.Type == CodecType.Video && i.ContainsCodec(videoCodec))
  253. .SelectMany(i => i.Conditions);
  254. if (!conditions.All(i => conditionProcessor.IsVideoConditionSatisfied(i,
  255. audioBitrate,
  256. audioChannels,
  257. width,
  258. height,
  259. bitDepth,
  260. videoBitrate,
  261. videoProfile,
  262. videoLevel,
  263. videoFramerate,
  264. packetLength,
  265. timestamp)))
  266. {
  267. return null;
  268. }
  269. if (audioStream != null)
  270. {
  271. string audioCodec = audioStream.Codec;
  272. if (string.IsNullOrEmpty(audioCodec))
  273. {
  274. return null;
  275. }
  276. conditions = profile.CodecProfiles
  277. .Where(i => i.Type == CodecType.VideoAudio && i.ContainsCodec(audioCodec))
  278. .SelectMany(i => i.Conditions);
  279. if (!conditions.All(i => conditionProcessor.IsVideoAudioConditionSatisfied(i,
  280. audioChannels,
  281. audioBitrate,
  282. audioProfile)))
  283. {
  284. return null;
  285. }
  286. }
  287. return directPlay;
  288. }
  289. private bool IsEligibleForDirectPlay(MediaSourceInfo item, VideoOptions options, int? maxBitrate)
  290. {
  291. if (options.SubtitleStreamIndex.HasValue)
  292. {
  293. return false;
  294. }
  295. if (options.AudioStreamIndex.HasValue &&
  296. item.MediaStreams.Count(i => i.Type == MediaStreamType.Audio) > 1)
  297. {
  298. return false;
  299. }
  300. return IsAudioEligibleForDirectPlay(item, maxBitrate);
  301. }
  302. private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, int? maxBitrate)
  303. {
  304. // Honor the max bitrate setting
  305. return !maxBitrate.HasValue || (item.Bitrate.HasValue && item.Bitrate.Value <= maxBitrate.Value);
  306. }
  307. private void ValidateInput(VideoOptions options)
  308. {
  309. ValidateAudioInput(options);
  310. if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
  311. {
  312. throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
  313. }
  314. if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
  315. {
  316. throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
  317. }
  318. }
  319. private void ValidateAudioInput(AudioOptions options)
  320. {
  321. if (string.IsNullOrEmpty(options.ItemId))
  322. {
  323. throw new ArgumentException("ItemId is required");
  324. }
  325. if (string.IsNullOrEmpty(options.DeviceId))
  326. {
  327. throw new ArgumentException("DeviceId is required");
  328. }
  329. if (options.Profile == null)
  330. {
  331. throw new ArgumentException("Profile is required");
  332. }
  333. if (options.MediaSources == null)
  334. {
  335. throw new ArgumentException("MediaSources is required");
  336. }
  337. }
  338. private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions)
  339. {
  340. foreach (ProfileCondition condition in conditions
  341. .Where(i => !string.IsNullOrEmpty(i.Value)))
  342. {
  343. string value = condition.Value;
  344. switch (condition.Property)
  345. {
  346. case ProfileConditionValue.AudioBitrate:
  347. {
  348. int num;
  349. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  350. {
  351. item.AudioBitrate = num;
  352. }
  353. break;
  354. }
  355. case ProfileConditionValue.AudioChannels:
  356. {
  357. int num;
  358. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  359. {
  360. item.MaxAudioChannels = num;
  361. }
  362. break;
  363. }
  364. case ProfileConditionValue.AudioProfile:
  365. case ProfileConditionValue.Has64BitOffsets:
  366. case ProfileConditionValue.PacketLength:
  367. case ProfileConditionValue.VideoTimestamp:
  368. case ProfileConditionValue.VideoBitDepth:
  369. {
  370. // Not supported yet
  371. break;
  372. }
  373. case ProfileConditionValue.VideoProfile:
  374. {
  375. item.VideoProfile = value;
  376. break;
  377. }
  378. case ProfileConditionValue.Height:
  379. {
  380. int num;
  381. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  382. {
  383. item.MaxHeight = num;
  384. }
  385. break;
  386. }
  387. case ProfileConditionValue.VideoBitrate:
  388. {
  389. int num;
  390. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  391. {
  392. item.VideoBitrate = num;
  393. }
  394. break;
  395. }
  396. case ProfileConditionValue.VideoFramerate:
  397. {
  398. int num;
  399. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  400. {
  401. item.MaxFramerate = num;
  402. }
  403. break;
  404. }
  405. case ProfileConditionValue.VideoLevel:
  406. {
  407. int num;
  408. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  409. {
  410. item.VideoLevel = num;
  411. }
  412. break;
  413. }
  414. case ProfileConditionValue.Width:
  415. {
  416. int num;
  417. if (int.TryParse(value, NumberStyles.Any, _usCulture, out num))
  418. {
  419. item.MaxWidth = num;
  420. }
  421. break;
  422. }
  423. default:
  424. throw new ArgumentException("Unrecognized ProfileConditionValue");
  425. }
  426. }
  427. }
  428. private bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
  429. {
  430. if (profile.Container.Length > 0)
  431. {
  432. // Check container type
  433. string mediaContainer = item.Container ?? string.Empty;
  434. if (!profile.GetContainers().Any(i => string.Equals(i, mediaContainer, StringComparison.OrdinalIgnoreCase)))
  435. {
  436. return false;
  437. }
  438. }
  439. return true;
  440. }
  441. private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
  442. {
  443. // Only plain video files can be direct played
  444. if (item.VideoType != VideoType.VideoFile)
  445. {
  446. return false;
  447. }
  448. if (profile.Container.Length > 0)
  449. {
  450. // Check container type
  451. string mediaContainer = item.Container ?? string.Empty;
  452. if (!profile.GetContainers().Any(i => string.Equals(i, mediaContainer, StringComparison.OrdinalIgnoreCase)))
  453. {
  454. return false;
  455. }
  456. }
  457. // Check video codec
  458. List<string> videoCodecs = profile.GetVideoCodecs();
  459. if (videoCodecs.Count > 0)
  460. {
  461. string videoCodec = videoStream == null ? null : videoStream.Codec;
  462. if (string.IsNullOrEmpty(videoCodec) || !videoCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
  463. {
  464. return false;
  465. }
  466. }
  467. List<string> audioCodecs = profile.GetAudioCodecs();
  468. if (audioCodecs.Count > 0)
  469. {
  470. // Check audio codecs
  471. string audioCodec = audioStream == null ? null : audioStream.Codec;
  472. if (string.IsNullOrEmpty(audioCodec) || !audioCodecs.Contains(audioCodec, StringComparer.OrdinalIgnoreCase))
  473. {
  474. return false;
  475. }
  476. }
  477. return true;
  478. }
  479. }
  480. }