2023-02-23 00:13:55 -07:00
|
|
|
using System;
|
|
|
|
using System.ComponentModel.DataAnnotations;
|
|
|
|
using System.Globalization;
|
|
|
|
using System.Net.Mime;
|
|
|
|
using System.Text;
|
|
|
|
using Jellyfin.Api.Attributes;
|
|
|
|
using Jellyfin.Api.Extensions;
|
|
|
|
using MediaBrowser.Controller.Library;
|
|
|
|
using MediaBrowser.Controller.Trickplay;
|
|
|
|
using MediaBrowser.Model;
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
|
|
|
namespace Jellyfin.Api.Controllers;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Trickplay controller.
|
|
|
|
/// </summary>
|
|
|
|
[Route("")]
|
|
|
|
[Authorize]
|
|
|
|
public class TrickplayController : BaseJellyfinApiController
|
|
|
|
{
|
|
|
|
private readonly ILibraryManager _libraryManager;
|
|
|
|
private readonly ITrickplayManager _trickplayManager;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="TrickplayController"/> class.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param>
|
|
|
|
/// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
|
|
|
|
public TrickplayController(
|
|
|
|
ILibraryManager libraryManager,
|
|
|
|
ITrickplayManager trickplayManager)
|
|
|
|
{
|
|
|
|
_libraryManager = libraryManager;
|
|
|
|
_trickplayManager = trickplayManager;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets an image tiles playlist for trickplay.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="itemId">The item id.</param>
|
|
|
|
/// <param name="width">The width of a single tile.</param>
|
|
|
|
/// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
|
|
|
|
/// <response code="200">Tiles stream returned.</response>
|
|
|
|
/// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns>
|
|
|
|
[HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
|
|
|
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
2023-05-24 14:41:38 -07:00
|
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
2023-02-23 00:13:55 -07:00
|
|
|
[ProducesPlaylistFile]
|
|
|
|
public ActionResult GetTrickplayHlsPlaylist(
|
|
|
|
[FromRoute, Required] Guid itemId,
|
|
|
|
[FromRoute, Required] int width,
|
2023-05-17 23:25:52 -07:00
|
|
|
[FromQuery] Guid? mediaSourceId)
|
2023-02-23 00:13:55 -07:00
|
|
|
{
|
2023-05-17 23:25:52 -07:00
|
|
|
return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId);
|
2023-02-23 00:13:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets a trickplay tile grid image.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="itemId">The item id.</param>
|
|
|
|
/// <param name="width">The width of a single tile.</param>
|
|
|
|
/// <param name="index">The index of the desired tile grid.</param>
|
|
|
|
/// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
|
|
|
|
/// <response code="200">Tiles image returned.</response>
|
|
|
|
/// <response code="200">Tiles image not found at specified index.</response>
|
|
|
|
/// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns>
|
|
|
|
[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,
|
2023-05-17 23:25:52 -07:00
|
|
|
[FromQuery] Guid? mediaSourceId)
|
2023-02-23 00:13:55 -07:00
|
|
|
{
|
2023-05-17 23:25:52 -07:00
|
|
|
var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
|
2023-02-23 00:13:55 -07:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2023-05-17 23:25:52 -07:00
|
|
|
private ActionResult GetTrickplayPlaylistInternal(int width, Guid mediaSourceId)
|
2023-02-23 00:13:55 -07:00
|
|
|
{
|
2023-05-17 23:25:52 -07:00
|
|
|
var tilesResolutions = _trickplayManager.GetTilesResolutions(mediaSourceId);
|
|
|
|
if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo))
|
2023-02-23 00:13:55 -07:00
|
|
|
{
|
|
|
|
var builder = new StringBuilder(128);
|
|
|
|
|
|
|
|
if (tilesInfo.TileCount > 0)
|
|
|
|
{
|
|
|
|
const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
|
|
|
|
const string decimalFormat = "{0:0.###}";
|
|
|
|
|
2023-05-17 23:25:52 -07:00
|
|
|
var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}";
|
|
|
|
var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}";
|
2023-02-23 00:13:55 -07:00
|
|
|
var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
|
2023-05-24 14:41:38 -07:00
|
|
|
var tileDuration = tilesInfo.Interval / 1000d;
|
2023-02-23 19:03:22 -07:00
|
|
|
var infDuration = tileDuration * tilesPerGrid;
|
2023-02-23 00:13:55 -07:00
|
|
|
var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid);
|
|
|
|
|
2023-05-18 00:11:08 -07:00
|
|
|
builder
|
|
|
|
.AppendLine("#EXTM3U")
|
|
|
|
.Append("#EXT-X-TARGETDURATION:")
|
|
|
|
.AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture))
|
|
|
|
.AppendLine("#EXT-X-VERSION:7")
|
|
|
|
.AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
|
|
|
|
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
|
|
|
|
.AppendLine("#EXT-X-IMAGES-ONLY");
|
2023-02-23 00:13:55 -07:00
|
|
|
|
|
|
|
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);
|
2023-02-23 19:03:22 -07:00
|
|
|
infDuration = tileDuration * tilesPerGrid;
|
2023-02-23 00:13:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// EXTINF
|
2023-05-17 23:25:52 -07:00
|
|
|
builder
|
|
|
|
.Append("#EXTINF:")
|
2023-05-24 14:41:38 -07:00
|
|
|
.AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
|
2023-02-23 00:13:55 -07:00
|
|
|
.AppendLine(",");
|
|
|
|
|
|
|
|
// EXT-X-TILES
|
2023-05-17 23:25:52 -07:00
|
|
|
builder
|
|
|
|
.Append("#EXT-X-TILES:RESOLUTION=")
|
|
|
|
.Append(resolution)
|
|
|
|
.Append(",LAYOUT=")
|
|
|
|
.Append(layout)
|
|
|
|
.Append(",DURATION=")
|
2023-05-24 14:41:38 -07:00
|
|
|
.AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration)
|
|
|
|
.AppendLine();
|
2023-02-23 00:13:55 -07:00
|
|
|
|
|
|
|
// URL
|
2023-05-24 14:41:38 -07:00
|
|
|
builder
|
|
|
|
.AppendFormat(
|
|
|
|
CultureInfo.InvariantCulture,
|
|
|
|
urlFormat,
|
|
|
|
width.ToString(CultureInfo.InvariantCulture),
|
|
|
|
i.ToString(CultureInfo.InvariantCulture),
|
|
|
|
mediaSourceId.ToString("N"),
|
|
|
|
User.GetToken())
|
|
|
|
.AppendLine();
|
2023-02-23 00:13:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
builder.AppendLine("#EXT-X-ENDLIST");
|
|
|
|
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-24 14:41:38 -07:00
|
|
|
return NotFound();
|
2023-02-23 00:13:55 -07:00
|
|
|
}
|
|
|
|
}
|