TrickplayController.cs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.DataAnnotations;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net.Mime;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10. using Jellyfin.Api.Attributes;
  11. using Jellyfin.Api.Extensions;
  12. using Jellyfin.Api.Helpers;
  13. using MediaBrowser.Common.Extensions;
  14. using MediaBrowser.Controller.Library;
  15. using MediaBrowser.Controller.Trickplay;
  16. using MediaBrowser.Model;
  17. using Microsoft.AspNetCore.Authorization;
  18. using Microsoft.AspNetCore.Http;
  19. using Microsoft.AspNetCore.Mvc;
  20. using Microsoft.Extensions.Logging;
  21. namespace Jellyfin.Api.Controllers;
  22. /// <summary>
  23. /// Trickplay controller.
  24. /// </summary>
  25. [Route("")]
  26. [Authorize]
  27. public class TrickplayController : BaseJellyfinApiController
  28. {
  29. private readonly ILogger<TrickplayController> _logger;
  30. private readonly IHttpContextAccessor _httpContextAccessor;
  31. private readonly ILibraryManager _libraryManager;
  32. private readonly ITrickplayManager _trickplayManager;
  33. /// <summary>
  34. /// Initializes a new instance of the <see cref="TrickplayController"/> class.
  35. /// </summary>
  36. /// <param name="logger">Instance of the <see cref="ILogger{TrickplayController}"/> interface.</param>
  37. /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
  38. /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param>
  39. /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
  40. public TrickplayController(
  41. ILogger<TrickplayController> logger,
  42. IHttpContextAccessor httpContextAccessor,
  43. ILibraryManager libraryManager,
  44. ITrickplayManager trickplayManager)
  45. {
  46. _logger = logger;
  47. _httpContextAccessor = httpContextAccessor;
  48. _libraryManager = libraryManager;
  49. _trickplayManager = trickplayManager;
  50. }
  51. /// <summary>
  52. /// Gets an image tiles playlist for trickplay.
  53. /// </summary>
  54. /// <param name="itemId">The item id.</param>
  55. /// <param name="width">The width of a single tile.</param>
  56. /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
  57. /// <response code="200">Tiles stream returned.</response>
  58. /// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns>
  59. [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
  60. [ProducesResponseType(StatusCodes.Status200OK)]
  61. [ProducesPlaylistFile]
  62. public ActionResult GetTrickplayHlsPlaylist(
  63. [FromRoute, Required] Guid itemId,
  64. [FromRoute, Required] int width,
  65. [FromQuery] string? mediaSourceId)
  66. {
  67. return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId.ToString("N"));
  68. }
  69. /// <summary>
  70. /// Gets a trickplay tile grid image.
  71. /// </summary>
  72. /// <param name="itemId">The item id.</param>
  73. /// <param name="width">The width of a single tile.</param>
  74. /// <param name="index">The index of the desired tile grid.</param>
  75. /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
  76. /// <response code="200">Tiles image returned.</response>
  77. /// <response code="200">Tiles image not found at specified index.</response>
  78. /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns>
  79. [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
  80. [ProducesResponseType(StatusCodes.Status200OK)]
  81. [ProducesResponseType(StatusCodes.Status404NotFound)]
  82. [ProducesImageFile]
  83. public ActionResult GetTrickplayHlsPlaylist(
  84. [FromRoute, Required] Guid itemId,
  85. [FromRoute, Required] int width,
  86. [FromRoute, Required] int index,
  87. [FromQuery] string? mediaSourceId)
  88. {
  89. var item = _libraryManager.GetItemById(mediaSourceId ?? itemId.ToString("N"));
  90. if (item is null)
  91. {
  92. return NotFound();
  93. }
  94. var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
  95. if (System.IO.File.Exists(path))
  96. {
  97. return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
  98. }
  99. return NotFound();
  100. }
  101. private ActionResult GetTrickplayPlaylistInternal(int width, string mediaSourceId)
  102. {
  103. if (_httpContextAccessor.HttpContext is null)
  104. {
  105. throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
  106. }
  107. var tilesResolutions = _trickplayManager.GetTilesResolutions(Guid.Parse(mediaSourceId));
  108. if (tilesResolutions is not null && tilesResolutions.ContainsKey(width))
  109. {
  110. var builder = new StringBuilder(128);
  111. var tilesInfo = tilesResolutions[width];
  112. if (tilesInfo.TileCount > 0)
  113. {
  114. const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
  115. const string decimalFormat = "{0:0.###}";
  116. var resolution = tilesInfo.Width.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.Height.ToString(CultureInfo.InvariantCulture);
  117. var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture);
  118. var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
  119. var tileDuration = (decimal)tilesInfo.Interval / 1000;
  120. var infDuration = tileDuration * tilesPerGrid;
  121. var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid);
  122. builder.AppendLine("#EXTM3U");
  123. builder.Append("#EXT-X-TARGETDURATION:").AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture));
  124. builder.AppendLine("#EXT-X-VERSION:7");
  125. builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1");
  126. builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
  127. builder.AppendLine("#EXT-X-IMAGES-ONLY");
  128. for (int i = 0; i < tileGridCount; i++)
  129. {
  130. // All tile grids before the last one must contain full amount of tiles.
  131. // The final grid will be 0 < count <= maxTiles
  132. if (i == tileGridCount - 1)
  133. {
  134. tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid);
  135. infDuration = tileDuration * tilesPerGrid;
  136. }
  137. var url = string.Format(
  138. CultureInfo.InvariantCulture,
  139. urlFormat,
  140. width.ToString(CultureInfo.InvariantCulture),
  141. i.ToString(CultureInfo.InvariantCulture),
  142. mediaSourceId,
  143. _httpContextAccessor.HttpContext.User.GetToken());
  144. // EXTINF
  145. builder.Append("#EXTINF:").Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration))
  146. .AppendLine(",");
  147. // EXT-X-TILES
  148. builder.Append("#EXT-X-TILES:RESOLUTION=").Append(resolution).Append(",LAYOUT=").Append(layout).Append(",DURATION=")
  149. .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration));
  150. // URL
  151. builder.AppendLine(url);
  152. }
  153. builder.AppendLine("#EXT-X-ENDLIST");
  154. return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
  155. }
  156. }
  157. return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
  158. }
  159. }