StreamBuilder.cs 22 KB

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