mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-04-17 15:53:42 +01:00
Merge branch 'master' into embytv
This commit is contained in:
281
MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
Normal file
281
MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
Normal file
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Attachments
|
||||
{
|
||||
public class AttachmentExtractor : IAttachmentExtractor, IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
||||
new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public AttachmentExtractor(
|
||||
ILogger<AttachmentExtractor> logger,
|
||||
IApplicationPaths appPaths,
|
||||
IFileSystem fileSystem,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(MediaAttachment attachment, Stream stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mediaSourceId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mediaSourceId));
|
||||
}
|
||||
|
||||
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
|
||||
var mediaSource = mediaSources
|
||||
.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
|
||||
if (mediaSource == null)
|
||||
{
|
||||
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} not found");
|
||||
}
|
||||
|
||||
var mediaAttachment = mediaSource.MediaAttachments
|
||||
.FirstOrDefault(i => i.Index == attachmentStreamIndex);
|
||||
if (mediaAttachment == null)
|
||||
{
|
||||
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}");
|
||||
}
|
||||
|
||||
var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return (mediaAttachment, attachmentStream);
|
||||
}
|
||||
|
||||
private async Task<Stream> GetAttachmentStream(
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaAttachment mediaAttachment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false);
|
||||
return File.OpenRead(attachmentPath);
|
||||
}
|
||||
|
||||
private async Task<string> GetReadableFile(
|
||||
string mediaPath,
|
||||
string inputFile,
|
||||
MediaProtocol protocol,
|
||||
MediaAttachment mediaAttachment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var outputPath = GetAttachmentCachePath(mediaPath, protocol, mediaAttachment.Index);
|
||||
await ExtractAttachment(inputFile, protocol, mediaAttachment.Index, outputPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
private async Task ExtractAttachment(
|
||||
string inputFile,
|
||||
MediaProtocol protocol,
|
||||
int attachmentStreamIndex,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await ExtractAttachmentInternal(
|
||||
_mediaEncoder.GetInputArgument(new[] { inputFile }, protocol),
|
||||
attachmentStreamIndex,
|
||||
outputPath,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAttachmentInternal(
|
||||
string inputPath,
|
||||
int attachmentStreamIndex,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(inputPath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(inputPath));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(outputPath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(outputPath));
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:{1} {2} -i {0} -t 0 -f null null",
|
||||
inputPath,
|
||||
attachmentStreamIndex,
|
||||
outputPath);
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = processArgs,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
};
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Start();
|
||||
|
||||
var processTcs = new TaskCompletionSource<bool>();
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (sender, args) => processTcs.TrySetResult(true);
|
||||
var unregister = cancellationToken.Register(() => processTcs.TrySetResult(process.HasExited));
|
||||
var ranToCompletion = await processTcs.Task.ConfigureAwait(false);
|
||||
unregister.Dispose();
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
||||
}
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
|
||||
process.Dispose();
|
||||
|
||||
var failed = false;
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
failed = true;
|
||||
|
||||
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
|
||||
try
|
||||
{
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
else if (!File.Exists(outputPath))
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (failed)
|
||||
{
|
||||
var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}";
|
||||
|
||||
_logger.LogError(msg);
|
||||
|
||||
throw new InvalidOperationException(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex)
|
||||
{
|
||||
string filename;
|
||||
if (protocol == MediaProtocol.File)
|
||||
{
|
||||
var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
|
||||
}
|
||||
else
|
||||
{
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
|
||||
}
|
||||
|
||||
var prefix = filename.Substring(0, 1);
|
||||
return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
Normal file
74
MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BDInfo.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.BdInfo
|
||||
{
|
||||
class BdInfoDirectoryInfo : BDInfo.IO.IDirectoryInfo
|
||||
{
|
||||
IFileSystem _fileSystem = null;
|
||||
|
||||
FileSystemMetadata _impl = null;
|
||||
|
||||
public string Name => _impl.Name;
|
||||
|
||||
public string FullName => _impl.FullName;
|
||||
|
||||
public IDirectoryInfo Parent
|
||||
{
|
||||
get
|
||||
{
|
||||
var parentFolder = System.IO.Path.GetDirectoryName(_impl.FullName);
|
||||
if (parentFolder != null)
|
||||
{
|
||||
return new BdInfoDirectoryInfo(_fileSystem, parentFolder);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_impl = _fileSystem.GetDirectoryInfo(path);
|
||||
}
|
||||
|
||||
private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_impl = impl;
|
||||
}
|
||||
|
||||
public IDirectoryInfo[] GetDirectories()
|
||||
{
|
||||
return Array.ConvertAll(_fileSystem.GetDirectories(_impl.FullName).ToArray(),
|
||||
x => new BdInfoDirectoryInfo(_fileSystem, x));
|
||||
}
|
||||
|
||||
public IFileInfo[] GetFiles()
|
||||
{
|
||||
return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName).ToArray(),
|
||||
x => new BdInfoFileInfo(_fileSystem, x));
|
||||
}
|
||||
|
||||
public IFileInfo[] GetFiles(string searchPattern)
|
||||
{
|
||||
return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(),
|
||||
x => new BdInfoFileInfo(_fileSystem, x));
|
||||
}
|
||||
|
||||
public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption)
|
||||
{
|
||||
return Array.ConvertAll(_fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false,
|
||||
searchOption.HasFlag(System.IO.SearchOption.AllDirectories)).ToArray(),
|
||||
x => new BdInfoFileInfo(_fileSystem, x));
|
||||
}
|
||||
|
||||
public static IDirectoryInfo FromFileSystemPath(Model.IO.IFileSystem fs, string path)
|
||||
{
|
||||
return new BdInfoDirectoryInfo(fs, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BDInfo;
|
||||
@@ -32,7 +32,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
var bdrom = new BDROM(path, _fileSystem);
|
||||
var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path));
|
||||
|
||||
bdrom.Scan();
|
||||
|
||||
|
||||
40
MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
Normal file
40
MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.BdInfo
|
||||
{
|
||||
class BdInfoFileInfo : BDInfo.IO.IFileInfo
|
||||
{
|
||||
IFileSystem _fileSystem = null;
|
||||
|
||||
FileSystemMetadata _impl = null;
|
||||
|
||||
public string Name => _impl.Name;
|
||||
|
||||
public string FullName => _impl.FullName;
|
||||
|
||||
public string Extension => _impl.Extension;
|
||||
|
||||
public long Length => _impl.Length;
|
||||
|
||||
public bool IsDir => _impl.IsDirectory;
|
||||
|
||||
public BdInfoFileInfo(IFileSystem fileSystem, FileSystemMetadata impl)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_impl = impl;
|
||||
}
|
||||
|
||||
public System.IO.Stream OpenRead()
|
||||
{
|
||||
return _fileSystem.GetFileStream(FullName,
|
||||
FileOpenMode.Open,
|
||||
FileAccessMode.Read,
|
||||
FileShareMode.Read);
|
||||
}
|
||||
|
||||
public System.IO.StreamReader OpenText()
|
||||
{
|
||||
return new System.IO.StreamReader(OpenRead());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
"h264_qsv",
|
||||
"hevc_qsv",
|
||||
"mpeg2_qsv",
|
||||
"mpeg2_mmal",
|
||||
"mpeg4_mmal",
|
||||
"vc1_qsv",
|
||||
"vc1_mmal",
|
||||
"h264_cuvid",
|
||||
"hevc_cuvid",
|
||||
"dts",
|
||||
@@ -26,6 +29,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
"aac",
|
||||
"mp3",
|
||||
"h264",
|
||||
"h264_mmal",
|
||||
"hevc"
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.MediaEncoding.Probing;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
@@ -19,9 +19,9 @@ using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
@@ -31,55 +31,60 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
public class MediaEncoder : IMediaEncoder, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the encoder path.
|
||||
/// The default image extraction timeout in milliseconds.
|
||||
/// </summary>
|
||||
/// <value>The encoder path.</value>
|
||||
public string EncoderPath => FFmpegPath;
|
||||
|
||||
/// <summary>
|
||||
/// The location of the discovered FFmpeg tool.
|
||||
/// </summary>
|
||||
public FFmpegLocation EncoderLocation { get; private set; }
|
||||
internal const int DefaultImageExtractionTimeout = 5000;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private string FFmpegPath;
|
||||
private string FFprobePath;
|
||||
protected readonly IServerConfigurationManager ConfigurationManager;
|
||||
protected readonly IFileSystem FileSystem;
|
||||
protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
|
||||
protected readonly Func<IMediaSourceManager> MediaSourceManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IProcessFactory _processFactory;
|
||||
private readonly int DefaultImageExtractionTimeoutMs;
|
||||
private readonly string StartupOptionFFmpegPath;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly Func<ISubtitleEncoder> _subtitleEncoder;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly string _startupOptionFFmpegPath;
|
||||
|
||||
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2);
|
||||
|
||||
private readonly object _runningProcessesLock = new object();
|
||||
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
private EncodingHelper _encodingHelper;
|
||||
|
||||
private string _ffmpegPath;
|
||||
private string _ffprobePath;
|
||||
|
||||
public MediaEncoder(
|
||||
ILoggerFactory loggerFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
string startupOptionsFFmpegPath,
|
||||
ILogger<MediaEncoder> logger,
|
||||
IServerConfigurationManager configurationManager,
|
||||
IFileSystem fileSystem,
|
||||
Func<ISubtitleEncoder> subtitleEncoder,
|
||||
Func<IMediaSourceManager> mediaSourceManager,
|
||||
IProcessFactory processFactory,
|
||||
int defaultImageExtractionTimeoutMs,
|
||||
ILocalizationManager localization)
|
||||
ILocalizationManager localization,
|
||||
Func<ISubtitleEncoder> subtitleEncoder,
|
||||
IConfiguration configuration,
|
||||
string startupOptionsFFmpegPath)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger(nameof(MediaEncoder));
|
||||
_jsonSerializer = jsonSerializer;
|
||||
StartupOptionFFmpegPath = startupOptionsFFmpegPath;
|
||||
ConfigurationManager = configurationManager;
|
||||
FileSystem = fileSystem;
|
||||
SubtitleEncoder = subtitleEncoder;
|
||||
_logger = logger;
|
||||
_configurationManager = configurationManager;
|
||||
_fileSystem = fileSystem;
|
||||
_processFactory = processFactory;
|
||||
DefaultImageExtractionTimeoutMs = defaultImageExtractionTimeoutMs;
|
||||
_localization = localization;
|
||||
_startupOptionFFmpegPath = startupOptionsFFmpegPath;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
private EncodingHelper EncodingHelper
|
||||
=> LazyInitializer.EnsureInitialized(
|
||||
ref _encodingHelper,
|
||||
() => new EncodingHelper(this, _fileSystem, _subtitleEncoder(), _configuration));
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EncoderPath => _ffmpegPath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public FFmpegLocation EncoderLocation { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Run at startup or if the user removes a Custom path from transcode page.
|
||||
/// Sets global variables FFmpegPath.
|
||||
@@ -88,39 +93,39 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
public void SetFFmpegPath()
|
||||
{
|
||||
// 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
|
||||
if (!ValidatePath(ConfigurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
|
||||
if (!ValidatePath(_configurationManager.GetConfiguration<EncodingOptions>("encoding").EncoderAppPath, FFmpegLocation.Custom))
|
||||
{
|
||||
// 2) Check if the --ffmpeg CLI switch has been given
|
||||
if (!ValidatePath(StartupOptionFFmpegPath, FFmpegLocation.SetByArgument))
|
||||
if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument))
|
||||
{
|
||||
// 3) Search system $PATH environment variable for valid FFmpeg
|
||||
if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System))
|
||||
{
|
||||
EncoderLocation = FFmpegLocation.NotFound;
|
||||
FFmpegPath = null;
|
||||
_ffmpegPath = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
|
||||
var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||
config.EncoderAppPathDisplay = FFmpegPath ?? string.Empty;
|
||||
ConfigurationManager.SaveConfiguration("encoding", config);
|
||||
var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||
config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
|
||||
_configurationManager.SaveConfiguration("encoding", config);
|
||||
|
||||
// Only if mpeg path is set, try and set path to probe
|
||||
if (FFmpegPath != null)
|
||||
if (_ffmpegPath != null)
|
||||
{
|
||||
// Determine a probe path from the mpeg path
|
||||
FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
|
||||
_ffprobePath = Regex.Replace(_ffmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1");
|
||||
|
||||
// Interrogate to understand what coders are supported
|
||||
var validator = new EncoderValidator(_logger, FFmpegPath);
|
||||
var validator = new EncoderValidator(_logger, _ffmpegPath);
|
||||
|
||||
SetAvailableDecoders(validator.GetDecoders());
|
||||
SetAvailableEncoders(validator.GetEncoders());
|
||||
}
|
||||
|
||||
_logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, FFmpegPath ?? string.Empty);
|
||||
_logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, _ffmpegPath ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -160,9 +165,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
// Write the new ffmpeg path to the xml as <EncoderAppPath>
|
||||
// This ensures its not lost on next startup
|
||||
var config = ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||
var config = _configurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||
config.EncoderAppPath = newPath;
|
||||
ConfigurationManager.SaveConfiguration("encoding", config);
|
||||
_configurationManager.SaveConfiguration("encoding", config);
|
||||
|
||||
// Trigger SetFFmpegPath so we validate the new path and setup probe path
|
||||
SetFFmpegPath();
|
||||
@@ -193,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
// ToDo - Enable the ffmpeg validator. At the moment any version can be used.
|
||||
rc = true;
|
||||
|
||||
FFmpegPath = path;
|
||||
_ffmpegPath = path;
|
||||
EncoderLocation = location;
|
||||
}
|
||||
else
|
||||
@@ -209,7 +214,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
try
|
||||
{
|
||||
var files = FileSystem.GetFilePaths(path);
|
||||
var files = _fileSystem.GetFilePaths(path);
|
||||
|
||||
var excludeExtensions = new[] { ".c" };
|
||||
|
||||
@@ -304,7 +309,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
|
||||
|
||||
var inputFiles = MediaEncoderHelpers.GetInputArgument(FileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames);
|
||||
var inputFiles = MediaEncoderHelpers.GetInputArgument(_fileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames);
|
||||
|
||||
var probeSize = EncodingHelper.GetProbeSizeArgument(inputFiles.Length);
|
||||
string analyzeDuration;
|
||||
@@ -365,7 +370,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||
RedirectStandardOutput = true,
|
||||
|
||||
FileName = FFprobePath,
|
||||
FileName = _ffprobePath,
|
||||
Arguments = args,
|
||||
|
||||
|
||||
@@ -383,7 +388,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
}
|
||||
|
||||
using (var processWrapper = new ProcessWrapper(process, this, _logger))
|
||||
using (var processWrapper = new ProcessWrapper(process, this))
|
||||
{
|
||||
_logger.LogDebug("Starting ffprobe with args {Args}", args);
|
||||
StartProcess(processWrapper);
|
||||
@@ -391,8 +396,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
InternalMediaInfoResult result;
|
||||
try
|
||||
{
|
||||
result = await _jsonSerializer.DeserializeFromStreamAsync<InternalMediaInfoResult>(
|
||||
process.StandardOutput.BaseStream).ConfigureAwait(false);
|
||||
result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
|
||||
process.StandardOutput.BaseStream,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -401,29 +407,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
throw;
|
||||
}
|
||||
|
||||
if (result == null || (result.streams == null && result.format == null))
|
||||
if (result == null || (result.Streams == null && result.Format == null))
|
||||
{
|
||||
throw new Exception("ffprobe failed - streams and format are both null.");
|
||||
}
|
||||
|
||||
if (result.streams != null)
|
||||
if (result.Streams != null)
|
||||
{
|
||||
// Normalize aspect ratio if invalid
|
||||
foreach (var stream in result.streams)
|
||||
foreach (var stream in result.Streams)
|
||||
{
|
||||
if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.display_aspect_ratio = string.Empty;
|
||||
stream.DisplayAspectRatio = string.Empty;
|
||||
}
|
||||
|
||||
if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.sample_aspect_ratio = string.Empty;
|
||||
stream.SampleAspectRatio = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ProbeResultNormalizer(_logger, FileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol);
|
||||
return new ProbeResultNormalizer(_logger, _fileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +492,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
throw new ArgumentNullException(nameof(inputPath));
|
||||
}
|
||||
|
||||
var tempExtractPath = Path.Combine(ConfigurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
|
||||
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
|
||||
|
||||
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600.
|
||||
@@ -545,7 +551,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args;
|
||||
}
|
||||
|
||||
var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder());
|
||||
if (videoStream != null)
|
||||
{
|
||||
/* fix
|
||||
@@ -559,7 +564,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(container))
|
||||
{
|
||||
var inputFormat = encodinghelper.GetInputFormat(container);
|
||||
var inputFormat = EncodingHelper.GetInputFormat(container);
|
||||
if (!string.IsNullOrWhiteSpace(inputFormat))
|
||||
{
|
||||
args = "-f " + inputFormat + " " + args;
|
||||
@@ -570,7 +575,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = FFmpegPath,
|
||||
FileName = _ffmpegPath,
|
||||
Arguments = args,
|
||||
IsHidden = true,
|
||||
ErrorDialog = false,
|
||||
@@ -579,7 +584,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
using (var processWrapper = new ProcessWrapper(process, this, _logger))
|
||||
using (var processWrapper = new ProcessWrapper(process, this))
|
||||
{
|
||||
bool ranToCompletion;
|
||||
|
||||
@@ -588,10 +593,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
StartProcess(processWrapper);
|
||||
|
||||
var timeoutMs = ConfigurationManager.Configuration.ImageExtractionTimeoutMs;
|
||||
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
|
||||
if (timeoutMs <= 0)
|
||||
{
|
||||
timeoutMs = DefaultImageExtractionTimeoutMs;
|
||||
timeoutMs = DefaultImageExtractionTimeout;
|
||||
}
|
||||
|
||||
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
|
||||
@@ -607,7 +612,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
var file = FileSystem.GetFileInfo(tempExtractPath);
|
||||
var file = _fileSystem.GetFileInfo(tempExtractPath);
|
||||
|
||||
if (exitCode == -1 || !file.Exists || file.Length == 0)
|
||||
{
|
||||
@@ -675,7 +680,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
args = analyzeDurationArgument + " " + args;
|
||||
}
|
||||
|
||||
var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder());
|
||||
if (videoStream != null)
|
||||
{
|
||||
/* fix
|
||||
@@ -689,7 +693,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(container))
|
||||
{
|
||||
var inputFormat = encodinghelper.GetInputFormat(container);
|
||||
var inputFormat = EncodingHelper.GetInputFormat(container);
|
||||
if (!string.IsNullOrWhiteSpace(inputFormat))
|
||||
{
|
||||
args = "-f " + inputFormat + " " + args;
|
||||
@@ -700,7 +704,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = FFmpegPath,
|
||||
FileName = _ffmpegPath,
|
||||
Arguments = args,
|
||||
IsHidden = true,
|
||||
ErrorDialog = false,
|
||||
@@ -713,7 +717,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
bool ranToCompletion = false;
|
||||
|
||||
using (var processWrapper = new ProcessWrapper(process, this, _logger))
|
||||
using (var processWrapper = new ProcessWrapper(process, this))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -736,10 +740,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var jpegCount = FileSystem.GetFilePaths(targetDirectory)
|
||||
var jpegCount = _fileSystem.GetFilePaths(targetDirectory)
|
||||
.Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
isResponsive = (jpegCount > lastCount);
|
||||
isResponsive = jpegCount > lastCount;
|
||||
lastCount = jpegCount;
|
||||
}
|
||||
|
||||
@@ -770,11 +774,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
process.Process.Start();
|
||||
|
||||
lock (_runningProcesses)
|
||||
lock (_runningProcessesLock)
|
||||
{
|
||||
_runningProcesses.Add(process);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopProcess(ProcessWrapper process, int waitTimeMs)
|
||||
{
|
||||
try
|
||||
@@ -783,18 +788,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in WaitForExit");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Killing ffmpeg process");
|
||||
|
||||
process.Process.Kill();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// The process has already exited or
|
||||
// there is no process associated with this Process object.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing process");
|
||||
@@ -804,7 +807,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private void StopProcesses()
|
||||
{
|
||||
List<ProcessWrapper> proceses;
|
||||
lock (_runningProcesses)
|
||||
lock (_runningProcessesLock)
|
||||
{
|
||||
proceses = _runningProcesses.ToList();
|
||||
_runningProcesses.Clear();
|
||||
@@ -827,12 +830,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return path.Replace('\\', '/').Replace(":", "\\:").Replace("'", "'\\\\\\''");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -852,11 +854,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public string[] GetPlayableStreamFileNames(string path, VideoType videoType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
@@ -870,21 +867,24 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
private class ProcessWrapper : IDisposable
|
||||
{
|
||||
public readonly IProcess Process;
|
||||
public bool HasExited;
|
||||
public int? ExitCode;
|
||||
private readonly MediaEncoder _mediaEncoder;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder, ILogger logger)
|
||||
private bool _disposed = false;
|
||||
|
||||
public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder)
|
||||
{
|
||||
Process = process;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_logger = logger;
|
||||
Process.Exited += Process_Exited;
|
||||
Process.Exited += OnProcessExited;
|
||||
}
|
||||
|
||||
void Process_Exited(object sender, EventArgs e)
|
||||
public IProcess Process { get; }
|
||||
|
||||
public bool HasExited { get; private set; }
|
||||
|
||||
public int? ExitCode { get; private set; }
|
||||
|
||||
void OnProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
var process = (IProcess)sender;
|
||||
|
||||
@@ -903,7 +903,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
private void DisposeProcess(IProcess process)
|
||||
{
|
||||
lock (_mediaEncoder._runningProcesses)
|
||||
lock (_mediaEncoder._runningProcessesLock)
|
||||
{
|
||||
_mediaEncoder._runningProcesses.Remove(this);
|
||||
}
|
||||
@@ -917,23 +917,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
private readonly object _syncLock = new object();
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_syncLock)
|
||||
if (!_disposed)
|
||||
{
|
||||
if (!_disposed)
|
||||
if (Process != null)
|
||||
{
|
||||
if (Process != null)
|
||||
{
|
||||
Process.Exited -= Process_Exited;
|
||||
DisposeProcess(Process);
|
||||
}
|
||||
Process.Exited -= OnProcessExited;
|
||||
DisposeProcess(Process);
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BDInfo\BDInfo.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BDInfo" Version="0.7.6.1" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.6.0" />
|
||||
<PackageReference Include="UTF.Unknown" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -16,24 +16,19 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
if (result.format != null && result.format.tags != null)
|
||||
if (result.Format != null && result.Format.Tags != null)
|
||||
{
|
||||
result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
|
||||
result.Format.Tags = ConvertDictionaryToCaseInsensitive(result.Format.Tags);
|
||||
}
|
||||
|
||||
if (result.streams != null)
|
||||
if (result.Streams != null)
|
||||
{
|
||||
// Convert all dictionaries to case insensitive
|
||||
foreach (var stream in result.streams)
|
||||
foreach (var stream in result.Streams)
|
||||
{
|
||||
if (stream.tags != null)
|
||||
if (stream.Tags != null)
|
||||
{
|
||||
stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
|
||||
}
|
||||
|
||||
if (stream.disposition != null)
|
||||
{
|
||||
stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition);
|
||||
stream.Tags = ConvertDictionaryToCaseInsensitive(stream.Tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +40,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
public static string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
@@ -103,7 +98,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <param name="dict">The dict.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
|
||||
private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict)
|
||||
{
|
||||
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaInfoResult
|
||||
/// Class MediaInfoResult.
|
||||
/// </summary>
|
||||
public class InternalMediaInfoResult
|
||||
{
|
||||
@@ -11,331 +12,21 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// Gets or sets the streams.
|
||||
/// </summary>
|
||||
/// <value>The streams.</value>
|
||||
public MediaStreamInfo[] streams { get; set; }
|
||||
[JsonPropertyName("streams")]
|
||||
public IReadOnlyList<MediaStreamInfo> Streams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format.
|
||||
/// </summary>
|
||||
/// <value>The format.</value>
|
||||
public MediaFormatInfo format { get; set; }
|
||||
[JsonPropertyName("format")]
|
||||
public MediaFormatInfo Format { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the chapters.
|
||||
/// </summary>
|
||||
/// <value>The chapters.</value>
|
||||
public MediaChapter[] Chapters { get; set; }
|
||||
}
|
||||
|
||||
public class MediaChapter
|
||||
{
|
||||
public int id { get; set; }
|
||||
public string time_base { get; set; }
|
||||
public long start { get; set; }
|
||||
public string start_time { get; set; }
|
||||
public long end { get; set; }
|
||||
public string end_time { get; set; }
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stream within the output
|
||||
/// </summary>
|
||||
public class MediaStreamInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index.
|
||||
/// </summary>
|
||||
/// <value>The index.</value>
|
||||
public int index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile.
|
||||
/// </summary>
|
||||
/// <value>The profile.</value>
|
||||
public string profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_name.</value>
|
||||
public string codec_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_long_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_long_name.</value>
|
||||
public string codec_long_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_type.
|
||||
/// </summary>
|
||||
/// <value>The codec_type.</value>
|
||||
public string codec_type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_rate.
|
||||
/// </summary>
|
||||
/// <value>The sample_rate.</value>
|
||||
public string sample_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channels.
|
||||
/// </summary>
|
||||
/// <value>The channels.</value>
|
||||
public int channels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channel_layout.
|
||||
/// </summary>
|
||||
/// <value>The channel_layout.</value>
|
||||
public string channel_layout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the avg_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The avg_frame_rate.</value>
|
||||
public string avg_frame_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
public string duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
public string bit_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
public int width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the refs.
|
||||
/// </summary>
|
||||
/// <value>The refs.</value>
|
||||
public int refs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
public int height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The display_aspect_ratio.</value>
|
||||
public string display_aspect_ratio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_sample.</value>
|
||||
public int bits_per_sample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_raw_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_raw_sample.</value>
|
||||
public int bits_per_raw_sample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the r_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The r_frame_rate.</value>
|
||||
public string r_frame_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the has_b_frames.
|
||||
/// </summary>
|
||||
/// <value>The has_b_frames.</value>
|
||||
public int has_b_frames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The sample_aspect_ratio.</value>
|
||||
public string sample_aspect_ratio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pix_fmt.
|
||||
/// </summary>
|
||||
/// <value>The pix_fmt.</value>
|
||||
public string pix_fmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
/// <value>The level.</value>
|
||||
public int level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time_base.
|
||||
/// </summary>
|
||||
/// <value>The time_base.</value>
|
||||
public string time_base { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
public string start_time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_time_base.
|
||||
/// </summary>
|
||||
/// <value>The codec_time_base.</value>
|
||||
public string codec_time_base { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag.</value>
|
||||
public string codec_tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag_string.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag_string.</value>
|
||||
public string codec_tag_string { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_fmt.
|
||||
/// </summary>
|
||||
/// <value>The sample_fmt.</value>
|
||||
public string sample_fmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dmix_mode.
|
||||
/// </summary>
|
||||
/// <value>The dmix_mode.</value>
|
||||
public string dmix_mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_pts.
|
||||
/// </summary>
|
||||
/// <value>The start_pts.</value>
|
||||
public string start_pts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the is_avc.
|
||||
/// </summary>
|
||||
/// <value>The is_avc.</value>
|
||||
public string is_avc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nal_length_size.
|
||||
/// </summary>
|
||||
/// <value>The nal_length_size.</value>
|
||||
public string nal_length_size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_cmixlev.</value>
|
||||
public string ltrt_cmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_surmixlev.</value>
|
||||
public string ltrt_surmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_cmixlev.</value>
|
||||
public string loro_cmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_surmixlev.</value>
|
||||
public string loro_surmixlev { get; set; }
|
||||
|
||||
public string field_order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the disposition.
|
||||
/// </summary>
|
||||
/// <value>The disposition.</value>
|
||||
public Dictionary<string, string> disposition { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class MediaFormat
|
||||
/// </summary>
|
||||
public class MediaFormatInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the filename.
|
||||
/// </summary>
|
||||
/// <value>The filename.</value>
|
||||
public string filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nb_streams.
|
||||
/// </summary>
|
||||
/// <value>The nb_streams.</value>
|
||||
public int nb_streams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_name.
|
||||
/// </summary>
|
||||
/// <value>The format_name.</value>
|
||||
public string format_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_long_name.
|
||||
/// </summary>
|
||||
/// <value>The format_long_name.</value>
|
||||
public string format_long_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
public string start_time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
public string duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
/// <value>The size.</value>
|
||||
public string size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
public string bit_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the probe_score.
|
||||
/// </summary>
|
||||
/// <value>The probe_score.</value>
|
||||
public int probe_score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
[JsonPropertyName("chapters")]
|
||||
public IReadOnlyList<MediaChapter> Chapters { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
32
MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
Normal file
32
MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaChapter.
|
||||
/// </summary>
|
||||
public class MediaChapter
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("time_base")]
|
||||
public string TimeBase { get; set; }
|
||||
|
||||
[JsonPropertyName("start")]
|
||||
public long Start { get; set; }
|
||||
|
||||
[JsonPropertyName("start_time")]
|
||||
public string StartTime { get; set; }
|
||||
|
||||
[JsonPropertyName("end")]
|
||||
public long End { get; set; }
|
||||
|
||||
[JsonPropertyName("end_time")]
|
||||
public string EndTime { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
public IReadOnlyDictionary<string, string> Tags { get; set; }
|
||||
}
|
||||
}
|
||||
81
MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
Normal file
81
MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaFormat.
|
||||
/// </summary>
|
||||
public class MediaFormatInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the filename.
|
||||
/// </summary>
|
||||
/// <value>The filename.</value>
|
||||
[JsonPropertyName("filename")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nb_streams.
|
||||
/// </summary>
|
||||
/// <value>The nb_streams.</value>
|
||||
[JsonPropertyName("nb_streams")]
|
||||
public int NbStreams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_name.
|
||||
/// </summary>
|
||||
/// <value>The format_name.</value>
|
||||
[JsonPropertyName("format_name")]
|
||||
public string FormatName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_long_name.
|
||||
/// </summary>
|
||||
/// <value>The format_long_name.</value>
|
||||
[JsonPropertyName("format_long_name")]
|
||||
public string FormatLongName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
[JsonPropertyName("start_time")]
|
||||
public string StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
[JsonPropertyName("duration")]
|
||||
public string Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
/// <value>The size.</value>
|
||||
[JsonPropertyName("size")]
|
||||
public string Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
[JsonPropertyName("bit_rate")]
|
||||
public string BitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the probe_score.
|
||||
/// </summary>
|
||||
/// <value>The probe_score.</value>
|
||||
[JsonPropertyName("probe_score")]
|
||||
public int ProbeScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
[JsonPropertyName("tags")]
|
||||
public IReadOnlyDictionary<string, string> Tags { get; set; }
|
||||
}
|
||||
}
|
||||
282
MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
Normal file
282
MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
Normal file
@@ -0,0 +1,282 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using MediaBrowser.Common.Json.Converters;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a stream within the output.
|
||||
/// </summary>
|
||||
public class MediaStreamInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index.
|
||||
/// </summary>
|
||||
/// <value>The index.</value>
|
||||
[JsonPropertyName("index")]
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile.
|
||||
/// </summary>
|
||||
/// <value>The profile.</value>
|
||||
[JsonPropertyName("profile")]
|
||||
public string Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_name.</value>
|
||||
[JsonPropertyName("codec_name")]
|
||||
public string CodecName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_long_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_long_name.</value>
|
||||
[JsonPropertyName("codec_long_name")]
|
||||
public string CodecLongName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_type.
|
||||
/// </summary>
|
||||
/// <value>The codec_type.</value>
|
||||
[JsonPropertyName("codec_type")]
|
||||
public string CodecType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_rate.
|
||||
/// </summary>
|
||||
/// <value>The sample_rate.</value>
|
||||
[JsonPropertyName("sample_rate")]
|
||||
public string SampleRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channels.
|
||||
/// </summary>
|
||||
/// <value>The channels.</value>
|
||||
[JsonPropertyName("channels")]
|
||||
public int Channels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channel_layout.
|
||||
/// </summary>
|
||||
/// <value>The channel_layout.</value>
|
||||
[JsonPropertyName("channel_layout")]
|
||||
public string ChannelLayout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the avg_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The avg_frame_rate.</value>
|
||||
[JsonPropertyName("avg_frame_rate")]
|
||||
public string AverageFrameRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
[JsonPropertyName("duration")]
|
||||
public string Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
[JsonPropertyName("bit_rate")]
|
||||
public string BitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
[JsonPropertyName("width")]
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the refs.
|
||||
/// </summary>
|
||||
/// <value>The refs.</value>
|
||||
[JsonPropertyName("refs")]
|
||||
public int Refs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
[JsonPropertyName("height")]
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The display_aspect_ratio.</value>
|
||||
[JsonPropertyName("display_aspect_ratio")]
|
||||
public string DisplayAspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
[JsonPropertyName("tags")]
|
||||
public IReadOnlyDictionary<string, string> Tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_sample.</value>
|
||||
[JsonPropertyName("bits_per_sample")]
|
||||
public int BitsPerSample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_raw_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_raw_sample.</value>
|
||||
[JsonPropertyName("bits_per_raw_sample")]
|
||||
[JsonConverter(typeof(JsonInt32Converter))]
|
||||
public int BitsPerRawSample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the r_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The r_frame_rate.</value>
|
||||
[JsonPropertyName("r_frame_rate")]
|
||||
public string RFrameRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the has_b_frames.
|
||||
/// </summary>
|
||||
/// <value>The has_b_frames.</value>
|
||||
[JsonPropertyName("has_b_frames")]
|
||||
public int HasBFrames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The sample_aspect_ratio.</value>
|
||||
[JsonPropertyName("sample_aspect_ratio")]
|
||||
public string SampleAspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pix_fmt.
|
||||
/// </summary>
|
||||
/// <value>The pix_fmt.</value>
|
||||
[JsonPropertyName("pix_fmt")]
|
||||
public string PixelFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
/// <value>The level.</value>
|
||||
[JsonPropertyName("level")]
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time_base.
|
||||
/// </summary>
|
||||
/// <value>The time_base.</value>
|
||||
[JsonPropertyName("time_base")]
|
||||
public string TimeBase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
[JsonPropertyName("start_time")]
|
||||
public string StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_time_base.
|
||||
/// </summary>
|
||||
/// <value>The codec_time_base.</value>
|
||||
[JsonPropertyName("codec_time_base")]
|
||||
public string CodecTimeBase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag.</value>
|
||||
[JsonPropertyName("codec_tag")]
|
||||
public string CodecTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag_string.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag_string.</value>
|
||||
[JsonPropertyName("codec_tag_string")]
|
||||
public string CodecTagString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_fmt.
|
||||
/// </summary>
|
||||
/// <value>The sample_fmt.</value>
|
||||
[JsonPropertyName("sample_fmt")]
|
||||
public string SampleFmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dmix_mode.
|
||||
/// </summary>
|
||||
/// <value>The dmix_mode.</value>
|
||||
[JsonPropertyName("dmix_mode")]
|
||||
public string DmixMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_pts.
|
||||
/// </summary>
|
||||
/// <value>The start_pts.</value>
|
||||
[JsonPropertyName("start_pts")]
|
||||
public int StartPts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the is_avc.
|
||||
/// </summary>
|
||||
/// <value>The is_avc.</value>
|
||||
[JsonPropertyName("is_avc")]
|
||||
public string IsAvc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nal_length_size.
|
||||
/// </summary>
|
||||
/// <value>The nal_length_size.</value>
|
||||
[JsonPropertyName("nal_length_size")]
|
||||
public string NalLengthSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_cmixlev.</value>
|
||||
[JsonPropertyName("ltrt_cmixlev")]
|
||||
public string LtrtCmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_surmixlev.</value>
|
||||
[JsonPropertyName("ltrt_surmixlev")]
|
||||
public string LtrtSurmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_cmixlev.</value>
|
||||
[JsonPropertyName("loro_cmixlev")]
|
||||
public string LoroCmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_surmixlev.</value>
|
||||
[JsonPropertyName("loro_surmixlev")]
|
||||
public string LoroSurmixlev { get; set; }
|
||||
|
||||
[JsonPropertyName("field_order")]
|
||||
public string FieldOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the disposition.
|
||||
/// </summary>
|
||||
/// <value>The disposition.</value>
|
||||
[JsonPropertyName("disposition")]
|
||||
public IReadOnlyDictionary<string, int> Disposition { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ using System.Xml;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
@@ -41,21 +40,25 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
FFProbeHelpers.NormalizeFFProbeResult(data);
|
||||
SetSize(data, info);
|
||||
|
||||
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
|
||||
var internalStreams = data.Streams ?? new MediaStreamInfo[] { };
|
||||
|
||||
info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.format))
|
||||
info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format))
|
||||
.Where(i => i != null)
|
||||
// Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them
|
||||
.Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec))
|
||||
.ToList();
|
||||
|
||||
if (data.format != null)
|
||||
{
|
||||
info.Container = NormalizeFormat(data.format.format_name);
|
||||
info.MediaAttachments = internalStreams.Select(s => GetMediaAttachment(s))
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.format.bit_rate))
|
||||
if (data.Format != null)
|
||||
{
|
||||
info.Container = NormalizeFormat(data.Format.FormatName);
|
||||
|
||||
if (!string.IsNullOrEmpty(data.Format.BitRate))
|
||||
{
|
||||
if (int.TryParse(data.format.bit_rate, NumberStyles.Any, _usCulture, out var value))
|
||||
if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value))
|
||||
{
|
||||
info.Bitrate = value;
|
||||
}
|
||||
@@ -65,22 +68,22 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var tagStreamType = isAudio ? "audio" : "video";
|
||||
|
||||
if (data.streams != null)
|
||||
if (data.Streams != null)
|
||||
{
|
||||
var tagStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, tagStreamType, StringComparison.OrdinalIgnoreCase));
|
||||
var tagStream = data.Streams.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (tagStream != null && tagStream.tags != null)
|
||||
if (tagStream != null && tagStream.Tags != null)
|
||||
{
|
||||
foreach (var pair in tagStream.tags)
|
||||
foreach (var pair in tagStream.Tags)
|
||||
{
|
||||
tags[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.format != null && data.format.tags != null)
|
||||
if (data.Format != null && data.Format.Tags != null)
|
||||
{
|
||||
foreach (var pair in data.format.tags)
|
||||
foreach (var pair in data.Format.Tags)
|
||||
{
|
||||
tags[pair.Key] = pair.Value;
|
||||
}
|
||||
@@ -153,9 +156,9 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
FetchFromItunesInfo(itunesXml, info);
|
||||
}
|
||||
|
||||
if (data.format != null && !string.IsNullOrEmpty(data.format.duration))
|
||||
if (data.Format != null && !string.IsNullOrEmpty(data.Format.Duration))
|
||||
{
|
||||
info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks;
|
||||
info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, _usCulture)).Ticks;
|
||||
}
|
||||
|
||||
FetchWtvInfo(info, data);
|
||||
@@ -513,6 +516,39 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
return codec;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaAttachment class
|
||||
/// </summary>
|
||||
/// <param name="streamInfo">The stream info.</param>
|
||||
/// <returns>MediaAttachments.</returns>
|
||||
private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
|
||||
{
|
||||
if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var attachment = new MediaAttachment
|
||||
{
|
||||
Codec = streamInfo.CodecName,
|
||||
Index = streamInfo.Index
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString))
|
||||
{
|
||||
attachment.CodecTag = streamInfo.CodecTagString;
|
||||
}
|
||||
|
||||
if (streamInfo.Tags != null)
|
||||
{
|
||||
attachment.FileName = GetDictionaryValue(streamInfo.Tags, "filename");
|
||||
attachment.MimeType = GetDictionaryValue(streamInfo.Tags, "mimetype");
|
||||
attachment.Comment = GetDictionaryValue(streamInfo.Tags, "comment");
|
||||
}
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaStream class
|
||||
/// </summary>
|
||||
@@ -523,7 +559,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
|
||||
{
|
||||
// These are mp4 chapters
|
||||
if (string.Equals(streamInfo.codec_name, "mov_text", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Edit: but these are also sometimes subtitles?
|
||||
//return null;
|
||||
@@ -531,71 +567,71 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = streamInfo.codec_name,
|
||||
Profile = streamInfo.profile,
|
||||
Level = streamInfo.level,
|
||||
Index = streamInfo.index,
|
||||
PixelFormat = streamInfo.pix_fmt,
|
||||
NalLengthSize = streamInfo.nal_length_size,
|
||||
TimeBase = streamInfo.time_base,
|
||||
CodecTimeBase = streamInfo.codec_time_base
|
||||
Codec = streamInfo.CodecName,
|
||||
Profile = streamInfo.Profile,
|
||||
Level = streamInfo.Level,
|
||||
Index = streamInfo.Index,
|
||||
PixelFormat = streamInfo.PixelFormat,
|
||||
NalLengthSize = streamInfo.NalLengthSize,
|
||||
TimeBase = streamInfo.TimeBase,
|
||||
CodecTimeBase = streamInfo.CodecTimeBase
|
||||
};
|
||||
|
||||
if (string.Equals(streamInfo.is_avc, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamInfo.is_avc, "1", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(streamInfo.IsAvc, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamInfo.IsAvc, "1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.IsAVC = true;
|
||||
}
|
||||
else if (string.Equals(streamInfo.is_avc, "false", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamInfo.is_avc, "0", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(streamInfo.IsAvc, "false", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamInfo.IsAvc, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.IsAVC = false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.field_order) && !string.Equals(streamInfo.field_order, "progressive", StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.IsInterlaced = true;
|
||||
}
|
||||
|
||||
// Filter out junk
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string) && streamInfo.codec_tag_string.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && streamInfo.CodecTagString.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
stream.CodecTag = streamInfo.codec_tag_string;
|
||||
stream.CodecTag = streamInfo.CodecTagString;
|
||||
}
|
||||
|
||||
if (streamInfo.tags != null)
|
||||
if (streamInfo.Tags != null)
|
||||
{
|
||||
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
|
||||
stream.Comment = GetDictionaryValue(streamInfo.tags, "comment");
|
||||
stream.Title = GetDictionaryValue(streamInfo.tags, "title");
|
||||
stream.Language = GetDictionaryValue(streamInfo.Tags, "language");
|
||||
stream.Comment = GetDictionaryValue(streamInfo.Tags, "comment");
|
||||
stream.Title = GetDictionaryValue(streamInfo.Tags, "title");
|
||||
}
|
||||
|
||||
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Audio;
|
||||
|
||||
stream.Channels = streamInfo.channels;
|
||||
stream.Channels = streamInfo.Channels;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
|
||||
if (!string.IsNullOrEmpty(streamInfo.SampleRate))
|
||||
{
|
||||
if (int.TryParse(streamInfo.sample_rate, NumberStyles.Any, _usCulture, out var value))
|
||||
if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value))
|
||||
{
|
||||
stream.SampleRate = value;
|
||||
}
|
||||
}
|
||||
|
||||
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
|
||||
stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout);
|
||||
|
||||
if (streamInfo.bits_per_sample > 0)
|
||||
if (streamInfo.BitsPerSample > 0)
|
||||
{
|
||||
stream.BitDepth = streamInfo.bits_per_sample;
|
||||
stream.BitDepth = streamInfo.BitsPerSample;
|
||||
}
|
||||
else if (streamInfo.bits_per_raw_sample > 0)
|
||||
else if (streamInfo.BitsPerRawSample > 0)
|
||||
{
|
||||
stream.BitDepth = streamInfo.bits_per_raw_sample;
|
||||
stream.BitDepth = streamInfo.BitsPerRawSample;
|
||||
}
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Subtitle;
|
||||
stream.Codec = NormalizeSubtitleCodec(stream.Codec);
|
||||
@@ -603,14 +639,14 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
stream.localizedDefault = _localization.GetLocalizedString("Default");
|
||||
stream.localizedForced = _localization.GetLocalizedString("Forced");
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||
? MediaStreamType.EmbeddedImage
|
||||
: MediaStreamType.Video;
|
||||
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
|
||||
|
||||
if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -635,17 +671,17 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
stream.Type = MediaStreamType.Video;
|
||||
}
|
||||
|
||||
stream.Width = streamInfo.width;
|
||||
stream.Height = streamInfo.height;
|
||||
stream.Width = streamInfo.Width;
|
||||
stream.Height = streamInfo.Height;
|
||||
stream.AspectRatio = GetAspectRatio(streamInfo);
|
||||
|
||||
if (streamInfo.bits_per_sample > 0)
|
||||
if (streamInfo.BitsPerSample > 0)
|
||||
{
|
||||
stream.BitDepth = streamInfo.bits_per_sample;
|
||||
stream.BitDepth = streamInfo.BitsPerSample;
|
||||
}
|
||||
else if (streamInfo.bits_per_raw_sample > 0)
|
||||
else if (streamInfo.BitsPerRawSample > 0)
|
||||
{
|
||||
stream.BitDepth = streamInfo.bits_per_raw_sample;
|
||||
stream.BitDepth = streamInfo.BitsPerRawSample;
|
||||
}
|
||||
|
||||
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
@@ -653,11 +689,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
|
||||
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
|
||||
stream.IsAnamorphic = string.Equals(streamInfo.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (streamInfo.refs > 0)
|
||||
if (streamInfo.Refs > 0)
|
||||
{
|
||||
stream.RefFrames = streamInfo.refs;
|
||||
stream.RefFrames = streamInfo.Refs;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -668,18 +704,18 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
// Get stream bitrate
|
||||
var bitrate = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
|
||||
if (!string.IsNullOrEmpty(streamInfo.BitRate))
|
||||
{
|
||||
if (int.TryParse(streamInfo.bit_rate, NumberStyles.Any, _usCulture, out var value))
|
||||
if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
|
||||
{
|
||||
bitrate = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
|
||||
if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.BitRate) && stream.Type == MediaStreamType.Video)
|
||||
{
|
||||
// If the stream info doesn't have a bitrate get the value from the media format info
|
||||
if (int.TryParse(formatInfo.bit_rate, NumberStyles.Any, _usCulture, out var value))
|
||||
if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
|
||||
{
|
||||
bitrate = value;
|
||||
}
|
||||
@@ -690,14 +726,18 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
stream.BitRate = bitrate;
|
||||
}
|
||||
|
||||
if (streamInfo.disposition != null)
|
||||
var disposition = streamInfo.Disposition;
|
||||
if (disposition != null)
|
||||
{
|
||||
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
|
||||
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
|
||||
if (disposition.GetValueOrDefault("default") == 1)
|
||||
{
|
||||
stream.IsDefault = true;
|
||||
}
|
||||
|
||||
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
|
||||
if (disposition.GetValueOrDefault("forced") == 1)
|
||||
{
|
||||
stream.IsForced = true;
|
||||
}
|
||||
}
|
||||
|
||||
NormalizeStreamTitle(stream);
|
||||
@@ -724,7 +764,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// <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)
|
||||
private string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
@@ -747,7 +787,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
private string GetAspectRatio(MediaStreamInfo info)
|
||||
{
|
||||
var original = info.display_aspect_ratio;
|
||||
var original = info.DisplayAspectRatio;
|
||||
|
||||
var parts = (original ?? string.Empty).Split(':');
|
||||
if (!(parts.Length == 2 &&
|
||||
@@ -756,8 +796,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
width > 0 &&
|
||||
height > 0))
|
||||
{
|
||||
width = info.width;
|
||||
height = info.height;
|
||||
width = info.Width;
|
||||
height = info.Height;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
@@ -850,20 +890,20 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
|
||||
{
|
||||
if (result.streams != null)
|
||||
if (result.Streams != null)
|
||||
{
|
||||
// Get the first info stream
|
||||
var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
|
||||
var stream = result.Streams.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
// Get duration from stream properties
|
||||
var duration = stream.duration;
|
||||
var duration = stream.Duration;
|
||||
|
||||
// If it's not there go into format properties
|
||||
if (string.IsNullOrEmpty(duration))
|
||||
{
|
||||
duration = result.format.duration;
|
||||
duration = result.Format.Duration;
|
||||
}
|
||||
|
||||
// If we got something, parse it
|
||||
@@ -877,11 +917,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
private void SetSize(InternalMediaInfoResult data, MediaInfo info)
|
||||
{
|
||||
if (data.format != null)
|
||||
if (data.Format != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(data.format.size))
|
||||
if (!string.IsNullOrEmpty(data.Format.Size))
|
||||
{
|
||||
info.Size = long.Parse(data.format.size, _usCulture);
|
||||
info.Size = long.Parse(data.Format.Size, _usCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1194,16 +1234,16 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
var info = new ChapterInfo();
|
||||
|
||||
if (chapter.tags != null)
|
||||
if (chapter.Tags != null)
|
||||
{
|
||||
if (chapter.tags.TryGetValue("title", out string name))
|
||||
if (chapter.Tags.TryGetValue("title", out string name))
|
||||
{
|
||||
info.Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit accuracy to milliseconds to match xml saving
|
||||
var secondsString = chapter.start_time;
|
||||
var secondsString = chapter.StartTime;
|
||||
|
||||
if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
|
||||
{
|
||||
@@ -1218,12 +1258,12 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data)
|
||||
{
|
||||
if (data.format == null || data.format.tags == null)
|
||||
if (data.Format == null || data.Format.Tags == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre");
|
||||
var genres = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/Genre");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
@@ -1239,14 +1279,14 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
|
||||
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
|
||||
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");
|
||||
var people = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaCredits");
|
||||
|
||||
if (!string.IsNullOrEmpty(people))
|
||||
{
|
||||
@@ -1256,7 +1296,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
|
||||
var year = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/OriginalReleaseTime");
|
||||
if (!string.IsNullOrWhiteSpace(year))
|
||||
{
|
||||
if (int.TryParse(year, NumberStyles.Integer, _usCulture, out var val))
|
||||
@@ -1265,7 +1305,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
|
||||
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime");
|
||||
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaOriginalBroadcastDateTime");
|
||||
if (!string.IsNullOrWhiteSpace(premiereDateString))
|
||||
{
|
||||
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
@@ -1276,9 +1316,9 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
|
||||
var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
|
||||
var description = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitleDescription");
|
||||
|
||||
var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle");
|
||||
var subTitle = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitle");
|
||||
|
||||
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
|
||||
@@ -1334,24 +1374,25 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
video.Timestamp = GetMpegTimestamp(video.Path);
|
||||
|
||||
_logger.LogDebug("Video has {timestamp} timestamp", video.Timestamp);
|
||||
_logger.LogDebug("Video has {Timestamp} timestamp", video.Timestamp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error extracting timestamp info from {path}", video.Path);
|
||||
_logger.LogError(ex, "Error extracting timestamp info from {Path}", video.Path);
|
||||
video.Timestamp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// REVIEW: find out why the byte array needs to be 197 bytes long and comment the reason
|
||||
private TransportStreamTimestamp GetMpegTimestamp(string path)
|
||||
{
|
||||
var packetBuffer = new byte['Å'];
|
||||
var packetBuffer = new byte[197];
|
||||
|
||||
using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
fs.Read(packetBuffer, 0, packetBuffer.Length);
|
||||
fs.Read(packetBuffer);
|
||||
}
|
||||
|
||||
if (packetBuffer[0] == 71)
|
||||
@@ -1359,7 +1400,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
return TransportStreamTimestamp.None;
|
||||
}
|
||||
|
||||
if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71))
|
||||
if ((packetBuffer[4] == 71) && (packetBuffer[196] == 71))
|
||||
{
|
||||
if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using MediaBrowser.Model.MediaInfo;
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface ISubtitleWriter
|
||||
/// Interface ISubtitleWriter.
|
||||
/// </summary>
|
||||
public interface ISubtitleWriter
|
||||
{
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON subtitle writer.
|
||||
/// </summary>
|
||||
public class JsonWriter : ISubtitleWriter
|
||||
{
|
||||
private readonly IJsonSerializer _json;
|
||||
|
||||
public JsonWriter(IJsonSerializer json)
|
||||
{
|
||||
_json = json;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
var json = _json.SerializeToString(info);
|
||||
var trackevents = info.TrackEvents;
|
||||
writer.WriteStartObject();
|
||||
writer.WriteStartArray("TrackEvents");
|
||||
|
||||
writer.Write(json);
|
||||
for (int i = 0; i < trackevents.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var current = trackevents[i];
|
||||
writer.WriteStartObject();
|
||||
|
||||
writer.WriteString("Id", current.Id);
|
||||
writer.WriteString("Text", current.Text);
|
||||
writer.WriteNumber("StartPositionTicks", current.StartPositionTicks);
|
||||
writer.WriteNumber("EndPositionTicks", current.EndPositionTicks);
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
|
||||
writer.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
var index = 1;
|
||||
var trackEvents = info.TrackEvents;
|
||||
|
||||
foreach (var trackEvent in info.TrackEvents)
|
||||
for (int i = 0; i < trackEvents.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
writer.WriteLine(index.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteLine(@"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}", TimeSpan.FromTicks(trackEvent.StartPositionTicks), TimeSpan.FromTicks(trackEvent.EndPositionTicks));
|
||||
var trackEvent = trackEvents[i];
|
||||
|
||||
writer.WriteLine((i + 1).ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteLine(
|
||||
@"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}",
|
||||
TimeSpan.FromTicks(trackEvent.StartPositionTicks),
|
||||
TimeSpan.FromTicks(trackEvent.EndPositionTicks));
|
||||
|
||||
var text = trackEvent.Text;
|
||||
|
||||
@@ -29,9 +34,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase);
|
||||
|
||||
writer.WriteLine(text);
|
||||
writer.WriteLine(string.Empty);
|
||||
|
||||
index++;
|
||||
writer.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using UtfUnknown;
|
||||
|
||||
@@ -30,28 +29,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IProcessFactory _processFactory;
|
||||
|
||||
public SubtitleEncoder(
|
||||
ILibraryManager libraryManager,
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<SubtitleEncoder> logger,
|
||||
IApplicationPaths appPaths,
|
||||
IFileSystem fileSystem,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IJsonSerializer json,
|
||||
IHttpClient httpClient,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IProcessFactory processFactory)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = loggerFactory.CreateLogger(nameof(SubtitleEncoder));
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_json = json;
|
||||
_httpClient = httpClient;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_processFactory = processFactory;
|
||||
@@ -59,7 +55,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
|
||||
|
||||
private Stream ConvertSubtitles(Stream stream,
|
||||
private Stream ConvertSubtitles(
|
||||
Stream stream,
|
||||
string inputFormat,
|
||||
string outputFormat,
|
||||
long startTimeTicks,
|
||||
@@ -170,7 +167,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
&& (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd))
|
||||
{
|
||||
var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSource.Id));
|
||||
inputFiles = mediaSourceItem.GetPlayableStreamFileNames(_mediaEncoder);
|
||||
inputFiles = mediaSourceItem.GetPlayableStreamFileNames();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -179,32 +176,27 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource.Protocol, subtitleStream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var stream = await GetSubtitleStream(fileInfo.Path, subtitleStream.Language, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
|
||||
var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return (stream, fileInfo.Format);
|
||||
}
|
||||
|
||||
private async Task<Stream> GetSubtitleStream(string path, string language, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
|
||||
private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken)
|
||||
{
|
||||
if (requiresCharset)
|
||||
{
|
||||
var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
|
||||
_logger.LogDebug("charset {CharSet} detected for {Path}", charset ?? "null", path);
|
||||
|
||||
if (!string.IsNullOrEmpty(charset))
|
||||
using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// Make sure we have all the code pages we can get
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
using (var inputStream = new MemoryStream(bytes))
|
||||
using (var reader = new StreamReader(inputStream, Encoding.GetEncoding(charset)))
|
||||
var result = CharsetDetector.DetectFromStream(stream).Detected;
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
_logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, path);
|
||||
|
||||
using var reader = new StreamReader(stream, result.Encoding);
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
bytes = Encoding.UTF8.GetBytes(text);
|
||||
|
||||
return new MemoryStream(bytes);
|
||||
return new MemoryStream(Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,7 +315,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new JsonWriter(_json);
|
||||
return new JsonWriter();
|
||||
}
|
||||
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -544,7 +536,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await ExtractTextSubtitleInternal(_mediaEncoder.GetInputArgument(inputFiles, protocol), subtitleStreamIndex, outputCodec, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
await ExtractTextSubtitleInternal(
|
||||
_mediaEncoder.GetInputArgument(inputFiles, protocol),
|
||||
subtitleStreamIndex,
|
||||
outputCodec,
|
||||
outputPath,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -572,8 +569,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||
|
||||
var processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath,
|
||||
subtitleStreamIndex, outputCodec, outputPath);
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||
inputPath,
|
||||
subtitleStreamIndex,
|
||||
outputCodec,
|
||||
outputPath);
|
||||
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
{
|
||||
@@ -721,41 +723,38 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken)
|
||||
{
|
||||
var bytes = await GetBytes(path, protocol, cancellationToken).ConfigureAwait(false);
|
||||
using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName;
|
||||
|
||||
var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
|
||||
_logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path);
|
||||
|
||||
_logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path);
|
||||
|
||||
return charset;
|
||||
return charset;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<byte[]> GetBytes(string path, MediaProtocol protocol, CancellationToken cancellationToken)
|
||||
private Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken)
|
||||
{
|
||||
if (protocol == MediaProtocol.Http)
|
||||
switch (protocol)
|
||||
{
|
||||
var opts = new HttpRequestOptions()
|
||||
{
|
||||
Url = path,
|
||||
CancellationToken = cancellationToken
|
||||
};
|
||||
using (var file = await _httpClient.Get(opts).ConfigureAwait(false))
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
await file.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
memoryStream.Position = 0;
|
||||
case MediaProtocol.Http:
|
||||
var opts = new HttpRequestOptions()
|
||||
{
|
||||
Url = path,
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = true
|
||||
};
|
||||
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
if (protocol == MediaProtocol.File)
|
||||
{
|
||||
return File.ReadAllBytes(path);
|
||||
}
|
||||
return _httpClient.Get(opts);
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(protocol));
|
||||
case MediaProtocol.File:
|
||||
return Task.FromResult<Stream>(File.OpenRead(path));
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(protocol));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,12 +49,5 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
writer.WriteLine("</tt>");
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatTime(long ticks)
|
||||
{
|
||||
var time = TimeSpan.FromTicks(ticks);
|
||||
|
||||
return string.Format(@"{0:hh\:mm\:ss\,fff}", time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
</packages>
|
||||
Reference in New Issue
Block a user