display additional transcoding info in dashboard

This commit is contained in:
Luke Pulverenti 2014-06-05 20:39:02 -04:00
parent 7049ad66f4
commit f7cd7182d5
15 changed files with 260 additions and 74 deletions

View File

@ -1,5 +1,7 @@
using MediaBrowser.Controller;
using MediaBrowser.Api.Playback;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Logging;
using System;
using System.Collections.Generic;
@ -9,6 +11,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Api
{
@ -33,15 +36,18 @@ namespace MediaBrowser.Api
/// </summary>
private readonly IServerApplicationPaths _appPaths;
private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="appPaths">The application paths.</param>
public ApiEntryPoint(ILogger logger, IServerApplicationPaths appPaths)
public ApiEntryPoint(ILogger logger, IServerApplicationPaths appPaths, ISessionManager sessionManager)
{
Logger = logger;
_appPaths = appPaths;
_sessionManager = sessionManager;
Instance = this;
}
@ -115,10 +121,16 @@ namespace MediaBrowser.Api
/// <param name="type">The type.</param>
/// <param name="process">The process.</param>
/// <param name="startTimeTicks">The start time ticks.</param>
/// <param name="sourcePath">The source path.</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, TranscodingJobType type, Process process, long? startTimeTicks, string sourcePath, string deviceId, CancellationTokenSource cancellationTokenSource)
public void OnTranscodeBeginning(string path,
TranscodingJobType type,
Process process,
long? startTimeTicks,
string deviceId,
StreamState state,
CancellationTokenSource cancellationTokenSource)
{
lock (_activeTranscodingJobs)
{
@ -129,10 +141,43 @@ namespace MediaBrowser.Api
Process = process,
ActiveRequestCount = 1,
StartTimeTicks = startTimeTicks,
SourcePath = sourcePath,
DeviceId = deviceId,
CancellationTokenSource = cancellationTokenSource
});
ReportTranscodingProgress(state, null, null);
}
}
public void ReportTranscodingProgress(StreamState state, float? framerate, double? percentComplete)
{
var deviceId = state.Request.DeviceId;
if (!string.IsNullOrWhiteSpace(deviceId))
{
var audioCodec = state.Request.AudioCodec;
var videoCodec = state.VideoRequest == null ? null : state.VideoRequest.VideoCodec;
if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrEmpty(audioCodec))
{
audioCodec = state.OutputAudioCodec;
}
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrEmpty(videoCodec))
{
videoCodec = state.OutputVideoCodec;
}
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
{
Bitrate = state.TotalOutputBitrate,
AudioCodec = audioCodec,
VideoCodec = videoCodec,
Container = state.OutputContainer,
Framerate = framerate,
CompletionPercentage = percentComplete
});
}
}
@ -144,7 +189,8 @@ namespace MediaBrowser.Api
/// </summary>
/// <param name="path">The path.</param>
/// <param name="type">The type.</param>
public void OnTranscodeFailedToStart(string path, TranscodingJobType type)
/// <param name="state">The state.</param>
public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
{
lock (_activeTranscodingJobs)
{
@ -152,6 +198,11 @@ namespace MediaBrowser.Api
_activeTranscodingJobs.Remove(job);
}
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
}
}
/// <summary>
@ -437,7 +488,6 @@ namespace MediaBrowser.Api
public Timer KillTimer { get; set; }
public long? StartTimeTicks { get; set; }
public string SourcePath { get; set; }
public string DeviceId { get; set; }
public CancellationTokenSource CancellationTokenSource { get; set; }

View File

@ -126,7 +126,7 @@ namespace MediaBrowser.Api.Playback
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.String.</returns>
protected string GetOutputFilePath(StreamState state)
private string GetOutputFilePath(StreamState state)
{
var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath;
@ -726,12 +726,13 @@ namespace MediaBrowser.Api.Playback
/// </summary>
/// <param name="request">The request.</param>
/// <param name="audioStream">The audio stream.</param>
/// <param name="outputAudioCodec">The output audio codec.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private int? GetNumAudioChannelsParam(StreamRequest request, MediaStream audioStream)
private int? GetNumAudioChannelsParam(StreamRequest request, MediaStream audioStream, string outputAudioCodec)
{
if (audioStream != null)
{
var codec = request.AudioCodec ?? string.Empty;
var codec = outputAudioCodec ?? string.Empty;
if (audioStream.Channels > 2 && codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
{
@ -769,7 +770,7 @@ namespace MediaBrowser.Api.Playback
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.String.</returns>
protected string GetAudioCodec(StreamRequest request)
private string GetAudioCodec(StreamRequest request)
{
var codec = request.AudioCodec;
@ -798,7 +799,7 @@ namespace MediaBrowser.Api.Playback
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.String.</returns>
protected string GetVideoCodec(VideoStreamRequest request)
private string GetVideoCodec(VideoStreamRequest request)
{
var codec = request.VideoCodec;
@ -866,7 +867,7 @@ namespace MediaBrowser.Api.Playback
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
if (state.IsInputVideo && state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
{
state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
}
@ -900,7 +901,13 @@ namespace MediaBrowser.Api.Playback
EnableRaisingEvents = true
};
ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, state.Request.StartTimeTicks, state.MediaPath, state.Request.DeviceId, cancellationTokenSource);
ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
TranscodingJobType,
process,
state.Request.StartTimeTicks,
state.Request.DeviceId,
state,
cancellationTokenSource);
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
Logger.Info(commandLineLogMessage);
@ -924,7 +931,7 @@ namespace MediaBrowser.Api.Playback
{
Logger.ErrorException("Error starting ffmpeg", ex);
ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType);
ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
throw;
}
@ -932,10 +939,8 @@ namespace MediaBrowser.Api.Playback
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
process.BeginOutputReadLine();
#pragma warning disable 4014
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
process.StandardError.BaseStream.CopyToAsync(state.LogFileStream);
#pragma warning restore 4014
StartStreamingLog(state, process.StandardError.BaseStream, state.LogFileStream);
// Wait for the file to exist before proceeeding
while (!File.Exists(outputPath))
@ -956,6 +961,82 @@ namespace MediaBrowser.Api.Playback
}
}
private async void StartStreamingLog(StreamState state, Stream source, Stream target)
{
try
{
using (var reader = new StreamReader(source))
{
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync().ConfigureAwait(false);
ParseLogLine(line, state);
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
Logger.ErrorException("Error reading ffmpeg log", ex);
}
}
private void ParseLogLine(string line, StreamState state)
{
float? framerate = null;
double? percent = null;
var parts = line.Split(' ');
var totalMs = state.RunTimeTicks.HasValue
? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
: 0;
var startMs = state.Request.StartTimeTicks.HasValue
? TimeSpan.FromTicks(state.Request.StartTimeTicks.Value).TotalMilliseconds
: 0;
for (var i = 0; i < parts.Length; i++)
{
var part = parts[i];
if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) &&
(i + 1 < parts.Length))
{
var rate = parts[i + 1];
float val;
if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
{
framerate = val;
}
}
else if (state.RunTimeTicks.HasValue &&
part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
{
var time = part.Split(new[] { '=' }, 2).Last();
TimeSpan val;
if (TimeSpan.TryParse(time, UsCulture, out val))
{
var currentMs = startMs + val.TotalMilliseconds;
var percentVal = currentMs / totalMs;
percent = 100 * percentVal;
}
}
}
if (framerate.HasValue || percent.HasValue)
{
ApiEntryPoint.Instance.ReportTranscodingProgress(state, framerate, percent);
}
}
private int? GetVideoBitrateParamValue(VideoStreamRequest request, MediaStream videoStream)
{
var bitrate = request.VideoBitRate;
@ -1500,23 +1581,27 @@ namespace MediaBrowser.Api.Playback
state.OutputAudioBitrate = GetAudioBitrateParam(state.Request, state.AudioStream);
state.OutputAudioSampleRate = request.AudioSampleRate;
state.OutputAudioChannels = GetNumAudioChannelsParam(state.Request, state.AudioStream);
state.OutputAudioCodec = GetAudioCodec(state.Request);
if (videoRequest != null)
{
state.OutputVideoCodec = GetVideoCodec(videoRequest);
state.OutputVideoBitrate = GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream);
if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
{
videoRequest.VideoCodec = "copy";
state.OutputVideoCodec = "copy";
}
if (state.AudioStream != null && CanStreamCopyAudio(request, state.AudioStream, state.SupportedAudioCodecs))
{
request.AudioCodec = "copy";
state.OutputAudioCodec = "copy";
}
}
state.OutputFilePath = GetOutputFilePath(state);
return state;
}
@ -1729,14 +1814,14 @@ namespace MediaBrowser.Api.Playback
return;
}
var audioCodec = state.Request.AudioCodec;
var audioCodec = state.OutputAudioCodec;
if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.AudioStream != null)
{
audioCodec = state.AudioStream.Codec;
}
var videoCodec = state.VideoRequest == null ? null : state.VideoRequest.VideoCodec;
var videoCodec = state.OutputVideoCodec;
if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.VideoStream != null)
{
@ -1807,7 +1892,12 @@ namespace MediaBrowser.Api.Playback
profile = DlnaManager.GetDefaultProfile();
}
var audioCodec = state.Request.AudioCodec;
var audioCodec = state.OutputAudioCodec;
if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.AudioStream != null)
{
audioCodec = state.AudioStream.Codec;
}
if (state.VideoRequest == null)
{
@ -1825,12 +1915,7 @@ namespace MediaBrowser.Api.Playback
}
else
{
if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.AudioStream != null)
{
audioCodec = state.AudioStream.Codec;
}
var videoCodec = state.VideoRequest == null ? null : state.VideoRequest.VideoCodec;
var videoCodec = state.OutputVideoCodec;
if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.VideoStream != null)
{

View File

@ -86,7 +86,7 @@ namespace MediaBrowser.Api.Playback.Hls
var state = GetState(request, cancellationTokenSource.Token).Result;
var playlist = GetOutputFilePath(state);
var playlist = state.OutputFilePath;
if (File.Exists(playlist))
{
@ -307,7 +307,7 @@ namespace MediaBrowser.Api.Playback.Hls
if (hlsVideoRequest != null)
{
if (hlsVideoRequest.AppendBaselineStream && state.IsInputVideo)
if (hlsVideoRequest.AppendBaselineStream)
{
var lowBitratePath = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + "-low.m3u8");

View File

@ -89,7 +89,7 @@ namespace MediaBrowser.Api.Playback.Hls
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
var playlistPath = Path.ChangeExtension(GetOutputFilePath(state), ".m3u8");
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
var path = GetSegmentPath(playlistPath, index);
@ -231,7 +231,7 @@ namespace MediaBrowser.Api.Playback.Hls
protected override string GetAudioArguments(StreamState state)
{
var codec = GetAudioCodec(state.Request);
var codec = state.OutputAudioCodec;
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
{
@ -266,7 +266,7 @@ namespace MediaBrowser.Api.Playback.Hls
protected override string GetVideoArguments(StreamState state, bool performSubtitleConversion)
{
var codec = GetVideoCodec(state.VideoRequest);
var codec = state.OutputVideoCodec;
// See if we can save come cpu cycles by avoiding encoding
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))

View File

@ -118,7 +118,7 @@ namespace MediaBrowser.Api.Playback.Hls
/// <returns>System.String.</returns>
protected override string GetAudioArguments(StreamState state)
{
var codec = GetAudioCodec(state.Request);
var codec = state.OutputAudioCodec;
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
{
@ -160,7 +160,7 @@ namespace MediaBrowser.Api.Playback.Hls
protected override string GetVideoArguments(StreamState state,
bool performSubtitleConversion)
{
var codec = GetVideoCodec(state.VideoRequest);
var codec = state.OutputVideoCodec;
// See if we can save come cpu cycles by avoiding encoding
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))

View File

@ -78,8 +78,6 @@ namespace MediaBrowser.Api.Playback.Progressive
/// <exception cref="System.InvalidOperationException">Only aac and mp3 audio codecs are supported.</exception>
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions)
{
var request = state.Request;
var audioTranscodeParams = new List<string>();
var bitrate = state.OutputAudioBitrate;

View File

@ -45,53 +45,51 @@ namespace MediaBrowser.Api.Playback.Progressive
return ext;
}
var videoRequest = state.Request as VideoStreamRequest;
var isVideoRequest = state.VideoRequest != null;
// Try to infer based on the desired video codec
if (videoRequest != null && !string.IsNullOrEmpty(videoRequest.VideoCodec))
if (isVideoRequest)
{
if (state.IsInputVideo)
{
if (string.Equals(videoRequest.VideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
var videoCodec = state.VideoRequest.VideoCodec;
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
return ".ts";
}
if (string.Equals(videoRequest.VideoCodec, "theora", StringComparison.OrdinalIgnoreCase))
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
{
return ".ogv";
}
if (string.Equals(videoRequest.VideoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
{
return ".webm";
}
if (string.Equals(videoRequest.VideoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
{
return ".asf";
}
}
}
// Try to infer based on the desired audio codec
if (!string.IsNullOrEmpty(state.Request.AudioCodec))
if (!isVideoRequest)
{
if (!state.IsInputVideo)
var audioCodec = state.Request.AudioCodec;
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals("aac", state.Request.AudioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".aac";
}
if (string.Equals("mp3", state.Request.AudioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".mp3";
}
if (string.Equals("vorbis", state.Request.AudioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".ogg";
}
if (string.Equals("wma", state.Request.AudioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".wma";
}
return ".aac";
}
if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".mp3";
}
if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".ogg";
}
if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
{
return ".wma";
}
}
@ -134,7 +132,7 @@ namespace MediaBrowser.Api.Playback.Progressive
}
}
var outputPath = GetOutputFilePath(state);
var outputPath = state.OutputFilePath;
var outputPathExists = File.Exists(outputPath);
var isStatic = request.Static ||
@ -243,7 +241,7 @@ namespace MediaBrowser.Api.Playback.Progressive
private async Task<object> GetStreamResult(StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest)
{
// Use the command line args with a dummy playlist path
var outputPath = GetOutputFilePath(state);
var outputPath = state.OutputFilePath;
responseHeaders["Accept-Ranges"] = "none";

View File

@ -95,7 +95,7 @@ namespace MediaBrowser.Api.Playback.Progressive
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions)
{
// Get the output codec name
var videoCodec = GetVideoCodec(state.VideoRequest);
var videoCodec = state.OutputVideoCodec;
var format = string.Empty;
var keyFrame = string.Empty;
@ -190,10 +190,8 @@ namespace MediaBrowser.Api.Playback.Progressive
return string.Empty;
}
var request = state.Request;
// Get the output codec name
var codec = GetAudioCodec(request);
var codec = state.OutputAudioCodec;
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
{

View File

@ -163,6 +163,9 @@ namespace MediaBrowser.Api.Playback
}
}
public string OutputFilePath { get; set; }
public string OutputVideoCodec { get; set; }
public string OutputAudioCodec { get; set; }
public int? OutputAudioChannels;
public int? OutputAudioSampleRate;
public int? OutputAudioBitrate;

View File

@ -226,5 +226,18 @@ namespace MediaBrowser.Controller.Session
/// <param name="sessionId">The session identifier.</param>
/// <param name="capabilities">The capabilities.</param>
void ReportCapabilities(string sessionId, SessionCapabilities capabilities);
/// <summary>
/// Reports the transcoding information.
/// </summary>
/// <param name="deviceId">The device identifier.</param>
/// <param name="info">The information.</param>
void ReportTranscodingInfo(string deviceId, TranscodingInfo info);
/// <summary>
/// Clears the transcoding information.
/// </summary>
/// <param name="deviceId">The device identifier.</param>
void ClearTranscodingInfo(string deviceId);
}
}

View File

@ -122,6 +122,8 @@ namespace MediaBrowser.Controller.Session
/// <value>The supported commands.</value>
public List<string> SupportedCommands { get; set; }
public TranscodingInfo TranscodingInfo { get; set; }
/// <summary>
/// Gets a value indicating whether this instance is active.
/// </summary>

View File

@ -56,4 +56,15 @@
/// <value>The play method.</value>
public PlayMethod? PlayMethod { get; set; }
}
public class TranscodingInfo
{
public string AudioCodec { get; set; }
public string VideoCodec { get; set; }
public string Container { get; set; }
public int? Bitrate { get; set; }
public float? Framerate { get; set; }
public double? CompletionPercentage { get; set; }
}
}

View File

@ -137,6 +137,8 @@ namespace MediaBrowser.Model.Session
public PlayerStateInfo PlayState { get; set; }
public TranscodingInfo TranscodingInfo { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public SessionInfoDto()

View File

@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Folders
public bool Supports(IHasImages item)
{
return item is ICollectionFolder;
return item is CollectionFolder;
}
public int Order

View File

@ -347,6 +347,11 @@ namespace MediaBrowser.Server.Implementations.Session
{
session.NowPlayingItem = null;
session.PlayState = new PlayerStateInfo();
if (!string.IsNullOrEmpty(session.DeviceId))
{
ClearTranscodingInfo(session.DeviceId);
}
}
private string GetSessionKey(string clientType, string appVersion, string deviceId)
@ -459,6 +464,11 @@ namespace MediaBrowser.Server.Implementations.Session
UpdateNowPlayingItem(session, info, libraryItem);
if (!string.IsNullOrEmpty(session.DeviceId))
{
ClearTranscodingInfo(session.DeviceId);
}
session.QueueableMediaTypes = info.QueueableMediaTypes;
var users = GetUsers(session);
@ -1264,7 +1274,8 @@ namespace MediaBrowser.Server.Implementations.Session
UserName = session.UserName,
NowPlayingItem = session.NowPlayingItem,
SupportsRemoteControl = session.SupportsMediaControl,
PlayState = session.PlayState
PlayState = session.PlayState,
TranscodingInfo = session.TranscodingInfo
};
if (session.UserId.HasValue)
@ -1490,5 +1501,20 @@ namespace MediaBrowser.Server.Implementations.Session
session.NowViewingItem = item;
}
public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
{
var session = Sessions.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId));
if (session != null)
{
session.TranscodingInfo = info;
}
}
public void ClearTranscodingInfo(string deviceId)
{
ReportTranscodingInfo(deviceId, null);
}
}
}