StreamBuilder.cs 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228
  1. using MediaBrowser.Model.Dto;
  2. using MediaBrowser.Model.Entities;
  3. using MediaBrowser.Model.Extensions;
  4. using MediaBrowser.Model.Logging;
  5. using MediaBrowser.Model.MediaInfo;
  6. using MediaBrowser.Model.Session;
  7. using System;
  8. using System.Collections.Generic;
  9. namespace MediaBrowser.Model.Dlna
  10. {
  11. public class StreamBuilder
  12. {
  13. private readonly ILocalPlayer _localPlayer;
  14. private readonly ILogger _logger;
  15. private readonly ITranscoderSupport _transcoderSupport;
  16. public StreamBuilder(ILocalPlayer localPlayer, ITranscoderSupport transcoderSupport, ILogger logger)
  17. {
  18. _transcoderSupport = transcoderSupport;
  19. _localPlayer = localPlayer;
  20. _logger = logger;
  21. }
  22. public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
  23. : this(new NullLocalPlayer(), transcoderSupport, logger)
  24. {
  25. }
  26. public StreamBuilder(ILocalPlayer localPlayer, ILogger logger)
  27. : this(localPlayer, new FullTranscoderSupport(), logger)
  28. {
  29. }
  30. public StreamBuilder(ILogger logger)
  31. : this(new NullLocalPlayer(), new FullTranscoderSupport(), logger)
  32. {
  33. }
  34. public StreamInfo BuildAudioItem(AudioOptions options)
  35. {
  36. ValidateAudioInput(options);
  37. List<MediaSourceInfo> mediaSources = new List<MediaSourceInfo>();
  38. foreach (MediaSourceInfo i in options.MediaSources)
  39. {
  40. if (string.IsNullOrEmpty(options.MediaSourceId) ||
  41. StringHelper.EqualsIgnoreCase(i.Id, options.MediaSourceId))
  42. {
  43. mediaSources.Add(i);
  44. }
  45. }
  46. List<StreamInfo> streams = new List<StreamInfo>();
  47. foreach (MediaSourceInfo i in mediaSources)
  48. {
  49. StreamInfo streamInfo = BuildAudioItem(i, options);
  50. if (streamInfo != null)
  51. {
  52. streams.Add(streamInfo);
  53. }
  54. }
  55. foreach (StreamInfo stream in streams)
  56. {
  57. stream.DeviceId = options.DeviceId;
  58. stream.DeviceProfileId = options.Profile.Id;
  59. }
  60. return GetOptimalStream(streams, options.GetMaxBitrate());
  61. }
  62. public StreamInfo BuildVideoItem(VideoOptions options)
  63. {
  64. ValidateInput(options);
  65. List<MediaSourceInfo> mediaSources = new List<MediaSourceInfo>();
  66. foreach (MediaSourceInfo i in options.MediaSources)
  67. {
  68. if (string.IsNullOrEmpty(options.MediaSourceId) ||
  69. StringHelper.EqualsIgnoreCase(i.Id, options.MediaSourceId))
  70. {
  71. mediaSources.Add(i);
  72. }
  73. }
  74. List<StreamInfo> streams = new List<StreamInfo>();
  75. foreach (MediaSourceInfo i in mediaSources)
  76. {
  77. StreamInfo streamInfo = BuildVideoItem(i, options);
  78. if (streamInfo != null)
  79. {
  80. streams.Add(streamInfo);
  81. }
  82. }
  83. foreach (StreamInfo stream in streams)
  84. {
  85. stream.DeviceId = options.DeviceId;
  86. stream.DeviceProfileId = options.Profile.Id;
  87. }
  88. return GetOptimalStream(streams, options.GetMaxBitrate());
  89. }
  90. private StreamInfo GetOptimalStream(List<StreamInfo> streams, int? maxBitrate)
  91. {
  92. streams = StreamInfoSorter.SortMediaSources(streams, maxBitrate);
  93. foreach (StreamInfo stream in streams)
  94. {
  95. return stream;
  96. }
  97. return null;
  98. }
  99. private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
  100. {
  101. StreamInfo playlistItem = new StreamInfo
  102. {
  103. ItemId = options.ItemId,
  104. MediaType = DlnaProfileType.Audio,
  105. MediaSource = item,
  106. RunTimeTicks = item.RunTimeTicks,
  107. Context = options.Context,
  108. DeviceProfile = options.Profile
  109. };
  110. MediaStream audioStream = item.GetDefaultAudioStream(null);
  111. List<PlayMethod> directPlayMethods = GetAudioDirectPlayMethods(item, audioStream, options);
  112. ConditionProcessor conditionProcessor = new ConditionProcessor();
  113. int? inputAudioChannels = audioStream == null ? null : audioStream.Channels;
  114. int? inputAudioBitrate = audioStream == null ? null : audioStream.BitDepth;
  115. if (directPlayMethods.Count > 0)
  116. {
  117. string audioCodec = audioStream == null ? null : audioStream.Codec;
  118. // Make sure audio codec profiles are satisfied
  119. if (!string.IsNullOrEmpty(audioCodec))
  120. {
  121. List<ProfileCondition> conditions = new List<ProfileCondition>();
  122. foreach (CodecProfile i in options.Profile.CodecProfiles)
  123. {
  124. if (i.Type == CodecType.Audio && i.ContainsCodec(audioCodec, item.Container))
  125. {
  126. bool applyConditions = true;
  127. foreach (ProfileCondition applyCondition in i.ApplyConditions)
  128. {
  129. if (!conditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate))
  130. {
  131. LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
  132. applyConditions = false;
  133. break;
  134. }
  135. }
  136. if (applyConditions)
  137. {
  138. foreach (ProfileCondition c in i.Conditions)
  139. {
  140. conditions.Add(c);
  141. }
  142. }
  143. }
  144. }
  145. bool all = true;
  146. foreach (ProfileCondition c in conditions)
  147. {
  148. if (!conditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate))
  149. {
  150. LogConditionFailure(options.Profile, "AudioCodecProfile", c, item);
  151. all = false;
  152. break;
  153. }
  154. }
  155. if (all)
  156. {
  157. if (item.Protocol == MediaProtocol.File &&
  158. directPlayMethods.Contains(PlayMethod.DirectPlay) &&
  159. _localPlayer.CanAccessFile(item.Path))
  160. {
  161. playlistItem.PlayMethod = PlayMethod.DirectPlay;
  162. }
  163. else if (item.Protocol == MediaProtocol.Http &&
  164. directPlayMethods.Contains(PlayMethod.DirectPlay) &&
  165. _localPlayer.CanAccessUrl(item.Path, item.RequiredHttpHeaders.Count > 0))
  166. {
  167. playlistItem.PlayMethod = PlayMethod.DirectPlay;
  168. }
  169. else if (directPlayMethods.Contains(PlayMethod.DirectStream))
  170. {
  171. playlistItem.PlayMethod = PlayMethod.DirectStream;
  172. }
  173. playlistItem.Container = item.Container;
  174. return playlistItem;
  175. }
  176. }
  177. }
  178. TranscodingProfile transcodingProfile = null;
  179. foreach (TranscodingProfile i in options.Profile.TranscodingProfiles)
  180. {
  181. if (i.Type == playlistItem.MediaType && i.Context == options.Context)
  182. {
  183. if (_transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container))
  184. {
  185. transcodingProfile = i;
  186. break;
  187. }
  188. }
  189. }
  190. if (transcodingProfile != null)
  191. {
  192. if (!item.SupportsTranscoding)
  193. {
  194. return null;
  195. }
  196. playlistItem.PlayMethod = PlayMethod.Transcode;
  197. playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  198. playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
  199. playlistItem.Container = transcodingProfile.Container;
  200. if (string.IsNullOrEmpty(transcodingProfile.AudioCodec))
  201. {
  202. playlistItem.AudioCodecs = new string[] { };
  203. }
  204. else
  205. {
  206. playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(',');
  207. }
  208. playlistItem.SubProtocol = transcodingProfile.Protocol;
  209. List<CodecProfile> audioCodecProfiles = new List<CodecProfile>();
  210. foreach (CodecProfile i in options.Profile.CodecProfiles)
  211. {
  212. if (i.Type == CodecType.Audio && i.ContainsCodec(transcodingProfile.AudioCodec, transcodingProfile.Container))
  213. {
  214. audioCodecProfiles.Add(i);
  215. }
  216. if (audioCodecProfiles.Count >= 1) break;
  217. }
  218. List<ProfileCondition> audioTranscodingConditions = new List<ProfileCondition>();
  219. foreach (CodecProfile i in audioCodecProfiles)
  220. {
  221. bool applyConditions = true;
  222. foreach (ProfileCondition applyCondition in i.ApplyConditions)
  223. {
  224. if (!conditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate))
  225. {
  226. LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
  227. applyConditions = false;
  228. break;
  229. }
  230. }
  231. if (applyConditions)
  232. {
  233. foreach (ProfileCondition c in i.Conditions)
  234. {
  235. audioTranscodingConditions.Add(c);
  236. }
  237. }
  238. }
  239. ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
  240. // Honor requested max channels
  241. if (options.MaxAudioChannels.HasValue)
  242. {
  243. int currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
  244. playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
  245. }
  246. int configuredBitrate = options.AudioTranscodingBitrate ??
  247. (options.Context == EncodingContext.Static ? options.Profile.MusicSyncBitrate : options.Profile.MusicStreamingTranscodingBitrate) ??
  248. 128000;
  249. playlistItem.AudioBitrate = Math.Min(configuredBitrate, playlistItem.AudioBitrate ?? configuredBitrate);
  250. }
  251. return playlistItem;
  252. }
  253. private int? GetBitrateForDirectPlayCheck(MediaSourceInfo item, AudioOptions options)
  254. {
  255. if (item.Protocol == MediaProtocol.File)
  256. {
  257. return options.Profile.MaxStaticBitrate;
  258. }
  259. return options.GetMaxBitrate();
  260. }
  261. private List<PlayMethod> GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
  262. {
  263. DirectPlayProfile directPlayProfile = null;
  264. foreach (DirectPlayProfile i in options.Profile.DirectPlayProfiles)
  265. {
  266. if (i.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(i, item, audioStream))
  267. {
  268. directPlayProfile = i;
  269. break;
  270. }
  271. }
  272. List<PlayMethod> playMethods = new List<PlayMethod>();
  273. if (directPlayProfile != null)
  274. {
  275. // While options takes the network and other factors into account. Only applies to direct stream
  276. if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate()) && options.EnableDirectStream)
  277. {
  278. playMethods.Add(PlayMethod.DirectStream);
  279. }
  280. // The profile describes what the device supports
  281. // If device requirements are satisfied then allow both direct stream and direct play
  282. if (item.SupportsDirectPlay &&
  283. IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options)) && options.EnableDirectPlay)
  284. {
  285. playMethods.Add(PlayMethod.DirectPlay);
  286. }
  287. }
  288. else
  289. {
  290. _logger.Info("Profile: {0}, No direct play profiles found for Path: {1}",
  291. options.Profile.Name ?? "Unknown Profile",
  292. item.Path ?? "Unknown path");
  293. }
  294. return playMethods;
  295. }
  296. private int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
  297. {
  298. int highestScore = -1;
  299. foreach (MediaStream stream in item.MediaStreams)
  300. {
  301. if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue)
  302. {
  303. if (stream.Score.Value > highestScore)
  304. {
  305. highestScore = stream.Score.Value;
  306. }
  307. }
  308. }
  309. List<MediaStream> topStreams = new List<MediaStream>();
  310. foreach (MediaStream stream in item.MediaStreams)
  311. {
  312. if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore)
  313. {
  314. topStreams.Add(stream);
  315. }
  316. }
  317. // If multiple streams have an equal score, try to pick the most efficient one
  318. if (topStreams.Count > 1)
  319. {
  320. foreach (MediaStream stream in topStreams)
  321. {
  322. foreach (SubtitleProfile profile in subtitleProfiles)
  323. {
  324. if (profile.Method == SubtitleDeliveryMethod.External && StringHelper.EqualsIgnoreCase(profile.Format, stream.Codec))
  325. {
  326. return stream.Index;
  327. }
  328. }
  329. }
  330. }
  331. // If no optimization panned out, just use the original default
  332. return item.DefaultSubtitleStreamIndex;
  333. }
  334. private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
  335. {
  336. StreamInfo playlistItem = new StreamInfo
  337. {
  338. ItemId = options.ItemId,
  339. MediaType = DlnaProfileType.Video,
  340. MediaSource = item,
  341. RunTimeTicks = item.RunTimeTicks,
  342. Context = options.Context,
  343. DeviceProfile = options.Profile
  344. };
  345. playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles);
  346. MediaStream subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null;
  347. MediaStream audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
  348. int? audioStreamIndex = null;
  349. if (audioStream != null)
  350. {
  351. audioStreamIndex = audioStream.Index;
  352. }
  353. MediaStream videoStream = item.VideoStream;
  354. // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough
  355. bool isEligibleForDirectPlay = options.EnableDirectPlay && IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options), subtitleStream, options, PlayMethod.DirectPlay);
  356. bool isEligibleForDirectStream = options.EnableDirectStream && IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options, PlayMethod.DirectStream);
  357. _logger.Info("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
  358. options.Profile.Name ?? "Unknown Profile",
  359. item.Path ?? "Unknown path",
  360. isEligibleForDirectPlay,
  361. isEligibleForDirectStream);
  362. if (isEligibleForDirectPlay || isEligibleForDirectStream)
  363. {
  364. // See if it can be direct played
  365. PlayMethod? directPlay = GetVideoDirectPlayProfile(options.Profile, item, videoStream, audioStream, isEligibleForDirectPlay, isEligibleForDirectStream);
  366. if (directPlay != null)
  367. {
  368. playlistItem.PlayMethod = directPlay.Value;
  369. playlistItem.Container = item.Container;
  370. if (subtitleStream != null)
  371. {
  372. SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value);
  373. playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
  374. playlistItem.SubtitleFormat = subtitleProfile.Format;
  375. }
  376. return playlistItem;
  377. }
  378. }
  379. // Can't direct play, find the transcoding profile
  380. TranscodingProfile transcodingProfile = null;
  381. foreach (TranscodingProfile i in options.Profile.TranscodingProfiles)
  382. {
  383. if (i.Type == playlistItem.MediaType && i.Context == options.Context)
  384. {
  385. transcodingProfile = i;
  386. break;
  387. }
  388. }
  389. if (transcodingProfile != null)
  390. {
  391. if (!item.SupportsTranscoding)
  392. {
  393. return null;
  394. }
  395. if (subtitleStream != null)
  396. {
  397. SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode);
  398. playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
  399. playlistItem.SubtitleFormat = subtitleProfile.Format;
  400. }
  401. playlistItem.PlayMethod = PlayMethod.Transcode;
  402. playlistItem.Container = transcodingProfile.Container;
  403. playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
  404. playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
  405. playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(',');
  406. playlistItem.VideoCodec = transcodingProfile.VideoCodec;
  407. playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
  408. playlistItem.ForceLiveStream = transcodingProfile.ForceLiveStream;
  409. if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels))
  410. {
  411. int transcodingMaxAudioChannels;
  412. if (IntHelper.TryParseCultureInvariant(transcodingProfile.MaxAudioChannels, out transcodingMaxAudioChannels))
  413. {
  414. playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
  415. }
  416. }
  417. playlistItem.SubProtocol = transcodingProfile.Protocol;
  418. playlistItem.AudioStreamIndex = audioStreamIndex;
  419. ConditionProcessor conditionProcessor = new ConditionProcessor();
  420. List<ProfileCondition> videoTranscodingConditions = new List<ProfileCondition>();
  421. foreach (CodecProfile i in options.Profile.CodecProfiles)
  422. {
  423. if (i.Type == CodecType.Video && i.ContainsCodec(transcodingProfile.VideoCodec, transcodingProfile.Container))
  424. {
  425. bool applyConditions = true;
  426. foreach (ProfileCondition applyCondition in i.ApplyConditions)
  427. {
  428. bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream);
  429. int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate;
  430. int? audioChannels = audioStream == null ? null : audioStream.Channels;
  431. string audioProfile = audioStream == null ? null : audioStream.Profile;
  432. if (!conditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, audioProfile, isSecondaryAudio))
  433. {
  434. LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
  435. applyConditions = false;
  436. break;
  437. }
  438. }
  439. if (applyConditions)
  440. {
  441. foreach (ProfileCondition c in i.Conditions)
  442. {
  443. videoTranscodingConditions.Add(c);
  444. }
  445. break;
  446. }
  447. }
  448. }
  449. ApplyTranscodingConditions(playlistItem, videoTranscodingConditions);
  450. List<ProfileCondition> audioTranscodingConditions = new List<ProfileCondition>();
  451. foreach (CodecProfile i in options.Profile.CodecProfiles)
  452. {
  453. if (i.Type == CodecType.VideoAudio && i.ContainsCodec(playlistItem.TargetAudioCodec, transcodingProfile.Container))
  454. {
  455. bool applyConditions = true;
  456. foreach (ProfileCondition applyCondition in i.ApplyConditions)
  457. {
  458. int? width = videoStream == null ? null : videoStream.Width;
  459. int? height = videoStream == null ? null : videoStream.Height;
  460. int? bitDepth = videoStream == null ? null : videoStream.BitDepth;
  461. int? videoBitrate = videoStream == null ? null : videoStream.BitRate;
  462. double? videoLevel = videoStream == null ? null : videoStream.Level;
  463. string videoProfile = videoStream == null ? null : videoStream.Profile;
  464. float? videoFramerate = videoStream == null ? null : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate;
  465. bool? isAnamorphic = videoStream == null ? null : videoStream.IsAnamorphic;
  466. string videoCodecTag = videoStream == null ? null : videoStream.CodecTag;
  467. TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
  468. int? packetLength = videoStream == null ? null : videoStream.PacketLength;
  469. int? refFrames = videoStream == null ? null : videoStream.RefFrames;
  470. int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
  471. int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
  472. if (!conditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, refFrames, numVideoStreams, numAudioStreams, videoCodecTag))
  473. {
  474. LogConditionFailure(options.Profile, "VideoCodecProfile", applyCondition, item);
  475. applyConditions = false;
  476. break;
  477. }
  478. }
  479. if (applyConditions)
  480. {
  481. foreach (ProfileCondition c in i.Conditions)
  482. {
  483. audioTranscodingConditions.Add(c);
  484. }
  485. break;
  486. }
  487. }
  488. }
  489. ApplyTranscodingConditions(playlistItem, audioTranscodingConditions);
  490. // Honor requested max channels
  491. if (options.MaxAudioChannels.HasValue)
  492. {
  493. int currentValue = playlistItem.MaxAudioChannels ?? options.MaxAudioChannels.Value;
  494. playlistItem.MaxAudioChannels = Math.Min(options.MaxAudioChannels.Value, currentValue);
  495. }
  496. int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(), playlistItem.TargetAudioChannels, playlistItem.TargetAudioCodec, audioStream);
  497. playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
  498. int? maxBitrateSetting = options.GetMaxBitrate();
  499. // Honor max rate
  500. if (maxBitrateSetting.HasValue)
  501. {
  502. int videoBitrate = maxBitrateSetting.Value;
  503. if (playlistItem.AudioBitrate.HasValue)
  504. {
  505. videoBitrate -= playlistItem.AudioBitrate.Value;
  506. }
  507. // Make sure the video bitrate is lower than bitrate settings but at least 64k
  508. int currentValue = playlistItem.VideoBitrate ?? videoBitrate;
  509. playlistItem.VideoBitrate = Math.Max(Math.Min(videoBitrate, currentValue), 64000);
  510. }
  511. }
  512. return playlistItem;
  513. }
  514. private int GetAudioBitrate(int? maxTotalBitrate, int? targetAudioChannels, string targetAudioCodec, MediaStream audioStream)
  515. {
  516. var defaultBitrate = 128000;
  517. if (StringHelper.EqualsIgnoreCase(targetAudioCodec, "ac3"))
  518. {
  519. defaultBitrate = 192000;
  520. }
  521. if (targetAudioChannels.HasValue)
  522. {
  523. if (targetAudioChannels.Value >= 5 && (maxTotalBitrate ?? 0) >= 2000000)
  524. {
  525. if (StringHelper.EqualsIgnoreCase(targetAudioCodec, "ac3"))
  526. {
  527. defaultBitrate = 448000;
  528. }
  529. else
  530. {
  531. defaultBitrate = 320000;
  532. }
  533. }
  534. }
  535. int encoderAudioBitrateLimit = int.MaxValue;
  536. if (audioStream != null)
  537. {
  538. // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
  539. // Any attempts to transcode over 64k will fail
  540. if (audioStream.Channels.HasValue &&
  541. audioStream.Channels.Value == 1)
  542. {
  543. if ((audioStream.BitRate ?? 0) < 64000)
  544. {
  545. encoderAudioBitrateLimit = 64000;
  546. }
  547. }
  548. }
  549. return Math.Min(defaultBitrate, encoderAudioBitrateLimit);
  550. }
  551. private PlayMethod? GetVideoDirectPlayProfile(DeviceProfile profile,
  552. MediaSourceInfo mediaSource,
  553. MediaStream videoStream,
  554. MediaStream audioStream,
  555. bool isEligibleForDirectPlay,
  556. bool isEligibleForDirectStream)
  557. {
  558. if (videoStream == null)
  559. {
  560. _logger.Info("Profile: {0}, Cannot direct stream with no known video stream. Path: {1}",
  561. profile.Name ?? "Unknown Profile",
  562. mediaSource.Path ?? "Unknown path");
  563. return null;
  564. }
  565. // See if it can be direct played
  566. DirectPlayProfile directPlay = null;
  567. foreach (DirectPlayProfile i in profile.DirectPlayProfiles)
  568. {
  569. if (i.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(i, mediaSource, videoStream, audioStream))
  570. {
  571. directPlay = i;
  572. break;
  573. }
  574. }
  575. if (directPlay == null)
  576. {
  577. _logger.Info("Profile: {0}, No direct play profiles found for Path: {1}",
  578. profile.Name ?? "Unknown Profile",
  579. mediaSource.Path ?? "Unknown path");
  580. return null;
  581. }
  582. string container = mediaSource.Container;
  583. List<ProfileCondition> conditions = new List<ProfileCondition>();
  584. foreach (ContainerProfile i in profile.ContainerProfiles)
  585. {
  586. if (i.Type == DlnaProfileType.Video &&
  587. ListHelper.ContainsIgnoreCase(i.GetContainers(), container))
  588. {
  589. foreach (ProfileCondition c in i.Conditions)
  590. {
  591. conditions.Add(c);
  592. }
  593. }
  594. }
  595. ConditionProcessor conditionProcessor = new ConditionProcessor();
  596. int? width = videoStream == null ? null : videoStream.Width;
  597. int? height = videoStream == null ? null : videoStream.Height;
  598. int? bitDepth = videoStream == null ? null : videoStream.BitDepth;
  599. int? videoBitrate = videoStream == null ? null : videoStream.BitRate;
  600. double? videoLevel = videoStream == null ? null : videoStream.Level;
  601. string videoProfile = videoStream == null ? null : videoStream.Profile;
  602. float? videoFramerate = videoStream == null ? null : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate;
  603. bool? isAnamorphic = videoStream == null ? null : videoStream.IsAnamorphic;
  604. string videoCodecTag = videoStream == null ? null : videoStream.CodecTag;
  605. int? audioBitrate = audioStream == null ? null : audioStream.BitRate;
  606. int? audioChannels = audioStream == null ? null : audioStream.Channels;
  607. string audioProfile = audioStream == null ? null : audioStream.Profile;
  608. TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
  609. int? packetLength = videoStream == null ? null : videoStream.PacketLength;
  610. int? refFrames = videoStream == null ? null : videoStream.RefFrames;
  611. int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
  612. int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
  613. // Check container conditions
  614. foreach (ProfileCondition i in conditions)
  615. {
  616. if (!conditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, refFrames, numVideoStreams, numAudioStreams, videoCodecTag))
  617. {
  618. LogConditionFailure(profile, "VideoContainerProfile", i, mediaSource);
  619. return null;
  620. }
  621. }
  622. string videoCodec = videoStream == null ? null : videoStream.Codec;
  623. if (string.IsNullOrEmpty(videoCodec))
  624. {
  625. _logger.Info("Profile: {0}, DirectPlay=false. Reason=Unknown video codec. Path: {1}",
  626. profile.Name ?? "Unknown Profile",
  627. mediaSource.Path ?? "Unknown path");
  628. return null;
  629. }
  630. conditions = new List<ProfileCondition>();
  631. foreach (CodecProfile i in profile.CodecProfiles)
  632. {
  633. if (i.Type == CodecType.Video && i.ContainsCodec(videoCodec, container))
  634. {
  635. bool applyConditions = true;
  636. foreach (ProfileCondition applyCondition in i.ApplyConditions)
  637. {
  638. if (!conditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, refFrames, numVideoStreams, numAudioStreams, videoCodecTag))
  639. {
  640. LogConditionFailure(profile, "VideoCodecProfile", applyCondition, mediaSource);
  641. applyConditions = false;
  642. break;
  643. }
  644. }
  645. if (applyConditions)
  646. {
  647. foreach (ProfileCondition c in i.Conditions)
  648. {
  649. conditions.Add(c);
  650. }
  651. }
  652. }
  653. }
  654. foreach (ProfileCondition i in conditions)
  655. {
  656. if (!conditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, refFrames, numVideoStreams, numAudioStreams, videoCodecTag))
  657. {
  658. LogConditionFailure(profile, "VideoCodecProfile", i, mediaSource);
  659. return null;
  660. }
  661. }
  662. if (audioStream != null)
  663. {
  664. string audioCodec = audioStream.Codec;
  665. if (string.IsNullOrEmpty(audioCodec))
  666. {
  667. _logger.Info("Profile: {0}, DirectPlay=false. Reason=Unknown audio codec. Path: {1}",
  668. profile.Name ?? "Unknown Profile",
  669. mediaSource.Path ?? "Unknown path");
  670. return null;
  671. }
  672. conditions = new List<ProfileCondition>();
  673. bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream);
  674. foreach (CodecProfile i in profile.CodecProfiles)
  675. {
  676. if (i.Type == CodecType.VideoAudio && i.ContainsCodec(audioCodec, container))
  677. {
  678. bool applyConditions = true;
  679. foreach (ProfileCondition applyCondition in i.ApplyConditions)
  680. {
  681. if (!conditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioProfile, isSecondaryAudio))
  682. {
  683. LogConditionFailure(profile, "VideoAudioCodecProfile", applyCondition, mediaSource);
  684. applyConditions = false;
  685. break;
  686. }
  687. }
  688. if (applyConditions)
  689. {
  690. foreach (ProfileCondition c in i.Conditions)
  691. {
  692. conditions.Add(c);
  693. }
  694. }
  695. }
  696. }
  697. foreach (ProfileCondition i in conditions)
  698. {
  699. if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioProfile, isSecondaryAudio))
  700. {
  701. LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource);
  702. return null;
  703. }
  704. }
  705. }
  706. if (isEligibleForDirectPlay && mediaSource.SupportsDirectPlay)
  707. {
  708. if (mediaSource.Protocol == MediaProtocol.Http)
  709. {
  710. if (_localPlayer.CanAccessUrl(mediaSource.Path, mediaSource.RequiredHttpHeaders.Count > 0))
  711. {
  712. return PlayMethod.DirectPlay;
  713. }
  714. }
  715. else if (mediaSource.Protocol == MediaProtocol.File)
  716. {
  717. if (_localPlayer.CanAccessFile(mediaSource.Path))
  718. {
  719. return PlayMethod.DirectPlay;
  720. }
  721. }
  722. }
  723. if (isEligibleForDirectStream && mediaSource.SupportsDirectStream)
  724. {
  725. return PlayMethod.DirectStream;
  726. }
  727. return null;
  728. }
  729. private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
  730. {
  731. _logger.Info("Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
  732. type,
  733. profile.Name ?? "Unknown Profile",
  734. condition.Property,
  735. condition.Condition,
  736. condition.Value ?? string.Empty,
  737. condition.IsRequired,
  738. mediaSource.Path ?? "Unknown path");
  739. }
  740. private bool IsEligibleForDirectPlay(MediaSourceInfo item,
  741. int? maxBitrate,
  742. MediaStream subtitleStream,
  743. VideoOptions options,
  744. PlayMethod playMethod)
  745. {
  746. if (subtitleStream != null)
  747. {
  748. SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, playMethod);
  749. if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
  750. {
  751. _logger.Info("Not eligible for {0} due to unsupported subtitles", playMethod);
  752. return false;
  753. }
  754. }
  755. return IsAudioEligibleForDirectPlay(item, maxBitrate);
  756. }
  757. public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod)
  758. {
  759. if (playMethod != PlayMethod.Transcode && !subtitleStream.IsExternal)
  760. {
  761. // Look for supported embedded subs
  762. foreach (SubtitleProfile profile in subtitleProfiles)
  763. {
  764. if (!profile.SupportsLanguage(subtitleStream.Language))
  765. {
  766. continue;
  767. }
  768. if (profile.Method != SubtitleDeliveryMethod.Embed)
  769. {
  770. continue;
  771. }
  772. if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && StringHelper.EqualsIgnoreCase(profile.Format, subtitleStream.Codec))
  773. {
  774. return profile;
  775. }
  776. }
  777. }
  778. // Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require conversion
  779. return GetExternalSubtitleProfile(subtitleStream, subtitleProfiles, playMethod, false) ?? GetExternalSubtitleProfile(subtitleStream, subtitleProfiles, playMethod, true) ?? new SubtitleProfile
  780. {
  781. Method = SubtitleDeliveryMethod.Encode,
  782. Format = subtitleStream.Codec
  783. };
  784. }
  785. private static SubtitleProfile GetExternalSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, bool allowConversion)
  786. {
  787. foreach (SubtitleProfile profile in subtitleProfiles)
  788. {
  789. if (profile.Method != SubtitleDeliveryMethod.External && profile.Method != SubtitleDeliveryMethod.Hls)
  790. {
  791. continue;
  792. }
  793. if (profile.Method == SubtitleDeliveryMethod.Hls && playMethod != PlayMethod.Transcode)
  794. {
  795. continue;
  796. }
  797. if (!profile.SupportsLanguage(subtitleStream.Language))
  798. {
  799. continue;
  800. }
  801. if ((profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format)) ||
  802. (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream))
  803. {
  804. bool requiresConversion = !StringHelper.EqualsIgnoreCase(subtitleStream.Codec, profile.Format);
  805. if (!requiresConversion)
  806. {
  807. return profile;
  808. }
  809. if (!allowConversion)
  810. {
  811. continue;
  812. }
  813. if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsExternalStream && subtitleStream.SupportsSubtitleConversionTo(profile.Format))
  814. {
  815. return profile;
  816. }
  817. }
  818. }
  819. return null;
  820. }
  821. private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, int? maxBitrate)
  822. {
  823. if (!maxBitrate.HasValue)
  824. {
  825. _logger.Info("Cannot direct play due to unknown supported bitrate");
  826. return false;
  827. }
  828. if (!item.Bitrate.HasValue)
  829. {
  830. _logger.Info("Cannot direct play due to unknown content bitrate");
  831. return false;
  832. }
  833. if (item.Bitrate.Value > maxBitrate.Value)
  834. {
  835. _logger.Info("Bitrate exceeds DirectPlay limit");
  836. return false;
  837. }
  838. return true;
  839. }
  840. private void ValidateInput(VideoOptions options)
  841. {
  842. ValidateAudioInput(options);
  843. if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
  844. {
  845. throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
  846. }
  847. if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
  848. {
  849. throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
  850. }
  851. }
  852. private void ValidateAudioInput(AudioOptions options)
  853. {
  854. if (string.IsNullOrEmpty(options.ItemId))
  855. {
  856. throw new ArgumentException("ItemId is required");
  857. }
  858. if (string.IsNullOrEmpty(options.DeviceId))
  859. {
  860. throw new ArgumentException("DeviceId is required");
  861. }
  862. if (options.Profile == null)
  863. {
  864. throw new ArgumentException("Profile is required");
  865. }
  866. if (options.MediaSources == null)
  867. {
  868. throw new ArgumentException("MediaSources is required");
  869. }
  870. }
  871. private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions)
  872. {
  873. foreach (ProfileCondition condition in conditions)
  874. {
  875. string value = condition.Value;
  876. if (string.IsNullOrEmpty(value))
  877. {
  878. continue;
  879. }
  880. // No way to express this
  881. if (condition.Condition == ProfileConditionType.GreaterThanEqual)
  882. {
  883. continue;
  884. }
  885. switch (condition.Property)
  886. {
  887. case ProfileConditionValue.AudioBitrate:
  888. {
  889. int num;
  890. if (IntHelper.TryParseCultureInvariant(value, out num))
  891. {
  892. item.AudioBitrate = num;
  893. }
  894. break;
  895. }
  896. case ProfileConditionValue.AudioChannels:
  897. {
  898. int num;
  899. if (IntHelper.TryParseCultureInvariant(value, out num))
  900. {
  901. item.MaxAudioChannels = num;
  902. }
  903. break;
  904. }
  905. case ProfileConditionValue.IsAnamorphic:
  906. case ProfileConditionValue.AudioProfile:
  907. case ProfileConditionValue.Has64BitOffsets:
  908. case ProfileConditionValue.PacketLength:
  909. case ProfileConditionValue.NumAudioStreams:
  910. case ProfileConditionValue.NumVideoStreams:
  911. case ProfileConditionValue.IsSecondaryAudio:
  912. case ProfileConditionValue.VideoTimestamp:
  913. {
  914. // Not supported yet
  915. break;
  916. }
  917. case ProfileConditionValue.RefFrames:
  918. {
  919. int num;
  920. if (IntHelper.TryParseCultureInvariant(value, out num))
  921. {
  922. item.MaxRefFrames = num;
  923. }
  924. break;
  925. }
  926. case ProfileConditionValue.VideoBitDepth:
  927. {
  928. int num;
  929. if (IntHelper.TryParseCultureInvariant(value, out num))
  930. {
  931. item.MaxVideoBitDepth = num;
  932. }
  933. break;
  934. }
  935. case ProfileConditionValue.VideoProfile:
  936. {
  937. item.VideoProfile = (value ?? string.Empty).Split('|')[0];
  938. break;
  939. }
  940. case ProfileConditionValue.Height:
  941. {
  942. int num;
  943. if (IntHelper.TryParseCultureInvariant(value, out num))
  944. {
  945. item.MaxHeight = num;
  946. }
  947. break;
  948. }
  949. case ProfileConditionValue.VideoBitrate:
  950. {
  951. int num;
  952. if (IntHelper.TryParseCultureInvariant(value, out num))
  953. {
  954. item.VideoBitrate = num;
  955. }
  956. break;
  957. }
  958. case ProfileConditionValue.VideoFramerate:
  959. {
  960. float num;
  961. if (FloatHelper.TryParseCultureInvariant(value, out num))
  962. {
  963. item.MaxFramerate = num;
  964. }
  965. break;
  966. }
  967. case ProfileConditionValue.VideoLevel:
  968. {
  969. int num;
  970. if (IntHelper.TryParseCultureInvariant(value, out num))
  971. {
  972. item.VideoLevel = num;
  973. }
  974. break;
  975. }
  976. case ProfileConditionValue.Width:
  977. {
  978. int num;
  979. if (IntHelper.TryParseCultureInvariant(value, out num))
  980. {
  981. item.MaxWidth = num;
  982. }
  983. break;
  984. }
  985. }
  986. }
  987. }
  988. private bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
  989. {
  990. if (profile.Container.Length > 0)
  991. {
  992. // Check container type
  993. string mediaContainer = item.Container ?? string.Empty;
  994. bool any = false;
  995. foreach (string i in profile.GetContainers())
  996. {
  997. if (StringHelper.EqualsIgnoreCase(i, mediaContainer))
  998. {
  999. any = true;
  1000. break;
  1001. }
  1002. }
  1003. if (!any)
  1004. {
  1005. return false;
  1006. }
  1007. }
  1008. // Check audio codec
  1009. List<string> audioCodecs = profile.GetAudioCodecs();
  1010. if (audioCodecs.Count > 0)
  1011. {
  1012. // Check audio codecs
  1013. string audioCodec = audioStream == null ? null : audioStream.Codec;
  1014. if (string.IsNullOrEmpty(audioCodec) || !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec))
  1015. {
  1016. return false;
  1017. }
  1018. }
  1019. return true;
  1020. }
  1021. private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
  1022. {
  1023. if (profile.Container.Length > 0)
  1024. {
  1025. // Check container type
  1026. string mediaContainer = item.Container ?? string.Empty;
  1027. bool any = false;
  1028. foreach (string i in profile.GetContainers())
  1029. {
  1030. if (StringHelper.EqualsIgnoreCase(i, mediaContainer))
  1031. {
  1032. any = true;
  1033. break;
  1034. }
  1035. }
  1036. if (!any)
  1037. {
  1038. return false;
  1039. }
  1040. }
  1041. // Check video codec
  1042. List<string> videoCodecs = profile.GetVideoCodecs();
  1043. if (videoCodecs.Count > 0)
  1044. {
  1045. string videoCodec = videoStream == null ? null : videoStream.Codec;
  1046. if (string.IsNullOrEmpty(videoCodec) || !ListHelper.ContainsIgnoreCase(videoCodecs, videoCodec))
  1047. {
  1048. return false;
  1049. }
  1050. }
  1051. // Check audio codec
  1052. List<string> audioCodecs = profile.GetAudioCodecs();
  1053. if (audioCodecs.Count > 0)
  1054. {
  1055. // Check audio codecs
  1056. string audioCodec = audioStream == null ? null : audioStream.Codec;
  1057. if (string.IsNullOrEmpty(audioCodec) || !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec))
  1058. {
  1059. return false;
  1060. }
  1061. }
  1062. return true;
  1063. }
  1064. }
  1065. }