From 1fdaee1bb9935092e15cff3bbb8a976c811b0e4a Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 13 Apr 2015 15:14:37 -0400 Subject: [PATCH] fix hls seeking --- MediaBrowser.Api/ApiEntryPoint.cs | 132 +++++++++++++----- .../Playback/Hls/DynamicHlsService.cs | 24 +++- .../Progressive/ProgressiveStreamWriter.cs | 7 + .../UserLibrary/PlaystateService.cs | 40 +++++- 4 files changed, 158 insertions(+), 45 deletions(-) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index ba51f4f8c5..e91dd1a9b0 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -151,7 +151,7 @@ namespace MediaBrowser.Api { lock (_activeTranscodingJobs) { - var job = new TranscodingJob + var job = new TranscodingJob(Logger) { Type = type, Path = path, @@ -286,7 +286,7 @@ namespace MediaBrowser.Api if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive) { - job.DisposeKillTimer(); + job.StopKillTimer(); } } @@ -299,29 +299,22 @@ namespace MediaBrowser.Api 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(); lock (_activeTranscodingJobs) { // This is really only needed for HLS. // Progressive streams can stop on their own reliably - jobs = jobs.Where(j => - { - if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)) - { - return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase); - } - - return false; - - }).ToList(); + jobs = jobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList(); } foreach (var job in jobs) @@ -332,6 +325,12 @@ namespace MediaBrowser.Api private void PingTimer(TranscodingJob job, bool isProgressCheckIn) { + if (job.HasExited) + { + job.StopKillTimer(); + return; + } + // TODO: Lower this hls timeout var timerDuration = job.Type == TranscodingJobType.Progressive ? 1000 : @@ -343,19 +342,14 @@ namespace MediaBrowser.Api timerDuration = 20000; } - if (job.KillTimer == null) + // Don't start the timer for playback checkins with progressive streaming + if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn) { - // Don't start the timer for playback checkins with progressive streaming - if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn) - { - Logger.Debug("Starting kill timer at {0}ms", timerDuration); - job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite); - } + job.StartKillTimer(timerDuration, OnTranscodeKillTimerStopped); } else { - Logger.Debug("Changing kill timer to {0}ms", timerDuration); - job.KillTimer.Change(timerDuration, Timeout.Infinite); + job.ChangeKillTimerIfStarted(timerDuration); } } @@ -367,6 +361,8 @@ namespace MediaBrowser.Api { 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); } @@ -379,19 +375,14 @@ namespace MediaBrowser.Api /// Task. internal void KillTranscodingJobs(string deviceId, string playSessionId, Func deleteFiles) { - if (string.IsNullOrEmpty(deviceId)) - { - throw new ArgumentNullException("deviceId"); - } - 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); } @@ -431,6 +422,10 @@ namespace MediaBrowser.Api /// The delete. private void KillTranscodingJob(TranscodingJob job, Func delete) { + job.DisposeKillTimer(); + + Logger.Debug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId); + lock (_activeTranscodingJobs) { _activeTranscodingJobs.Remove(job); @@ -439,8 +434,6 @@ namespace MediaBrowser.Api { job.CancellationTokenSource.Cancel(); } - - job.DisposeKillTimer(); } lock (job.ProcessLock) @@ -599,6 +592,7 @@ namespace MediaBrowser.Api /// /// The process. public Process Process { get; set; } + public ILogger Logger { get; private set; } /// /// Gets or sets the active request count. /// @@ -608,7 +602,7 @@ namespace MediaBrowser.Api /// Gets or sets the kill timer. /// /// The kill timer. - public Timer KillTimer { get; set; } + private Timer KillTimer { get; set; } public string DeviceId { get; set; } @@ -631,12 +625,74 @@ namespace MediaBrowser.Api 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() { - if (KillTimer != null) + lock (_timerLock) { - KillTimer.Dispose(); - KillTimer = null; + if (KillTimer != null) + { + KillTimer.Dispose(); + KillTimer = null; + } + } + } + + 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"); } } } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 455113da90..c06bbe143c 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -127,9 +127,27 @@ namespace MediaBrowser.Api.Playback.Hls } else { + var startTranscoding = false; + var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); - var segmentGapRequiringTranscodingChange = 24/state.SegmentLength; - if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange) + var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; + + 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 try @@ -151,7 +169,7 @@ namespace MediaBrowser.Api.Playback.Hls throw; } - await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false); + //await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false); } else { diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 4a51f86444..6a3443f359 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -63,6 +63,13 @@ namespace MediaBrowser.Api.Playback.Progressive new ProgressiveFileCopier(_fileSystem, _job) .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) { Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex); diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs index 6c767596ea..4661abf4cc 100644 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -114,6 +114,15 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] 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; } } /// @@ -160,6 +169,15 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] 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; } } /// @@ -191,6 +209,12 @@ namespace MediaBrowser.Api.UserLibrary /// The position ticks. [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; } + + [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] @@ -260,7 +284,10 @@ namespace MediaBrowser.Api.UserLibrary QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(), MediaSourceId = request.MediaSourceId, 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, AudioStreamIndex = request.AudioStreamIndex, 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)) { - ApiEntryPoint.Instance.PingTranscodingJob(AuthorizationContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId); + ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId); } request.SessionId = GetSession().Result.Id; @@ -316,7 +346,9 @@ namespace MediaBrowser.Api.UserLibrary { ItemId = request.Id, PositionTicks = request.PositionTicks, - MediaSourceId = request.MediaSourceId + MediaSourceId = request.MediaSourceId, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId }); }