improve direct play of live streams

This commit is contained in:
Luke Pulverenti 2015-04-04 15:35:29 -04:00
parent 8c61abf6d2
commit 2b7a80cfb5
25 changed files with 1220 additions and 898 deletions

View File

@ -223,7 +223,7 @@ namespace MediaBrowser.Api.Playback
int? subtitleStreamIndex,
string playSessionId)
{
var streamBuilder = new StreamBuilder();
var streamBuilder = new StreamBuilder(Logger);
var options = new VideoOptions
{

View File

@ -211,7 +211,6 @@
<Compile Include="MediaEncoding\IEncodingManager.cs" />
<Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
<Compile Include="MediaEncoding\IMediaEncoder.cs" />
<Compile Include="MediaEncoding\InternalMediaInfoResult.cs" />
<Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
<Compile Include="MediaEncoding\MediaStreamSelector.cs" />
<Compile Include="Net\AuthenticatedAttribute.cs" />

View File

@ -63,16 +63,18 @@ namespace MediaBrowser.Controller.MediaEncoding
string filenamePrefix,
int? maxWidth,
CancellationToken cancellationToken);
/// <summary>
/// Gets the media info.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <param name="primaryPath">The primary path.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio, CancellationToken cancellationToken);
Task<MediaInfo> GetMediaInfo(string[] inputFiles, string primaryPath, MediaProtocol protocol, bool isAudio, bool extractChapters, CancellationToken cancellationToken);
/// <summary>
/// Gets the probe size argument.

View File

@ -1,9 +1,7 @@
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
@ -46,291 +44,5 @@ namespace MediaBrowser.Controller.MediaEncoding
.Where(f => !string.IsNullOrEmpty(f))
.ToList();
}
public static MediaInfo GetMediaInfo(InternalMediaInfoResult data)
{
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
var info = new MediaInfo
{
MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
.Where(i => i != null)
.ToList()
};
if (data.format != null)
{
info.Format = data.format.format_name;
if (!string.IsNullOrEmpty(data.format.bit_rate))
{
info.TotalBitrate = int.Parse(data.format.bit_rate, UsCulture);
}
}
return info;
}
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
/// <summary>
/// Converts ffprobe stream info to our MediaStream class
/// </summary>
/// <param name="streamInfo">The stream info.</param>
/// <param name="formatInfo">The format info.</param>
/// <returns>MediaStream.</returns>
private static MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
{
var stream = new MediaStream
{
Codec = streamInfo.codec_name,
Profile = streamInfo.profile,
Level = streamInfo.level,
Index = streamInfo.index,
PixelFormat = streamInfo.pix_fmt
};
if (streamInfo.tags != null)
{
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
}
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.Audio;
stream.Channels = streamInfo.channels;
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
{
stream.SampleRate = int.Parse(streamInfo.sample_rate, UsCulture);
}
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
}
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.Subtitle;
}
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
{
stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
? MediaStreamType.EmbeddedImage
: MediaStreamType.Video;
stream.Width = streamInfo.width;
stream.Height = streamInfo.height;
stream.AspectRatio = GetAspectRatio(streamInfo);
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
stream.BitDepth = GetBitDepth(stream.PixelFormat);
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
}
else
{
return null;
}
// Get stream bitrate
var bitrate = 0;
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
{
bitrate = int.Parse(streamInfo.bit_rate, UsCulture);
}
else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
{
// If the stream info doesn't have a bitrate get the value from the media format info
bitrate = int.Parse(formatInfo.bit_rate, UsCulture);
}
if (bitrate > 0)
{
stream.BitRate = bitrate;
}
if (streamInfo.disposition != null)
{
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
}
return stream;
}
private static int? GetBitDepth(string pixelFormat)
{
var eightBit = new List<string>
{
"yuv420p",
"yuv411p",
"yuvj420p",
"uyyvyy411",
"nv12",
"nv21",
"rgb444le",
"rgb444be",
"bgr444le",
"bgr444be",
"yuvj411p"
};
if (!string.IsNullOrEmpty(pixelFormat))
{
if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
{
return 8;
}
}
return null;
}
/// <summary>
/// Gets a string from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.String.</returns>
private static string GetDictionaryValue(Dictionary<string, string> tags, string key)
{
if (tags == null)
{
return null;
}
string val;
tags.TryGetValue(key, out val);
return val;
}
private static string ParseChannelLayout(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
return input.Split('(').FirstOrDefault();
}
private static string GetAspectRatio(MediaStreamInfo info)
{
var original = info.display_aspect_ratio;
int height;
int width;
var parts = (original ?? string.Empty).Split(':');
if (!(parts.Length == 2 &&
int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width) &&
int.TryParse(parts[1], NumberStyles.Any, UsCulture, out height) &&
width > 0 &&
height > 0))
{
width = info.width;
height = info.height;
}
if (width > 0 && height > 0)
{
double ratio = width;
ratio /= height;
if (IsClose(ratio, 1.777777778, .03))
{
return "16:9";
}
if (IsClose(ratio, 1.3333333333, .05))
{
return "4:3";
}
if (IsClose(ratio, 1.41))
{
return "1.41:1";
}
if (IsClose(ratio, 1.5))
{
return "1.5:1";
}
if (IsClose(ratio, 1.6))
{
return "1.6:1";
}
if (IsClose(ratio, 1.66666666667))
{
return "5:3";
}
if (IsClose(ratio, 1.85, .02))
{
return "1.85:1";
}
if (IsClose(ratio, 2.35, .025))
{
return "2.35:1";
}
if (IsClose(ratio, 2.4, .025))
{
return "2.40:1";
}
}
return original;
}
private static bool IsClose(double d1, double d2, double variance = .005)
{
return Math.Abs(d1 - d2) <= variance;
}
/// <summary>
/// Gets a frame rate from a string value in ffprobe output
/// This could be a number or in the format of 2997/125.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.Nullable{System.Single}.</returns>
private static float? GetFrameRate(string value)
{
if (!string.IsNullOrEmpty(value))
{
var parts = value.Split('/');
float result;
if (parts.Length == 2)
{
result = float.Parse(parts[0], UsCulture) / float.Parse(parts[1], UsCulture);
}
else
{
result = float.Parse(parts[0], UsCulture);
}
return float.IsNaN(result) ? (float?)null : result;
}
return null;
}
}
}

View File

@ -12,6 +12,7 @@ using MediaBrowser.Dlna.ContentDirectory;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Net;
using System;
using System.Globalization;
@ -126,7 +127,7 @@ namespace MediaBrowser.Dlna.Didl
{
var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
streamInfo = new StreamBuilder(new NullLogger()).BuildVideoItem(new VideoOptions
{
ItemId = GetClientId(video),
MediaSources = sources,
@ -353,7 +354,7 @@ namespace MediaBrowser.Dlna.Didl
{
var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
streamInfo = new StreamBuilder(new NullLogger()).BuildAudioItem(new AudioOptions
{
ItemId = GetClientId(audio),
MediaSources = sources,

View File

@ -542,7 +542,7 @@ namespace MediaBrowser.Dlna.PlayTo
{
return new PlaylistItem
{
StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
StreamInfo = new StreamBuilder(_logger).BuildVideoItem(new VideoOptions
{
ItemId = item.Id.ToString("N"),
MediaSources = mediaSources,
@ -562,7 +562,7 @@ namespace MediaBrowser.Dlna.PlayTo
{
return new PlaylistItem
{
StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
StreamInfo = new StreamBuilder(_logger).BuildAudioItem(new AudioOptions
{
ItemId = item.Id.ToString("N"),
MediaSources = mediaSources,

View File

@ -5,6 +5,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
@ -103,15 +104,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// Gets the media info.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <param name="primaryPath">The primary path.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio,
CancellationToken cancellationToken)
public Task<Model.Entities.MediaInfo> GetMediaInfo(string[] inputFiles, string primaryPath, MediaProtocol protocol, bool isAudio,
bool extractChapters, CancellationToken cancellationToken)
{
return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), !isAudio,
GetProbeSizeArgument(inputFiles, protocol), cancellationToken);
return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), primaryPath, protocol, !isAudio && extractChapters,
GetProbeSizeArgument(inputFiles, protocol), isAudio, cancellationToken);
}
/// <summary>
@ -141,13 +144,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// Gets the media info internal.
/// </summary>
/// <param name="inputPath">The input path.</param>
/// <param name="primaryPath">The primary path.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
/// <param name="probeSizeArgument">The probe size argument.</param>
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{MediaInfoResult}.</returns>
/// <exception cref="System.ApplicationException"></exception>
private async Task<InternalMediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters,
private async Task<Model.Entities.MediaInfo> GetMediaInfoInternal(string inputPath, string primaryPath, MediaProtocol protocol, bool extractChapters,
string probeSizeArgument,
bool isAudio,
CancellationToken cancellationToken)
{
var args = extractChapters
@ -244,7 +251,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
return result;
return new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, isAudio, primaryPath, protocol);
}
/// <summary>

View File

@ -68,6 +68,9 @@
<Compile Include="Encoder\JobLogger.cs" />
<Compile Include="Encoder\MediaEncoder.cs" />
<Compile Include="Encoder\VideoEncoder.cs" />
<Compile Include="Probing\FFProbeHelpers.cs" />
<Compile Include="Probing\InternalMediaInfoResult.cs" />
<Compile Include="Probing\ProbeResultNormalizer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Subtitles\ISubtitleParser.cs" />
<Compile Include="Subtitles\ISubtitleWriter.cs" />
@ -91,6 +94,10 @@
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
<Name>MediaBrowser.Controller</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.MediaInfo\MediaBrowser.MediaInfo.csproj">
<Project>{6e4145e4-c6d4-4e4d-94f2-87188db6e239}</Project>
<Name>MediaBrowser.MediaInfo</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
<Name>MediaBrowser.Model</Name>

View File

@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
namespace MediaBrowser.Providers.MediaInfo
namespace MediaBrowser.MediaEncoding.Probing
{
public static class FFProbeHelpers
{

View File

@ -1,6 +1,6 @@
using System.Collections.Generic;
namespace MediaBrowser.Controller.MediaEncoding
namespace MediaBrowser.MediaEncoding.Probing
{
/// <summary>
/// Class MediaInfoResult
@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <value>The channel_layout.</value>
public string channel_layout { get; set; }
/// <summary>
/// Gets or sets the avg_frame_rate.
/// </summary>
@ -317,7 +317,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <value>The probe_score.</value>
public int probe_score { get; set; }
/// <summary>
/// Gets or sets the tags.
/// </summary>

View File

@ -0,0 +1,882 @@
using MediaBrowser.Common.IO;
using MediaBrowser.MediaInfo;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Probing
{
public class ProbeResultNormalizer
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem)
{
_logger = logger;
_fileSystem = fileSystem;
}
public Model.Entities.MediaInfo GetMediaInfo(InternalMediaInfoResult data, bool isAudio, string path, MediaProtocol protocol)
{
var info = new Model.Entities.MediaInfo
{
Path = path,
Protocol = protocol
};
FFProbeHelpers.NormalizeFFProbeResult(data);
SetSize(data, info);
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
info.MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
.Where(i => i != null)
.ToList();
if (data.format != null)
{
info.Container = data.format.format_name;
if (!string.IsNullOrEmpty(data.format.bit_rate))
{
info.Bitrate = int.Parse(data.format.bit_rate, _usCulture);
}
}
if (isAudio)
{
SetAudioRuntimeTicks(data, info);
if (data.format != null && data.format.tags != null)
{
SetAudioInfoFromTags(info, data.format.tags);
}
}
else
{
FetchWtvInfo(info, data);
if (data.Chapters != null)
{
info.Chapters = data.Chapters.Select(GetChapterInfo).ToList();
}
ExtractTimestamp(info);
var videoStream = info.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (videoStream != null)
{
UpdateFromMediaInfo(info, videoStream);
}
}
return info;
}
/// <summary>
/// Converts ffprobe stream info to our MediaStream class
/// </summary>
/// <param name="streamInfo">The stream info.</param>
/// <param name="formatInfo">The format info.</param>
/// <returns>MediaStream.</returns>
private MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
{
var stream = new MediaStream
{
Codec = streamInfo.codec_name,
Profile = streamInfo.profile,
Level = streamInfo.level,
Index = streamInfo.index,
PixelFormat = streamInfo.pix_fmt
};
if (streamInfo.tags != null)
{
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
}
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.Audio;
stream.Channels = streamInfo.channels;
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
{
stream.SampleRate = int.Parse(streamInfo.sample_rate, _usCulture);
}
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
}
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.Subtitle;
}
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
{
stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
? MediaStreamType.EmbeddedImage
: MediaStreamType.Video;
stream.Width = streamInfo.width;
stream.Height = streamInfo.height;
stream.AspectRatio = GetAspectRatio(streamInfo);
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
stream.BitDepth = GetBitDepth(stream.PixelFormat);
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
}
else
{
return null;
}
// Get stream bitrate
var bitrate = 0;
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
{
bitrate = int.Parse(streamInfo.bit_rate, _usCulture);
}
else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
{
// If the stream info doesn't have a bitrate get the value from the media format info
bitrate = int.Parse(formatInfo.bit_rate, _usCulture);
}
if (bitrate > 0)
{
stream.BitRate = bitrate;
}
if (streamInfo.disposition != null)
{
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
}
return stream;
}
private int? GetBitDepth(string pixelFormat)
{
var eightBit = new List<string>
{
"yuv420p",
"yuv411p",
"yuvj420p",
"uyyvyy411",
"nv12",
"nv21",
"rgb444le",
"rgb444be",
"bgr444le",
"bgr444be",
"yuvj411p"
};
if (!string.IsNullOrEmpty(pixelFormat))
{
if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
{
return 8;
}
}
return null;
}
/// <summary>
/// Gets a string from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.String.</returns>
private string GetDictionaryValue(Dictionary<string, string> tags, string key)
{
if (tags == null)
{
return null;
}
string val;
tags.TryGetValue(key, out val);
return val;
}
private string ParseChannelLayout(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
return input.Split('(').FirstOrDefault();
}
private string GetAspectRatio(MediaStreamInfo info)
{
var original = info.display_aspect_ratio;
int height;
int width;
var parts = (original ?? string.Empty).Split(':');
if (!(parts.Length == 2 &&
int.TryParse(parts[0], NumberStyles.Any, _usCulture, out width) &&
int.TryParse(parts[1], NumberStyles.Any, _usCulture, out height) &&
width > 0 &&
height > 0))
{
width = info.width;
height = info.height;
}
if (width > 0 && height > 0)
{
double ratio = width;
ratio /= height;
if (IsClose(ratio, 1.777777778, .03))
{
return "16:9";
}
if (IsClose(ratio, 1.3333333333, .05))
{
return "4:3";
}
if (IsClose(ratio, 1.41))
{
return "1.41:1";
}
if (IsClose(ratio, 1.5))
{
return "1.5:1";
}
if (IsClose(ratio, 1.6))
{
return "1.6:1";
}
if (IsClose(ratio, 1.66666666667))
{
return "5:3";
}
if (IsClose(ratio, 1.85, .02))
{
return "1.85:1";
}
if (IsClose(ratio, 2.35, .025))
{
return "2.35:1";
}
if (IsClose(ratio, 2.4, .025))
{
return "2.40:1";
}
}
return original;
}
private bool IsClose(double d1, double d2, double variance = .005)
{
return Math.Abs(d1 - d2) <= variance;
}
/// <summary>
/// Gets a frame rate from a string value in ffprobe output
/// This could be a number or in the format of 2997/125.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.Nullable{System.Single}.</returns>
private float? GetFrameRate(string value)
{
if (!string.IsNullOrEmpty(value))
{
var parts = value.Split('/');
float result;
if (parts.Length == 2)
{
result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture);
}
else
{
result = float.Parse(parts[0], _usCulture);
}
return float.IsNaN(result) ? (float?)null : result;
}
return null;
}
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, Model.Entities.MediaInfo data)
{
if (result.streams != null)
{
// Get the first audio stream
var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
if (stream != null)
{
// Get duration from stream properties
var duration = stream.duration;
// If it's not there go into format properties
if (string.IsNullOrEmpty(duration))
{
duration = result.format.duration;
}
// If we got something, parse it
if (!string.IsNullOrEmpty(duration))
{
data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
}
}
}
}
private void SetSize(InternalMediaInfoResult data, Model.Entities.MediaInfo info)
{
if (data.format != null)
{
if (!string.IsNullOrEmpty(data.format.size))
{
info.Size = long.Parse(data.format.size, _usCulture);
}
else
{
info.Size = null;
}
}
}
private void SetAudioInfoFromTags(Model.Entities.MediaInfo audio, Dictionary<string, string> tags)
{
var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
// Only set Name if title was found in the dictionary
if (!string.IsNullOrEmpty(title))
{
audio.Title = title;
}
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
if (!string.IsNullOrWhiteSpace(composer))
{
foreach (var person in Split(composer, false))
{
audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
}
}
audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
if (!string.IsNullOrWhiteSpace(artists))
{
audio.Artists = artists.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
else
{
var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
if (string.IsNullOrWhiteSpace(artist))
{
audio.Artists.Clear();
}
else
{
audio.Artists = SplitArtists(artist)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
}
var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
if (string.IsNullOrWhiteSpace(albumArtist))
{
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist");
}
if (string.IsNullOrWhiteSpace(albumArtist))
{
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
}
if (string.IsNullOrWhiteSpace(albumArtist))
{
audio.AlbumArtists = new List<string>();
}
else
{
audio.AlbumArtists = SplitArtists(albumArtist)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
// Track number
audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
// Disc number
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
// Several different forms of retaildate
audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
// If we don't have a ProductionYear try and get it from PremiereDate
if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
{
audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
}
FetchGenres(audio, tags);
// There's several values in tags may or may not be present
FetchStudios(audio, tags, "organization");
FetchStudios(audio, tags, "ensemble");
FetchStudios(audio, tags, "publisher");
// These support mulitple values, but for now we only store the first.
audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")));
audio.SetProviderId(MetadataProviders.MusicBrainzArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")));
audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")));
audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")));
audio.SetProviderId(MetadataProviders.MusicBrainzTrack, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")));
}
private string GetMultipleMusicBrainzId(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
}
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
/// <summary>
/// Splits the specified val.
/// </summary>
/// <param name="val">The val.</param>
/// <param name="allowCommaDelimiter">if set to <c>true</c> [allow comma delimiter].</param>
/// <returns>System.String[][].</returns>
private IEnumerable<string> Split(string val, bool allowCommaDelimiter)
{
// Only use the comma as a delimeter if there are no slashes or pipes.
// We want to be careful not to split names that have commas in them
var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i) != -1) ?
_nameDelimiters :
new[] { ',' };
return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Trim());
}
private const string ArtistReplaceValue = " | ";
private IEnumerable<string> SplitArtists(string val)
{
val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase)
.Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase);
var artistsFound = new List<string>();
foreach (var whitelistArtist in GetSplitWhitelist())
{
var originalVal = val;
val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase);
if (!string.Equals(originalVal, val, StringComparison.OrdinalIgnoreCase))
{
artistsFound.Add(whitelistArtist);
}
}
// Only use the comma as a delimeter if there are no slashes or pipes.
// We want to be careful not to split names that have commas in them
var delimeter = _nameDelimiters;
var artists = val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Trim());
artistsFound.AddRange(artists);
return artistsFound;
}
private List<string> _splitWhiteList = null;
private IEnumerable<string> GetSplitWhitelist()
{
if (_splitWhiteList == null)
{
var file = GetType().Namespace + ".whitelist.txt";
using (var stream = GetType().Assembly.GetManifestResourceStream(file))
{
using (var reader = new StreamReader(stream))
{
var list = new List<string>();
while (!reader.EndOfStream)
{
var val = reader.ReadLine();
if (!string.IsNullOrWhiteSpace(val))
{
list.Add(val);
}
}
_splitWhiteList = list;
}
}
}
return _splitWhiteList;
}
/// <summary>
/// Gets the studios from the tags collection
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="tags">The tags.</param>
/// <param name="tagName">Name of the tag.</param>
private void FetchStudios(Model.Entities.MediaInfo audio, Dictionary<string, string> tags, string tagName)
{
var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
if (!string.IsNullOrEmpty(val))
{
var studios = Split(val, true);
foreach (var studio in studios)
{
// Sometimes the artist name is listed here, account for that
if (audio.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase))
{
continue;
}
if (audio.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase))
{
continue;
}
audio.Studios.Add(studio);
}
audio.Studios = audio.Studios
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
}
/// <summary>
/// Gets the genres from the tags collection
/// </summary>
/// <param name="info">The information.</param>
/// <param name="tags">The tags.</param>
private void FetchGenres(Model.Entities.MediaInfo info, Dictionary<string, string> tags)
{
var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
if (!string.IsNullOrEmpty(val))
{
foreach (var genre in Split(val, true))
{
info.Genres.Add(genre);
}
info.Genres = info.Genres
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
}
/// <summary>
/// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="tagName">Name of the tag.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
{
var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
if (!string.IsNullOrEmpty(disc))
{
disc = disc.Split('/')[0];
int num;
if (int.TryParse(disc, out num))
{
return num;
}
}
return null;
}
private ChapterInfo GetChapterInfo(MediaChapter chapter)
{
var info = new ChapterInfo();
if (chapter.tags != null)
{
string name;
if (chapter.tags.TryGetValue("title", out name))
{
info.Name = name;
}
}
// Limit accuracy to milliseconds to match xml saving
var secondsString = chapter.start_time;
double seconds;
if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out seconds))
{
var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds);
info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks;
}
return info;
}
private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
private void FetchWtvInfo(Model.Entities.MediaInfo video, InternalMediaInfoResult data)
{
if (data.format == null || data.format.tags == null)
{
return;
}
var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre");
if (!string.IsNullOrWhiteSpace(genres))
{
//genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
}
if (!string.IsNullOrWhiteSpace(genres))
{
video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Trim())
.ToList();
}
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
if (!string.IsNullOrWhiteSpace(officialRating))
{
video.OfficialRating = officialRating;
}
var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
if (!string.IsNullOrEmpty(people))
{
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonType.Actor })
.ToList();
}
var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
if (!string.IsNullOrWhiteSpace(year))
{
int val;
if (int.TryParse(year, NumberStyles.Integer, _usCulture, out val))
{
video.ProductionYear = val;
}
}
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime");
if (!string.IsNullOrWhiteSpace(premiereDateString))
{
DateTime val;
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
if (DateTime.TryParse(year, null, DateTimeStyles.None, out val))
{
video.PremiereDate = val.ToUniversalTime();
}
}
var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle");
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// Sometimes for TV Shows the Subtitle field is empty and the subtitle description contains the subtitle, extract if possible. See ticket https://mcebuddy2x.codeplex.com/workitem/1910
// The format is -> EPISODE/TOTAL_EPISODES_IN_SEASON. SUBTITLE: DESCRIPTION
// OR -> COMMENT. SUBTITLE: DESCRIPTION
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
if (String.IsNullOrWhiteSpace(subTitle) && !String.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string[] parts = description.Split(':');
if (parts.Length > 0)
{
string subtitle = parts[0];
try
{
if (subtitle.Contains("/")) // It contains a episode number and season number
{
string[] numbers = subtitle.Split(' ');
video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]);
description = String.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
}
else
throw new Exception(); // Switch to default parsing
}
catch // Default parsing
{
if (subtitle.Contains(".")) // skip the comment, keep the subtitle
description = String.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
else
description = subtitle.Trim(); // Clean up whitespaces and save it
}
}
}
if (!string.IsNullOrWhiteSpace(description))
{
video.Overview = description;
}
}
private void ExtractTimestamp(Model.Entities.MediaInfo video)
{
if (video.VideoType == VideoType.VideoFile)
{
if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) ||
string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) ||
string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
{
try
{
video.Timestamp = GetMpegTimestamp(video.Path);
_logger.Debug("Video has {0} timestamp", video.Timestamp);
}
catch (Exception ex)
{
_logger.ErrorException("Error extracting timestamp info from {0}", ex, video.Path);
video.Timestamp = null;
}
}
}
}
private TransportStreamTimestamp GetMpegTimestamp(string path)
{
var packetBuffer = new byte['Å'];
using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
fs.Read(packetBuffer, 0, packetBuffer.Length);
}
if (packetBuffer[0] == 71)
{
return TransportStreamTimestamp.None;
}
if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71))
{
if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
{
return TransportStreamTimestamp.Zero;
}
return TransportStreamTimestamp.Valid;
}
return TransportStreamTimestamp.None;
}
private void UpdateFromMediaInfo(MediaSourceInfo video, MediaStream videoStream)
{
if (video.VideoType == VideoType.VideoFile && video.Protocol == MediaProtocol.File)
{
if (videoStream != null)
{
try
{
var result = new MediaInfoLib().GetVideoInfo(video.Path);
videoStream.IsCabac = result.IsCabac ?? videoStream.IsCabac;
videoStream.IsInterlaced = result.IsInterlaced ?? videoStream.IsInterlaced;
videoStream.BitDepth = result.BitDepth ?? videoStream.BitDepth;
videoStream.RefFrames = result.RefFrames;
}
catch (Exception ex)
{
_logger.ErrorException("Error running MediaInfo on {0}", ex, video.Path);
}
}
}
}
}
}

View File

@ -1,6 +1,7 @@
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using System;
@ -11,13 +12,16 @@ namespace MediaBrowser.Model.Dlna
public class StreamBuilder
{
private readonly ILocalPlayer _localPlayer;
private readonly ILogger _logger;
public StreamBuilder(ILocalPlayer localPlayer)
public StreamBuilder(ILocalPlayer localPlayer, ILogger logger)
{
_localPlayer = localPlayer;
_logger = logger;
}
public StreamBuilder()
: this(new NullLocalPlayer())
public StreamBuilder(ILogger logger)
: this(new NullLocalPlayer(), logger)
{
}
@ -353,6 +357,12 @@ namespace MediaBrowser.Model.Dlna
bool isEligibleForDirectPlay = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options), subtitleStream, options);
bool isEligibleForDirectStream = IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options);
_logger.Debug("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
options.Profile.Name ?? "Unknown Profile",
item.Path ?? "Unknown path",
isEligibleForDirectPlay,
isEligibleForDirectStream);
if (isEligibleForDirectPlay || isEligibleForDirectStream)
{
// See if it can be direct played
@ -504,6 +514,10 @@ namespace MediaBrowser.Model.Dlna
if (directPlay == null)
{
_logger.Debug("Profile: {0}, No direct play profiles found for Path: {1}",
profile.Name ?? "Unknown Profile",
mediaSource.Path ?? "Unknown path");
return null;
}
@ -550,6 +564,11 @@ namespace MediaBrowser.Model.Dlna
{
if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames, numVideoStreams, numAudioStreams))
{
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=VideoContainerProfile.{1} Path: {2}",
profile.Name ?? "Unknown Profile",
i.Property,
mediaSource.Path ?? "Unknown path");
return null;
}
}
@ -558,6 +577,10 @@ namespace MediaBrowser.Model.Dlna
if (string.IsNullOrEmpty(videoCodec))
{
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=Unknown video codec. Path: {1}",
profile.Name ?? "Unknown Profile",
mediaSource.Path ?? "Unknown path");
return null;
}
@ -577,6 +600,11 @@ namespace MediaBrowser.Model.Dlna
{
if (!conditionProcessor.IsVideoConditionSatisfied(i, audioBitrate, audioChannels, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isCabac, refFrames, numVideoStreams, numAudioStreams))
{
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=VideoCodecProfile.{1} Path: {2}",
profile.Name ?? "Unknown Profile",
i.Property,
mediaSource.Path ?? "Unknown path");
return null;
}
}
@ -587,6 +615,10 @@ namespace MediaBrowser.Model.Dlna
if (string.IsNullOrEmpty(audioCodec))
{
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=Unknown audio codec. Path: {1}",
profile.Name ?? "Unknown Profile",
mediaSource.Path ?? "Unknown path");
return null;
}
@ -607,6 +639,11 @@ namespace MediaBrowser.Model.Dlna
bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream);
if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioProfile, isSecondaryAudio))
{
_logger.Debug("Profile: {0}, DirectPlay=false. Reason=VideoAudioCodecProfile.{1} Path: {2}",
profile.Name ?? "Unknown Profile",
i.Property,
mediaSource.Path ?? "Unknown path");
return null;
}
}

View File

@ -70,6 +70,7 @@ namespace MediaBrowser.Model.Dlna
public string SubtitleFormat { get; set; }
public string PlaySessionId { get; set; }
public List<MediaSourceInfo> AllMediaSources { get; set; }
public string MediaSourceId
{

View File

@ -1,7 +1,7 @@
using System.ComponentModel;
using MediaBrowser.Model.Extensions;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.Serialization;
using MediaBrowser.Model.Extensions;
namespace MediaBrowser.Model.Dto
{

View File

@ -1,26 +1,65 @@
using MediaBrowser.Model.Dto;
using System;
using System.Collections.Generic;
namespace MediaBrowser.Model.Entities
{
public class MediaInfo
public class MediaInfo : MediaSourceInfo, IHasProviderIds
{
/// <summary>
/// Gets or sets the media streams.
/// </summary>
/// <value>The media streams.</value>
public List<MediaStream> MediaStreams { get; set; }
public List<ChapterInfo> Chapters { get; set; }
/// <summary>
/// Gets or sets the format.
/// Gets or sets the title.
/// </summary>
/// <value>The format.</value>
public string Format { get; set; }
public int? TotalBitrate { get; set; }
/// <value>The title.</value>
public string Title { get; set; }
/// <summary>
/// Gets or sets the album.
/// </summary>
/// <value>The album.</value>
public string Album { get; set; }
/// <summary>
/// Gets or sets the artists.
/// </summary>
/// <value>The artists.</value>
public List<string> Artists { get; set; }
/// <summary>
/// Gets or sets the album artists.
/// </summary>
/// <value>The album artists.</value>
public List<string> AlbumArtists { get; set; }
/// <summary>
/// Gets or sets the studios.
/// </summary>
/// <value>The studios.</value>
public List<string> Studios { get; set; }
public List<string> Genres { get; set; }
public int? IndexNumber { get; set; }
public int? ParentIndexNumber { get; set; }
public int? ProductionYear { get; set; }
public DateTime? PremiereDate { get; set; }
public List<BaseItemPerson> People { get; set; }
public Dictionary<string, string> ProviderIds { get; set; }
/// <summary>
/// Gets or sets the official rating.
/// </summary>
/// <value>The official rating.</value>
public string OfficialRating { get; set; }
/// <summary>
/// Gets or sets the overview.
/// </summary>
/// <value>The overview.</value>
public string Overview { get; set; }
public MediaInfo()
{
MediaStreams = new List<MediaStream>();
Chapters = new List<ChapterInfo>();
Artists = new List<string>();
AlbumArtists = new List<string>();
Studios = new List<string>();
Genres = new List<string>();
People = new List<BaseItemPerson>();
ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
}

View File

@ -102,7 +102,6 @@
<Compile Include="Manager\MetadataService.cs" />
<Compile Include="Manager\SeriesOrderManager.cs" />
<Compile Include="MediaInfo\FFProbeAudioInfo.cs" />
<Compile Include="MediaInfo\FFProbeHelpers.cs" />
<Compile Include="MediaInfo\FFProbeProvider.cs" />
<Compile Include="MediaInfo\FFProbeVideoInfo.cs" />
<Compile Include="MediaInfo\SubtitleDownloader.cs" />
@ -198,10 +197,6 @@
<Project>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</Project>
<Name>MediaBrowser.Controller</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.MediaInfo\MediaBrowser.MediaInfo.csproj">
<Project>{6e4145e4-c6d4-4e4d-94f2-87188db6e239}</Project>
<Name>MediaBrowser.MediaInfo</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
<Project>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</Project>
<Name>MediaBrowser.Model</Name>

View File

@ -1,5 +1,4 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@ -8,8 +7,6 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
@ -49,18 +46,14 @@ namespace MediaBrowser.Providers.MediaInfo
cancellationToken.ThrowIfCancellationRequested();
FFProbeHelpers.NormalizeFFProbeResult(result);
cancellationToken.ThrowIfCancellationRequested();
await Fetch(item, cancellationToken, result).ConfigureAwait(false);
return ItemUpdateType.MetadataImport;
}
private const string SchemaVersion = "1";
private const string SchemaVersion = "2";
private async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item, CancellationToken cancellationToken)
private async Task<Model.Entities.MediaInfo> GetMediaInfo(BaseItem item, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@ -71,7 +64,7 @@ namespace MediaBrowser.Providers.MediaInfo
try
{
return _json.DeserializeFromFile<InternalMediaInfoResult>(cachePath);
return _json.DeserializeFromFile<Model.Entities.MediaInfo>(cachePath);
}
catch (FileNotFoundException)
{
@ -83,7 +76,7 @@ namespace MediaBrowser.Providers.MediaInfo
var inputPath = new[] { item.Path };
var result = await _mediaEncoder.GetMediaInfo(inputPath, MediaProtocol.File, false, cancellationToken).ConfigureAwait(false);
var result = await _mediaEncoder.GetMediaInfo(inputPath, item.Path, MediaProtocol.File, true, false, cancellationToken).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(cachePath));
_json.SerializeToFile(result, cachePath);
@ -96,61 +89,23 @@ namespace MediaBrowser.Providers.MediaInfo
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="data">The data.</param>
/// <param name="mediaInfo">The media information.</param>
/// <returns>Task.</returns>
protected Task Fetch(Audio audio, CancellationToken cancellationToken, InternalMediaInfoResult data)
protected Task Fetch(Audio audio, CancellationToken cancellationToken, Model.Entities.MediaInfo mediaInfo)
{
var mediaInfo = MediaEncoderHelpers.GetMediaInfo(data);
var mediaStreams = mediaInfo.MediaStreams;
audio.FormatName = mediaInfo.Format;
audio.TotalBitrate = mediaInfo.TotalBitrate;
audio.FormatName = mediaInfo.Container;
audio.TotalBitrate = mediaInfo.Bitrate;
audio.HasEmbeddedImage = mediaStreams.Any(i => i.Type == MediaStreamType.EmbeddedImage);
if (data.streams != null)
{
// Get the first audio stream
var stream = data.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
audio.Size = mediaInfo.Size;
if (stream != null)
{
// Get duration from stream properties
var duration = stream.duration;
var extension = (Path.GetExtension(audio.Path) ?? string.Empty).TrimStart('.');
audio.Container = extension;
// If it's not there go into format properties
if (string.IsNullOrEmpty(duration))
{
duration = data.format.duration;
}
// If we got something, parse it
if (!string.IsNullOrEmpty(duration))
{
audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
}
}
}
if (data.format != null)
{
var extension = (Path.GetExtension(audio.Path) ?? string.Empty).TrimStart('.');
audio.Container = extension;
if (!string.IsNullOrEmpty(data.format.size))
{
audio.Size = long.Parse(data.format.size, _usCulture);
}
else
{
audio.Size = null;
}
if (data.format.tags != null)
{
FetchDataFromTags(audio, data.format.tags);
}
}
FetchDataFromTags(audio, mediaInfo);
return _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
}
@ -159,92 +114,36 @@ namespace MediaBrowser.Providers.MediaInfo
/// Fetches data from the tags dictionary
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="tags">The tags.</param>
private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
/// <param name="data">The data.</param>
private void FetchDataFromTags(Audio audio, Model.Entities.MediaInfo data)
{
var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
// Only set Name if title was found in the dictionary
if (!string.IsNullOrEmpty(title))
if (!string.IsNullOrEmpty(data.Title))
{
audio.Name = title;
audio.Name = data.Title;
}
if (!audio.LockedFields.Contains(MetadataFields.Cast))
{
audio.People.Clear();
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
if (!string.IsNullOrWhiteSpace(composer))
foreach (var person in data.People)
{
foreach (var person in Split(composer, false))
audio.AddPerson(new PersonInfo
{
audio.AddPerson(new PersonInfo { Name = person, Type = PersonType.Composer });
}
Name = person.Name,
Type = person.Type,
Role = person.Role
});
}
}
audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
if (!string.IsNullOrWhiteSpace(artists))
{
audio.Artists = artists.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
else
{
var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
if (string.IsNullOrWhiteSpace(artist))
{
audio.Artists.Clear();
}
else
{
audio.Artists = SplitArtists(artist)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
}
var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
if (string.IsNullOrWhiteSpace(albumArtist))
{
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist");
}
if (string.IsNullOrWhiteSpace(albumArtist))
{
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
}
if (string.IsNullOrWhiteSpace(albumArtist))
{
audio.AlbumArtists = new List<string>();
}
else
{
audio.AlbumArtists = SplitArtists(albumArtist)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
// Track number
audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
// Disc number
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
// Several different forms of retaildate
audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
audio.Album = data.Album;
audio.Artists = data.Artists;
audio.AlbumArtists = data.AlbumArtists;
audio.IndexNumber = data.IndexNumber;
audio.ProductionYear = data.ProductionYear;
audio.PremiereDate = data.PremiereDate;
// If we don't have a ProductionYear try and get it from PremiereDate
if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
@ -254,192 +153,29 @@ namespace MediaBrowser.Providers.MediaInfo
if (!audio.LockedFields.Contains(MetadataFields.Genres))
{
FetchGenres(audio, tags);
audio.Genres.Clear();
foreach (var genre in data.Genres)
{
audio.AddGenre(genre);
}
}
if (!audio.LockedFields.Contains(MetadataFields.Studios))
{
audio.Studios.Clear();
// There's several values in tags may or may not be present
FetchStudios(audio, tags, "organization");
FetchStudios(audio, tags, "ensemble");
FetchStudios(audio, tags, "publisher");
}
// These support mulitple values, but for now we only store the first.
audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")));
audio.SetProviderId(MetadataProviders.MusicBrainzArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")));
audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")));
audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")));
audio.SetProviderId(MetadataProviders.MusicBrainzTrack, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")));
}
private string GetMultipleMusicBrainzId(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
}
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
/// <summary>
/// Splits the specified val.
/// </summary>
/// <param name="val">The val.</param>
/// <param name="allowCommaDelimiter">if set to <c>true</c> [allow comma delimiter].</param>
/// <returns>System.String[][].</returns>
private IEnumerable<string> Split(string val, bool allowCommaDelimiter)
{
// Only use the comma as a delimeter if there are no slashes or pipes.
// We want to be careful not to split names that have commas in them
var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i) != -1) ?
_nameDelimiters :
new[] { ',' };
return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Trim());
}
private const string ArtistReplaceValue = " | ";
private IEnumerable<string> SplitArtists(string val)
{
val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase)
.Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase);
var artistsFound = new List<string>();
foreach (var whitelistArtist in GetSplitWhitelist())
{
var originalVal = val;
val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase);
if (!string.Equals(originalVal, val, StringComparison.OrdinalIgnoreCase))
{
artistsFound.Add(whitelistArtist);
}
}
// Only use the comma as a delimeter if there are no slashes or pipes.
// We want to be careful not to split names that have commas in them
var delimeter = _nameDelimiters;
var artists = val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Trim());
artistsFound.AddRange(artists);
return artistsFound;
}
private List<string> _splitWhiteList = null;
private IEnumerable<string> GetSplitWhitelist()
{
if (_splitWhiteList == null)
{
var file = GetType().Namespace + ".whitelist.txt";
using (var stream = GetType().Assembly.GetManifestResourceStream(file))
{
using (var reader = new StreamReader(stream))
{
var list = new List<string>();
while (!reader.EndOfStream)
{
var val = reader.ReadLine();
if (!string.IsNullOrWhiteSpace(val))
{
list.Add(val);
}
}
_splitWhiteList = list;
}
}
}
return _splitWhiteList;
}
/// <summary>
/// Gets the studios from the tags collection
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="tags">The tags.</param>
/// <param name="tagName">Name of the tag.</param>
private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
{
var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
if (!string.IsNullOrEmpty(val))
{
// Sometimes the artist name is listed here, account for that
var studios = Split(val, true).Where(i => !audio.HasAnyArtist(i));
foreach (var studio in studios)
foreach (var studio in data.Studios)
{
audio.AddStudio(studio);
}
}
audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, data.GetProviderId(MetadataProviders.MusicBrainzAlbumArtist));
audio.SetProviderId(MetadataProviders.MusicBrainzArtist, data.GetProviderId(MetadataProviders.MusicBrainzArtist));
audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, data.GetProviderId(MetadataProviders.MusicBrainzAlbum));
audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, data.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup));
audio.SetProviderId(MetadataProviders.MusicBrainzTrack, data.GetProviderId(MetadataProviders.MusicBrainzTrack));
}
/// <summary>
/// Gets the genres from the tags collection
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="tags">The tags.</param>
private void FetchGenres(Audio audio, Dictionary<string, string> tags)
{
var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
if (!string.IsNullOrEmpty(val))
{
audio.Genres.Clear();
foreach (var genre in Split(val, true))
{
audio.AddGenre(genre);
}
}
}
/// <summary>
/// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="tagName">Name of the tag.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
{
var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
if (!string.IsNullOrEmpty(disc))
{
disc = disc.Split('/')[0];
int num;
if (int.TryParse(disc, out num))
{
return num;
}
}
return null;
}
}
}

View File

@ -13,7 +13,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.MediaInfo;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@ -116,10 +115,6 @@ namespace MediaBrowser.Providers.MediaInfo
cancellationToken.ThrowIfCancellationRequested();
FFProbeHelpers.NormalizeFFProbeResult(result);
cancellationToken.ThrowIfCancellationRequested();
await Fetch(item, cancellationToken, result, isoMount, blurayDiscInfo, options).ConfigureAwait(false);
}
@ -136,7 +131,7 @@ namespace MediaBrowser.Providers.MediaInfo
private const string SchemaVersion = "1";
private async Task<InternalMediaInfoResult> GetMediaInfo(Video item,
private async Task<Model.Entities.MediaInfo> GetMediaInfo(Video item,
IIsoMount isoMount,
CancellationToken cancellationToken)
{
@ -149,7 +144,7 @@ namespace MediaBrowser.Providers.MediaInfo
try
{
return _json.DeserializeFromFile<InternalMediaInfoResult>(cachePath);
return _json.DeserializeFromFile<Model.Entities.MediaInfo>(cachePath);
}
catch (FileNotFoundException)
{
@ -165,7 +160,7 @@ namespace MediaBrowser.Providers.MediaInfo
var inputPath = MediaEncoderHelpers.GetInputArgument(item.Path, protocol, isoMount, item.PlayableStreamFileNames);
var result = await _mediaEncoder.GetMediaInfo(inputPath, protocol, false, cancellationToken).ConfigureAwait(false);
var result = await _mediaEncoder.GetMediaInfo(inputPath, item.Path, protocol, false, true, cancellationToken).ConfigureAwait(false);
Directory.CreateDirectory(Path.GetDirectoryName(cachePath));
_json.SerializeToFile(result, cachePath);
@ -175,52 +170,37 @@ namespace MediaBrowser.Providers.MediaInfo
protected async Task Fetch(Video video,
CancellationToken cancellationToken,
InternalMediaInfoResult data,
Model.Entities.MediaInfo mediaInfo,
IIsoMount isoMount,
BlurayDiscInfo blurayInfo,
MetadataRefreshOptions options)
{
var mediaInfo = MediaEncoderHelpers.GetMediaInfo(data);
var mediaStreams = mediaInfo.MediaStreams;
video.TotalBitrate = mediaInfo.TotalBitrate;
video.FormatName = (mediaInfo.Format ?? string.Empty)
video.TotalBitrate = mediaInfo.Bitrate;
video.FormatName = (mediaInfo.Container ?? string.Empty)
.Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
if (data.format != null)
// For dvd's this may not always be accurate, so don't set the runtime if the item already has one
var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0;
if (needToSetRuntime)
{
// For dvd's this may not always be accurate, so don't set the runtime if the item already has one
var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0;
if (needToSetRuntime && !string.IsNullOrEmpty(data.format.duration))
{
video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks;
}
if (video.VideoType == VideoType.VideoFile)
{
var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.');
video.Container = extension;
}
else
{
video.Container = null;
}
if (!string.IsNullOrEmpty(data.format.size))
{
video.Size = long.Parse(data.format.size, _usCulture);
}
else
{
video.Size = null;
}
video.RunTimeTicks = mediaInfo.RunTimeTicks;
}
var mediaChapters = (data.Chapters ?? new MediaChapter[] { }).ToList();
var chapters = mediaChapters.Select(GetChapterInfo).ToList();
if (video.VideoType == VideoType.VideoFile)
{
var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.');
video.Container = extension;
}
else
{
video.Container = null;
}
var chapters = mediaInfo.Chapters ?? new List<ChapterInfo>();
if (video.VideoType == VideoType.BluRay || (video.IsoType.HasValue && video.IsoType.Value == IsoType.BluRay))
{
FetchBdInfo(video, chapters, mediaStreams, blurayInfo);
@ -228,7 +208,7 @@ namespace MediaBrowser.Providers.MediaInfo
await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
FetchWtvInfo(video, data);
FetchEmbeddedInfo(video, mediaInfo);
video.IsHD = mediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1270);
@ -238,9 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo
video.DefaultVideoStreamIndex = videoStream == null ? (int?)null : videoStream.Index;
video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
ExtractTimestamp(video);
UpdateFromMediaInfo(video, videoStream);
video.Timestamp = mediaInfo.Timestamp;
await _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken).ConfigureAwait(false);
@ -283,29 +261,6 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
private void UpdateFromMediaInfo(Video video, MediaStream videoStream)
{
if (video.VideoType == VideoType.VideoFile && video.LocationType != LocationType.Remote && video.LocationType != LocationType.Virtual)
{
if (videoStream != null)
{
try
{
var result = new MediaInfoLib().GetVideoInfo(video.Path);
videoStream.IsCabac = result.IsCabac ?? videoStream.IsCabac;
videoStream.IsInterlaced = result.IsInterlaced ?? videoStream.IsInterlaced;
videoStream.BitDepth = result.BitDepth ?? videoStream.BitDepth;
videoStream.RefFrames = result.RefFrames;
}
catch (Exception ex)
{
_logger.ErrorException("Error running MediaInfo on {0}", ex, video.Path);
}
}
}
}
private void NormalizeChapterNames(List<ChapterInfo> chapters)
{
var index = 1;
@ -325,32 +280,6 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
private ChapterInfo GetChapterInfo(MediaChapter chapter)
{
var info = new ChapterInfo();
if (chapter.tags != null)
{
string name;
if (chapter.tags.TryGetValue("title", out name))
{
info.Name = name;
}
}
// Limit accuracy to milliseconds to match xml saving
var secondsString = chapter.start_time;
double seconds;
if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out seconds))
{
var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds);
info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks;
}
return info;
}
private void FetchBdInfo(BaseItem item, List<ChapterInfo> chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
{
var video = (Video)item;
@ -419,129 +348,79 @@ namespace MediaBrowser.Providers.MediaInfo
return _blurayExaminer.GetDiscInfo(path);
}
public const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
private void FetchWtvInfo(Video video, InternalMediaInfoResult data)
private void FetchEmbeddedInfo(Video video, Model.Entities.MediaInfo data)
{
if (data.format == null || data.format.tags == null)
{
return;
}
if (!video.LockedFields.Contains(MetadataFields.Genres))
{
var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre");
if (!string.IsNullOrWhiteSpace(genres))
{
//genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
}
if (!string.IsNullOrWhiteSpace(genres))
{
video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Trim())
.ToList();
}
}
if (!video.LockedFields.Contains(MetadataFields.OfficialRating))
{
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
if (!string.IsNullOrWhiteSpace(officialRating))
if (!string.IsNullOrWhiteSpace(data.OfficialRating))
{
video.OfficialRating = officialRating;
video.OfficialRating = data.OfficialRating;
}
}
if (!video.LockedFields.Contains(MetadataFields.Cast))
{
var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
video.People.Clear();
if (!string.IsNullOrEmpty(people))
foreach (var person in data.People)
{
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new PersonInfo { Name = i.Trim(), Type = PersonType.Actor })
.ToList();
}
}
var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
if (!string.IsNullOrWhiteSpace(year))
{
int val;
if (int.TryParse(year, NumberStyles.Integer, _usCulture, out val))
{
video.ProductionYear = val;
}
}
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime");
if (!string.IsNullOrWhiteSpace(premiereDateString))
{
DateTime val;
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
if (DateTime.TryParse(year, null, DateTimeStyles.None, out val))
{
video.PremiereDate = val.ToUniversalTime();
}
}
var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
var episode = video as Episode;
if (episode != null)
{
var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle");
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// Sometimes for TV Shows the Subtitle field is empty and the subtitle description contains the subtitle, extract if possible. See ticket https://mcebuddy2x.codeplex.com/workitem/1910
// The format is -> EPISODE/TOTAL_EPISODES_IN_SEASON. SUBTITLE: DESCRIPTION
// OR -> COMMENT. SUBTITLE: DESCRIPTION
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
if (String.IsNullOrWhiteSpace(subTitle) && !String.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string[] parts = description.Split(':');
if (parts.Length > 0)
video.AddPerson(new PersonInfo
{
string subtitle = parts[0];
try
{
if (subtitle.Contains("/")) // It contains a episode number and season number
{
string[] numbers = subtitle.Split(' ');
episode.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]);
description = String.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
}
else
throw new Exception(); // Switch to default parsing
}
catch // Default parsing
{
if (subtitle.Contains(".")) // skip the comment, keep the subtitle
description = String.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
else
description = subtitle.Trim(); // Clean up whitespaces and save it
}
}
Name = person.Name,
Type = person.Type,
Role = person.Role
});
}
}
if (!video.LockedFields.Contains(MetadataFields.Genres))
{
video.Genres.Clear();
foreach (var genre in data.Genres)
{
video.AddGenre(genre);
}
}
if (!video.LockedFields.Contains(MetadataFields.Studios))
{
video.Studios.Clear();
foreach (var studio in data.Studios)
{
video.AddStudio(studio);
}
}
if (data.ProductionYear.HasValue)
{
video.ProductionYear = data.ProductionYear;
}
if (data.PremiereDate.HasValue)
{
video.PremiereDate = data.PremiereDate;
}
if (data.IndexNumber.HasValue)
{
video.IndexNumber = data.IndexNumber;
}
if (data.ParentIndexNumber.HasValue)
{
video.ParentIndexNumber = data.ParentIndexNumber;
}
// If we don't have a ProductionYear try and get it from PremiereDate
if (video.PremiereDate.HasValue && !video.ProductionYear.HasValue)
{
video.ProductionYear = video.PremiereDate.Value.ToLocalTime().Year;
}
if (!video.LockedFields.Contains(MetadataFields.Overview))
{
if (!string.IsNullOrWhiteSpace(description))
if (!string.IsNullOrWhiteSpace(data.Overview))
{
video.Overview = description;
video.Overview = data.Overview;
}
}
}
@ -709,56 +588,6 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
private void ExtractTimestamp(Video video)
{
if (video.VideoType == VideoType.VideoFile)
{
if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) ||
string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) ||
string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
{
try
{
video.Timestamp = GetMpegTimestamp(video.Path);
_logger.Debug("Video has {0} timestamp", video.Timestamp);
}
catch (Exception ex)
{
_logger.ErrorException("Error extracting timestamp info from {0}", ex, video.Path);
video.Timestamp = null;
}
}
}
}
private TransportStreamTimestamp GetMpegTimestamp(string path)
{
var packetBuffer = new byte['Å'];
using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
fs.Read(packetBuffer, 0, packetBuffer.Length);
}
if (packetBuffer[0] == 71)
{
return TransportStreamTimestamp.None;
}
if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71))
{
if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
{
return TransportStreamTimestamp.Zero;
}
return TransportStreamTimestamp.Valid;
}
return TransportStreamTimestamp.None;
}
private void FetchFromDvdLib(Video item, IIsoMount mount)
{
var path = mount == null ? item.Path : mount.MountedPath;

View File

@ -1,6 +1,7 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
@ -18,12 +19,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger _logger;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager)
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
_liveTvManager = liveTvManager;
_jsonSerializer = jsonSerializer;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_logger = logManager.GetLogger(GetType().Name);
}
@ -90,14 +93,86 @@ namespace MediaBrowser.Server.Implementations.LiveTv
public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
{
MediaSourceInfo stream;
var isAudio = false;
var keys = openToken.Split(new[] { '|' }, 2);
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
{
return await _liveTvManager.GetChannelStream(keys[1], cancellationToken).ConfigureAwait(false);
stream = await _liveTvManager.GetChannelStream(keys[1], cancellationToken).ConfigureAwait(false);
}
else
{
stream = await _liveTvManager.GetRecordingStream(keys[1], cancellationToken).ConfigureAwait(false);
}
return await _liveTvManager.GetRecordingStream(keys[1], cancellationToken).ConfigureAwait(false);
try
{
await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error probing live tv stream", ex);
}
return stream;
}
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
{
var inputPaths = new[] { mediaSource.Path };
var info = await _mediaEncoder.GetMediaInfo(inputPaths, mediaSource.Path, mediaSource.Protocol, isAudio, false, cancellationToken)
.ConfigureAwait(false);
mediaSource.Bitrate = info.Bitrate;
mediaSource.Container = info.Container;
mediaSource.Formats = info.Formats;
mediaSource.MediaStreams = info.MediaStreams;
mediaSource.RunTimeTicks = info.RunTimeTicks;
mediaSource.Size = info.Size;
mediaSource.Timestamp = info.Timestamp;
mediaSource.Video3DFormat = info.Video3DFormat;
mediaSource.VideoType = info.VideoType;
mediaSource.DefaultSubtitleStreamIndex = null;
var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
if (audioStream == null || audioStream.Index == -1)
{
mediaSource.DefaultAudioStreamIndex = null;
}
else
{
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
}
// Try to estimate this
if (!mediaSource.Bitrate.HasValue)
{
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
if (videoStream != null)
{
var width = videoStream.Width ?? 1920;
if (width >= 1900)
{
mediaSource.Bitrate = 10000000;
}
else if (width >= 1260)
{
mediaSource.Bitrate = 6000000;
}
else if (width >= 700)
{
mediaSource.Bitrate = 4000000;
}
}
}
}
public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)

View File

@ -493,7 +493,7 @@ namespace MediaBrowser.Server.Implementations.Sync
conversionOptions.ItemId = item.Id.ToString("N");
conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList();
var streamInfo = new StreamBuilder().BuildVideoItem(conversionOptions);
var streamInfo = new StreamBuilder(_logger).BuildVideoItem(conversionOptions);
var mediaSource = streamInfo.MediaSource;
// No sense creating external subs if we're already burning one into the video
@ -690,7 +690,7 @@ namespace MediaBrowser.Server.Implementations.Sync
conversionOptions.ItemId = item.Id.ToString("N");
conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList();
var streamInfo = new StreamBuilder().BuildAudioItem(conversionOptions);
var streamInfo = new StreamBuilder(_logger).BuildAudioItem(conversionOptions);
var mediaSource = streamInfo.MediaSource;
jobItem.MediaSourceId = streamInfo.MediaSourceId;

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>MediaBrowser.Common.Internal</id>
<version>3.0.613</version>
<version>3.0.615</version>
<title>MediaBrowser.Common.Internal</title>
<authors>Luke</authors>
<owners>ebr,Luke,scottisafool</owners>
@ -12,7 +12,7 @@
<description>Contains common components shared by Emby Theater and Emby Server. Not intended for plugin developer consumption.</description>
<copyright>Copyright © Emby 2013</copyright>
<dependencies>
<dependency id="MediaBrowser.Common" version="3.0.613" />
<dependency id="MediaBrowser.Common" version="3.0.615" />
<dependency id="NLog" version="3.2.0.0" />
<dependency id="SimpleInjector" version="2.7.0" />
</dependencies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>MediaBrowser.Common</id>
<version>3.0.613</version>
<version>3.0.615</version>
<title>MediaBrowser.Common</title>
<authors>Emby Team</authors>
<owners>ebr,Luke,scottisafool</owners>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>MediaBrowser.Model.Signed</id>
<version>3.0.613</version>
<version>3.0.615</version>
<title>MediaBrowser.Model - Signed Edition</title>
<authors>Emby Team</authors>
<owners>ebr,Luke,scottisafool</owners>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>MediaBrowser.Server.Core</id>
<version>3.0.613</version>
<version>3.0.615</version>
<title>Media Browser.Server.Core</title>
<authors>Emby Team</authors>
<owners>ebr,Luke,scottisafool</owners>
@ -12,7 +12,7 @@
<description>Contains core components required to build plugins for Emby Server.</description>
<copyright>Copyright © Emby 2013</copyright>
<dependencies>
<dependency id="MediaBrowser.Common" version="3.0.613" />
<dependency id="MediaBrowser.Common" version="3.0.615" />
</dependencies>
</metadata>
<files>

View File

@ -1,4 +1,4 @@
using System.Reflection;
//[assembly: AssemblyVersion("3.0.*")]
[assembly: AssemblyVersion("3.0.5572.0")]
[assembly: AssemblyVersion("3.0.*")]
//[assembly: AssemblyVersion("3.0.5572.0")]