TrickplayController.cs 6.7 KB

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