TrickplayController.cs 6.7 KB

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