Merge branch 'master' into embytv

This commit is contained in:
Bond_009
2020-01-10 21:16:46 +01:00
410 changed files with 5390 additions and 12613 deletions

View 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;
}
}
}

View 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);
}
}
}

View File

@@ -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();

View 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());
}
}
}

View File

@@ -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"
};

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View File

@@ -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))
{

View File

@@ -5,7 +5,7 @@ using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
/// Interface ISubtitleWriter
/// Interface ISubtitleWriter.
/// </summary>
public interface ISubtitleWriter
{

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
</packages>