2
0

MediaInfoHelper.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. using System;
  2. using System.Globalization;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Security.Claims;
  6. using System.Text.Json;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using Jellyfin.Api.Extensions;
  10. using Jellyfin.Data;
  11. using Jellyfin.Data.Enums;
  12. using Jellyfin.Database.Implementations.Entities;
  13. using Jellyfin.Database.Implementations.Enums;
  14. using Jellyfin.Extensions;
  15. using MediaBrowser.Common.Extensions;
  16. using MediaBrowser.Common.Net;
  17. using MediaBrowser.Controller.Configuration;
  18. using MediaBrowser.Controller.Devices;
  19. using MediaBrowser.Controller.Entities;
  20. using MediaBrowser.Controller.Entities.Audio;
  21. using MediaBrowser.Controller.Library;
  22. using MediaBrowser.Controller.MediaEncoding;
  23. using MediaBrowser.Model.Dlna;
  24. using MediaBrowser.Model.Dto;
  25. using MediaBrowser.Model.Entities;
  26. using MediaBrowser.Model.MediaInfo;
  27. using MediaBrowser.Model.Session;
  28. using Microsoft.AspNetCore.Http;
  29. using Microsoft.AspNetCore.Http.HttpResults;
  30. using Microsoft.Extensions.Logging;
  31. namespace Jellyfin.Api.Helpers;
  32. /// <summary>
  33. /// Media info helper.
  34. /// </summary>
  35. public class MediaInfoHelper
  36. {
  37. private readonly IUserManager _userManager;
  38. private readonly ILibraryManager _libraryManager;
  39. private readonly IMediaSourceManager _mediaSourceManager;
  40. private readonly IMediaEncoder _mediaEncoder;
  41. private readonly IServerConfigurationManager _serverConfigurationManager;
  42. private readonly ILogger<MediaInfoHelper> _logger;
  43. private readonly INetworkManager _networkManager;
  44. private readonly IDeviceManager _deviceManager;
  45. /// <summary>
  46. /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
  47. /// </summary>
  48. /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
  49. /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
  50. /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
  51. /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
  52. /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
  53. /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
  54. /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
  55. /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
  56. public MediaInfoHelper(
  57. IUserManager userManager,
  58. ILibraryManager libraryManager,
  59. IMediaSourceManager mediaSourceManager,
  60. IMediaEncoder mediaEncoder,
  61. IServerConfigurationManager serverConfigurationManager,
  62. ILogger<MediaInfoHelper> logger,
  63. INetworkManager networkManager,
  64. IDeviceManager deviceManager)
  65. {
  66. _userManager = userManager;
  67. _libraryManager = libraryManager;
  68. _mediaSourceManager = mediaSourceManager;
  69. _mediaEncoder = mediaEncoder;
  70. _serverConfigurationManager = serverConfigurationManager;
  71. _logger = logger;
  72. _networkManager = networkManager;
  73. _deviceManager = deviceManager;
  74. }
  75. /// <summary>
  76. /// Get playback info.
  77. /// </summary>
  78. /// <param name="item">The item.</param>
  79. /// <param name="user">The user.</param>
  80. /// <param name="mediaSourceId">Media source id.</param>
  81. /// <param name="liveStreamId">Live stream id.</param>
  82. /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
  83. public async Task<PlaybackInfoResponse> GetPlaybackInfo(
  84. BaseItem item,
  85. User? user,
  86. string? mediaSourceId = null,
  87. string? liveStreamId = null)
  88. {
  89. var result = new PlaybackInfoResponse();
  90. MediaSourceInfo[] mediaSources;
  91. if (string.IsNullOrWhiteSpace(liveStreamId))
  92. {
  93. // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
  94. var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
  95. if (string.IsNullOrWhiteSpace(mediaSourceId))
  96. {
  97. mediaSources = mediaSourcesList.ToArray();
  98. }
  99. else
  100. {
  101. mediaSources = mediaSourcesList
  102. .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
  103. .ToArray();
  104. }
  105. }
  106. else
  107. {
  108. var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
  109. mediaSources = new[] { mediaSource };
  110. }
  111. if (mediaSources.Length == 0)
  112. {
  113. result.MediaSources = Array.Empty<MediaSourceInfo>();
  114. result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
  115. }
  116. else
  117. {
  118. // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
  119. // Should we move this directly into MediaSourceManager?
  120. var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
  121. if (mediaSourcesClone is not null)
  122. {
  123. // Carry over the default audio index source.
  124. // This field is not intended to be exposed to API clients, but it is used internally by the server
  125. for (int i = 0; i < mediaSourcesClone.Length && i < mediaSources.Length; i++)
  126. {
  127. mediaSourcesClone[i].DefaultAudioIndexSource = mediaSources[i].DefaultAudioIndexSource;
  128. }
  129. result.MediaSources = mediaSourcesClone;
  130. }
  131. result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
  132. }
  133. return result;
  134. }
  135. /// <summary>
  136. /// SetDeviceSpecificData.
  137. /// </summary>
  138. /// <param name="item">Item to set data for.</param>
  139. /// <param name="mediaSource">Media source info.</param>
  140. /// <param name="profile">Device profile.</param>
  141. /// <param name="claimsPrincipal">Current claims principal.</param>
  142. /// <param name="maxBitrate">Max bitrate.</param>
  143. /// <param name="startTimeTicks">Start time ticks.</param>
  144. /// <param name="mediaSourceId">Media source id.</param>
  145. /// <param name="audioStreamIndex">Audio stream index.</param>
  146. /// <param name="subtitleStreamIndex">Subtitle stream index.</param>
  147. /// <param name="maxAudioChannels">Max audio channels.</param>
  148. /// <param name="playSessionId">Play session id.</param>
  149. /// <param name="userId">User id.</param>
  150. /// <param name="enableDirectPlay">Enable direct play.</param>
  151. /// <param name="enableDirectStream">Enable direct stream.</param>
  152. /// <param name="enableTranscoding">Enable transcoding.</param>
  153. /// <param name="allowVideoStreamCopy">Allow video stream copy.</param>
  154. /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param>
  155. /// <param name="alwaysBurnInSubtitleWhenTranscoding">Always burn-in subtitle when transcoding.</param>
  156. /// <param name="ipAddress">Requesting IP address.</param>
  157. public void SetDeviceSpecificData(
  158. BaseItem item,
  159. MediaSourceInfo mediaSource,
  160. DeviceProfile profile,
  161. ClaimsPrincipal claimsPrincipal,
  162. int? maxBitrate,
  163. long startTimeTicks,
  164. string mediaSourceId,
  165. int? audioStreamIndex,
  166. int? subtitleStreamIndex,
  167. int? maxAudioChannels,
  168. string playSessionId,
  169. Guid userId,
  170. bool enableDirectPlay,
  171. bool enableDirectStream,
  172. bool enableTranscoding,
  173. bool allowVideoStreamCopy,
  174. bool allowAudioStreamCopy,
  175. bool alwaysBurnInSubtitleWhenTranscoding,
  176. IPAddress ipAddress)
  177. {
  178. var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
  179. var options = new MediaOptions
  180. {
  181. MediaSources = new[] { mediaSource },
  182. Context = EncodingContext.Streaming,
  183. DeviceId = claimsPrincipal.GetDeviceId(),
  184. ItemId = item.Id,
  185. Profile = profile,
  186. MaxAudioChannels = maxAudioChannels,
  187. AllowAudioStreamCopy = allowAudioStreamCopy,
  188. AllowVideoStreamCopy = allowVideoStreamCopy,
  189. AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding,
  190. };
  191. if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
  192. {
  193. options.MediaSourceId = mediaSourceId;
  194. options.AudioStreamIndex = audioStreamIndex;
  195. options.SubtitleStreamIndex = subtitleStreamIndex;
  196. }
  197. var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
  198. if (!enableDirectPlay)
  199. {
  200. mediaSource.SupportsDirectPlay = false;
  201. }
  202. if (!enableDirectStream || !allowVideoStreamCopy)
  203. {
  204. mediaSource.SupportsDirectStream = false;
  205. }
  206. if (!enableTranscoding)
  207. {
  208. mediaSource.SupportsTranscoding = false;
  209. }
  210. if (item is Audio)
  211. {
  212. _logger.LogInformation(
  213. "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
  214. user.Username,
  215. user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
  216. }
  217. else
  218. {
  219. _logger.LogInformation(
  220. "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
  221. user.Username,
  222. user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
  223. user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
  224. user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
  225. }
  226. options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress);
  227. if (!options.ForceDirectStream)
  228. {
  229. // direct-stream http streaming is currently broken
  230. options.EnableDirectStream = false;
  231. }
  232. // Beginning of Playback Determination
  233. var streamInfo = item.MediaType == MediaType.Audio
  234. ? streamBuilder.GetOptimalAudioStream(options)
  235. : streamBuilder.GetOptimalVideoStream(options);
  236. if (streamInfo is not null)
  237. {
  238. streamInfo.PlaySessionId = playSessionId;
  239. streamInfo.StartPositionTicks = startTimeTicks;
  240. mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
  241. // Players do not handle this being set according to PlayMethod
  242. mediaSource.SupportsDirectStream =
  243. options.EnableDirectStream
  244. ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
  245. : streamInfo.PlayMethod == PlayMethod.DirectPlay;
  246. mediaSource.SupportsTranscoding =
  247. streamInfo.PlayMethod == PlayMethod.DirectStream
  248. || mediaSource.TranscodingContainer is not null
  249. || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
  250. if (item is Audio)
  251. {
  252. if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
  253. {
  254. mediaSource.SupportsTranscoding = false;
  255. }
  256. }
  257. else if (item is Video)
  258. {
  259. if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
  260. && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
  261. && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
  262. {
  263. mediaSource.SupportsTranscoding = false;
  264. }
  265. }
  266. if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
  267. {
  268. mediaSource.SupportsDirectPlay = false;
  269. mediaSource.SupportsDirectStream = false;
  270. mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), "&allowVideoStreamCopy=false&allowAudioStreamCopy=false");
  271. mediaSource.TranscodingContainer = streamInfo.Container;
  272. mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
  273. if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
  274. {
  275. mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
  276. }
  277. }
  278. else
  279. {
  280. if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
  281. {
  282. streamInfo.PlayMethod = PlayMethod.Transcode;
  283. mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), null);
  284. if (!allowVideoStreamCopy)
  285. {
  286. mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
  287. }
  288. if (!allowAudioStreamCopy)
  289. {
  290. mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
  291. }
  292. if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
  293. {
  294. mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
  295. }
  296. }
  297. }
  298. // Do this after the above so that StartPositionTicks is set
  299. // The token must not be null
  300. SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!);
  301. mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
  302. }
  303. foreach (var attachment in mediaSource.MediaAttachments)
  304. {
  305. attachment.DeliveryUrl = string.Format(
  306. CultureInfo.InvariantCulture,
  307. "/Videos/{0}/{1}/Attachments/{2}",
  308. item.Id,
  309. mediaSource.Id,
  310. attachment.Index);
  311. }
  312. }
  313. /// <summary>
  314. /// Sort media source.
  315. /// </summary>
  316. /// <param name="result">Playback info response.</param>
  317. /// <param name="maxBitrate">Max bitrate.</param>
  318. public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
  319. {
  320. var originalList = result.MediaSources.ToList();
  321. result.MediaSources = result.MediaSources.OrderBy(i =>
  322. {
  323. // Nothing beats direct playing a file
  324. if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
  325. {
  326. return 0;
  327. }
  328. return 1;
  329. })
  330. .ThenBy(i =>
  331. {
  332. // Let's assume direct streaming a file is just as desirable as direct playing a remote url
  333. if (i.SupportsDirectPlay || i.SupportsDirectStream)
  334. {
  335. return 0;
  336. }
  337. return 1;
  338. })
  339. .ThenBy(i =>
  340. {
  341. return i.Protocol switch
  342. {
  343. MediaProtocol.File => 0,
  344. _ => 1,
  345. };
  346. })
  347. .ThenBy(i =>
  348. {
  349. if (maxBitrate.HasValue && i.Bitrate.HasValue)
  350. {
  351. return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
  352. }
  353. return 1;
  354. })
  355. .ThenBy(originalList.IndexOf)
  356. .ToArray();
  357. }
  358. /// <summary>
  359. /// Open media source.
  360. /// </summary>
  361. /// <param name="httpContext">Http Context.</param>
  362. /// <param name="request">Live stream request.</param>
  363. /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
  364. public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
  365. {
  366. var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
  367. var profile = request.DeviceProfile;
  368. if (profile is null)
  369. {
  370. var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId());
  371. if (clientCapabilities is not null)
  372. {
  373. profile = clientCapabilities.DeviceProfile;
  374. }
  375. }
  376. if (profile is not null)
  377. {
  378. var item = _libraryManager.GetItemById<BaseItem>(request.ItemId)
  379. ?? throw new ResourceNotFoundException();
  380. SetDeviceSpecificData(
  381. item,
  382. result.MediaSource,
  383. profile,
  384. httpContext.User,
  385. request.MaxStreamingBitrate,
  386. request.StartTimeTicks ?? 0,
  387. result.MediaSource.Id,
  388. request.AudioStreamIndex,
  389. request.SubtitleStreamIndex,
  390. request.MaxAudioChannels,
  391. request.PlaySessionId,
  392. request.UserId,
  393. request.EnableDirectPlay,
  394. request.EnableDirectStream,
  395. true,
  396. true,
  397. true,
  398. request.AlwaysBurnInSubtitleWhenTranscoding,
  399. httpContext.GetNormalizedRemoteIP());
  400. }
  401. else
  402. {
  403. if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
  404. {
  405. result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
  406. }
  407. }
  408. // here was a check if (result.MediaSource is not null) but Rider said it will never be null
  409. NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
  410. return result;
  411. }
  412. /// <summary>
  413. /// Normalize media source container.
  414. /// </summary>
  415. /// <param name="mediaSource">Media source.</param>
  416. /// <param name="profile">Device profile.</param>
  417. /// <param name="type">Dlna profile type.</param>
  418. public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
  419. {
  420. mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type);
  421. }
  422. private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
  423. {
  424. var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
  425. mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
  426. mediaSource.TranscodeReasons = info.TranscodeReasons;
  427. foreach (var profile in profiles)
  428. {
  429. foreach (var stream in mediaSource.MediaStreams)
  430. {
  431. if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
  432. {
  433. stream.DeliveryMethod = profile.DeliveryMethod;
  434. if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
  435. {
  436. stream.DeliveryUrl = profile.Url.TrimStart('-');
  437. stream.IsExternalUrl = profile.IsExternalUrl;
  438. }
  439. }
  440. }
  441. }
  442. }
  443. private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress)
  444. {
  445. var maxBitrate = clientMaxBitrate;
  446. var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
  447. if (remoteClientMaxBitrate <= 0)
  448. {
  449. remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
  450. }
  451. if (remoteClientMaxBitrate > 0)
  452. {
  453. var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
  454. _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
  455. if (!isInLocalNetwork)
  456. {
  457. maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
  458. }
  459. }
  460. return maxBitrate;
  461. }
  462. }