diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-09-02 22:30:24 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-09-02 22:30:24 -0400 |
| commit | 60d1d5cdee642ee9d5be7e91be5caeb9a1a756df (patch) | |
| tree | 67dcd2270977afafc596a1c4568cdaa4cf9d26dc /MediaBrowser.Api | |
| parent | a3d553a7fbe93fe416c940e80d5e511a2d753f25 (diff) | |
restore nuget targets for mono build
Diffstat (limited to 'MediaBrowser.Api')
| -rw-r--r-- | MediaBrowser.Api/ApiEntryPoint.cs | 94 | ||||
| -rw-r--r-- | MediaBrowser.Api/MediaBrowser.Api.csproj | 2 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/BaseStreamingService.cs | 63 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/BaseHlsService.cs | 28 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs | 63 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs | 38 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/VideoHlsService.cs | 22 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Progressive/AudioService.cs | 12 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs | 95 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs | 29 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Progressive/VideoService.cs | 11 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/StreamRequest.cs | 1 |
12 files changed, 301 insertions, 157 deletions
diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index c51d9e7c0..b49fbce48 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -120,12 +120,15 @@ namespace MediaBrowser.Api /// Called when [transcode beginning]. /// </summary> /// <param name="path">The path.</param> + /// <param name="transcodingJobId">The transcoding job identifier.</param> /// <param name="type">The type.</param> /// <param name="process">The process.</param> /// <param name="deviceId">The device id.</param> /// <param name="state">The state.</param> /// <param name="cancellationTokenSource">The cancellation token source.</param> - public void OnTranscodeBeginning(string path, + /// <returns>TranscodingJob.</returns> + public TranscodingJob OnTranscodeBeginning(string path, + string transcodingJobId, TranscodingJobType type, Process process, string deviceId, @@ -134,22 +137,37 @@ namespace MediaBrowser.Api { lock (_activeTranscodingJobs) { - _activeTranscodingJobs.Add(new TranscodingJob + var job = new TranscodingJob { Type = type, Path = path, Process = process, ActiveRequestCount = 1, DeviceId = deviceId, - CancellationTokenSource = cancellationTokenSource - }); + CancellationTokenSource = cancellationTokenSource, + Id = transcodingJobId + }; + + _activeTranscodingJobs.Add(job); + + ReportTranscodingProgress(job, state, null, null, null, null); - ReportTranscodingProgress(state, null, null); + return job; } } - public void ReportTranscodingProgress(StreamState state, float? framerate, double? percentComplete) + public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded) { + var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null; + + if (job != null) + { + job.Framerate = framerate; + job.CompletionPercentage = percentComplete; + job.TranscodingPositionTicks = ticks; + job.BytesTranscoded = bytesTranscoded; + } + var deviceId = state.Request.DeviceId; if (!string.IsNullOrWhiteSpace(deviceId)) @@ -226,12 +244,20 @@ namespace MediaBrowser.Api } } + public TranscodingJob GetTranscodingJob(string id) + { + lock (_activeTranscodingJobs) + { + return _activeTranscodingJobs.FirstOrDefault(j => j.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); + } + } + /// <summary> /// Called when [transcode begin request]. /// </summary> /// <param name="path">The path.</param> /// <param name="type">The type.</param> - public void OnTranscodeBeginRequest(string path, TranscodingJobType type) + public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { @@ -239,7 +265,7 @@ namespace MediaBrowser.Api if (job == null) { - return; + return null; } job.ActiveRequestCount++; @@ -249,40 +275,27 @@ namespace MediaBrowser.Api job.KillTimer.Dispose(); job.KillTimer = null; } + + return job; } } - /// <summary> - /// Called when [transcode end request]. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="type">The type.</param> - public void OnTranscodeEndRequest(string path, TranscodingJobType type) + public void OnTranscodeEndRequest(TranscodingJob job) { - lock (_activeTranscodingJobs) + job.ActiveRequestCount--; + + if (job.ActiveRequestCount == 0) { - var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); + // The HLS kill timer is long - 1/2 hr. clients should use the manual kill command when stopping. + var timerDuration = job.Type == TranscodingJobType.Progressive ? 1000 : 1800000; - if (job == null) + if (job.KillTimer == null) { - return; + job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite); } - - job.ActiveRequestCount--; - - if (job.ActiveRequestCount == 0) + else { - // The HLS kill timer is long - 1/2 hr. clients should use the manual kill command when stopping. - var timerDuration = type == TranscodingJobType.Progressive ? 1000 : 1800000; - - if (job.KillTimer == null) - { - job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite); - } - else - { - job.KillTimer.Change(timerDuration, Timeout.Infinite); - } + job.KillTimer.Change(timerDuration, Timeout.Infinite); } } } @@ -306,7 +319,6 @@ namespace MediaBrowser.Api /// <param name="acquireLock">if set to <c>true</c> [acquire lock].</param> /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">deviceId</exception> - /// <exception cref="System.ArgumentNullException">sourcePath</exception> internal Task KillTranscodingJobs(string deviceId, Func<string, bool> deleteFiles, bool acquireLock) { if (string.IsNullOrEmpty(deviceId)) @@ -324,8 +336,7 @@ namespace MediaBrowser.Api /// <param name="deleteFiles">The delete files.</param> /// <param name="acquireLock">if set to <c>true</c> [acquire lock].</param> /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">deviceId</exception> - internal async Task KillTranscodingJobs(Func<TranscodingJob,bool> killJob, Func<string, bool> deleteFiles, bool acquireLock) + internal async Task KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles, bool acquireLock) { var jobs = new List<TranscodingJob>(); @@ -542,6 +553,17 @@ namespace MediaBrowser.Api public object ProcessLock = new object(); public bool HasExited { get; set; } + + public string Id { get; set; } + + public float? Framerate { get; set; } + public double? CompletionPercentage { get; set; } + + public long? BytesDownloaded { get; set; } + public long? BytesTranscoded { get; set; } + + public long? TranscodingPositionTicks { get; set; } + public long? DownloadPositionTicks { get; set; } } /// <summary> diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index b0131b5c2..5cb9ebb1b 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -169,7 +169,7 @@ <PostBuildEvent> </PostBuildEvent> </PropertyGroup> - <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> + <Import Project="$(SolutionDir)\.nuget\NuGet.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 19f9db636..5cae6b010 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -90,10 +90,11 @@ namespace MediaBrowser.Api.Playback /// Gets the command line arguments. /// </summary> /// <param name="outputPath">The output path.</param> + /// <param name="transcodingJobId">The transcoding job identifier.</param> /// <param name="state">The state.</param> /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param> /// <returns>System.String.</returns> - protected abstract string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding); + protected abstract string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding); /// <summary> /// Gets the type of the transcoding job. @@ -122,7 +123,7 @@ namespace MediaBrowser.Api.Playback var outputFileExtension = GetOutputFileExtension(state); - var data = GetCommandLineArguments("dummy\\dummy", state, false); + var data = GetCommandLineArguments("dummy\\dummy", "dummyTranscodingId", state, false); data += "-" + (state.Request.DeviceId ?? string.Empty); @@ -782,9 +783,10 @@ namespace MediaBrowser.Api.Playback /// <summary> /// Gets the input argument. /// </summary> + /// <param name="transcodingJobId">The transcoding job identifier.</param> /// <param name="state">The state.</param> /// <returns>System.String.</returns> - protected string GetInputArgument(StreamState state) + protected string GetInputArgument(string transcodingJobId, StreamState state) { if (state.InputProtocol == MediaProtocol.File && state.RunTimeTicks.HasValue && @@ -795,6 +797,8 @@ namespace MediaBrowser.Api.Playback { var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/mediabrowser/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId; + url += "&transcodingJobId=" + transcodingJobId; + return string.Format("\"{0}\"", url); } } @@ -897,7 +901,7 @@ namespace MediaBrowser.Api.Playback /// <param name="cancellationTokenSource">The cancellation token source.</param> /// <returns>Task.</returns> /// <exception cref="System.InvalidOperationException">ffmpeg was not found at + MediaEncoder.EncoderPath</exception> - protected async Task StartFfMpeg(StreamState state, string outputPath, CancellationTokenSource cancellationTokenSource) + protected async Task<TranscodingJob> StartFfMpeg(StreamState state, string outputPath, CancellationTokenSource cancellationTokenSource) { if (!File.Exists(MediaEncoder.EncoderPath)) { @@ -908,7 +912,8 @@ namespace MediaBrowser.Api.Playback await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false); - var commandLineArgs = GetCommandLineArguments(outputPath, state, true); + var transcodingId = Guid.NewGuid().ToString("N"); + var commandLineArgs = GetCommandLineArguments(outputPath, transcodingId, state, true); if (ServerConfigurationManager.Configuration.EnableDebugEncodingLogging) { @@ -938,7 +943,8 @@ namespace MediaBrowser.Api.Playback EnableRaisingEvents = true }; - ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, + var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, + transcodingId, TranscodingJobType, process, state.Request.DeviceId, @@ -957,7 +963,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, outputPath); + process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); try { @@ -976,16 +982,18 @@ namespace MediaBrowser.Api.Playback process.BeginOutputReadLine(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - StartStreamingLog(state, process.StandardError.BaseStream, state.LogFileStream); + StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream); // Wait for the file to exist before proceeeding while (!File.Exists(outputPath)) { await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false); } + + return transcodingJob; } - private async void StartStreamingLog(StreamState state, Stream source, Stream target) + private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) { try { @@ -995,7 +1003,7 @@ namespace MediaBrowser.Api.Playback { var line = await reader.ReadLineAsync().ConfigureAwait(false); - ParseLogLine(line, state); + ParseLogLine(line, transcodingJob, state); var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); @@ -1009,10 +1017,12 @@ namespace MediaBrowser.Api.Playback } } - private void ParseLogLine(string line, StreamState state) + private void ParseLogLine(string line, TranscodingJob transcodingJob, StreamState state) { float? framerate = null; double? percent = null; + TimeSpan? transcodingPosition = null; + long? bytesTranscoded = null; var parts = line.Split(' '); @@ -1051,13 +1061,36 @@ namespace MediaBrowser.Api.Playback var percentVal = currentMs / totalMs; percent = 100 * percentVal; + + transcodingPosition = val; + } + } + else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase)) + { + var size = part.Split(new[] { '=' }, 2).Last(); + + int? scale = null; + if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1) + { + scale = 1024; + size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + if (scale.HasValue) + { + long val; + + if (long.TryParse(size, NumberStyles.Any, UsCulture, out val)) + { + bytesTranscoded = val * scale.Value; + } } } } if (framerate.HasValue || percent.HasValue) { - ApiEntryPoint.Instance.ReportTranscodingProgress(state, framerate, percent); + ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded); } } @@ -1170,12 +1203,10 @@ namespace MediaBrowser.Api.Playback /// Processes the exited. /// </summary> /// <param name="process">The process.</param> + /// <param name="job">The job.</param> /// <param name="state">The state.</param> - /// <param name="outputPath">The output path.</param> - private void OnFfMpegProcessExited(Process process, StreamState state, string outputPath) + private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state) { - var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType); - if (job != null) { job.HasExited = true; diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index bd17dc0b2..a3a86ba2e 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -88,10 +88,11 @@ namespace MediaBrowser.Api.Playback.Hls } var playlist = state.OutputFilePath; + TranscodingJob job; if (File.Exists(playlist)) { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); + job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); } else { @@ -100,14 +101,14 @@ namespace MediaBrowser.Api.Playback.Hls { if (File.Exists(playlist)) { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); + job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); } else { // If the playlist doesn't already exist, startup ffmpeg try { - await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false); + job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false); } catch { @@ -137,7 +138,10 @@ namespace MediaBrowser.Api.Playback.Hls } finally { - ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); + if (job != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(job); + } } } @@ -162,7 +166,10 @@ namespace MediaBrowser.Api.Playback.Hls } finally { - ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); + if (job != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(job); + } } } @@ -241,14 +248,7 @@ namespace MediaBrowser.Api.Playback.Hls } } - /// <summary> - /// Gets the command line arguments. - /// </summary> - /// <param name="outputPath">The output path.</param> - /// <param name="state">The state.</param> - /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param> - /// <returns>System.String.</returns> - protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) + protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding) { var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream; @@ -276,7 +276,7 @@ namespace MediaBrowser.Api.Playback.Hls 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}{10} -y \"{11}\"", itsOffset, inputModifier, - GetInputArgument(state), + GetInputArgument(transcodingJobId, state), threads, GetMapArgs(state), GetVideoArguments(state), diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 134c28524..40ec89f55 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -6,6 +6,7 @@ using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -107,11 +108,14 @@ namespace MediaBrowser.Api.Playback.Hls var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); var segmentPath = GetSegmentPath(playlistPath, index); + var segmentLength = state.SegmentLength; + + TranscodingJob job = null; if (File.Exists(segmentPath)) { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); - return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false); + job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); + return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); } await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); @@ -119,8 +123,8 @@ namespace MediaBrowser.Api.Playback.Hls { if (File.Exists(segmentPath)) { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); - return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false); + job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); + return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); } else { @@ -141,7 +145,7 @@ namespace MediaBrowser.Api.Playback.Hls var startSeconds = index * state.SegmentLength; request.StartTimeTicks = TimeSpan.FromSeconds(startSeconds).Ticks; - await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false); + job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false); } catch { @@ -165,7 +169,8 @@ namespace MediaBrowser.Api.Playback.Hls } Logger.Info("returning {0}", segmentPath); - return await GetSegmentResult(playlistPath, segmentPath, index, cancellationToken).ConfigureAwait(false); + job = job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType.Hls); + return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); } public int? GetCurrentTranscodingIndex(string playlist) @@ -258,12 +263,17 @@ namespace MediaBrowser.Api.Playback.Hls return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts"); } - private async Task<object> GetSegmentResult(string playlistPath, string segmentPath, int segmentIndex, CancellationToken cancellationToken) + private async Task<object> GetSegmentResult(string playlistPath, + string segmentPath, + int segmentIndex, + int segmentLength, + TranscodingJob transcodingJob, + CancellationToken cancellationToken) { // If all transcoding has completed, just return immediately if (!IsTranscoding(playlistPath)) { - return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite); + return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); } var segmentFilename = Path.GetFileName(segmentPath); @@ -277,7 +287,7 @@ namespace MediaBrowser.Api.Playback.Hls // If it appears in the playlist, it's done if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) { - return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite); + return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); } } } @@ -286,7 +296,7 @@ namespace MediaBrowser.Api.Playback.Hls //var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath); //if (currentTranscodingIndex > segmentIndex) //{ - // return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite); + //return GetSegmentResult(segmentPath, segmentIndex); //} // Wait for the file to stop being written to, then stream it @@ -317,7 +327,27 @@ namespace MediaBrowser.Api.Playback.Hls await Task.Delay(100, cancellationToken).ConfigureAwait(false); } - return ResultFactory.GetStaticFileResult(Request, segmentPath, FileShare.ReadWrite); + return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); + } + + private object GetSegmentResult(string segmentPath, int index, int segmentLength, TranscodingJob transcodingJob) + { + var segmentEndingSeconds = (1 + index) * segmentLength; + var segmentEndingPositionTicks = TimeSpan.FromSeconds(segmentEndingSeconds).Ticks; + + return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + { + Path = segmentPath, + FileShare = FileShare.ReadWrite, + OnComplete = () => + { + if (transcodingJob != null) + { + transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); + } + + } + }); } private bool IsTranscoding(string playlistPath) @@ -621,14 +651,7 @@ namespace MediaBrowser.Api.Playback.Hls return args; } - /// <summary> - /// Gets the command line arguments. - /// </summary> - /// <param name="outputPath">The output path.</param> - /// <param name="state">The state.</param> - /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param> - /// <returns>System.String.</returns> - protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) + protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding) { var threads = GetNumberOfThreads(state, false); @@ -639,7 +662,7 @@ namespace MediaBrowser.Api.Playback.Hls var args = string.Format("{0} -i {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"", inputModifier, - GetInputArgument(state), + GetInputArgument(transcodingJobId, state), threads, GetMapArgs(state), GetVideoArguments(state), diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index f28352588..12b2b8dae 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -63,7 +63,17 @@ namespace MediaBrowser.Api.Playback.Hls public object Get(GetHlsPlaylist request) { - OnBeginRequest(request.PlaylistId); + var normalizedPlaylistId = request.PlaylistId.Replace("-low", string.Empty); + + foreach (var playlist in Directory.EnumerateFiles(_appPaths.TranscodingTempPath, "*.m3u8") + .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) + .ToList()) + { + if (!string.IsNullOrEmpty(playlist)) + { + ExtendPlaylistTimer(playlist); + } + } var file = request.PlaylistId + Path.GetExtension(Request.PathInfo); @@ -93,32 +103,16 @@ namespace MediaBrowser.Api.Playback.Hls return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite); } - /// <summary> - /// Called when [begin request]. - /// </summary> - /// <param name="playlistId">The playlist id.</param> - protected void OnBeginRequest(string playlistId) - { - var normalizedPlaylistId = playlistId.Replace("-low", string.Empty); - - foreach (var playlist in Directory.EnumerateFiles(_appPaths.TranscodingTempPath, "*.m3u8") - .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) - .ToList()) - { - if (!string.IsNullOrEmpty(playlist)) - { - ExtendPlaylistTimer(playlist); - } - } - } - private async void ExtendPlaylistTimer(string playlist) { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); + var job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); await Task.Delay(20000).ConfigureAwait(false); - ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); + if (job != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(job); + } } } } diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 346af016e..3581b9e17 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -81,18 +81,7 @@ namespace MediaBrowser.Api.Playback.Hls file = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, file); - OnBeginRequest(request.PlaylistId); - - return ResultFactory.GetStaticFileResult(Request, file); - } - - /// <summary> - /// Called when [begin request]. - /// </summary> - /// <param name="playlistId">The playlist id.</param> - protected void OnBeginRequest(string playlistId) - { - var normalizedPlaylistId = playlistId.Replace("-low", string.Empty); + var normalizedPlaylistId = request.PlaylistId.Replace("-low", string.Empty); foreach (var playlist in Directory.EnumerateFiles(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, "*.m3u8") .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) @@ -100,15 +89,20 @@ namespace MediaBrowser.Api.Playback.Hls { ExtendPlaylistTimer(playlist); } + + return ResultFactory.GetStaticFileResult(Request, file); } private async void ExtendPlaylistTimer(string playlist) { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); + var job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); await Task.Delay(20000).ConfigureAwait(false); - ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); + if (job != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(job); + } } /// <summary> diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 6fae65ffc..ae592c428 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -55,15 +55,7 @@ namespace MediaBrowser.Api.Playback.Progressive return ProcessRequest(request, true); } - /// <summary> - /// Gets the command line arguments. - /// </summary> - /// <param name="outputPath">The output path.</param> - /// <param name="state">The state.</param> - /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param> - /// <returns>System.String.</returns> - /// <exception cref="System.InvalidOperationException">Only aac and mp3 audio codecs are supported.</exception> - protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) + protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding) { var audioTranscodeParams = new List<string>(); @@ -92,7 +84,7 @@ namespace MediaBrowser.Api.Playback.Progressive return string.Format("{0} -i {1} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1 -y \"{5}\"", inputModifier, - GetInputArgument(state), + GetInputArgument(transcodingJobId, state), threads, vn, string.Join(" ", audioTranscodeParams.ToArray()), diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index f0ad6ce5c..50929e6f3 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -142,10 +142,9 @@ namespace MediaBrowser.Api.Playback.Progressive var outputPath = state.OutputFilePath; var outputPathExists = File.Exists(outputPath); - var isStatic = request.Static || - (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)); + var isTranscodeCached = outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive); - AddDlnaHeaders(state, responseHeaders, isStatic); + AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached); // Static stream if (request.Static) @@ -154,6 +153,10 @@ namespace MediaBrowser.Api.Playback.Progressive using (state) { + var job = string.IsNullOrEmpty(request.TranscodingJobId) ? + null : + ApiEntryPoint.Instance.GetTranscodingJob(request.TranscodingJobId); + var limits = new List<long>(); if (state.InputBitrate.HasValue) { @@ -172,7 +175,13 @@ namespace MediaBrowser.Api.Playback.Progressive } // Take the greater of the above to methods, just to be safe - var throttleLimit = limits.Count > 0 ? limits.Max() : 0; + var throttleLimit = limits.Count > 0 ? limits.First() : 0; + + // Pad to play it safe + var bytesPerSecond = Convert.ToInt64(1.05 * throttleLimit); + + // Don't even start evaluating this until at least two minutes have content have been consumed + var targetGap = throttleLimit * 120; return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { @@ -182,17 +191,17 @@ namespace MediaBrowser.Api.Playback.Progressive Path = state.MediaPath, Throttle = request.Throttle, - // Pad by 20% to play it safe - ThrottleLimit = Convert.ToInt64(1.2 * throttleLimit), + ThrottleLimit = bytesPerSecond, - // 3.5 minutes - MinThrottlePosition = throttleLimit * 210 + MinThrottlePosition = targetGap, + + ThrottleCallback = (l1, l2) => ThrottleCallack(l1, l2, bytesPerSecond, job) }); } } // Not static but transcode cache file exists - if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)) + if (isTranscodeCached) { var contentType = state.GetMimeType(outputPath); @@ -225,6 +234,67 @@ namespace MediaBrowser.Api.Playback.Progressive } } + private readonly long _gapLengthInTicks = TimeSpan.FromMinutes(3).Ticks; + + private long ThrottleCallack(long currentBytesPerSecond, long bytesWritten, long originalBytesPerSecond, TranscodingJob job) + { + var bytesDownloaded = job.BytesDownloaded ?? 0; + var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; + var downloadPositionTicks = job.DownloadPositionTicks ?? 0; + + var path = job.Path; + + if (bytesDownloaded > 0 && transcodingPositionTicks > 0) + { + // Progressive Streaming - byte-based consideration + + try + { + var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length; + + // Estimate the bytes the transcoder should be ahead + double gapFactor = _gapLengthInTicks; + gapFactor /= transcodingPositionTicks; + var targetGap = bytesTranscoded * gapFactor; + + var gap = bytesTranscoded - bytesDownloaded; + + if (gap < targetGap) + { + //Logger.Debug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return 0; + } + + //Logger.Debug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + } + catch + { + //Logger.Error("Error getting output size"); + } + } + else if (downloadPositionTicks > 0 && transcodingPositionTicks > 0) + { + // HLS - time-based consideration + + var targetGap = _gapLengthInTicks; + var gap = transcodingPositionTicks - downloadPositionTicks; + + if (gap < targetGap) + { + //Logger.Debug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap); + return 0; + } + + //Logger.Debug("Throttling transcoder gap {0} target gap {1}", gap, targetGap); + } + else + { + //Logger.Debug("No throttle data for " + path); + } + + return originalBytesPerSecond; + } + /// <summary> /// Gets the static remote stream result. /// </summary> @@ -325,17 +395,18 @@ namespace MediaBrowser.Api.Playback.Progressive await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { + TranscodingJob job; + if (!File.Exists(outputPath)) { - await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false); + job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false); } else { - ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); + job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); state.Dispose(); } - var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem, job); result.Options["Content-Type"] = contentType; diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index a62f3dc9f..6e77e5eab 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -1,7 +1,8 @@ -using System; +using System.Threading; using MediaBrowser.Common.IO; using MediaBrowser.Model.Logging; using ServiceStack.Web; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -73,7 +74,10 @@ namespace MediaBrowser.Api.Playback.Progressive } finally { - ApiEntryPoint.Instance.OnTranscodeEndRequest(Path, TranscodingJobType.Progressive); + if (_job != null) + { + ApiEntryPoint.Instance.OnTranscodeEndRequest(_job); + } } } } @@ -83,6 +87,8 @@ namespace MediaBrowser.Api.Playback.Progressive private readonly IFileSystem _fileSystem; private readonly TranscodingJob _job; + private long _bytesWritten = 0; + public ProgressiveFileCopier(IFileSystem fileSystem, TranscodingJob job) { _fileSystem = fileSystem; @@ -98,7 +104,7 @@ namespace MediaBrowser.Api.Playback.Progressive { while (eofCount < 15) { - await fs.CopyToAsync(outputStream).ConfigureAwait(false); + await CopyToAsyncInternal(fs, outputStream, 81920, CancellationToken.None).ConfigureAwait(false); var fsPosition = fs.Position; @@ -123,5 +129,22 @@ namespace MediaBrowser.Api.Playback.Progressive } } } + + private async Task CopyToAsyncInternal(Stream source, Stream destination, int bufferSize, CancellationToken cancellationToken) + { + byte[] array = new byte[bufferSize]; + int count; + while ((count = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(array, 0, count, cancellationToken).ConfigureAwait(false); + + _bytesWritten += count; + + if (_job != null) + { + _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); + } + } + } } } diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 639381164..f82de5a6a 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -84,14 +84,7 @@ namespace MediaBrowser.Api.Playback.Progressive return ProcessRequest(request, true); } - /// <summary> - /// Gets the command line arguments. - /// </summary> - /// <param name="outputPath">The output path.</param> - /// <param name="state">The state.</param> - /// <param name="isEncoding">if set to <c>true</c> [is encoding].</param> - /// <returns>System.String.</returns> - protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) + protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding) { // Get the output codec name var videoCodec = state.OutputVideoCodec; @@ -110,7 +103,7 @@ namespace MediaBrowser.Api.Playback.Progressive return string.Format("{0} -i {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"", inputModifier, - GetInputArgument(state), + GetInputArgument(transcodingJobId, state), keyFrame, GetMapArgs(state), GetVideoArguments(state, videoCodec), diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index 0de8c28a9..7568afa46 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -72,6 +72,7 @@ namespace MediaBrowser.Api.Playback public string Params { get; set; } public bool Throttle { get; set; } + public string TranscodingJobId { get; set; } } public class VideoStreamRequest : StreamRequest |
