mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-19 23:35:25 +03:00
Add first draft of keyframe extraction for Matroska
This commit is contained in:
@@ -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
|
||||
{
|
||||
@@ -54,6 +54,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;
|
||||
|
||||
@@ -73,6 +74,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,
|
||||
@@ -86,7 +88,8 @@ namespace Jellyfin.Api.Controllers
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
ILogger<DynamicHlsController> logger,
|
||||
DynamicHlsHelper dynamicHlsHelper,
|
||||
EncodingHelper encodingHelper)
|
||||
EncodingHelper encodingHelper,
|
||||
IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
@@ -101,6 +104,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_logger = logger;
|
||||
_dynamicHlsHelper = dynamicHlsHelper;
|
||||
_encodingHelper = encodingHelper;
|
||||
_dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator;
|
||||
|
||||
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
|
||||
}
|
||||
@@ -772,13 +776,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>
|
||||
@@ -830,6 +836,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,
|
||||
@@ -881,6 +889,8 @@ namespace Jellyfin.Api.Controllers
|
||||
var streamingRequest = new VideoRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
CurrentRuntimeTicks = runtimeTicks,
|
||||
ActualSegmentLengthTicks = actualSegmentLengthTicks,
|
||||
Container = container,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
@@ -942,6 +952,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>
|
||||
@@ -1001,6 +1013,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,
|
||||
@@ -1054,6 +1068,8 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
CurrentRuntimeTicks = runtimeTicks,
|
||||
ActualSegmentLengthTicks = actualSegmentLengthTicks,
|
||||
Static = @static ?? false,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
@@ -1126,60 +1142,16 @@ namespace Jellyfin.Api.Controllers
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Response.Headers.Add(HeaderNames.Expires, "0");
|
||||
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);
|
||||
|
||||
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 = 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();
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -1280,7 +1252,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(
|
||||
@@ -1634,7 +1606,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);
|
||||
@@ -1643,7 +1615,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1658,8 +1630,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
|
||||
@@ -1690,16 +1662,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);
|
||||
@@ -1712,29 +1684,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);
|
||||
@@ -1813,29 +1762,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user