capture key frame info

This commit is contained in:
Luke Pulverenti 2015-04-10 15:08:09 -04:00
parent fbbab13b31
commit 2a681f205a
7 changed files with 341 additions and 54 deletions

View File

@ -1705,6 +1705,102 @@ namespace MediaBrowser.Api.Playback
{ {
state.OutputAudioCodec = "copy"; state.OutputAudioCodec = "copy";
} }
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
var segmentLength = GetSegmentLength(state);
if (segmentLength.HasValue)
{
state.SegmentLength = segmentLength.Value;
}
}
}
private int? GetSegmentLength(StreamState state)
{
var stream = state.VideoStream;
if (stream == null)
{
return null;
}
var frames = stream.KeyFrames;
if (frames == null || frames.Count < 2)
{
return null;
}
Logger.Debug("Found keyframes at {0}", string.Join(",", frames.ToArray()));
var intervals = new List<int>();
for (var i = 1; i < frames.Count; i++)
{
var start = frames[i - 1];
var end = frames[i];
intervals.Add(end - start);
}
Logger.Debug("Found keyframes intervals {0}", string.Join(",", intervals.ToArray()));
var results = new List<Tuple<int, int>>();
for (var i = 1; i <= 10; i++)
{
var idealMs = i*1000;
if (intervals.Max() < idealMs - 1000)
{
break;
}
var segments = PredictStreamCopySegments(intervals, idealMs);
var variance = segments.Select(s => Math.Abs(idealMs - s)).Sum();
results.Add(new Tuple<int, int>(i, variance));
}
if (results.Count == 0)
{
return null;
}
return results.OrderBy(i => i.Item2).ThenBy(i => i.Item1).Select(i => i.Item1).First();
}
private List<int> PredictStreamCopySegments(List<int> intervals, int idealMs)
{
var segments = new List<int>();
var currentLength = 0;
foreach (var interval in intervals)
{
if (currentLength == 0 || (currentLength + interval) <= idealMs)
{
currentLength += interval;
}
else
{
// The segment will either be above or below the ideal.
// Need to figure out which is preferable
var offset1 = Math.Abs(idealMs - currentLength);
var offset2 = Math.Abs(idealMs - (currentLength + interval));
if (offset1 <= offset2)
{
segments.Add(currentLength);
currentLength = interval;
}
else
{
currentLength += interval;
}
}
}
Logger.Debug("Predicted actual segment lengths for length {0}: {1}", idealMs, string.Join(",", segments.ToArray()));
return segments;
} }
private void AttachMediaSourceInfo(StreamState state, private void AttachMediaSourceInfo(StreamState state,

View File

@ -5,7 +5,6 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using ServiceStack; using ServiceStack;

View File

@ -15,6 +15,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public IIsoMount MountedIso { get; set; } public IIsoMount MountedIso { get; set; }
public VideoType VideoType { get; set; } public VideoType VideoType { get; set; }
public List<string> PlayableStreamFileNames { get; set; } public List<string> PlayableStreamFileNames { get; set; }
public bool ExtractKeyFrameInterval { get; set; }
public MediaInfoRequest() public MediaInfoRequest()
{ {

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -14,6 +13,7 @@ using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -75,7 +75,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
protected readonly Func<ISubtitleEncoder> SubtitleEncoder; protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
protected readonly Func<IMediaSourceManager> MediaSourceManager; protected readonly Func<IMediaSourceManager> MediaSourceManager;
private List<Process> _runningProcesses = new List<Process>(); private readonly List<Process> _runningProcesses = new List<Process>();
public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager) public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager)
{ {
@ -116,7 +116,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
var inputFiles = MediaEncoderHelpers.GetInputArgument(request.InputPath, request.Protocol, request.MountedIso, request.PlayableStreamFileNames); var inputFiles = MediaEncoderHelpers.GetInputArgument(request.InputPath, request.Protocol, request.MountedIso, request.PlayableStreamFileNames);
return GetMediaInfoInternal(GetInputArgument(inputFiles, request.Protocol), request.InputPath, request.Protocol, extractChapters, var extractKeyFrameInterval = request.ExtractKeyFrameInterval && request.Protocol == MediaProtocol.File && request.VideoType == VideoType.VideoFile;
return GetMediaInfoInternal(GetInputArgument(inputFiles, request.Protocol), request.InputPath, request.Protocol, extractChapters, extractKeyFrameInterval,
GetProbeSizeArgument(inputFiles, request.Protocol), request.MediaType == DlnaProfileType.Audio, cancellationToken); GetProbeSizeArgument(inputFiles, request.Protocol), request.MediaType == DlnaProfileType.Audio, cancellationToken);
} }
@ -150,12 +152,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <param name="primaryPath">The primary path.</param> /// <param name="primaryPath">The primary path.</param>
/// <param name="protocol">The protocol.</param> /// <param name="protocol">The protocol.</param>
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param> /// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
/// <param name="extractKeyFrameInterval">if set to <c>true</c> [extract key frame interval].</param>
/// <param name="probeSizeArgument">The probe size argument.</param> /// <param name="probeSizeArgument">The probe size argument.</param>
/// <param name="isAudio">if set to <c>true</c> [is audio].</param> /// <param name="isAudio">if set to <c>true</c> [is audio].</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{MediaInfoResult}.</returns> /// <returns>Task{MediaInfoResult}.</returns>
/// <exception cref="System.ApplicationException"></exception> /// <exception cref="System.ApplicationException"></exception>
private async Task<Model.MediaInfo.MediaInfo> GetMediaInfoInternal(string inputPath, string primaryPath, MediaProtocol protocol, bool extractChapters, private async Task<Model.MediaInfo.MediaInfo> GetMediaInfoInternal(string inputPath,
string primaryPath,
MediaProtocol protocol,
bool extractChapters,
bool extractKeyFrameInterval,
string probeSizeArgument, string probeSizeArgument,
bool isAudio, bool isAudio,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@ -174,6 +181,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Must consume both or ffmpeg may hang due to deadlocks. See comments below. // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
RedirectStandardInput = true,
FileName = FFProbePath, FileName = FFProbePath,
Arguments = string.Format(args, Arguments = string.Format(args,
probeSizeArgument, inputPath).Trim(), probeSizeArgument, inputPath).Trim(),
@ -187,12 +195,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
process.Exited += ProcessExited;
await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
InternalMediaInfoResult result;
try try
{ {
StartProcess(process); StartProcess(process);
@ -210,34 +214,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
process.BeginErrorReadLine(); process.BeginErrorReadLine();
result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream); var result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
}
catch
{
// Hate having to do this
try
{
process.Kill();
}
catch (Exception ex1)
{
_logger.ErrorException("Error killing ffprobe", ex1);
}
throw; if (result != null)
}
finally
{ {
_ffProbeResourcePool.Release();
}
if (result == null)
{
throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
}
cancellationToken.ThrowIfCancellationRequested();
if (result.streams != null) if (result.streams != null)
{ {
// Normalize aspect ratio if invalid // Normalize aspect ratio if invalid
@ -254,7 +234,145 @@ namespace MediaBrowser.MediaEncoding.Encoder
} }
} }
return new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, isAudio, primaryPath, protocol); var mediaInfo = new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, isAudio, primaryPath, protocol);
if (extractKeyFrameInterval && mediaInfo.RunTimeTicks.HasValue)
{
foreach (var stream in mediaInfo.MediaStreams.Where(i => i.Type == MediaStreamType.Video)
.ToList())
{
try
{
stream.KeyFrames = await GetKeyFrames(inputPath, stream.Index, cancellationToken)
.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logger.ErrorException("Error getting key frame interval", ex);
}
}
}
return mediaInfo;
}
}
catch
{
StopProcess(process, 100, true);
throw;
}
finally
{
_ffProbeResourcePool.Release();
}
throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
}
private async Task<List<int>> GetKeyFrames(string inputPath, int videoStreamIndex, CancellationToken cancellationToken)
{
const string args = "-i {0} -select_streams v:{1} -show_frames -print_format compact";
var process = new Process
{
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = FFProbePath,
Arguments = string.Format(args, inputPath, videoStreamIndex.ToString(CultureInfo.InvariantCulture)).Trim(),
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
EnableRaisingEvents = true
};
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
StartProcess(process);
var lines = new List<int>();
var outputCancellationSource = new CancellationTokenSource(4000);
try
{
process.BeginErrorReadLine();
var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(outputCancellationSource.Token, cancellationToken);
await StartReadingOutput(process.StandardOutput.BaseStream, lines, 120000, outputCancellationSource, linkedCancellationTokenSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
if (cancellationToken.IsCancellationRequested)
{
throw;
}
}
finally
{
StopProcess(process, 100, true);
}
return lines;
}
private async Task StartReadingOutput(Stream source, List<int> lines, int timeoutMs, CancellationTokenSource cancellationTokenSource, CancellationToken cancellationToken)
{
try
{
using (var reader = new StreamReader(source))
{
while (!reader.EndOfStream)
{
cancellationToken.ThrowIfCancellationRequested();
var line = await reader.ReadLineAsync().ConfigureAwait(false);
var values = (line ?? string.Empty).Split('|')
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Split('='))
.Where(i => i.Length == 2)
.ToDictionary(i => i[0], i => i[1]);
string pktDts;
int frameMs;
if (values.TryGetValue("pkt_dts", out pktDts) && int.TryParse(pktDts, NumberStyles.Any, CultureInfo.InvariantCulture, out frameMs))
{
string keyFrame;
if (values.TryGetValue("key_frame", out keyFrame) && string.Equals(keyFrame, "1", StringComparison.OrdinalIgnoreCase))
{
lines.Add(frameMs);
}
if (frameMs > timeoutMs)
{
cancellationTokenSource.Cancel();
}
}
}
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.ErrorException("Error reading ffprobe output", ex);
}
} }
/// <summary> /// <summary>
@ -269,7 +387,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
private void ProcessExited(object sender, EventArgs e) private void ProcessExited(object sender, EventArgs e)
{ {
((Process)sender).Dispose(); var process = (Process) sender;
lock (_runningProcesses)
{
_runningProcesses.Remove(process);
}
process.Dispose();
} }
public Task<Stream> ExtractAudioImage(string path, CancellationToken cancellationToken) public Task<Stream> ExtractAudioImage(string path, CancellationToken cancellationToken)
@ -574,6 +699,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private void StartProcess(Process process) private void StartProcess(Process process)
{ {
process.Exited += ProcessExited;
process.Start(); process.Start();
lock (_runningProcesses) lock (_runningProcesses)
@ -587,27 +714,36 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
_logger.Info("Killing ffmpeg process"); _logger.Info("Killing ffmpeg process");
process.StandardInput.WriteLine("q"); try
if (!process.WaitForExit(1000))
{ {
process.StandardInput.WriteLine("q");
}
catch (Exception)
{
_logger.Error("Error sending q command to process");
}
try
{
if (process.WaitForExit(waitTimeMs))
{
return;
}
}
catch (Exception ex)
{
_logger.Error("Error in WaitForExit", ex);
}
if (enableForceKill) if (enableForceKill)
{ {
process.Kill(); process.Kill();
} }
} }
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.ErrorException("Error killing process", ex); _logger.ErrorException("Error killing process", ex);
} }
finally
{
lock (_runningProcesses)
{
_runningProcesses.Remove(process);
}
}
} }
private void StopProcesses() private void StopProcesses()

View File

@ -1,4 +1,5 @@
using MediaBrowser.Model.Dlna; using System.Collections.Generic;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Extensions;
using System.Diagnostics; using System.Diagnostics;
@ -58,6 +59,12 @@ namespace MediaBrowser.Model.Entities
/// <value>The length of the packet.</value> /// <value>The length of the packet.</value>
public int? PacketLength { get; set; } public int? PacketLength { get; set; }
/// <summary>
/// Gets or sets the key frames.
/// </summary>
/// <value>The key frames.</value>
public List<int> KeyFrames { get; set; }
/// <summary> /// <summary>
/// Gets or sets the channels. /// Gets or sets the channels.
/// </summary> /// </summary>

View File

@ -130,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo
return ItemUpdateType.MetadataImport; return ItemUpdateType.MetadataImport;
} }
private const string SchemaVersion = "2"; private const string SchemaVersion = "3";
private async Task<Model.MediaInfo.MediaInfo> GetMediaInfo(Video item, private async Task<Model.MediaInfo.MediaInfo> GetMediaInfo(Video item,
IIsoMount isoMount, IIsoMount isoMount,
@ -145,7 +145,7 @@ namespace MediaBrowser.Providers.MediaInfo
try try
{ {
return _json.DeserializeFromFile<Model.MediaInfo.MediaInfo>(cachePath); //return _json.DeserializeFromFile<Model.MediaInfo.MediaInfo>(cachePath);
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
@ -167,7 +167,8 @@ namespace MediaBrowser.Providers.MediaInfo
VideoType = item.VideoType, VideoType = item.VideoType,
MediaType = DlnaProfileType.Video, MediaType = DlnaProfileType.Video,
InputPath = item.Path, InputPath = item.Path,
Protocol = protocol Protocol = protocol,
ExtractKeyFrameInterval = true
}, cancellationToken).ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false);

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Persistence; using System.Globalization;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using System; using System;
@ -40,7 +41,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
// Add PixelFormat column // Add PixelFormat column
createTableCommand += "(ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, IsCabac BIT NULL, PRIMARY KEY (ItemId, StreamIndex))"; createTableCommand += "(ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, IsCabac BIT NULL, KeyFrames TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))";
string[] queries = { string[] queries = {
@ -61,6 +62,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
AddIsAnamorphicColumn(); AddIsAnamorphicColumn();
AddIsCabacColumn(); AddIsCabacColumn();
AddRefFramesCommand(); AddRefFramesCommand();
AddKeyFramesCommand();
PrepareStatements(); PrepareStatements();
@ -160,6 +162,37 @@ namespace MediaBrowser.Server.Implementations.Persistence
_connection.RunQueries(new[] { builder.ToString() }, _logger); _connection.RunQueries(new[] { builder.ToString() }, _logger);
} }
private void AddKeyFramesCommand()
{
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = "PRAGMA table_info(mediastreams)";
using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
{
while (reader.Read())
{
if (!reader.IsDBNull(1))
{
var name = reader.GetString(1);
if (string.Equals(name, "KeyFrames", StringComparison.OrdinalIgnoreCase))
{
return;
}
}
}
}
}
var builder = new StringBuilder();
builder.AppendLine("alter table mediastreams");
builder.AppendLine("add column KeyFrames TEXT NULL");
_connection.RunQueries(new[] { builder.ToString() }, _logger);
}
private void AddIsCabacColumn() private void AddIsCabacColumn()
{ {
using (var cmd = _connection.CreateCommand()) using (var cmd = _connection.CreateCommand())
@ -249,6 +282,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
"BitDepth", "BitDepth",
"IsAnamorphic", "IsAnamorphic",
"RefFrames", "RefFrames",
"KeyFrames",
"IsCabac" "IsCabac"
}; };
@ -430,7 +464,12 @@ namespace MediaBrowser.Server.Implementations.Persistence
if (!reader.IsDBNull(25)) if (!reader.IsDBNull(25))
{ {
item.IsCabac = reader.GetBoolean(25); item.KeyFrames = reader.GetString(25).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => int.Parse(i, CultureInfo.InvariantCulture)).ToList();
}
if (!reader.IsDBNull(26))
{
item.IsCabac = reader.GetBoolean(26);
} }
return item; return item;
@ -498,7 +537,15 @@ namespace MediaBrowser.Server.Implementations.Persistence
_saveStreamCommand.GetParameter(22).Value = stream.BitDepth; _saveStreamCommand.GetParameter(22).Value = stream.BitDepth;
_saveStreamCommand.GetParameter(23).Value = stream.IsAnamorphic; _saveStreamCommand.GetParameter(23).Value = stream.IsAnamorphic;
_saveStreamCommand.GetParameter(24).Value = stream.RefFrames; _saveStreamCommand.GetParameter(24).Value = stream.RefFrames;
_saveStreamCommand.GetParameter(25).Value = stream.IsCabac; if (stream.KeyFrames != null)
{
_saveStreamCommand.GetParameter(25).Value = string.Join(",", stream.KeyFrames.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray());
}
else
{
_saveStreamCommand.GetParameter(25).Value = null;
}
_saveStreamCommand.GetParameter(26).Value = stream.IsCabac;
_saveStreamCommand.Transaction = transaction; _saveStreamCommand.Transaction = transaction;
_saveStreamCommand.ExecuteNonQuery(); _saveStreamCommand.ExecuteNonQuery();