From b18d6bd3562a5e5a69f806b461049fbf9f61b70e Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Wed, 22 Feb 2023 23:13:55 -0800 Subject: Trickplay playlist and image controller --- Jellyfin.Api/Controllers/TrickplayController.cs | 177 ++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 Jellyfin.Api/Controllers/TrickplayController.cs (limited to 'Jellyfin.Api/Controllers/TrickplayController.cs') diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs new file mode 100644 index 000000000..389eb43ff --- /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")); + } +} -- cgit v1.2.3