From a49e513bc2e772905da1a2c3a7e56ce96abb8a11 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 26 Jun 2014 13:04:11 -0400 Subject: [PATCH] get more exact hls segment times --- MediaBrowser.Api/ApiEntryPoint.cs | 36 ++-- MediaBrowser.Api/Library/LibraryService.cs | 1 + .../Playback/BaseStreamingService.cs | 28 +-- .../Playback/Hls/BaseHlsService.cs | 62 +++---- .../Playback/Hls/DynamicHlsService.cs | 170 ++++++++++++++++-- .../Playback/Hls/HlsSegmentService.cs | 2 +- .../Playback/Hls/VideoHlsService.cs | 5 +- .../Progressive/ProgressiveStreamWriter.cs | 21 ++- .../MediaEncoding/IMediaEncoder.cs | 7 + .../Encoder/MediaEncoder.cs | 54 ++---- .../ApplicationHost.cs | 2 +- 11 files changed, 254 insertions(+), 134 deletions(-) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 3e9a0926be..154966240f 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -42,6 +42,7 @@ namespace MediaBrowser.Api /// /// The logger. /// The application paths. + /// The session manager. public ApiEntryPoint(ILogger logger, IServerApplicationPaths appPaths, ISessionManager sessionManager) { Logger = logger; @@ -99,7 +100,7 @@ namespace MediaBrowser.Api { var jobCount = _activeTranscodingJobs.Count; - Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, true)); + Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, FileDeleteMode.All)); // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files if (jobCount > 0) @@ -119,14 +120,12 @@ namespace MediaBrowser.Api /// The path. /// The type. /// The process. - /// The start time ticks. /// The device id. /// The state. /// The cancellation token source. public void OnTranscodeBeginning(string path, TranscodingJobType type, Process process, - long? startTimeTicks, string deviceId, StreamState state, CancellationTokenSource cancellationTokenSource) @@ -139,7 +138,6 @@ namespace MediaBrowser.Api Path = path, Process = process, ActiveRequestCount = 1, - StartTimeTicks = startTimeTicks, DeviceId = deviceId, CancellationTokenSource = cancellationTokenSource }); @@ -214,10 +212,15 @@ namespace MediaBrowser.Api /// The type. /// true if [has active transcoding job] [the specified path]; otherwise, false. public bool HasActiveTranscodingJob(string path, TranscodingJobType type) + { + return GetTranscodingJob(path, type) != null; + } + + public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { - return _activeTranscodingJobs.Any(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); + return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); } } @@ -290,16 +293,16 @@ namespace MediaBrowser.Api { var job = (TranscodingJob)state; - KillTranscodingJob(job, true); + KillTranscodingJob(job, FileDeleteMode.All); } /// /// Kills the single transcoding job. /// /// The device id. - /// if set to true [delete files]. + /// The delete mode. /// sourcePath - internal void KillTranscodingJobs(string deviceId, bool deleteFiles) + internal void KillTranscodingJobs(string deviceId, FileDeleteMode deleteMode) { if (string.IsNullOrEmpty(deviceId)) { @@ -317,7 +320,7 @@ namespace MediaBrowser.Api foreach (var job in jobs) { - KillTranscodingJob(job, deleteFiles); + KillTranscodingJob(job, deleteMode); } } @@ -325,8 +328,8 @@ namespace MediaBrowser.Api /// Kills the transcoding job. /// /// The job. - /// if set to true [delete files]. - private void KillTranscodingJob(TranscodingJob job, bool deleteFiles) + /// The delete mode. + private void KillTranscodingJob(TranscodingJob job, FileDeleteMode deleteMode) { lock (_activeTranscodingJobs) { @@ -378,7 +381,7 @@ namespace MediaBrowser.Api } } - if (deleteFiles) + if (deleteMode == FileDeleteMode.All) { DeletePartialStreamFiles(job.Path, job.Type, 0, 1500); } @@ -486,12 +489,13 @@ namespace MediaBrowser.Api /// The kill timer. public Timer KillTimer { get; set; } - public long? StartTimeTicks { get; set; } public string DeviceId { get; set; } public CancellationTokenSource CancellationTokenSource { get; set; } public object ProcessLock = new object(); + + public bool HasExited { get; set; } } /// @@ -508,4 +512,10 @@ namespace MediaBrowser.Api /// Hls } + + public enum FileDeleteMode + { + None, + All + } } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 802df5cca6..e1494700c8 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -213,6 +213,7 @@ namespace MediaBrowser.Api.Library } + [Route("/Library/Series/Added", "POST")] [Route("/Library/Series/Updated", "POST")] [Api(Description = "Reports that new episodes of a series have been added by an external source")] public class PostUpdatedSeries : IReturnVoid diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 1b3ff8d689..235675b983 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -138,14 +138,9 @@ namespace MediaBrowser.Api.Playback { var time = request.StartTimeTicks; - if (time.HasValue) + if (time.HasValue && time.Value > 0) { - var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds; - - if (seconds > 0) - { - return string.Format("-ss {0}", seconds.ToString(UsCulture)); - } + return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value)); } return string.Empty; @@ -586,7 +581,7 @@ namespace MediaBrowser.Api.Playback protected string GetTextSubtitleParam(StreamState state, CancellationToken cancellationToken) { - var seconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds; + var seconds = Math.Round(TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds); if (state.SubtitleStream.IsExternal) { @@ -608,13 +603,13 @@ namespace MediaBrowser.Api.Playback return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB", subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"), charsetParam, - Math.Round(seconds).ToString(UsCulture)); + seconds.ToString(UsCulture)); } return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB", state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"), state.InternalSubtitleStreamOffset.ToString(UsCulture), - Math.Round(seconds).ToString(UsCulture)); + seconds.ToString(UsCulture)); } /// @@ -849,7 +844,6 @@ namespace MediaBrowser.Api.Playback ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, - state.Request.StartTimeTicks, state.Request.DeviceId, state, cancellationTokenSource); @@ -866,7 +860,7 @@ namespace MediaBrowser.Api.Playback var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine); await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); - process.Exited += (sender, args) => OnFfMpegProcessExited(process, state); + process.Exited += (sender, args) => OnFfMpegProcessExited(process, state, outputPath); try { @@ -1092,8 +1086,16 @@ namespace MediaBrowser.Api.Playback /// /// The process. /// The state. - private void OnFfMpegProcessExited(Process process, StreamState state) + /// The output path. + private void OnFfMpegProcessExited(Process process, StreamState state, string outputPath) { + var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType); + + if (job != null) + { + job.HasExited = true; + } + Logger.Debug("Disposing stream resources"); state.Dispose(); diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 39163a1037..8957d9fa1c 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -1,14 +1,11 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.IO; +using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using System; @@ -223,49 +220,34 @@ namespace MediaBrowser.Api.Playback.Hls protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken) { - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - - string fileText; + var count = 0; - // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written - using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written + using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + { + using (var reader = new StreamReader(fileStream)) { - using (var reader = new StreamReader(fileStream)) + while (true) { - fileText = await reader.ReadToEndAsync().ConfigureAwait(false); + if (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) + { + count++; + if (count >= segmentCount) + { + return; + } + } + } + await Task.Delay(25, cancellationToken).ConfigureAwait(false); } } - - if (CountStringOccurrences(fileText, "#EXTINF:") >= segmentCount) - { - break; - } - - await Task.Delay(25, cancellationToken).ConfigureAwait(false); } } - /// - /// Count occurrences of strings. - /// - /// The text. - /// The pattern. - /// System.Int32. - private static int CountStringOccurrences(string text, string pattern) - { - // Loop through all instances of the string 'text'. - var count = 0; - var i = 0; - while ((i = text.IndexOf(pattern, i, StringComparison.OrdinalIgnoreCase)) != -1) - { - i += pattern.Length; - count++; - } - return count; - } - /// /// Gets the command line arguments. /// @@ -290,7 +272,7 @@ namespace MediaBrowser.Api.Playback.Hls // If isEncoding is true we're actually starting ffmpeg var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0"; - var args = string.Format("{0} {1} -i {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"", + var args = string.Format("{0} {1} -i {2} -map_metadata -1 -threads {3} {4} {5} {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"", itsOffset, inputModifier, GetInputArgument(state), diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 352cbf365c..1c274d7079 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -58,7 +59,8 @@ namespace MediaBrowser.Api.Playback.Hls public class DynamicHlsService : BaseHlsService { - public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder) + public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder) { } @@ -82,6 +84,11 @@ namespace MediaBrowser.Api.Playback.Hls private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1); private async Task GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain) { + if ((request.StartTimeTicks ?? 0) > 0) + { + throw new ArgumentException("StartTimeTicks is not allowed."); + } + var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; @@ -96,7 +103,7 @@ namespace MediaBrowser.Api.Playback.Hls if (File.Exists(segmentPath)) { ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); - return GetSegementResult(segmentPath); + return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false); } await FfmpegStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); @@ -105,16 +112,26 @@ namespace MediaBrowser.Api.Playback.Hls if (File.Exists(segmentPath)) { ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); - return GetSegementResult(segmentPath); + return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false); } else { - if (index == 0) + var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath); + + if (currentTranscodingIndex == null || index < currentTranscodingIndex.Value || (index - currentTranscodingIndex.Value) > 3) { // If the playlist doesn't already exist, startup ffmpeg try { - ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, false); + if (currentTranscodingIndex.HasValue) + { + ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, FileDeleteMode.None); + + DeleteLastFile(playlistPath, 0); + } + + var startSeconds = index * state.SegmentLength; + request.StartTimeTicks = TimeSpan.FromSeconds(startSeconds).Ticks; await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false); } @@ -124,7 +141,7 @@ namespace MediaBrowser.Api.Playback.Hls throw; } - await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false); + await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false); } } } @@ -140,12 +157,75 @@ namespace MediaBrowser.Api.Playback.Hls } Logger.Info("returning {0}", segmentPath); - return GetSegementResult(segmentPath); + return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false); + } + + public int? GetCurrentTranscodingIndex(string playlist) + { + var file = GetLastTranscodingFile(playlist, FileSystem); + + if (file == null) + { + return null; + } + + var playlistFilename = Path.GetFileNameWithoutExtension(playlist); + + var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); + + return int.Parse(indexString, NumberStyles.Integer, UsCulture); + } + + private void DeleteLastFile(string path, int retryCount) + { + if (retryCount >= 5) + { + return; + } + + var file = GetLastTranscodingFile(path, FileSystem); + + if (file != null) + { + try + { + File.Delete(file.FullName); + } + catch (IOException ex) + { + Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName); + + Thread.Sleep(100); + DeleteLastFile(path, retryCount + 1); + } + catch (Exception ex) + { + Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName); + } + } + } + + private static FileInfo GetLastTranscodingFile(string playlist, IFileSystem fileSystem) + { + var folder = Path.GetDirectoryName(playlist); + + try + { + return new DirectoryInfo(folder) + .EnumerateFiles("*", SearchOption.TopDirectoryOnly) + .Where(i => string.Equals(i.Extension, ".ts", StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(fileSystem.GetLastWriteTimeUtc) + .FirstOrDefault(); + } + catch (DirectoryNotFoundException) + { + return null; + } } protected override int GetStartNumber(StreamState state) { - var request = (GetDynamicHlsVideoSegment) state.Request; + var request = (GetDynamicHlsVideoSegment)state.Request; return int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture); } @@ -159,10 +239,65 @@ namespace MediaBrowser.Api.Playback.Hls return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts"); } - private object GetSegementResult(string path) + private async Task GetSegmentResult(string playlistPath, string segmentPath, int segmentIndex, CancellationToken cancellationToken) { - // TODO: Handle if it's currently being written to - return ResultFactory.GetStaticFileResult(Request, path, FileShare.ReadWrite); + // If all transcoding has completed, just return immediately + if (!IsTranscoding(playlistPath)) + { + return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite); + } + + var segmentFilename = Path.GetFileName(segmentPath); + + // If it appears in the playlist, it's done + if (File.ReadAllText(playlistPath).IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) + { + return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite); + } + + // if a different file is encoding, it's done + //var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath); + //if (currentTranscodingIndex > segmentIndex) + //{ + // return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite); + //} + + // Wait for the file to stop being written to, then stream it + var length = new FileInfo(segmentPath).Length; + var eofCount = 0; + + while (eofCount < 10) + { + var info = new FileInfo(segmentPath); + + if (!info.Exists) + { + break; + } + + var newLength = info.Length; + + if (newLength == length) + { + eofCount++; + } + else + { + eofCount = 0; + } + + length = newLength; + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + + return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite); + } + + private bool IsTranscoding(string playlistPath) + { + var job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); + + return job != null && !job.HasExited; } private async Task GetAsync(GetMasterHlsVideoStream request) @@ -312,9 +447,8 @@ namespace MediaBrowser.Api.Playback.Hls return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy"; } - var keyFrameArg = state.ReadInputAtNativeFramerate ? - " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" : - " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))"; + var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", + state.SegmentLength.ToString(UsCulture)); var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; @@ -344,5 +478,13 @@ namespace MediaBrowser.Api.Playback.Hls { return ".ts"; } + + protected override TranscodingJobType TranscodingJobType + { + get + { + return TranscodingJobType.Hls; + } + } } } diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index 2099bcd3a5..f31671b1a1 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -74,7 +74,7 @@ namespace MediaBrowser.Api.Playback.Hls public void Delete(StopEncodingProcess request) { - ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, true); + ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, FileDeleteMode.All); } /// diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 2379fb0050..1a925378bb 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -159,9 +159,8 @@ namespace MediaBrowser.Api.Playback.Hls return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy"; } - var keyFrameArg = state.ReadInputAtNativeFramerate ? - " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" : - " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))"; + var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", + state.SegmentLength.ToString(UsCulture)); var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index f99cef8eb8..36ae7f1002 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -1,7 +1,6 @@ using MediaBrowser.Common.IO; using MediaBrowser.Model.Logging; using ServiceStack.Web; -using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -60,7 +59,7 @@ namespace MediaBrowser.Api.Playback.Progressive { try { - await StreamFile(Path, responseStream).ConfigureAwait(false); + await new ProgressiveFileCopier(_fileSystem).StreamFile(Path, responseStream).ConfigureAwait(false); } catch { @@ -73,14 +72,18 @@ namespace MediaBrowser.Api.Playback.Progressive ApiEntryPoint.Instance.OnTranscodeEndRequest(Path, TranscodingJobType.Progressive); } } + } - /// - /// Streams the file. - /// - /// The path. - /// The output stream. - /// Task{System.Boolean}. - private async Task StreamFile(string path, Stream outputStream) + public class ProgressiveFileCopier + { + private readonly IFileSystem _fileSystem; + + public ProgressiveFileCopier(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public async Task StreamFile(string path, Stream outputStream) { var eofCount = 0; long position = 0; diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index f7e8554d1b..9468fd9875 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -68,5 +68,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// The protocol. /// System.String. string GetInputArgument(string[] inputFiles, MediaProtocol protocol); + + /// + /// Gets the time parameter. + /// + /// The ticks. + /// System.String. + string GetTimeParameter(long ticks); } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 5ee119e133..1087c905c8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1,5 +1,3 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.IO; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; @@ -11,7 +9,6 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -27,11 +24,6 @@ namespace MediaBrowser.MediaEncoding.Encoder /// private readonly ILogger _logger; - /// - /// The _app paths - /// - private readonly IApplicationPaths _appPaths; - /// /// Gets the json serializer. /// @@ -53,23 +45,17 @@ namespace MediaBrowser.MediaEncoding.Encoder /// private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(2, 2); - private readonly IFileSystem _fileSystem; - public string FFMpegPath { get; private set; } public string FFProbePath { get; private set; } public string Version { get; private set; } - public MediaEncoder(ILogger logger, IApplicationPaths appPaths, - IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, - IFileSystem fileSystem) + public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version) { _logger = logger; - _appPaths = appPaths; _jsonSerializer = jsonSerializer; Version = version; - _fileSystem = fileSystem; FFProbePath = ffProbePath; FFMpegPath = ffMpegPath; } @@ -263,30 +249,6 @@ namespace MediaBrowser.MediaEncoding.Encoder ((Process)sender).Dispose(); } - private const int FastSeekOffsetSeconds = 1; - - protected string GetFastSeekCommandLineParameter(TimeSpan offset) - { - var seconds = offset.TotalSeconds - FastSeekOffsetSeconds; - - if (seconds > 0) - { - return string.Format("-ss {0} ", seconds.ToString(UsCulture)); - } - - return string.Empty; - } - - protected string GetSlowSeekCommandLineParameter(TimeSpan offset) - { - if (offset.TotalSeconds - FastSeekOffsetSeconds > 0) - { - return string.Format(" -ss {0}", FastSeekOffsetSeconds.ToString(UsCulture)); - } - - return string.Empty; - } - public Task ExtractAudioImage(string path, CancellationToken cancellationToken) { return ExtractImage(new[] { path }, MediaProtocol.File, true, null, null, cancellationToken); @@ -368,7 +330,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (offset.HasValue) { - args = string.Format("-ss {0} ", Convert.ToInt32(offset.Value.TotalSeconds)).ToString(UsCulture) + args; + args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args; } var process = new Process @@ -463,5 +425,17 @@ namespace MediaBrowser.MediaEncoding.Encoder _videoImageResourcePool.Dispose(); } } + + public string GetTimeParameter(long ticks) + { + var time = TimeSpan.FromTicks(ticks); + + return GetTimeParameter(time); + } + + public string GetTimeParameter(TimeSpan time) + { + return time.ToString(@"hh\:mm\:ss\.fff", UsCulture); + } } } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index e93785bac2..d03c5fe3dc 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -588,7 +588,7 @@ namespace MediaBrowser.ServerApplication { var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager).GetFFMpegInfo(progress).ConfigureAwait(false); - MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ApplicationPaths, JsonSerializer, info.EncoderPath, info.ProbePath, info.Version, FileSystemManager); + MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), JsonSerializer, info.EncoderPath, info.ProbePath, info.Version); RegisterSingleInstance(MediaEncoder); }