MediaInfoService.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. using MediaBrowser.Common.Net;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Devices;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.Net;
  7. using MediaBrowser.Model.Dlna;
  8. using MediaBrowser.Model.Dto;
  9. using MediaBrowser.Model.Entities;
  10. using MediaBrowser.Model.MediaInfo;
  11. using MediaBrowser.Model.Session;
  12. using System;
  13. using System.Collections.Generic;
  14. using System.Linq;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. using MediaBrowser.Controller.Entities.Audio;
  18. using MediaBrowser.Controller.MediaEncoding;
  19. using MediaBrowser.Model.Serialization;
  20. using MediaBrowser.Model.Services;
  21. namespace MediaBrowser.Api.Playback
  22. {
  23. [Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
  24. public class GetPlaybackInfo : IReturn<PlaybackInfoResponse>
  25. {
  26. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
  27. public string Id { get; set; }
  28. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
  29. public string UserId { get; set; }
  30. }
  31. [Route("/Items/{Id}/PlaybackInfo", "POST", Summary = "Gets live playback media info for an item")]
  32. public class GetPostedPlaybackInfo : PlaybackInfoRequest, IReturn<PlaybackInfoResponse>
  33. {
  34. }
  35. [Route("/LiveStreams/Open", "POST", Summary = "Opens a media source")]
  36. public class OpenMediaSource : LiveStreamRequest, IReturn<LiveStreamResponse>
  37. {
  38. }
  39. [Route("/LiveStreams/Close", "POST", Summary = "Closes a media source")]
  40. public class CloseMediaSource : IReturnVoid
  41. {
  42. [ApiMember(Name = "LiveStreamId", Description = "LiveStreamId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
  43. public string LiveStreamId { get; set; }
  44. }
  45. [Route("/Playback/BitrateTest", "GET")]
  46. public class GetBitrateTestBytes
  47. {
  48. [ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
  49. public long Size { get; set; }
  50. public GetBitrateTestBytes()
  51. {
  52. // 100k
  53. Size = 102400;
  54. }
  55. }
  56. [Authenticated]
  57. public class MediaInfoService : BaseApiService
  58. {
  59. private readonly IMediaSourceManager _mediaSourceManager;
  60. private readonly IDeviceManager _deviceManager;
  61. private readonly ILibraryManager _libraryManager;
  62. private readonly IServerConfigurationManager _config;
  63. private readonly INetworkManager _networkManager;
  64. private readonly IMediaEncoder _mediaEncoder;
  65. private readonly IUserManager _userManager;
  66. private readonly IJsonSerializer _json;
  67. private readonly IAuthorizationContext _authContext;
  68. public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager, IMediaEncoder mediaEncoder, IUserManager userManager, IJsonSerializer json, IAuthorizationContext authContext)
  69. {
  70. _mediaSourceManager = mediaSourceManager;
  71. _deviceManager = deviceManager;
  72. _libraryManager = libraryManager;
  73. _config = config;
  74. _networkManager = networkManager;
  75. _mediaEncoder = mediaEncoder;
  76. _userManager = userManager;
  77. _json = json;
  78. _authContext = authContext;
  79. }
  80. public object Get(GetBitrateTestBytes request)
  81. {
  82. var bytes = new byte[request.Size];
  83. for (var i = 0; i < bytes.Length; i++)
  84. {
  85. bytes[i] = 0;
  86. }
  87. return ResultFactory.GetResult(bytes, "application/octet-stream");
  88. }
  89. public async Task<object> Get(GetPlaybackInfo request)
  90. {
  91. var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
  92. return ToOptimizedResult(result);
  93. }
  94. public async Task<object> Post(OpenMediaSource request)
  95. {
  96. var result = await OpenMediaSource(request).ConfigureAwait(false);
  97. return ToOptimizedResult(result);
  98. }
  99. private async Task<LiveStreamResponse> OpenMediaSource(OpenMediaSource request)
  100. {
  101. var authInfo = _authContext.GetAuthorizationInfo(Request);
  102. var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
  103. var profile = request.DeviceProfile;
  104. if (profile == null)
  105. {
  106. var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
  107. if (caps != null)
  108. {
  109. profile = caps.DeviceProfile;
  110. }
  111. }
  112. if (profile != null)
  113. {
  114. var item = _libraryManager.GetItemById(request.ItemId);
  115. SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
  116. request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
  117. request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, true, true, true);
  118. }
  119. else
  120. {
  121. if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
  122. {
  123. result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
  124. }
  125. }
  126. if (result.MediaSource != null)
  127. {
  128. NormalizeMediaSourceContainer(result.MediaSource, profile, DlnaProfileType.Video);
  129. }
  130. return result;
  131. }
  132. public void Post(CloseMediaSource request)
  133. {
  134. var task = _mediaSourceManager.CloseLiveStream(request.LiveStreamId);
  135. Task.WaitAll(task);
  136. }
  137. public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request)
  138. {
  139. var authInfo = _authContext.GetAuthorizationInfo(Request);
  140. var profile = request.DeviceProfile;
  141. //Logger.Info("GetPostedPlaybackInfo profile: {0}", _json.SerializeToString(profile));
  142. if (profile == null)
  143. {
  144. var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
  145. if (caps != null)
  146. {
  147. profile = caps.DeviceProfile;
  148. }
  149. }
  150. var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
  151. if (profile != null)
  152. {
  153. var mediaSourceId = request.MediaSourceId;
  154. SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, request.EnableTranscoding, request.AllowVideoStreamCopy, request.AllowAudioStreamCopy);
  155. }
  156. if (request.AutoOpenLiveStream)
  157. {
  158. var mediaSource = string.IsNullOrWhiteSpace(request.MediaSourceId) ? info.MediaSources.FirstOrDefault() : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId, StringComparison.Ordinal));
  159. if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
  160. {
  161. var openStreamResult = await OpenMediaSource(new OpenMediaSource
  162. {
  163. AudioStreamIndex = request.AudioStreamIndex,
  164. DeviceProfile = request.DeviceProfile,
  165. EnableDirectPlay = request.EnableDirectPlay,
  166. EnableDirectStream = request.EnableDirectStream,
  167. ItemId = request.Id,
  168. MaxAudioChannels = request.MaxAudioChannels,
  169. MaxStreamingBitrate = request.MaxStreamingBitrate,
  170. PlaySessionId = info.PlaySessionId,
  171. StartTimeTicks = request.StartTimeTicks,
  172. SubtitleStreamIndex = request.SubtitleStreamIndex,
  173. UserId = request.UserId,
  174. OpenToken = mediaSource.OpenToken,
  175. EnableMediaProbe = request.EnableMediaProbe
  176. }).ConfigureAwait(false);
  177. info.MediaSources = new List<MediaSourceInfo> { openStreamResult.MediaSource };
  178. }
  179. }
  180. if (info.MediaSources != null)
  181. {
  182. foreach (var mediaSource in info.MediaSources)
  183. {
  184. NormalizeMediaSourceContainer(mediaSource, profile, DlnaProfileType.Video);
  185. }
  186. }
  187. return info;
  188. }
  189. private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
  190. {
  191. mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
  192. }
  193. public async Task<object> Post(GetPostedPlaybackInfo request)
  194. {
  195. var result = await GetPlaybackInfo(request).ConfigureAwait(false);
  196. return ToOptimizedResult(result);
  197. }
  198. private T Clone<T>(T obj)
  199. {
  200. // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
  201. // Should we move this directly into MediaSourceManager?
  202. var json = _json.SerializeToString(obj);
  203. return _json.DeserializeFromString<T>(json);
  204. }
  205. private async Task<PlaybackInfoResponse> GetPlaybackInfo(string id, string userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
  206. {
  207. var result = new PlaybackInfoResponse();
  208. if (string.IsNullOrWhiteSpace(liveStreamId))
  209. {
  210. IEnumerable<MediaSourceInfo> mediaSources;
  211. try
  212. {
  213. mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, supportedLiveMediaTypes, CancellationToken.None).ConfigureAwait(false);
  214. }
  215. catch (PlaybackException ex)
  216. {
  217. mediaSources = new List<MediaSourceInfo>();
  218. result.ErrorCode = ex.ErrorCode;
  219. }
  220. result.MediaSources = mediaSources.ToList();
  221. if (!string.IsNullOrWhiteSpace(mediaSourceId))
  222. {
  223. result.MediaSources = result.MediaSources
  224. .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
  225. .ToList();
  226. }
  227. }
  228. else
  229. {
  230. var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
  231. result.MediaSources = new List<MediaSourceInfo> { mediaSource };
  232. }
  233. if (result.MediaSources.Count == 0)
  234. {
  235. if (!result.ErrorCode.HasValue)
  236. {
  237. result.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
  238. }
  239. }
  240. else
  241. {
  242. result.MediaSources = Clone(result.MediaSources);
  243. result.PlaySessionId = Guid.NewGuid().ToString("N");
  244. }
  245. return result;
  246. }
  247. private void SetDeviceSpecificData(string itemId,
  248. PlaybackInfoResponse result,
  249. DeviceProfile profile,
  250. AuthorizationInfo auth,
  251. long? maxBitrate,
  252. long startTimeTicks,
  253. string mediaSourceId,
  254. int? audioStreamIndex,
  255. int? subtitleStreamIndex,
  256. int? maxAudioChannels,
  257. string userId,
  258. bool enableDirectPlay,
  259. bool forceDirectPlayRemoteMediaSource,
  260. bool enableDirectStream,
  261. bool enableTranscoding,
  262. bool allowVideoStreamCopy,
  263. bool allowAudioStreamCopy)
  264. {
  265. var item = _libraryManager.GetItemById(itemId);
  266. foreach (var mediaSource in result.MediaSources)
  267. {
  268. SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding, allowVideoStreamCopy, allowAudioStreamCopy);
  269. }
  270. SortMediaSources(result, maxBitrate);
  271. }
  272. private void SetDeviceSpecificData(BaseItem item,
  273. MediaSourceInfo mediaSource,
  274. DeviceProfile profile,
  275. AuthorizationInfo auth,
  276. long? maxBitrate,
  277. long startTimeTicks,
  278. string mediaSourceId,
  279. int? audioStreamIndex,
  280. int? subtitleStreamIndex,
  281. int? maxAudioChannels,
  282. string playSessionId,
  283. string userId,
  284. bool enableDirectPlay,
  285. bool forceDirectPlayRemoteMediaSource,
  286. bool enableDirectStream,
  287. bool enableTranscoding,
  288. bool allowVideoStreamCopy,
  289. bool allowAudioStreamCopy)
  290. {
  291. var streamBuilder = new StreamBuilder(_mediaEncoder, Logger);
  292. var options = new VideoOptions
  293. {
  294. MediaSources = new List<MediaSourceInfo> { mediaSource },
  295. Context = EncodingContext.Streaming,
  296. DeviceId = auth.DeviceId,
  297. ItemId = item.Id.ToString("N"),
  298. Profile = profile,
  299. MaxAudioChannels = maxAudioChannels
  300. };
  301. if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
  302. {
  303. options.MediaSourceId = mediaSourceId;
  304. options.AudioStreamIndex = audioStreamIndex;
  305. options.SubtitleStreamIndex = subtitleStreamIndex;
  306. }
  307. var user = _userManager.GetUserById(userId);
  308. if (!enableDirectPlay)
  309. {
  310. mediaSource.SupportsDirectPlay = false;
  311. }
  312. if (!enableDirectStream)
  313. {
  314. mediaSource.SupportsDirectStream = false;
  315. }
  316. if (!enableTranscoding)
  317. {
  318. mediaSource.SupportsTranscoding = false;
  319. }
  320. if (item is Audio)
  321. {
  322. Logger.Info("User policy for {0}. EnableAudioPlaybackTranscoding: {1}", user.Name, user.Policy.EnableAudioPlaybackTranscoding);
  323. }
  324. else
  325. {
  326. Logger.Info("User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
  327. user.Name,
  328. user.Policy.EnablePlaybackRemuxing,
  329. user.Policy.EnableVideoPlaybackTranscoding,
  330. user.Policy.EnableAudioPlaybackTranscoding);
  331. }
  332. if (mediaSource.SupportsDirectPlay)
  333. {
  334. if (mediaSource.IsRemote && forceDirectPlayRemoteMediaSource)
  335. {
  336. }
  337. else
  338. {
  339. var supportsDirectStream = mediaSource.SupportsDirectStream;
  340. // Dummy this up to fool StreamBuilder
  341. mediaSource.SupportsDirectStream = true;
  342. options.MaxBitrate = maxBitrate;
  343. if (item is Audio)
  344. {
  345. if (!user.Policy.EnableAudioPlaybackTranscoding)
  346. {
  347. options.ForceDirectPlay = true;
  348. }
  349. }
  350. else if (item is Video)
  351. {
  352. if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
  353. {
  354. options.ForceDirectPlay = true;
  355. }
  356. }
  357. // The MediaSource supports direct stream, now test to see if the client supports it
  358. var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
  359. streamBuilder.BuildAudioItem(options) :
  360. streamBuilder.BuildVideoItem(options);
  361. if (streamInfo == null || !streamInfo.IsDirectStream)
  362. {
  363. mediaSource.SupportsDirectPlay = false;
  364. }
  365. // Set this back to what it was
  366. mediaSource.SupportsDirectStream = supportsDirectStream;
  367. if (streamInfo != null)
  368. {
  369. SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
  370. }
  371. }
  372. }
  373. if (mediaSource.SupportsDirectStream)
  374. {
  375. options.MaxBitrate = GetMaxBitrate(maxBitrate);
  376. if (item is Audio)
  377. {
  378. if (!user.Policy.EnableAudioPlaybackTranscoding)
  379. {
  380. options.ForceDirectStream = true;
  381. }
  382. }
  383. else if (item is Video)
  384. {
  385. if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
  386. {
  387. options.ForceDirectStream = true;
  388. }
  389. }
  390. // The MediaSource supports direct stream, now test to see if the client supports it
  391. var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
  392. streamBuilder.BuildAudioItem(options) :
  393. streamBuilder.BuildVideoItem(options);
  394. if (streamInfo == null || !streamInfo.IsDirectStream)
  395. {
  396. mediaSource.SupportsDirectStream = false;
  397. }
  398. if (streamInfo != null)
  399. {
  400. SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
  401. }
  402. }
  403. if (mediaSource.SupportsTranscoding)
  404. {
  405. options.MaxBitrate = GetMaxBitrate(maxBitrate);
  406. // The MediaSource supports direct stream, now test to see if the client supports it
  407. var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
  408. streamBuilder.BuildAudioItem(options) :
  409. streamBuilder.BuildVideoItem(options);
  410. if (streamInfo != null)
  411. {
  412. streamInfo.PlaySessionId = playSessionId;
  413. if (streamInfo.PlayMethod == PlayMethod.Transcode)
  414. {
  415. streamInfo.StartPositionTicks = startTimeTicks;
  416. mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
  417. if (!allowVideoStreamCopy)
  418. {
  419. mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
  420. }
  421. if (!allowAudioStreamCopy)
  422. {
  423. mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
  424. }
  425. mediaSource.TranscodingContainer = streamInfo.Container;
  426. mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
  427. }
  428. // Do this after the above so that StartPositionTicks is set
  429. SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
  430. }
  431. }
  432. }
  433. private long? GetMaxBitrate(long? clientMaxBitrate)
  434. {
  435. var maxBitrate = clientMaxBitrate;
  436. var remoteClientMaxBitrate = _config.Configuration.RemoteClientBitrateLimit;
  437. if (remoteClientMaxBitrate > 0)
  438. {
  439. var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.RemoteIp);
  440. Logger.Info("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.RemoteIp, isInLocalNetwork);
  441. if (!isInLocalNetwork)
  442. {
  443. maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
  444. }
  445. }
  446. return maxBitrate;
  447. }
  448. private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
  449. {
  450. var profiles = info.GetSubtitleProfiles(false, "-", accessToken);
  451. mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
  452. mediaSource.TranscodeReasons = info.TranscodeReasons;
  453. foreach (var profile in profiles)
  454. {
  455. foreach (var stream in mediaSource.MediaStreams)
  456. {
  457. if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
  458. {
  459. stream.DeliveryMethod = profile.DeliveryMethod;
  460. if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
  461. {
  462. stream.DeliveryUrl = profile.Url.TrimStart('-');
  463. stream.IsExternalUrl = profile.IsExternalUrl;
  464. }
  465. }
  466. }
  467. }
  468. }
  469. private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
  470. {
  471. var originalList = result.MediaSources.ToList();
  472. result.MediaSources = result.MediaSources.OrderBy(i =>
  473. {
  474. // Nothing beats direct playing a file
  475. if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
  476. {
  477. return 0;
  478. }
  479. return 1;
  480. }).ThenBy(i =>
  481. {
  482. // Let's assume direct streaming a file is just as desirable as direct playing a remote url
  483. if (i.SupportsDirectPlay || i.SupportsDirectStream)
  484. {
  485. return 0;
  486. }
  487. return 1;
  488. }).ThenBy(i =>
  489. {
  490. switch (i.Protocol)
  491. {
  492. case MediaProtocol.File:
  493. return 0;
  494. default:
  495. return 1;
  496. }
  497. }).ThenBy(i =>
  498. {
  499. if (maxBitrate.HasValue)
  500. {
  501. if (i.Bitrate.HasValue)
  502. {
  503. if (i.Bitrate.Value <= maxBitrate.Value)
  504. {
  505. return 0;
  506. }
  507. return 2;
  508. }
  509. }
  510. return 1;
  511. }).ThenBy(originalList.IndexOf)
  512. .ToList();
  513. }
  514. }
  515. }