jellyfin/Jellyfin.Api/Controllers/HlsSegmentController.cs

192 lines
8.5 KiB
C#
Raw Normal View History

2020-11-21 08:26:03 -05:00
using System;
2020-09-05 19:11:44 -04:00
using System.ComponentModel.DataAnnotations;
2020-07-27 03:47:19 -04:00
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
2020-07-27 03:47:19 -04:00
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
2023-01-31 06:18:10 -05:00
namespace Jellyfin.Api.Controllers;
/// <summary>
/// The hls segment controller.
/// </summary>
[Route("")]
public class HlsSegmentController : BaseJellyfinApiController
2020-07-27 03:47:19 -04:00
{
2023-01-31 06:18:10 -05:00
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _serverConfigurationManager;
2023-10-31 13:26:37 -04:00
private readonly ITranscodeManager _transcodeManager;
2023-01-31 06:18:10 -05:00
/// <summary>
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
2023-10-31 13:26:37 -04:00
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
2023-01-31 06:18:10 -05:00
public HlsSegmentController(
IFileSystem fileSystem,
IServerConfigurationManager serverConfigurationManager,
2023-10-31 13:26:37 -04:00
ITranscodeManager transcodeManager)
2023-01-31 06:18:10 -05:00
{
_fileSystem = fileSystem;
_serverConfigurationManager = serverConfigurationManager;
2023-10-31 13:26:37 -04:00
_transcodeManager = transcodeManager;
2023-01-31 06:18:10 -05:00
}
2020-07-27 03:47:19 -04:00
/// <summary>
2023-01-31 06:18:10 -05:00
/// Gets the specified audio segment for an audio item.
2020-07-27 03:47:19 -04:00
/// </summary>
2023-01-31 06:18:10 -05:00
/// <param name="itemId">The item id.</param>
/// <param name="segmentId">The segment id.</param>
/// <response code="200">Hls audio segment returned.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
// [Authenticated]
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesAudioFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
2020-07-27 03:47:19 -04:00
{
2023-01-31 06:18:10 -05:00
// TODO: Deprecate with new iOS app
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
2023-01-31 06:18:10 -05:00
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
2020-07-27 03:47:19 -04:00
{
2023-01-31 06:18:10 -05:00
return BadRequest("Invalid segment.");
2020-07-27 03:47:19 -04:00
}
2023-01-31 06:18:10 -05:00
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
}
2020-07-27 03:47:19 -04:00
2023-01-31 06:18:10 -05:00
/// <summary>
/// Gets a hls video playlist.
/// </summary>
/// <param name="itemId">The video id.</param>
/// <param name="playlistId">The playlist id.</param>
/// <response code="200">Hls video playlist returned.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
2023-02-08 17:55:26 -05:00
[Authorize]
2023-01-31 06:18:10 -05:00
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
2023-01-31 06:18:10 -05:00
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
2023-01-31 06:18:10 -05:00
{
return BadRequest("Invalid segment.");
2020-07-27 03:47:19 -04:00
}
2023-01-31 06:18:10 -05:00
return GetFileResult(file, file);
}
2020-07-27 03:47:19 -04:00
2023-01-31 06:18:10 -05:00
/// <summary>
/// Stops an active encoding.
/// </summary>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
/// <param name="playSessionId">The play session id.</param>
/// <response code="204">Encoding stopped successfully.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("Videos/ActiveEncodings")]
2023-02-08 17:55:26 -05:00
[Authorize]
2023-01-31 06:18:10 -05:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult StopEncodingProcess(
[FromQuery, Required] string deviceId,
[FromQuery, Required] string playSessionId)
{
2023-10-31 13:26:37 -04:00
_transcodeManager.KillTranscodingJobs(deviceId, playSessionId, _ => true);
2023-01-31 06:18:10 -05:00
return NoContent();
}
/// <summary>
/// Gets a hls video segment.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="playlistId">The playlist id.</param>
/// <param name="segmentId">The segment id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <response code="200">Hls video segment returned.</response>
/// <response code="404">Hls segment not found.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
// [Authenticated]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsVideoSegmentLegacy(
[FromRoute, Required] string itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] string segmentId,
[FromRoute, Required] string segmentContainer)
{
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
2023-01-31 06:18:10 -05:00
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
2020-07-27 03:47:19 -04:00
2023-01-31 06:18:10 -05:00
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
2020-07-27 03:47:19 -04:00
{
2023-01-31 06:18:10 -05:00
return BadRequest("Invalid segment.");
2020-07-27 03:47:19 -04:00
}
2023-01-31 06:18:10 -05:00
var normalizedPlaylistId = playlistId;
2020-07-27 03:47:19 -04:00
2023-01-31 06:18:10 -05:00
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
// Add . to start of segment container for future use.
segmentContainer = segmentContainer.Insert(0, ".");
string? playlistPath = null;
foreach (var path in filePaths)
{
var pathExtension = Path.GetExtension(path);
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
&& path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase))
{
2023-01-31 06:18:10 -05:00
playlistPath = path;
break;
}
2023-01-31 06:18:10 -05:00
}
2020-07-27 03:47:19 -04:00
2023-01-31 06:18:10 -05:00
return playlistPath is null
? NotFound("Hls segment not found.")
: GetFileResult(file, playlistPath);
}
2023-01-31 06:18:10 -05:00
private ActionResult GetFileResult(string path, string playlistPath)
{
2023-10-31 13:26:37 -04:00
var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
2020-07-27 03:47:19 -04:00
2023-01-31 06:18:10 -05:00
Response.OnCompleted(() =>
2020-07-27 03:47:19 -04:00
{
2023-01-31 06:18:10 -05:00
if (transcodingJob is not null)
2020-07-27 03:47:19 -04:00
{
2023-10-31 13:26:37 -04:00
_transcodeManager.OnTranscodeEndRequest(transcodingJob);
2023-01-31 06:18:10 -05:00
}
2020-07-27 03:47:19 -04:00
2023-01-31 06:18:10 -05:00
return Task.CompletedTask;
});
2020-07-27 03:47:19 -04:00
2023-01-31 06:18:10 -05:00
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
2020-07-27 03:47:19 -04:00
}
}