StreamBuilder.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. using MediaBrowser.Model.Dto;
  2. using MediaBrowser.Model.Entities;
  3. using MediaBrowser.Model.Extensions;
  4. using MediaBrowser.Model.MediaInfo;
  5. using System;
  6. using System.Collections.Generic;
  7. namespace MediaBrowser.Model.Dlna
  8. {
  9. public class StreamBuilder
  10. {
  11. public StreamInfo BuildAudioItem(AudioOptions options)
  12. {
  13. ValidateAudioInput(options);
  14. List<MediaSourceInfo> mediaSources = options.MediaSources;
  15. // If the client wants a specific media soure, filter now
  16. if (!string.IsNullOrEmpty(options.MediaSourceId))
  17. {
  18. // Avoid implicitly captured closure
  19. string mediaSourceId = options.MediaSourceId;
  20. mediaSources = new List<MediaSourceInfo>();
  21. foreach (MediaSourceInfo i in mediaSources)
  22. {
  23. if (StringHelper.EqualsIgnoreCase(i.Id, mediaSourceId))
  24. mediaSources.Add(i);
  25. }
  26. }
  27. List<StreamInfo> streams = new List<StreamInfo>();
  28. foreach (MediaSourceInfo i in mediaSources)
  29. streams.Add(BuildAudioItem(i, options));
  30. foreach (StreamInfo stream in streams)
  31. {
  32. stream.DeviceId = options.DeviceId;
  33. stream.DeviceProfileId = options.Profile.Id;
  34. }
  35. return GetOptimalStream(streams);
  36. }
  37. public StreamInfo BuildVideoItem(VideoOptions options)
  38. {
  39. ValidateInput(options);
  40. List<MediaSourceInfo> mediaSources = options.MediaSources;
  41. // If the client wants a specific media soure, filter now
  42. if (!string.IsNullOrEmpty(options.MediaSourceId))
  43. {
  44. // Avoid implicitly captured closure
  45. string mediaSourceId = options.MediaSourceId;
  46. var newMediaSources = new List<MediaSourceInfo>();
  47. foreach (MediaSourceInfo i in mediaSources)
  48. {
  49. if (StringHelper.EqualsIgnoreCase(i.Id, mediaSourceId))
  50. newMediaSources.Add(i);
  51. }
  52. mediaSources = newMediaSources;
  53. }
  54. List<StreamInfo> streams = new List<StreamInfo>();
  55. foreach (MediaSourceInfo i in mediaSources)
  56. streams.Add(BuildVideoItem(i, options));
  57. foreach (StreamInfo stream in streams)
  58. {
  59. stream.DeviceId = options.DeviceId;
  60. stream.DeviceProfileId = options.Profile.Id;
  61. }
  62. return GetOptimalStream(streams);
  63. }
  64. private StreamInfo GetOptimalStream(List<StreamInfo> streams)
  65. {
  66. // Grab the first one that can be direct streamed
  67. // If that doesn't produce anything, just take the first
  68. foreach (StreamInfo i in streams)
  69. {
  70. if (i.IsDirectStream)
  71. {
  72. return i;
  73. }
  74. }
  75. foreach (StreamInfo stream in streams)
  76. {
  77. return stream;
  78. }
  79. return null;
  80. }
  81. private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
  82. {
  83. StreamInfo playlistItem = new StreamInfo
  84. {
  85. ItemId = options.ItemId,
  86. MediaType = DlnaProfileType.Audio,
  87. MediaSource = item,
  88. RunTimeTicks = item.RunTimeTicks
  89. };
  90. int? maxBitrateSetting = options.MaxBitrate ?? options.Profile.MaxBitrate;
  91. MediaStream audioStream = item.DefaultAudioStream;
  92. // Honor the max bitrate setting
  93. if (IsAudioEligibleForDirectPlay(item, maxBitrateSetting))
  94. {
  95. DirectPlayProfile directPlay = null;
  96. foreach (DirectPlayProfile i in options.Profile.DirectPlayProfiles)
  97. {
  98. if (i.Type == playlistItem.MediaType && IsAudioDirectPlaySupported(i, item, audioStream))
  99. {
  100. directPlay = i;
  101. break;
  102. }
  103. }
  104. if (directPlay != null)
  105. {
  106. string audioCodec = audioStream == null ? null : audioStream.Codec;
  107. // Make sure audio codec profiles are satisfied
  108. if (!string.IsNullOrEmpty(audioCodec))
  109. {
  110. ConditionProcessor conditionProcessor = new ConditionProcessor();
  111. List<ProfileCondition> conditions = new List<ProfileCondition>();
  112. foreach (CodecProfile i in options.Profile.CodecProfiles)
  113. {
  114. if (i.Type == CodecType.Audio && i.ContainsCodec(audioCodec))
  115. conditions.AddRange(i.Conditions);
  116. }
  117. int? audioChannels = audioStream.Channels;
  118. int? audioBitrate = audioStream.BitRate;
  119. bool all = true;
  120. foreach (ProfileCondition c in conditions)
  121. {
  122. if (!conditionProcessor.IsAudioConditionSatisfied(c, audioChannels, audioBitrate))
  123. {
  124. all = false;
  125. break;
  126. }
  127. }
  128. if (all)
  129. {
  130. playlistItem.IsDirectStream = true;
  131. playlistItem.Container = item.Container;
  132. return playlistItem;
  133. }
  134. }
  135. }
  136. }
  137. TranscodingProfile transcodingProfile = null;
  138. foreach (TranscodingProfile i in options.Profile.TranscodingProfiles)
  139. {
  140. if (i.Type == playlistItem.MediaType)
  141. {
  142. transcodingProfile = i;
  143. break;
  144. }
  145. }
  146. if (transcodingProfile != null)
  147. {
  148. playlistItem.IsDirectStream = false;
  149. playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  150. playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
  151. playlistItem.Container = transcodingProfile.Container;
  152. playlistItem.AudioCodec = transcodingProfile.AudioCodec;
  153. playlistItem.Protocol = transcodingProfile.Protocol;
  154. List<CodecProfile> audioCodecProfiles = new List<CodecProfile>();
  155. foreach (CodecProfile i in options.Profile.CodecProfiles)
  156. {
  157. if (i.Type == CodecType.Audio && i.ContainsCodec(transcodingProfile.AudioCodec))
  158. {
  159. audioCodecProfiles.Add(i);
  160. }
  161. if (audioCodecProfiles.Count >= 1) break;
  162. }
  163. List<ProfileCondition> audioTranscodingConditions = new List<ProfileCondition>();
  164. foreach (CodecProfile i in audioCodecProfiles)
  165. audioTranscodingConditions.AddRange(i.Conditions);
  166. ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
  167. // Honor requested max channels
  168. if (options.MaxAudioChannels.HasValue)
  169. {
  170. int currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
  171. playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
  172. }
  173. // Honor requested max bitrate
  174. if (maxBitrateSetting.HasValue)
  175. {
  176. int currentValue = playlistItem.AudioBitrate ?? maxBitrateSetting.Value;
  177. playlistItem.AudioBitrate = Math.Min(maxBitrateSetting.Value, currentValue);
  178. }
  179. }
  180. return playlistItem;
  181. }
  182. private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
  183. {
  184. StreamInfo playlistItem = new StreamInfo
  185. {
  186. ItemId = options.ItemId,
  187. MediaType = DlnaProfileType.Video,
  188. MediaSource = item,
  189. RunTimeTicks = item.RunTimeTicks
  190. };
  191. MediaStream audioStream = item.DefaultAudioStream;
  192. MediaStream videoStream = item.VideoStream;
  193. int? maxBitrateSetting = options.MaxBitrate ?? options.Profile.MaxBitrate;
  194. if (IsEligibleForDirectPlay(item, options, maxBitrateSetting))
  195. {
  196. // See if it can be direct played
  197. DirectPlayProfile directPlay = GetVideoDirectPlayProfile(options.Profile, item, videoStream, audioStream);
  198. if (directPlay != null)
  199. {
  200. playlistItem.IsDirectStream = true;
  201. playlistItem.Container = item.Container;
  202. return playlistItem;
  203. }
  204. }
  205. // Can't direct play, find the transcoding profile
  206. TranscodingProfile transcodingProfile = null;
  207. foreach (TranscodingProfile i in options.Profile.TranscodingProfiles)
  208. {
  209. if (i.Type == playlistItem.MediaType)
  210. {
  211. transcodingProfile = i;
  212. break;
  213. }
  214. }
  215. if (transcodingProfile != null)
  216. {
  217. playlistItem.IsDirectStream = false;
  218. playlistItem.Container = transcodingProfile.Container;
  219. playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
  220. playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  221. playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',')[0];
  222. playlistItem.VideoCodec = transcodingProfile.VideoCodec;
  223. playlistItem.Protocol = transcodingProfile.Protocol;
  224. playlistItem.AudioStreamIndex = options.AudioStreamIndex ?? item.DefaultAudioStreamIndex;
  225. playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? item.DefaultSubtitleStreamIndex;
  226. List<ProfileCondition> videoTranscodingConditions = new List<ProfileCondition>();
  227. foreach (CodecProfile i in options.Profile.CodecProfiles)
  228. {
  229. if (i.Type == CodecType.Video && i.ContainsCodec(transcodingProfile.VideoCodec))
  230. {
  231. videoTranscodingConditions.AddRange(i.Conditions);
  232. break;
  233. }
  234. }
  235. ApplyTranscodingConditions(playlistItem, videoTranscodingConditions);
  236. List<ProfileCondition> audioTranscodingConditions = new List<ProfileCondition>();
  237. foreach (CodecProfile i in options.Profile.CodecProfiles)
  238. {
  239. if (i.Type == CodecType.VideoAudio && i.ContainsCodec(transcodingProfile.AudioCodec))
  240. {
  241. audioTranscodingConditions.AddRange(i.Conditions);
  242. break;
  243. }
  244. }
  245. ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
  246. // Honor requested max channels
  247. if (options.MaxAudioChannels.HasValue)
  248. {
  249. int currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
  250. playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
  251. }
  252. // Honor requested max bitrate
  253. if (options.MaxAudioTranscodingBitrate.HasValue)
  254. {
  255. int currentValue = playlistItem.AudioBitrate ?? options.MaxAudioTranscodingBitrate.Value;
  256. playlistItem.AudioBitrate = Math.Min(options.MaxAudioTranscodingBitrate.Value, currentValue);
  257. }
  258. // Honor max rate
  259. if (maxBitrateSetting.HasValue)
  260. {
  261. int videoBitrate = maxBitrateSetting.Value;
  262. if (playlistItem.AudioBitrate.HasValue)
  263. {
  264. videoBitrate -= playlistItem.AudioBitrate.Value;
  265. }
  266. int currentValue = playlistItem.VideoBitrate ?? videoBitrate;
  267. playlistItem.VideoBitrate = Math.Min(videoBitrate, currentValue);
  268. }
  269. // Hate to hard-code this, but it's still probably better than being subjected to encoder defaults
  270. if (!playlistItem.VideoBitrate.HasValue)
  271. {
  272. playlistItem.VideoBitrate = 5000000;
  273. }
  274. }
  275. return playlistItem;
  276. }
  277. private DirectPlayProfile GetVideoDirectPlayProfile(DeviceProfile profile,
  278. MediaSourceInfo mediaSource,
  279. MediaStream videoStream,
  280. MediaStream audioStream)
  281. {
  282. // See if it can be direct played
  283. DirectPlayProfile directPlay = null;
  284. foreach (DirectPlayProfile i in profile.DirectPlayProfiles)
  285. {
  286. if (i.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(i, mediaSource, videoStream, audioStream))
  287. {
  288. directPlay = i;
  289. break;
  290. }
  291. }
  292. if (directPlay == null)
  293. {
  294. return null;
  295. }
  296. string container = mediaSource.Container;
  297. List<ProfileCondition> conditions = new List<ProfileCondition>();
  298. foreach (ContainerProfile i in profile.ContainerProfiles)
  299. {
  300. if (i.Type == DlnaProfileType.Video &&
  301. ListHelper.ContainsIgnoreCase(i.GetContainers(), container))
  302. {
  303. conditions.AddRange(i.Conditions);
  304. }
  305. }
  306. ConditionProcessor conditionProcessor = new ConditionProcessor();
  307. int? width = videoStream == null ? null : videoStream.Width;
  308. int? height = videoStream == null ? null : videoStream.Height;
  309. int? bitDepth = videoStream == null ? null : videoStream.BitDepth;
  310. int? videoBitrate = videoStream == null ? null : videoStream.BitRate;
  311. double? videoLevel = videoStream == null ? null : videoStream.Level;
  312. string videoProfile = videoStream == null ? null : videoStream.Profile;
  313. float? videoFramerate = videoStream == null ? null : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate;
  314. bool? isAnamorphic = videoStream == null ? null : videoStream.IsAnamorphic;
  315. int? audioBitrate = audioStream == null ? null : audioStream.BitRate;
  316. int? audioChannels = audioStream == null ? null : audioStream.Channels;
  317. string audioProfile = audioStream == null ? null : audioStream.Profile;
  318. TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
  319. int? packetLength = videoStream == null ? null : videoStream.PacketLength;
  320. // Check container conditions
  321. foreach (ProfileCondition i in conditions)
  322. {
  323. if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic))
  324. {
  325. return null;
  326. }
  327. }
  328. string videoCodec = videoStream == null ? null : videoStream.Codec;
  329. if (string.IsNullOrEmpty(videoCodec))
  330. {
  331. return null;
  332. }
  333. conditions = new List<ProfileCondition>();
  334. foreach (CodecProfile i in profile.CodecProfiles)
  335. {
  336. if (i.Type == CodecType.Video && i.ContainsCodec(videoCodec))
  337. conditions.AddRange(i.Conditions);
  338. }
  339. foreach (ProfileCondition i in conditions)
  340. {
  341. if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic))
  342. {
  343. return null;
  344. }
  345. }
  346. if (audioStream != null)
  347. {
  348. string audioCodec = audioStream.Codec;
  349. if (string.IsNullOrEmpty(audioCodec))
  350. {
  351. return null;
  352. }
  353. conditions = new List<ProfileCondition>();
  354. foreach (CodecProfile i in profile.CodecProfiles)
  355. {
  356. if (i.Type == CodecType.VideoAudio && i.ContainsCodec(audioCodec))
  357. conditions.AddRange(i.Conditions);
  358. }
  359. foreach (ProfileCondition i in conditions)
  360. {
  361. if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioProfile))
  362. {
  363. return null;
  364. }
  365. }
  366. }
  367. return directPlay;
  368. }
  369. private bool IsEligibleForDirectPlay(MediaSourceInfo item, VideoOptions options, int? maxBitrate)
  370. {
  371. if (options.SubtitleStreamIndex.HasValue)
  372. {
  373. return false;
  374. }
  375. return IsAudioEligibleForDirectPlay(item, maxBitrate);
  376. }
  377. private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, int? maxBitrate)
  378. {
  379. // Honor the max bitrate setting
  380. return !maxBitrate.HasValue || (item.Bitrate.HasValue && item.Bitrate.Value <= maxBitrate.Value);
  381. }
  382. private void ValidateInput(VideoOptions options)
  383. {
  384. ValidateAudioInput(options);
  385. if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
  386. {
  387. throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
  388. }
  389. if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
  390. {
  391. throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
  392. }
  393. }
  394. private void ValidateAudioInput(AudioOptions options)
  395. {
  396. if (string.IsNullOrEmpty(options.ItemId))
  397. {
  398. throw new ArgumentException("ItemId is required");
  399. }
  400. if (string.IsNullOrEmpty(options.DeviceId))
  401. {
  402. throw new ArgumentException("DeviceId is required");
  403. }
  404. if (options.Profile == null)
  405. {
  406. throw new ArgumentException("Profile is required");
  407. }
  408. if (options.MediaSources == null)
  409. {
  410. throw new ArgumentException("MediaSources is required");
  411. }
  412. }
  413. private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions)
  414. {
  415. foreach (ProfileCondition condition in conditions)
  416. {
  417. string value = condition.Value;
  418. if (string.IsNullOrEmpty(value))
  419. {
  420. continue;
  421. }
  422. switch (condition.Property)
  423. {
  424. case ProfileConditionValue.AudioBitrate:
  425. {
  426. int num;
  427. if (IntHelper.TryParseCultureInvariant(value, out num))
  428. {
  429. item.AudioBitrate = num;
  430. }
  431. break;
  432. }
  433. case ProfileConditionValue.AudioChannels:
  434. {
  435. int num;
  436. if (IntHelper.TryParseCultureInvariant(value, out num))
  437. {
  438. item.MaxAudioChannels = num;
  439. }
  440. break;
  441. }
  442. case ProfileConditionValue.AudioProfile:
  443. case ProfileConditionValue.IsAnamorphic:
  444. case ProfileConditionValue.Has64BitOffsets:
  445. case ProfileConditionValue.PacketLength:
  446. case ProfileConditionValue.VideoTimestamp:
  447. case ProfileConditionValue.VideoBitDepth:
  448. {
  449. // Not supported yet
  450. break;
  451. }
  452. case ProfileConditionValue.VideoProfile:
  453. {
  454. item.VideoProfile = value;
  455. break;
  456. }
  457. case ProfileConditionValue.Height:
  458. {
  459. int num;
  460. if (IntHelper.TryParseCultureInvariant(value, out num))
  461. {
  462. item.MaxHeight = num;
  463. }
  464. break;
  465. }
  466. case ProfileConditionValue.VideoBitrate:
  467. {
  468. int num;
  469. if (IntHelper.TryParseCultureInvariant(value, out num))
  470. {
  471. item.VideoBitrate = num;
  472. }
  473. break;
  474. }
  475. case ProfileConditionValue.VideoFramerate:
  476. {
  477. float num;
  478. if (FloatHelper.TryParseCultureInvariant(value, out num))
  479. {
  480. item.MaxFramerate = num;
  481. }
  482. break;
  483. }
  484. case ProfileConditionValue.VideoLevel:
  485. {
  486. int num;
  487. if (IntHelper.TryParseCultureInvariant(value, out num))
  488. {
  489. item.VideoLevel = num;
  490. }
  491. break;
  492. }
  493. case ProfileConditionValue.Width:
  494. {
  495. int num;
  496. if (IntHelper.TryParseCultureInvariant(value, out num))
  497. {
  498. item.MaxWidth = num;
  499. }
  500. break;
  501. }
  502. default:
  503. throw new ArgumentException("Unrecognized ProfileConditionValue");
  504. }
  505. }
  506. }
  507. private bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
  508. {
  509. if (profile.Container.Length > 0)
  510. {
  511. // Check container type
  512. string mediaContainer = item.Container ?? string.Empty;
  513. bool any = false;
  514. foreach (string i in profile.GetContainers())
  515. {
  516. if (StringHelper.EqualsIgnoreCase(i, mediaContainer))
  517. {
  518. any = true;
  519. break;
  520. }
  521. }
  522. if (!any)
  523. {
  524. return false;
  525. }
  526. }
  527. return true;
  528. }
  529. private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
  530. {
  531. // Only plain video files can be direct played
  532. if (item.VideoType != VideoType.VideoFile)
  533. {
  534. return false;
  535. }
  536. if (profile.Container.Length > 0)
  537. {
  538. // Check container type
  539. string mediaContainer = item.Container ?? string.Empty;
  540. bool any = false;
  541. foreach (string i in profile.GetContainers())
  542. {
  543. if (StringHelper.EqualsIgnoreCase(i, mediaContainer))
  544. {
  545. any = true;
  546. break;
  547. }
  548. }
  549. if (!any)
  550. {
  551. return false;
  552. }
  553. }
  554. // Check video codec
  555. List<string> videoCodecs = profile.GetVideoCodecs();
  556. if (videoCodecs.Count > 0)
  557. {
  558. string videoCodec = videoStream == null ? null : videoStream.Codec;
  559. if (string.IsNullOrEmpty(videoCodec) || !ListHelper.ContainsIgnoreCase(videoCodecs, videoCodec))
  560. {
  561. return false;
  562. }
  563. }
  564. List<string> audioCodecs = profile.GetAudioCodecs();
  565. if (audioCodecs.Count > 0)
  566. {
  567. // Check audio codecs
  568. string audioCodec = audioStream == null ? null : audioStream.Codec;
  569. if (string.IsNullOrEmpty(audioCodec) || !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec))
  570. {
  571. return false;
  572. }
  573. }
  574. return true;
  575. }
  576. }
  577. }