using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; 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; namespace Jellyfin.Api.Controllers { /// /// The hls segment controller. /// [Route("")] public class HlsSegmentController : BaseJellyfinApiController { private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly TranscodingJobHelper _transcodingJobHelper; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Initialized instance of the . public HlsSegmentController( IFileSystem fileSystem, IServerConfigurationManager serverConfigurationManager, TranscodingJobHelper transcodingJobHelper) { _fileSystem = fileSystem; _serverConfigurationManager = serverConfigurationManager; _transcodingJobHelper = transcodingJobHelper; } /// /// Gets the specified audio segment for an audio item. /// /// The item id. /// The segment id. /// Hls audio segment returned. /// A containing the audio stream. // 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) { // TODO: Deprecate with new iOS app var file = segmentId + Path.GetExtension(Request.Path); 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)) { return BadRequest("Invalid segment."); } return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)); } /// /// Gets a hls video playlist. /// /// The video id. /// The playlist id. /// Hls video playlist returned. /// A containing the playlist. [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] [Authorize(Policy = Policies.DefaultAuthorization)] [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 = playlistId + Path.GetExtension(Request.Path); 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) != ".m3u8") { return BadRequest("Invalid segment."); } return GetFileResult(file, file); } /// /// Stops an active encoding. /// /// The device id of the client requesting. Used to stop encoding processes when needed. /// The play session id. /// Encoding stopped successfully. /// A indicating success. [HttpDelete("Videos/ActiveEncodings")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult StopEncodingProcess( [FromQuery, Required] string deviceId, [FromQuery, Required] string playSessionId) { _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); return NoContent(); } /// /// Gets a hls video segment. /// /// The item id. /// The playlist id. /// The segment id. /// The segment container. /// Hls video segment returned. /// Hls segment not found. /// A containing the video segment. // 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 = segmentId + Path.GetExtension(Request.Path); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); var fileDir = Path.GetDirectoryName(file); if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } var normalizedPlaylistId = playlistId; 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.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) { playlistPath = path; break; } } return playlistPath is null ? NotFound("Hls segment not found.") : GetFileResult(file, playlistPath); } private ActionResult GetFileResult(string path, string playlistPath) { var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); Response.OnCompleted(() => { if (transcodingJob != null) { _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); } return Task.CompletedTask; }); return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)); } } }