mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-09-06 11:34:57 +02:00
fix hls seeking
This commit is contained in:
parent
88897141b0
commit
1fdaee1bb9
4 changed files with 158 additions and 45 deletions
|
@ -151,7 +151,7 @@ namespace MediaBrowser.Api
|
||||||
{
|
{
|
||||||
lock (_activeTranscodingJobs)
|
lock (_activeTranscodingJobs)
|
||||||
{
|
{
|
||||||
var job = new TranscodingJob
|
var job = new TranscodingJob(Logger)
|
||||||
{
|
{
|
||||||
Type = type,
|
Type = type,
|
||||||
Path = path,
|
Path = path,
|
||||||
|
@ -286,7 +286,7 @@ namespace MediaBrowser.Api
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
|
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
|
||||||
{
|
{
|
||||||
job.DisposeKillTimer();
|
job.StopKillTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,29 +299,22 @@ namespace MediaBrowser.Api
|
||||||
PingTimer(job, false);
|
PingTimer(job, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
internal void PingTranscodingJob(string deviceId, string playSessionId)
|
internal void PingTranscodingJob(string playSessionId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(deviceId))
|
if (string.IsNullOrEmpty(playSessionId))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("deviceId");
|
throw new ArgumentNullException("playSessionId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.Debug("PingTranscodingJob PlaySessionId={0}", playSessionId);
|
||||||
|
|
||||||
var jobs = new List<TranscodingJob>();
|
var jobs = new List<TranscodingJob>();
|
||||||
|
|
||||||
lock (_activeTranscodingJobs)
|
lock (_activeTranscodingJobs)
|
||||||
{
|
{
|
||||||
// This is really only needed for HLS.
|
// This is really only needed for HLS.
|
||||||
// Progressive streams can stop on their own reliably
|
// Progressive streams can stop on their own reliably
|
||||||
jobs = jobs.Where(j =>
|
jobs = jobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
{
|
|
||||||
if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var job in jobs)
|
foreach (var job in jobs)
|
||||||
|
@ -332,6 +325,12 @@ namespace MediaBrowser.Api
|
||||||
|
|
||||||
private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
|
private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
|
||||||
{
|
{
|
||||||
|
if (job.HasExited)
|
||||||
|
{
|
||||||
|
job.StopKillTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Lower this hls timeout
|
// TODO: Lower this hls timeout
|
||||||
var timerDuration = job.Type == TranscodingJobType.Progressive ?
|
var timerDuration = job.Type == TranscodingJobType.Progressive ?
|
||||||
1000 :
|
1000 :
|
||||||
|
@ -343,19 +342,14 @@ namespace MediaBrowser.Api
|
||||||
timerDuration = 20000;
|
timerDuration = 20000;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (job.KillTimer == null)
|
|
||||||
{
|
|
||||||
// Don't start the timer for playback checkins with progressive streaming
|
// Don't start the timer for playback checkins with progressive streaming
|
||||||
if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
|
if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
|
||||||
{
|
{
|
||||||
Logger.Debug("Starting kill timer at {0}ms", timerDuration);
|
job.StartKillTimer(timerDuration, OnTranscodeKillTimerStopped);
|
||||||
job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Debug("Changing kill timer to {0}ms", timerDuration);
|
job.ChangeKillTimerIfStarted(timerDuration);
|
||||||
job.KillTimer.Change(timerDuration, Timeout.Infinite);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,6 +361,8 @@ namespace MediaBrowser.Api
|
||||||
{
|
{
|
||||||
var job = (TranscodingJob)state;
|
var job = (TranscodingJob)state;
|
||||||
|
|
||||||
|
Logger.Debug("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
||||||
|
|
||||||
KillTranscodingJob(job, path => true);
|
KillTranscodingJob(job, path => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,19 +375,14 @@ namespace MediaBrowser.Api
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
|
internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(deviceId))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("deviceId");
|
|
||||||
}
|
|
||||||
|
|
||||||
KillTranscodingJobs(j =>
|
KillTranscodingJobs(j =>
|
||||||
{
|
{
|
||||||
if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrWhiteSpace(playSessionId))
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(playSessionId) || string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
|
return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
}, deleteFiles);
|
}, deleteFiles);
|
||||||
}
|
}
|
||||||
|
@ -431,6 +422,10 @@ namespace MediaBrowser.Api
|
||||||
/// <param name="delete">The delete.</param>
|
/// <param name="delete">The delete.</param>
|
||||||
private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
|
private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
|
||||||
{
|
{
|
||||||
|
job.DisposeKillTimer();
|
||||||
|
|
||||||
|
Logger.Debug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
||||||
|
|
||||||
lock (_activeTranscodingJobs)
|
lock (_activeTranscodingJobs)
|
||||||
{
|
{
|
||||||
_activeTranscodingJobs.Remove(job);
|
_activeTranscodingJobs.Remove(job);
|
||||||
|
@ -439,8 +434,6 @@ namespace MediaBrowser.Api
|
||||||
{
|
{
|
||||||
job.CancellationTokenSource.Cancel();
|
job.CancellationTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
job.DisposeKillTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (job.ProcessLock)
|
lock (job.ProcessLock)
|
||||||
|
@ -599,6 +592,7 @@ namespace MediaBrowser.Api
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The process.</value>
|
/// <value>The process.</value>
|
||||||
public Process Process { get; set; }
|
public Process Process { get; set; }
|
||||||
|
public ILogger Logger { get; private set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the active request count.
|
/// Gets or sets the active request count.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -608,7 +602,7 @@ namespace MediaBrowser.Api
|
||||||
/// Gets or sets the kill timer.
|
/// Gets or sets the kill timer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The kill timer.</value>
|
/// <value>The kill timer.</value>
|
||||||
public Timer KillTimer { get; set; }
|
private Timer KillTimer { get; set; }
|
||||||
|
|
||||||
public string DeviceId { get; set; }
|
public string DeviceId { get; set; }
|
||||||
|
|
||||||
|
@ -631,7 +625,27 @@ namespace MediaBrowser.Api
|
||||||
|
|
||||||
public TranscodingThrottler TranscodingThrottler { get; set; }
|
public TranscodingThrottler TranscodingThrottler { get; set; }
|
||||||
|
|
||||||
|
private readonly object _timerLock = new object();
|
||||||
|
|
||||||
|
public TranscodingJob(ILogger logger)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopKillTimer()
|
||||||
|
{
|
||||||
|
lock (_timerLock)
|
||||||
|
{
|
||||||
|
if (KillTimer != null)
|
||||||
|
{
|
||||||
|
KillTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void DisposeKillTimer()
|
public void DisposeKillTimer()
|
||||||
|
{
|
||||||
|
lock (_timerLock)
|
||||||
{
|
{
|
||||||
if (KillTimer != null)
|
if (KillTimer != null)
|
||||||
{
|
{
|
||||||
|
@ -641,6 +655,48 @@ namespace MediaBrowser.Api
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void StartKillTimer(int intervalMs, TimerCallback callback)
|
||||||
|
{
|
||||||
|
CheckHasExited();
|
||||||
|
|
||||||
|
lock (_timerLock)
|
||||||
|
{
|
||||||
|
if (KillTimer == null)
|
||||||
|
{
|
||||||
|
Logger.Debug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||||
|
KillTimer = new Timer(callback, this, intervalMs, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||||
|
KillTimer.Change(intervalMs, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeKillTimerIfStarted(int intervalMs)
|
||||||
|
{
|
||||||
|
CheckHasExited();
|
||||||
|
|
||||||
|
lock (_timerLock)
|
||||||
|
{
|
||||||
|
if (KillTimer != null)
|
||||||
|
{
|
||||||
|
Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||||
|
KillTimer.Change(intervalMs, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckHasExited()
|
||||||
|
{
|
||||||
|
if (HasExited)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException("Job");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enum TranscodingJobType
|
/// Enum TranscodingJobType
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -127,9 +127,27 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var startTranscoding = false;
|
||||||
|
|
||||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||||
var segmentGapRequiringTranscodingChange = 24/state.SegmentLength;
|
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
|
||||||
if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
|
|
||||||
|
if (currentTranscodingIndex == null)
|
||||||
|
{
|
||||||
|
Logger.Debug("Starting transcoding because currentTranscodingIndex=null");
|
||||||
|
startTranscoding = true;
|
||||||
|
}
|
||||||
|
else if (requestedIndex < currentTranscodingIndex.Value)
|
||||||
|
{
|
||||||
|
Logger.Debug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
|
||||||
|
startTranscoding = true;
|
||||||
|
}
|
||||||
|
else if ((requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
|
||||||
|
{
|
||||||
|
Logger.Debug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", (requestedIndex - currentTranscodingIndex.Value), segmentGapRequiringTranscodingChange, requestedIndex);
|
||||||
|
startTranscoding = true;
|
||||||
|
}
|
||||||
|
if (startTranscoding)
|
||||||
{
|
{
|
||||||
// If the playlist doesn't already exist, startup ffmpeg
|
// If the playlist doesn't already exist, startup ffmpeg
|
||||||
try
|
try
|
||||||
|
@ -151,7 +169,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
//await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -63,6 +63,13 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||||
new ProgressiveFileCopier(_fileSystem, _job)
|
new ProgressiveFileCopier(_fileSystem, _job)
|
||||||
.StreamFile(Path, responseStream);
|
.StreamFile(Path, responseStream);
|
||||||
}
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// These error are always the same so don't dump the whole stack trace
|
||||||
|
Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed.");
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex);
|
Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex);
|
||||||
|
|
|
@ -114,6 +114,15 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
|
|
||||||
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||||
public int? SubtitleStreamIndex { get; set; }
|
public int? SubtitleStreamIndex { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
|
public PlayMethod PlayMethod { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
|
public string LiveStreamId { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
|
public string PlaySessionId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -160,6 +169,15 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
|
|
||||||
[ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
[ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||||
public int? VolumeLevel { get; set; }
|
public int? VolumeLevel { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
|
public PlayMethod PlayMethod { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
|
public string LiveStreamId { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
|
public string PlaySessionId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -191,6 +209,12 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
/// <value>The position ticks.</value>
|
/// <value>The position ticks.</value>
|
||||||
[ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
|
[ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
|
||||||
public long? PositionTicks { get; set; }
|
public long? PositionTicks { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
|
public string LiveStreamId { get; set; }
|
||||||
|
|
||||||
|
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||||
|
public string PlaySessionId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authenticated]
|
[Authenticated]
|
||||||
|
@ -260,7 +284,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
|
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
|
||||||
MediaSourceId = request.MediaSourceId,
|
MediaSourceId = request.MediaSourceId,
|
||||||
AudioStreamIndex = request.AudioStreamIndex,
|
AudioStreamIndex = request.AudioStreamIndex,
|
||||||
SubtitleStreamIndex = request.SubtitleStreamIndex
|
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
||||||
|
PlayMethod = request.PlayMethod,
|
||||||
|
PlaySessionId = request.PlaySessionId,
|
||||||
|
LiveStreamId = request.LiveStreamId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,7 +315,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
MediaSourceId = request.MediaSourceId,
|
MediaSourceId = request.MediaSourceId,
|
||||||
AudioStreamIndex = request.AudioStreamIndex,
|
AudioStreamIndex = request.AudioStreamIndex,
|
||||||
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
||||||
VolumeLevel = request.VolumeLevel
|
VolumeLevel = request.VolumeLevel,
|
||||||
|
PlayMethod = request.PlayMethod,
|
||||||
|
PlaySessionId = request.PlaySessionId,
|
||||||
|
LiveStreamId = request.LiveStreamId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,7 +326,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
|
if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
|
||||||
{
|
{
|
||||||
ApiEntryPoint.Instance.PingTranscodingJob(AuthorizationContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId);
|
ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.SessionId = GetSession().Result.Id;
|
request.SessionId = GetSession().Result.Id;
|
||||||
|
@ -316,7 +346,9 @@ namespace MediaBrowser.Api.UserLibrary
|
||||||
{
|
{
|
||||||
ItemId = request.Id,
|
ItemId = request.Id,
|
||||||
PositionTicks = request.PositionTicks,
|
PositionTicks = request.PositionTicks,
|
||||||
MediaSourceId = request.MediaSourceId
|
MediaSourceId = request.MediaSourceId,
|
||||||
|
PlaySessionId = request.PlaySessionId,
|
||||||
|
LiveStreamId = request.LiveStreamId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue