mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-04 03:27:21 -05:00 
			
		
		
		
	Trickplay playlist and image controller
This commit is contained in:
		
							parent
							
								
									515ee90fb9
								
							
						
					
					
						commit
						b18d6bd356
					
				@ -1028,25 +1028,6 @@ public class DynamicHlsController : BaseJellyfinApiController
 | 
			
		||||
            .ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <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.</param>
 | 
			
		||||
    /// <response code="200">Tiles stream returned.</response>
 | 
			
		||||
    /// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns>
 | 
			
		||||
    [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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets a video stream using HTTP live streaming.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										177
									
								
								Jellyfin.Api/Controllers/TrickplayController.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								Jellyfin.Api/Controllers/TrickplayController.cs
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Trickplay controller.
 | 
			
		||||
/// </summary>
 | 
			
		||||
[Route("")]
 | 
			
		||||
[Authorize]
 | 
			
		||||
public class TrickplayController : BaseJellyfinApiController
 | 
			
		||||
{
 | 
			
		||||
    private readonly ILogger<TrickplayController> _logger;
 | 
			
		||||
    private readonly IHttpContextAccessor _httpContextAccessor;
 | 
			
		||||
    private readonly ILibraryManager _libraryManager;
 | 
			
		||||
    private readonly ITrickplayManager _trickplayManager;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initializes a new instance of the <see cref="TrickplayController"/> class.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="logger">Instance of the <see cref="ILogger{TrickplayController}"/> interface.</param>
 | 
			
		||||
    /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
 | 
			
		||||
    /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param>
 | 
			
		||||
    /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
 | 
			
		||||
    public TrickplayController(
 | 
			
		||||
        ILogger<TrickplayController> logger,
 | 
			
		||||
        IHttpContextAccessor httpContextAccessor,
 | 
			
		||||
        ILibraryManager libraryManager,
 | 
			
		||||
        ITrickplayManager trickplayManager)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
        _httpContextAccessor = httpContextAccessor;
 | 
			
		||||
        _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)]
 | 
			
		||||
    [ProducesPlaylistFile]
 | 
			
		||||
    public ActionResult GetTrickplayHlsPlaylist(
 | 
			
		||||
        [FromRoute, Required] Guid itemId,
 | 
			
		||||
        [FromRoute, Required] int width,
 | 
			
		||||
        [FromQuery] string? mediaSourceId)
 | 
			
		||||
    {
 | 
			
		||||
        return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId.ToString("N"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <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,
 | 
			
		||||
        [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<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -118,81 +118,6 @@ public class DynamicHlsHelper
 | 
			
		||||
            cancellationTokenSource).ConfigureAwait(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Get trickplay tiles hls playlist.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="width">The width of a single tile.</param>
 | 
			
		||||
    /// <param name="mediaSourceId">The media version id.</param>
 | 
			
		||||
    /// <returns>The resulting <see cref="ActionResult"/>.</returns>
 | 
			
		||||
    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<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<ActionResult> 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());
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user