aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api')
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs149
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj1
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs10
3 files changed, 48 insertions, 112 deletions
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 6ef3a2ff9..674daa8d1 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -13,6 +13,7 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.MediaEncoding.Hls.Playlist;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@@ -28,7 +29,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Controllers
{
@@ -55,6 +55,7 @@ namespace Jellyfin.Api.Controllers
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
+ private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator;
private readonly DynamicHlsHelper _dynamicHlsHelper;
private readonly EncodingOptions _encodingOptions;
@@ -74,6 +75,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ /// <param name="dynamicHlsPlaylistGenerator">Instance of <see cref="IDynamicHlsPlaylistGenerator"/>.</param>
public DynamicHlsController(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -87,7 +89,8 @@ namespace Jellyfin.Api.Controllers
TranscodingJobHelper transcodingJobHelper,
ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper,
- EncodingHelper encodingHelper)
+ EncodingHelper encodingHelper,
+ IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator)
{
_libraryManager = libraryManager;
_userManager = userManager;
@@ -102,6 +105,7 @@ namespace Jellyfin.Api.Controllers
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
_encodingHelper = encodingHelper;
+ _dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator;
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
}
@@ -1020,13 +1024,15 @@ namespace Jellyfin.Api.Controllers
/// <param name="playlistId">The playlist id.</param>
/// <param name="segmentId">The segment id.</param>
/// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
+ /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The desired segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -1078,6 +1084,8 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
[FromRoute, Required] string container,
+ [FromQuery, Required] long runtimeTicks,
+ [FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -1129,6 +1137,8 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new VideoRequestDto
{
Id = itemId,
+ CurrentRuntimeTicks = runtimeTicks,
+ ActualSegmentLengthTicks = actualSegmentLengthTicks,
Container = container,
Static = @static ?? false,
Params = @params,
@@ -1190,6 +1200,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="playlistId">The playlist id.</param>
/// <param name="segmentId">The segment id.</param>
/// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
+ /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
@@ -1249,6 +1261,8 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
[FromRoute, Required] string container,
+ [FromQuery, Required] long runtimeTicks,
+ [FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -1302,6 +1316,8 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
+ CurrentRuntimeTicks = runtimeTicks,
+ ActualSegmentLengthTicks = actualSegmentLengthTicks,
Static = @static ?? false,
Params = @params,
Tag = tag,
@@ -1374,60 +1390,16 @@ namespace Jellyfin.Api.Controllers
cancellationTokenSource.Token)
.ConfigureAwait(false);
- Response.Headers.Add(HeaderNames.Expires, "0");
-
- var segmentLengths = GetSegmentLengths(state);
-
- var segmentContainer = state.Request.SegmentContainer ?? "ts";
-
- // http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
- var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
- var hlsVersion = isHlsInFmp4 ? "7" : "3";
-
- var builder = new StringBuilder(128);
-
- builder.AppendLine("#EXTM3U")
- .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
- .Append("#EXT-X-VERSION:")
- .Append(hlsVersion)
- .AppendLine()
- .Append("#EXT-X-TARGETDURATION:")
- .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
- .AppendLine()
- .AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
-
- var index = 0;
- var segmentExtension = EncodingHelper.GetSegmentFileExtension(streamingRequest.SegmentContainer);
- var queryString = Request.QueryString;
-
- if (isHlsInFmp4)
- {
- builder.Append("#EXT-X-MAP:URI=\"")
- .Append("hls1/")
- .Append(name)
- .Append("/-1")
- .Append(segmentExtension)
- .Append(queryString)
- .Append('"')
- .AppendLine();
- }
-
- foreach (var length in segmentLengths)
- {
- builder.Append("#EXTINF:")
- .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
- .AppendLine(", nodesc")
- .Append("hls1/")
- .Append(name)
- .Append('/')
- .Append(index++)
- .Append(segmentExtension)
- .Append(queryString)
- .AppendLine();
- }
+ var request = new CreateMainPlaylistRequest(
+ state.MediaPath,
+ state.SegmentLength * 1000,
+ state.RunTimeTicks ?? 0,
+ state.Request.SegmentContainer ?? string.Empty,
+ "hls1/main/",
+ Request.QueryString.ToString());
+ var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
- builder.AppendLine("#EXT-X-ENDLIST");
- return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
}
private async Task<ActionResult> GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId)
@@ -1528,7 +1500,7 @@ namespace Jellyfin.Api.Controllers
DeleteLastFile(playlistPath, segmentExtension, 0);
}
- streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
+ streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
state.WaitForPath = segmentPath;
job = await _transcodingJobHelper.StartFfMpeg(
@@ -1879,7 +1851,7 @@ namespace Jellyfin.Api.Controllers
{
// Transcoding job is over, so assume all existing files are ready
_logger.LogDebug("serving up {0} as transcode is over", segmentPath);
- return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
}
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
@@ -1888,7 +1860,7 @@ namespace Jellyfin.Api.Controllers
if (segmentIndex < currentTranscodingIndex)
{
_logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
- return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
}
}
@@ -1903,8 +1875,8 @@ namespace Jellyfin.Api.Controllers
{
if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath))
{
- _logger.LogDebug("serving up {0} as it deemed ready", segmentPath);
- return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+ _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
}
}
else
@@ -1935,16 +1907,16 @@ namespace Jellyfin.Api.Controllers
_logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
}
- return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
}
- private ActionResult GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJobDto? transcodingJob)
+ private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob)
{
- var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
+ var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks;
Response.OnCompleted(() =>
{
- _logger.LogDebug("finished serving {0}", segmentPath);
+ _logger.LogDebug("Finished serving {SegmentPath}", segmentPath);
if (transcodingJob != null)
{
transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
@@ -1957,29 +1929,6 @@ namespace Jellyfin.Api.Controllers
return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext);
}
- private long GetEndPositionTicks(StreamState state, int requestedIndex)
- {
- double startSeconds = 0;
- var lengths = GetSegmentLengths(state);
-
- if (requestedIndex >= lengths.Length)
- {
- var msg = string.Format(
- CultureInfo.InvariantCulture,
- "Invalid segment index requested: {0} - Segment count: {1}",
- requestedIndex,
- lengths.Length);
- throw new ArgumentException(msg);
- }
-
- for (var i = 0; i <= requestedIndex; i++)
- {
- startSeconds += lengths[i];
- }
-
- return TimeSpan.FromSeconds(startSeconds).Ticks;
- }
-
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{
var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
@@ -2058,29 +2007,5 @@ namespace Jellyfin.Api.Controllers
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
}
}
-
- private long GetStartPositionTicks(StreamState state, int requestedIndex)
- {
- double startSeconds = 0;
- var lengths = GetSegmentLengths(state);
-
- if (requestedIndex >= lengths.Length)
- {
- var msg = string.Format(
- CultureInfo.InvariantCulture,
- "Invalid segment index requested: {0} - Segment count: {1}",
- requestedIndex,
- lengths.Length);
- throw new ArgumentException(msg);
- }
-
- for (var i = 0; i < requestedIndex; i++)
- {
- startSeconds += lengths[i];
- }
-
- var position = TimeSpan.FromSeconds(startSeconds).Ticks;
- return position;
- }
}
}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index b5af07408..4619d7a71 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -26,6 +26,7 @@
<ItemGroup>
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ <ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
</ItemGroup>
<!-- Code Analyzers-->
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
index 1791b0370..f8b0212b6 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs
@@ -41,5 +41,15 @@ namespace Jellyfin.Api.Models.StreamingDtos
/// Gets or sets the min segments.
/// </summary>
public int? MinSegments { get; set; }
+
+ /// <summary>
+ /// Gets or sets the position of the requested segment in ticks.
+ /// </summary>
+ public long CurrentRuntimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the actual segment length in ticks.
+ /// </summary>
+ public long ActualSegmentLengthTicks { get; set; }
}
}