diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 2dbc343e96..acd8111435 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1028,25 +1028,6 @@ public class DynamicHlsController : BaseJellyfinApiController
.ConfigureAwait(false);
}
- ///
- /// Gets an image tiles playlist for trickplay.
- ///
- /// The item id.
- /// The width of a single tile.
- /// The media version id.
- /// Tiles stream returned.
- /// A containing the trickplay tiles file.
- [HttpGet("Videos/{itemId}/tiles.m3u8")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public ActionResult GetTrickplayTilesHlsPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery, Required] int width,
- [FromQuery, Required] string mediaSourceId)
- {
- return _dynamicHlsHelper.GetTilesHlsPlaylist(width, mediaSourceId);
- }
-
///
/// Gets a video stream using HTTP live streaming.
///
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
new file mode 100644
index 0000000000..389eb43ffc
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Trickplay controller.
+///
+[Route("")]
+[Authorize]
+public class TrickplayController : BaseJellyfinApiController
+{
+ private readonly ILogger _logger;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ITrickplayManager _trickplayManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of .
+ /// Instance of .
+ public TrickplayController(
+ ILogger logger,
+ IHttpContextAccessor httpContextAccessor,
+ ILibraryManager libraryManager,
+ ITrickplayManager trickplayManager)
+ {
+ _logger = logger;
+ _httpContextAccessor = httpContextAccessor;
+ _libraryManager = libraryManager;
+ _trickplayManager = trickplayManager;
+ }
+
+ ///
+ /// Gets an image tiles playlist for trickplay.
+ ///
+ /// The item id.
+ /// The width of a single tile.
+ /// The media version id, if using an alternate version.
+ /// Tiles stream returned.
+ /// A containing the trickplay tiles file.
+ [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public ActionResult GetTrickplayHlsPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int width,
+ [FromQuery] string? mediaSourceId)
+ {
+ return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId.ToString("N"));
+ }
+
+ ///
+ /// Gets a trickplay tile grid image.
+ ///
+ /// The item id.
+ /// The width of a single tile.
+ /// The index of the desired tile grid.
+ /// The media version id, if using an alternate version.
+ /// Tiles image returned.
+ /// Tiles image not found at specified index.
+ /// A containing the trickplay tiles image.
+ [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public ActionResult GetTrickplayHlsPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int width,
+ [FromRoute, Required] int index,
+ [FromQuery] string? mediaSourceId)
+ {
+ var item = _libraryManager.GetItemById(mediaSourceId ?? itemId.ToString("N"));
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
+ if (System.IO.File.Exists(path))
+ {
+ return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
+ }
+
+ return NotFound();
+ }
+
+ private ActionResult GetTrickplayPlaylistInternal(int width, string mediaSourceId)
+ {
+ if (_httpContextAccessor.HttpContext is null)
+ {
+ throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
+ }
+
+ var tilesResolutions = _trickplayManager.GetTilesResolutions(Guid.Parse(mediaSourceId));
+ if (tilesResolutions is not null && tilesResolutions.ContainsKey(width))
+ {
+ var builder = new StringBuilder(128);
+ var tilesInfo = tilesResolutions[width];
+
+ if (tilesInfo.TileCount > 0)
+ {
+ const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
+ const string decimalFormat = "{0:0.###}";
+
+ var resolution = tilesInfo.Width.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.Height.ToString(CultureInfo.InvariantCulture);
+ var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture);
+ var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
+ var tileDuration = (decimal)tilesInfo.Interval / 1000;
+ var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid);
+
+ builder.AppendLine("#EXTM3U");
+ builder.Append("#EXT-X-TARGETDURATION:").AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture));
+ builder.AppendLine("#EXT-X-VERSION:7");
+ builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1");
+ builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
+ builder.AppendLine("#EXT-X-IMAGES-ONLY");
+
+ for (int i = 0; i < tileGridCount; i++)
+ {
+ // All tile grids before the last one must contain full amount of tiles.
+ // The final grid will be 0 < count <= maxTiles
+ if (i == tileGridCount - 1)
+ {
+ tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid);
+ }
+
+ var infDuration = tileDuration * tilesPerGrid;
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ urlFormat,
+ width.ToString(CultureInfo.InvariantCulture),
+ i.ToString(CultureInfo.InvariantCulture),
+ mediaSourceId,
+ _httpContextAccessor.HttpContext.User.GetToken());
+
+ // EXTINF
+ builder.Append("#EXTINF:").Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration))
+ .AppendLine(",");
+
+ // EXT-X-TILES
+ builder.Append("#EXT-X-TILES:RESOLUTION=").Append(resolution).Append(",LAYOUT=").Append(layout).Append(",DURATION=")
+ .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration));
+
+ // URL
+ builder.AppendLine(url);
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
+ }
+
+ return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
+}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index d8a16876df..6508dd8cf9 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -118,81 +118,6 @@ public class DynamicHlsHelper
cancellationTokenSource).ConfigureAwait(false);
}
- ///
- /// Get trickplay tiles hls playlist.
- ///
- /// The width of a single tile.
- /// The media version id.
- /// The resulting .
- public ActionResult GetTilesHlsPlaylist(int width, string mediaSourceId)
- {
- if (_httpContextAccessor.HttpContext is null)
- {
- throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
- }
-
- var tilesResolutions = _trickplayManager.GetTilesResolutions(Guid.Parse(mediaSourceId));
- if (tilesResolutions is not null && tilesResolutions.ContainsKey(width))
- {
- var builder = new StringBuilder(128);
- var tilesInfo = tilesResolutions[width];
-
- if (tilesInfo.TileCount > 0)
- {
- const string urlFormat = "{0}/Trickplay/{1}/{2}.jpg?&api_key={3}";
- const string decimalFormat = "{0:0.###}";
-
- var resolution = tilesInfo.Width.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.Height.ToString(CultureInfo.InvariantCulture);
- var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture);
- var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
- var tileDuration = (decimal)tilesInfo.Interval / 1000;
- var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid);
-
- builder.AppendLine("#EXTM3U");
- builder.Append("#EXT-X-TARGETDURATION:").AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture));
- builder.AppendLine("#EXT-X-VERSION:7");
- builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1");
- builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
- builder.AppendLine("#EXT-X-IMAGES-ONLY");
-
- for (int i = 0; i < tileGridCount; i++)
- {
- // All tile grids before the last one must contain full amount of tiles.
- // The final grid will be 0 < count <= maxTiles
- if (i == tileGridCount - 1)
- {
- tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid);
- }
-
- var infDuration = tileDuration * tilesPerGrid;
- var url = string.Format(
- CultureInfo.InvariantCulture,
- urlFormat,
- mediaSourceId,
- width.ToString(CultureInfo.InvariantCulture),
- i.ToString(CultureInfo.InvariantCulture),
- _httpContextAccessor.HttpContext.User.GetToken());
-
- // EXTINF
- builder.Append("#EXTINF:").Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration))
- .AppendLine(",");
-
- // EXT-X-TILES
- builder.Append("#EXT-X-TILES:RESOLUTION=").Append(resolution).Append(",LAYOUT=").Append(layout).Append(",DURATION=")
- .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration));
-
- // URL
- builder.AppendLine(url);
- }
-
- builder.AppendLine("#EXT-X-ENDLIST");
- return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
- }
- }
-
- return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8"));
- }
-
private async Task GetMasterPlaylistInternal(
StreamingRequestDto streamingRequest,
bool isHeadRequest,
@@ -633,7 +558,7 @@ public class DynamicHlsHelper
var url = string.Format(
CultureInfo.InvariantCulture,
- "tiles.m3u8?Width={0}&MediaSourceId={1}&api_key={2}",
+ "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
width.ToString(CultureInfo.InvariantCulture),
state.Request.MediaSourceId,
user.GetToken());