repo creation with initial code after cloning public repo
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.MediaEncoding.Encoder;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Attachments
|
||||
{
|
||||
/// <inheritdoc cref="IAttachmentExtractor"/>
|
||||
public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable
|
||||
{
|
||||
private readonly ILogger<AttachmentExtractor> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IPathManager _pathManager;
|
||||
|
||||
private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
|
||||
{
|
||||
o.PoolSize = 20;
|
||||
o.PoolInitialFill = 1;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AttachmentExtractor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger{AttachmentExtractor}"/>.</param>
|
||||
/// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
|
||||
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
|
||||
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
|
||||
/// <param name="pathManager">The <see cref="IPathManager"/>.</param>
|
||||
public AttachmentExtractor(
|
||||
ILogger<AttachmentExtractor> logger,
|
||||
IFileSystem fileSystem,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IPathManager pathManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_pathManager = pathManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mediaSourceId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mediaSourceId));
|
||||
}
|
||||
|
||||
var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
|
||||
var mediaSource = mediaSources
|
||||
.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
|
||||
if (mediaSource is null)
|
||||
{
|
||||
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} not found");
|
||||
}
|
||||
|
||||
var mediaAttachment = mediaSource.MediaAttachments
|
||||
.FirstOrDefault(i => i.Index == attachmentStreamIndex);
|
||||
if (mediaAttachment is null)
|
||||
{
|
||||
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}");
|
||||
}
|
||||
|
||||
if (string.Equals(mediaAttachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ResourceNotFoundException($"Attachment with stream index {attachmentStreamIndex} can't be extracted for MediaSource {mediaSourceId}");
|
||||
}
|
||||
|
||||
var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return (mediaAttachment, attachmentStream);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExtractAllAttachments(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
|
||||
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
|
||||
if (shouldExtractOneByOne && !inputFile.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var attachment in mediaSource.MediaAttachments)
|
||||
{
|
||||
if (!string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ExtractAttachment(inputFile, mediaSource, attachment, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await ExtractAllAttachmentsInternal(
|
||||
inputFile,
|
||||
mediaSource,
|
||||
false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAllAttachmentsInternal(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
bool isExternal,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||
|
||||
var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
|
||||
using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var directory = Directory.CreateDirectory(outputFolder);
|
||||
var fileNames = directory.GetFiles("*", SearchOption.TopDirectoryOnly).Select(f => f.Name).ToHashSet();
|
||||
var missingFiles = mediaSource.MediaAttachments.Where(a => a.FileName is not null && !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase));
|
||||
if (!missingFiles.Any())
|
||||
{
|
||||
// Skip extraction if all files already exist
|
||||
return;
|
||||
}
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
|
||||
inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
|
||||
inputPath);
|
||||
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = processArgs,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
WorkingDirectory = outputFolder,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Start();
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
if (isExternal && exitCode == 1)
|
||||
{
|
||||
// ffmpeg returns exitCode 1 because there is no video or audio stream
|
||||
// this can be ignored
|
||||
}
|
||||
else
|
||||
{
|
||||
failed = true;
|
||||
|
||||
_logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputFolder, exitCode);
|
||||
try
|
||||
{
|
||||
Directory.Delete(outputFolder);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted attachments {Path}", outputFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!Directory.Exists(outputFolder))
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (failed)
|
||||
{
|
||||
_logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputFolder);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputFolder));
|
||||
}
|
||||
|
||||
_logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Stream> GetAttachmentStream(
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaAttachment mediaAttachment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var attachmentPath = await ExtractAttachment(mediaSource.Path, mediaSource, mediaAttachment, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return AsyncFile.OpenRead(attachmentPath);
|
||||
}
|
||||
|
||||
private async Task<string> ExtractAttachment(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaAttachment mediaAttachment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var attachmentFolderPath = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
|
||||
using (await _semaphoreLocks.LockAsync(attachmentFolderPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture));
|
||||
if (!File.Exists(attachmentPath))
|
||||
{
|
||||
await ExtractAttachmentInternal(
|
||||
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
||||
mediaAttachment.Index,
|
||||
attachmentPath,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return attachmentPath;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAttachmentInternal(
|
||||
string inputPath,
|
||||
int attachmentStreamIndex,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(outputPath);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null",
|
||||
inputPath,
|
||||
attachmentStreamIndex,
|
||||
EncodingUtils.NormalizePath(outputPath));
|
||||
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = processArgs,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Start();
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
|
||||
|
||||
throw new InvalidOperationException(
|
||||
string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
|
||||
}
|
||||
|
||||
_logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_semaphoreLocks.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BDInfo.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.BdInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Class BdInfoDirectoryInfo.
|
||||
/// </summary>
|
||||
public class BdInfoDirectoryInfo : IDirectoryInfo
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
private readonly FileSystemMetadata _impl;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BdInfoDirectoryInfo" /> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">The filesystem.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_impl = _fileSystem.GetDirectoryInfo(path);
|
||||
}
|
||||
|
||||
private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_impl = impl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
public string Name => _impl.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full name.
|
||||
/// </summary>
|
||||
public string FullName => _impl.FullName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent directory information.
|
||||
/// </summary>
|
||||
public IDirectoryInfo? Parent
|
||||
{
|
||||
get
|
||||
{
|
||||
var parentFolder = Path.GetDirectoryName(_impl.FullName);
|
||||
if (parentFolder is not null)
|
||||
{
|
||||
return new BdInfoDirectoryInfo(_fileSystem, parentFolder);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHidden(ReadOnlySpan<char> name) => name.StartsWith('.');
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directories.
|
||||
/// </summary>
|
||||
/// <returns>An array with all directories.</returns>
|
||||
public IDirectoryInfo[] GetDirectories()
|
||||
{
|
||||
return _fileSystem.GetDirectories(_impl.FullName)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoDirectoryInfo(_fileSystem, x))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the files.
|
||||
/// </summary>
|
||||
/// <returns>All files of the directory.</returns>
|
||||
public IFileInfo[] GetFiles()
|
||||
{
|
||||
return _fileSystem.GetFiles(_impl.FullName)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the files matching a pattern.
|
||||
/// </summary>
|
||||
/// <param name="searchPattern">The search pattern.</param>
|
||||
/// <returns>All files of the directory matching the search pattern.</returns>
|
||||
public IFileInfo[] GetFiles(string searchPattern)
|
||||
{
|
||||
return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the files matching a pattern and search options.
|
||||
/// </summary>
|
||||
/// <param name="searchPattern">The search pattern.</param>
|
||||
/// <param name="searchOption">The search option.</param>
|
||||
/// <returns>All files of the directory matching the search pattern and options.</returns>
|
||||
public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
return _fileSystem.GetFiles(
|
||||
_impl.FullName,
|
||||
new[] { searchPattern },
|
||||
false,
|
||||
searchOption == SearchOption.AllDirectories)
|
||||
.Where(d => !IsHidden(d.Name))
|
||||
.Select(x => new BdInfoFileInfo(x))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bdinfo of a file system path.
|
||||
/// </summary>
|
||||
/// <param name="fs">The file system.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>The BD directory information of the path on the file system.</returns>
|
||||
public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
|
||||
{
|
||||
return new BdInfoDirectoryInfo(fs, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using BDInfo;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.BdInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Class BdInfoExaminer.
|
||||
/// </summary>
|
||||
public class BdInfoExaminer : IBlurayExaminer
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BdInfoExaminer" /> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">The filesystem.</param>
|
||||
public BdInfoExaminer(IFileSystem fileSystem)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the disc info.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>BlurayDiscInfo.</returns>
|
||||
public BlurayDiscInfo GetDiscInfo(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path));
|
||||
|
||||
bdrom.Scan();
|
||||
|
||||
// Get the longest playlist
|
||||
var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
|
||||
|
||||
var outputStream = new BlurayDiscInfo
|
||||
{
|
||||
MediaStreams = Array.Empty<MediaStream>()
|
||||
};
|
||||
|
||||
if (playlist is null)
|
||||
{
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
outputStream.Chapters = playlist.Chapters.ToArray();
|
||||
|
||||
outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
|
||||
|
||||
var sortedStreams = playlist.SortedStreams;
|
||||
var mediaStreams = new List<MediaStream>(sortedStreams.Count);
|
||||
|
||||
for (int i = 0; i < sortedStreams.Count; i++)
|
||||
{
|
||||
var stream = sortedStreams[i];
|
||||
switch (stream)
|
||||
{
|
||||
case TSVideoStream videoStream:
|
||||
AddVideoStream(mediaStreams, i, videoStream);
|
||||
break;
|
||||
case TSAudioStream audioStream:
|
||||
AddAudioStream(mediaStreams, i, audioStream);
|
||||
break;
|
||||
case TSTextStream:
|
||||
case TSGraphicsStream:
|
||||
AddSubtitleStream(mediaStreams, i, stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.MediaStreams = mediaStreams.ToArray();
|
||||
|
||||
outputStream.PlaylistName = playlist.Name;
|
||||
|
||||
if (playlist.StreamClips is not null && playlist.StreamClips.Count > 0)
|
||||
{
|
||||
// Get the files in the playlist
|
||||
outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.FileInfo.FullName).ToArray();
|
||||
}
|
||||
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the video stream.
|
||||
/// </summary>
|
||||
/// <param name="streams">The streams.</param>
|
||||
/// <param name="index">The stream index.</param>
|
||||
/// <param name="videoStream">The video stream.</param>
|
||||
private void AddVideoStream(List<MediaStream> streams, int index, TSVideoStream videoStream)
|
||||
{
|
||||
var mediaStream = new MediaStream
|
||||
{
|
||||
BitRate = Convert.ToInt32(videoStream.BitRate),
|
||||
Width = videoStream.Width,
|
||||
Height = videoStream.Height,
|
||||
Codec = GetNormalizedCodec(videoStream),
|
||||
IsInterlaced = videoStream.IsInterlaced,
|
||||
Type = MediaStreamType.Video,
|
||||
Index = index
|
||||
};
|
||||
|
||||
if (videoStream.FrameRateDenominator > 0)
|
||||
{
|
||||
float frameRateEnumerator = videoStream.FrameRateEnumerator;
|
||||
float frameRateDenominator = videoStream.FrameRateDenominator;
|
||||
|
||||
mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
|
||||
}
|
||||
|
||||
streams.Add(mediaStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="streams">The streams.</param>
|
||||
/// <param name="index">The stream index.</param>
|
||||
/// <param name="audioStream">The audio stream.</param>
|
||||
private void AddAudioStream(List<MediaStream> streams, int index, TSAudioStream audioStream)
|
||||
{
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = GetNormalizedCodec(audioStream),
|
||||
Language = audioStream.LanguageCode,
|
||||
ChannelLayout = string.Format(CultureInfo.InvariantCulture, "{0:D}.{1:D}", audioStream.ChannelCount, audioStream.LFE),
|
||||
Channels = audioStream.ChannelCount + audioStream.LFE,
|
||||
SampleRate = audioStream.SampleRate,
|
||||
Type = MediaStreamType.Audio,
|
||||
Index = index
|
||||
};
|
||||
|
||||
var bitrate = Convert.ToInt32(audioStream.BitRate);
|
||||
|
||||
if (bitrate > 0)
|
||||
{
|
||||
stream.BitRate = bitrate;
|
||||
}
|
||||
|
||||
streams.Add(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the subtitle stream.
|
||||
/// </summary>
|
||||
/// <param name="streams">The streams.</param>
|
||||
/// <param name="index">The stream index.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
private void AddSubtitleStream(List<MediaStream> streams, int index, TSStream stream)
|
||||
{
|
||||
streams.Add(new MediaStream
|
||||
{
|
||||
Language = stream.LanguageCode,
|
||||
Codec = GetNormalizedCodec(stream),
|
||||
Type = MediaStreamType.Subtitle,
|
||||
Index = index
|
||||
});
|
||||
}
|
||||
|
||||
private string GetNormalizedCodec(TSStream stream)
|
||||
=> stream.StreamType switch
|
||||
{
|
||||
TSStreamType.MPEG1_VIDEO => "mpeg1video",
|
||||
TSStreamType.MPEG2_VIDEO => "mpeg2video",
|
||||
TSStreamType.VC1_VIDEO => "vc1",
|
||||
TSStreamType.AC3_PLUS_AUDIO or TSStreamType.AC3_PLUS_SECONDARY_AUDIO => "eac3",
|
||||
TSStreamType.DTS_AUDIO or TSStreamType.DTS_HD_AUDIO or TSStreamType.DTS_HD_MASTER_AUDIO or TSStreamType.DTS_HD_SECONDARY_AUDIO => "dts",
|
||||
TSStreamType.PRESENTATION_GRAPHICS => "pgssub",
|
||||
_ => stream.CodecShortName
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.BdInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Class BdInfoFileInfo.
|
||||
/// </summary>
|
||||
public class BdInfoFileInfo : BDInfo.IO.IFileInfo
|
||||
{
|
||||
private readonly FileSystemMetadata _impl;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BdInfoFileInfo" /> class.
|
||||
/// </summary>
|
||||
/// <param name="impl">The <see cref="FileSystemMetadata" />.</param>
|
||||
public BdInfoFileInfo(FileSystemMetadata impl)
|
||||
{
|
||||
_impl = impl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
public string Name => _impl.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full name.
|
||||
/// </summary>
|
||||
public string FullName => _impl.FullName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the extension.
|
||||
/// </summary>
|
||||
public string Extension => _impl.Extension;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length.
|
||||
/// </summary>
|
||||
public long Length => _impl.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this is a directory.
|
||||
/// </summary>
|
||||
public bool IsDir => _impl.IsDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a file as file stream.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="FileStream" /> for the file.</returns>
|
||||
public Stream OpenRead()
|
||||
{
|
||||
return new FileStream(
|
||||
FullName,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a files's content with a stream reader.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="StreamReader" /> for the file's content.</returns>
|
||||
public StreamReader OpenText()
|
||||
{
|
||||
return new StreamReader(OpenRead());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Configuration
|
||||
{
|
||||
public class EncodingConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new EncodingConfigurationStore()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Configuration
|
||||
{
|
||||
public class EncodingConfigurationStore : ConfigurationStore, IValidatingConfiguration
|
||||
{
|
||||
public EncodingConfigurationStore()
|
||||
{
|
||||
ConfigurationType = typeof(EncodingOptions);
|
||||
Key = "encoding";
|
||||
}
|
||||
|
||||
public void Validate(object oldConfig, object newConfig)
|
||||
{
|
||||
var oldEncodingOptions = (EncodingOptions)oldConfig;
|
||||
var newEncodingOptions = (EncodingOptions)newConfig;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(oldEncodingOptions, nameof(oldConfig));
|
||||
ArgumentNullException.ThrowIfNull(newEncodingOptions, nameof(newConfig));
|
||||
|
||||
var newPath = newEncodingOptions.TranscodingTempPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newPath)
|
||||
&& !string.Equals(oldEncodingOptions.TranscodingTempPath, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
// Validate
|
||||
if (!Directory.Exists(newPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} does not exist.",
|
||||
newPath));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(newEncodingOptions.EncoderAppPath)
|
||||
&& !string.Equals(oldEncodingOptions.EncoderAppPath, newEncodingOptions.EncoderAppPath, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("Unable to update encoder app path.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma warning disable CA1031
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for Apple platform specific operations.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("macos")]
|
||||
public static class ApplePlatformHelper
|
||||
{
|
||||
private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"];
|
||||
|
||||
private static string GetSysctlValue(ReadOnlySpan<byte> name)
|
||||
{
|
||||
IntPtr length = IntPtr.Zero;
|
||||
// Get length of the value
|
||||
int osStatus = SysctlByName(name, IntPtr.Zero, ref length, IntPtr.Zero, 0);
|
||||
|
||||
if (osStatus != 0)
|
||||
{
|
||||
throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}");
|
||||
}
|
||||
|
||||
IntPtr buffer = Marshal.AllocHGlobal(length.ToInt32());
|
||||
try
|
||||
{
|
||||
osStatus = SysctlByName(name, buffer, ref length, IntPtr.Zero, 0);
|
||||
if (osStatus != 0)
|
||||
{
|
||||
throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}");
|
||||
}
|
||||
|
||||
return Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static int SysctlByName(ReadOnlySpan<byte> name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen)
|
||||
{
|
||||
return NativeMethods.SysctlByName(name.ToArray(), oldp, ref oldlenp, newp, newlen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the current system has hardware acceleration for AV1 decoding.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger used for error logging.</param>
|
||||
/// <returns>Boolean indicates the hwaccel support.</returns>
|
||||
public static bool HasAv1HardwareAccel(ILogger logger)
|
||||
{
|
||||
if (!RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"u8);
|
||||
return !_av1DecodeBlacklistedCpuClass.Any(blacklistedCpuClass => cpuBrandString.Contains(blacklistedCpuClass, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch (NotSupportedException e)
|
||||
{
|
||||
logger.LogError("Error getting CPU brand string: {Message}", e.Message);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError("Unknown error occured: {Exception}", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class NativeMethods
|
||||
{
|
||||
[DllImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)]
|
||||
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
|
||||
internal static extern int SysctlByName(byte[] name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
public partial class EncoderValidator
|
||||
{
|
||||
private static readonly string[] _requiredDecoders =
|
||||
[
|
||||
"h264",
|
||||
"hevc",
|
||||
"vp8",
|
||||
"libvpx",
|
||||
"vp9",
|
||||
"libvpx-vp9",
|
||||
"av1",
|
||||
"libdav1d",
|
||||
"mpeg2video",
|
||||
"mpeg4",
|
||||
"msmpeg4",
|
||||
"dca",
|
||||
"ac3",
|
||||
"ac4",
|
||||
"aac",
|
||||
"mp3",
|
||||
"flac",
|
||||
"truehd",
|
||||
"h264_qsv",
|
||||
"hevc_qsv",
|
||||
"mpeg2_qsv",
|
||||
"vc1_qsv",
|
||||
"vp8_qsv",
|
||||
"vp9_qsv",
|
||||
"av1_qsv",
|
||||
"h264_cuvid",
|
||||
"hevc_cuvid",
|
||||
"mpeg2_cuvid",
|
||||
"vc1_cuvid",
|
||||
"mpeg4_cuvid",
|
||||
"vp8_cuvid",
|
||||
"vp9_cuvid",
|
||||
"av1_cuvid",
|
||||
"h264_rkmpp",
|
||||
"hevc_rkmpp",
|
||||
"mpeg1_rkmpp",
|
||||
"mpeg2_rkmpp",
|
||||
"mpeg4_rkmpp",
|
||||
"vp8_rkmpp",
|
||||
"vp9_rkmpp",
|
||||
"av1_rkmpp"
|
||||
];
|
||||
|
||||
private static readonly string[] _requiredEncoders =
|
||||
[
|
||||
"libx264",
|
||||
"libx265",
|
||||
"libsvtav1",
|
||||
"aac",
|
||||
"aac_at",
|
||||
"libfdk_aac",
|
||||
"ac3",
|
||||
"alac",
|
||||
"dca",
|
||||
"libmp3lame",
|
||||
"libopus",
|
||||
"libvorbis",
|
||||
"flac",
|
||||
"truehd",
|
||||
"srt",
|
||||
"h264_amf",
|
||||
"hevc_amf",
|
||||
"av1_amf",
|
||||
"h264_qsv",
|
||||
"hevc_qsv",
|
||||
"mjpeg_qsv",
|
||||
"av1_qsv",
|
||||
"h264_nvenc",
|
||||
"hevc_nvenc",
|
||||
"av1_nvenc",
|
||||
"h264_vaapi",
|
||||
"hevc_vaapi",
|
||||
"av1_vaapi",
|
||||
"mjpeg_vaapi",
|
||||
"h264_v4l2m2m",
|
||||
"h264_videotoolbox",
|
||||
"hevc_videotoolbox",
|
||||
"mjpeg_videotoolbox",
|
||||
"h264_rkmpp",
|
||||
"hevc_rkmpp",
|
||||
"mjpeg_rkmpp"
|
||||
];
|
||||
|
||||
private static readonly string[] _requiredFilters =
|
||||
[
|
||||
// sw
|
||||
"alphasrc",
|
||||
"zscale",
|
||||
"tonemapx",
|
||||
// qsv
|
||||
"scale_qsv",
|
||||
"vpp_qsv",
|
||||
"deinterlace_qsv",
|
||||
"overlay_qsv",
|
||||
// cuda
|
||||
"scale_cuda",
|
||||
"yadif_cuda",
|
||||
"bwdif_cuda",
|
||||
"tonemap_cuda",
|
||||
"overlay_cuda",
|
||||
"transpose_cuda",
|
||||
"hwupload_cuda",
|
||||
// opencl
|
||||
"scale_opencl",
|
||||
"tonemap_opencl",
|
||||
"overlay_opencl",
|
||||
"transpose_opencl",
|
||||
"yadif_opencl",
|
||||
"bwdif_opencl",
|
||||
// vaapi
|
||||
"scale_vaapi",
|
||||
"deinterlace_vaapi",
|
||||
"tonemap_vaapi",
|
||||
"procamp_vaapi",
|
||||
"overlay_vaapi",
|
||||
"transpose_vaapi",
|
||||
"hwupload_vaapi",
|
||||
// vulkan
|
||||
"libplacebo",
|
||||
"scale_vulkan",
|
||||
"overlay_vulkan",
|
||||
"transpose_vulkan",
|
||||
"flip_vulkan",
|
||||
// videotoolbox
|
||||
"yadif_videotoolbox",
|
||||
"bwdif_videotoolbox",
|
||||
"scale_vt",
|
||||
"transpose_vt",
|
||||
"overlay_videotoolbox",
|
||||
"tonemap_videotoolbox",
|
||||
// rkrga
|
||||
"scale_rkrga",
|
||||
"vpp_rkrga",
|
||||
"overlay_rkrga"
|
||||
];
|
||||
|
||||
private static readonly Dictionary<FilterOptionType, (string, string)> _filterOptionsDict = new Dictionary<FilterOptionType, (string, string)>
|
||||
{
|
||||
{ FilterOptionType.ScaleCudaFormat, ("scale_cuda", "format") },
|
||||
{ FilterOptionType.TonemapCudaName, ("tonemap_cuda", "GPU accelerated HDR to SDR tonemapping") },
|
||||
{ FilterOptionType.TonemapOpenclBt2390, ("tonemap_opencl", "bt2390") },
|
||||
{ FilterOptionType.OverlayOpenclFrameSync, ("overlay_opencl", "Action to take when encountering EOF from secondary input") },
|
||||
{ FilterOptionType.OverlayVaapiFrameSync, ("overlay_vaapi", "Action to take when encountering EOF from secondary input") },
|
||||
{ FilterOptionType.OverlayVulkanFrameSync, ("overlay_vulkan", "Action to take when encountering EOF from secondary input") },
|
||||
{ FilterOptionType.TransposeOpenclReversal, ("transpose_opencl", "rotate by half-turn") },
|
||||
{ FilterOptionType.OverlayOpenclAlphaFormat, ("overlay_opencl", "alpha_format") },
|
||||
{ FilterOptionType.OverlayCudaAlphaFormat, ("overlay_cuda", "alpha_format") }
|
||||
};
|
||||
|
||||
private static readonly Dictionary<BitStreamFilterOptionType, (string, string)> _bsfOptionsDict = new Dictionary<BitStreamFilterOptionType, (string, string)>
|
||||
{
|
||||
{ BitStreamFilterOptionType.HevcMetadataRemoveDovi, ("hevc_metadata", "remove_dovi") },
|
||||
{ BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus, ("hevc_metadata", "remove_hdr10plus") },
|
||||
{ BitStreamFilterOptionType.Av1MetadataRemoveDovi, ("av1_metadata", "remove_dovi") },
|
||||
{ BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus, ("av1_metadata", "remove_hdr10plus") },
|
||||
{ BitStreamFilterOptionType.DoviRpuStrip, ("dovi_rpu", "strip") }
|
||||
};
|
||||
|
||||
// These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below
|
||||
// Refers to the versions in https://ffmpeg.org/download.html
|
||||
private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
|
||||
{
|
||||
{ "libavutil", new Version(56, 70) },
|
||||
{ "libavcodec", new Version(58, 134) },
|
||||
{ "libavformat", new Version(58, 76) },
|
||||
{ "libavdevice", new Version(58, 13) },
|
||||
{ "libavfilter", new Version(7, 110) },
|
||||
{ "libswscale", new Version(5, 9) },
|
||||
{ "libswresample", new Version(3, 9) },
|
||||
{ "libpostproc", new Version(55, 9) }
|
||||
};
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly string _encoderPath;
|
||||
|
||||
private readonly Version _minFFmpegMultiThreadedCli = new Version(7, 0);
|
||||
|
||||
public EncoderValidator(ILogger logger, string encoderPath)
|
||||
{
|
||||
_logger = logger;
|
||||
_encoderPath = encoderPath;
|
||||
}
|
||||
|
||||
private enum Codec
|
||||
{
|
||||
Encoder,
|
||||
Decoder
|
||||
}
|
||||
|
||||
// When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions
|
||||
public static Version MinVersion { get; } = new Version(4, 4);
|
||||
|
||||
public static Version? MaxVersion { get; } = null;
|
||||
|
||||
[GeneratedRegex(@"^ffmpeg version n?((?:[0-9]+\.?)+)")]
|
||||
private static partial Regex FfmpegVersionRegex();
|
||||
|
||||
[GeneratedRegex(@"((?<name>lib\w+)\s+(?<major>[0-9]+)\.\s*(?<minor>[0-9]+))", RegexOptions.Multiline)]
|
||||
private static partial Regex LibraryRegex();
|
||||
|
||||
public bool ValidateVersion()
|
||||
{
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-version", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating encoder");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
_logger.LogError("FFmpeg validation: The process returned no result");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogDebug("ffmpeg output: {Output}", output);
|
||||
|
||||
return ValidateVersionInternal(output);
|
||||
}
|
||||
|
||||
internal bool ValidateVersionInternal(string versionOutput)
|
||||
{
|
||||
if (versionOutput.Contains("Libav developers", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Work out what the version under test is
|
||||
var version = GetFFmpegVersionInternal(versionOutput);
|
||||
|
||||
_logger.LogInformation("Found ffmpeg version {Version}", version is not null ? version.ToString() : "unknown");
|
||||
|
||||
if (version is null)
|
||||
{
|
||||
if (MaxVersion is not null) // Version is unknown
|
||||
{
|
||||
if (MinVersion == MaxVersion)
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: We recommend version {MinVersion}", MinVersion);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: We recommend a minimum of {MinVersion} and maximum of {MaxVersion}", MinVersion, MaxVersion);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: We recommend minimum version {MinVersion}", MinVersion);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version < MinVersion) // Version is below what we recommend
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: The minimum recommended version is {MinVersion}", MinVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (MaxVersion is not null && version > MaxVersion) // Version is above what we recommend
|
||||
{
|
||||
_logger.LogWarning("FFmpeg validation: The maximum recommended version is {MaxVersion}", MaxVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetDecoders() => GetCodecs(Codec.Decoder);
|
||||
|
||||
public IEnumerable<string> GetEncoders() => GetCodecs(Codec.Encoder);
|
||||
|
||||
public IEnumerable<string> GetHwaccels() => GetHwaccelTypes();
|
||||
|
||||
public IEnumerable<string> GetFilters() => GetFFmpegFilters();
|
||||
|
||||
public IDictionary<FilterOptionType, bool> GetFiltersWithOption() => _filterOptionsDict
|
||||
.ToDictionary(item => item.Key, item => CheckFilterWithOption(item.Value.Item1, item.Value.Item2));
|
||||
|
||||
public IDictionary<BitStreamFilterOptionType, bool> GetBitStreamFiltersWithOption() => _bsfOptionsDict
|
||||
.ToDictionary(item => item.Key, item => CheckBitStreamFilterWithOption(item.Value.Item1, item.Value.Item2));
|
||||
|
||||
public Version? GetFFmpegVersion()
|
||||
{
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-version", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating encoder");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
_logger.LogError("FFmpeg validation: The process returned no result");
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogDebug("ffmpeg output: {Output}", output);
|
||||
|
||||
return GetFFmpegVersionInternal(output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Using the output from "ffmpeg -version" work out the FFmpeg version.
|
||||
/// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
|
||||
/// to parse. If this is not available, then we try to match known library versions to FFmpeg versions.
|
||||
/// If that fails then we test the libraries to determine if they're newer than our minimum versions.
|
||||
/// </summary>
|
||||
/// <param name="output">The output from "ffmpeg -version".</param>
|
||||
/// <returns>The FFmpeg version.</returns>
|
||||
internal Version? GetFFmpegVersionInternal(string output)
|
||||
{
|
||||
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
|
||||
var match = FfmpegVersionRegex().Match(output);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
if (Version.TryParse(match.Groups[1].ValueSpan, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
var versionMap = GetFFmpegLibraryVersions(output);
|
||||
|
||||
var allVersionsValidated = true;
|
||||
|
||||
foreach (var minimumVersion in _ffmpegMinimumLibraryVersions)
|
||||
{
|
||||
if (versionMap.TryGetValue(minimumVersion.Key, out var foundVersion))
|
||||
{
|
||||
if (foundVersion >= minimumVersion.Value)
|
||||
{
|
||||
_logger.LogInformation("Found {Library} version {FoundVersion} ({MinimumVersion})", minimumVersion.Key, foundVersion, minimumVersion.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Found {Library} version {FoundVersion} lower than recommended version {MinimumVersion}", minimumVersion.Key, foundVersion, minimumVersion.Value);
|
||||
allVersionsValidated = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("{Library} version not found", minimumVersion.Key);
|
||||
allVersionsValidated = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allVersionsValidated ? MinVersion : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output
|
||||
/// and condenses them on to one line. Output format is "name1=major.minor,name2=major.minor,etc.".
|
||||
/// </summary>
|
||||
/// <param name="output">The 'ffmpeg -version' output.</param>
|
||||
/// <returns>The library names and major.minor version numbers.</returns>
|
||||
private static Dictionary<string, Version> GetFFmpegLibraryVersions(string output)
|
||||
{
|
||||
var map = new Dictionary<string, Version>();
|
||||
|
||||
foreach (Match match in LibraryRegex().Matches(output))
|
||||
{
|
||||
var version = new Version(
|
||||
int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture),
|
||||
int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture));
|
||||
|
||||
map.Add(match.Groups["name"].Value, version);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public bool CheckVaapiDeviceByDriverName(string driverName, string renderNodePath)
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(driverName) || string.IsNullOrEmpty(renderNodePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + renderNodePath, true, null);
|
||||
return output.Contains(driverName, StringComparison.Ordinal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting the given vaapi render node path");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckVulkanDrmDeviceByExtensionName(string renderNodePath, string[] vulkanExtensions)
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(renderNodePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var command = "-v verbose -hide_banner -init_hw_device drm=dr:" + renderNodePath + " -init_hw_device vulkan=vk@dr";
|
||||
var output = GetProcessOutput(_encoderPath, command, true, null);
|
||||
foreach (string ext in vulkanExtensions)
|
||||
{
|
||||
if (!output.Contains(ext, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting the given drm render node path");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("macos")]
|
||||
public bool CheckIsVideoToolboxAv1DecodeAvailable()
|
||||
{
|
||||
return ApplePlatformHelper.HasAv1HardwareAccel(_logger);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetHwaccelTypes()
|
||||
{
|
||||
string? output = null;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-hwaccels", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting available hwaccel types");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var found = output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList();
|
||||
_logger.LogInformation("Available hwaccel types: {Types}", found);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
public bool CheckFilterWithOption(string filter, string option)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting the given filter");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (output.Contains("Filter " + filter, StringComparison.Ordinal))
|
||||
{
|
||||
return output.Contains(option, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Filter: {Name} with option {Option} is not available", filter, option);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CheckBitStreamFilterWithOption(string filter, string option)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-h bsf=" + filter, false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting the given bit stream filter");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (output.Contains("Bit stream filter " + filter, StringComparison.Ordinal))
|
||||
{
|
||||
return output.Contains(option, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Bit stream filter: {Name} with option {Option} is not available", filter, option);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyDesc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string output;
|
||||
try
|
||||
{
|
||||
// With multi-threaded cli support, FFmpeg 7 is less sensitive to keyboard input
|
||||
var duration = ffmpegVersion >= _minFFmpegMultiThreadedCli ? 10000 : 1000;
|
||||
output = GetProcessOutput(_encoderPath, $"-hide_banner -f lavfi -i nullsrc=s=1x1:d={duration} -f null -", true, "?");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking supported runtime key");
|
||||
return false;
|
||||
}
|
||||
|
||||
return output.Contains(keyDesc, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public bool CheckSupportedHwaccelFlag(string flag)
|
||||
{
|
||||
return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{flag} -hide_banner -f lavfi -i nullsrc=s=1x1:d=100 -f null -");
|
||||
}
|
||||
|
||||
public bool CheckSupportedProberOption(string option, string proberPath)
|
||||
{
|
||||
return !string.IsNullOrEmpty(option) && GetProcessExitCode(proberPath, $"-loglevel quiet -f lavfi -i nullsrc=s=1x1:d=1 -{option}");
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetCodecs(Codec codec)
|
||||
{
|
||||
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-" + codecstr, false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting available {Codec}", codecstr);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders;
|
||||
|
||||
var found = CodecRegex()
|
||||
.Matches(output)
|
||||
.Select(x => x.Groups["codec"].Value)
|
||||
.Where(x => required.Contains(x));
|
||||
|
||||
_logger.LogInformation("Available {Codec}: {Codecs}", codecstr, found);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetFFmpegFilters()
|
||||
{
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-filters", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error detecting available filters");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var found = FilterRegex()
|
||||
.Matches(output)
|
||||
.Select(x => x.Groups["filter"].Value)
|
||||
.Where(x => _requiredFilters.Contains(x));
|
||||
|
||||
_logger.LogInformation("Available filters: {Filters}", found);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
|
||||
{
|
||||
var redirectStandardIn = !string.IsNullOrEmpty(testKey);
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo(path, arguments)
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false,
|
||||
RedirectStandardInput = redirectStandardIn,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
}
|
||||
})
|
||||
{
|
||||
_logger.LogDebug("Running {Path} {Arguments}", path, arguments);
|
||||
|
||||
process.Start();
|
||||
|
||||
if (redirectStandardIn)
|
||||
{
|
||||
using var writer = process.StandardInput;
|
||||
writer.Write(testKey);
|
||||
}
|
||||
|
||||
using var reader = readStdErr ? process.StandardError : process.StandardOutput;
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private bool GetProcessExitCode(string path, string arguments)
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo(path, arguments)
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
};
|
||||
_logger.LogDebug("Running {Path} {Arguments}", path, arguments);
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Running {Path} {Arguments} failed with exception {Exception}", path, arguments, ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
private static partial Regex CodecRegex();
|
||||
|
||||
[GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
|
||||
private static partial Regex FilterRegex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
public static class EncodingUtils
|
||||
{
|
||||
public static string GetInputArgument(string inputPrefix, string inputFile, MediaProtocol protocol)
|
||||
{
|
||||
if (protocol != MediaProtocol.File)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFile);
|
||||
}
|
||||
|
||||
return GetFileInputArgument(inputFile, inputPrefix);
|
||||
}
|
||||
|
||||
public static string GetInputArgument(string inputPrefix, IReadOnlyList<string> inputFiles, MediaProtocol protocol)
|
||||
{
|
||||
if (protocol != MediaProtocol.File)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFiles[0]);
|
||||
}
|
||||
|
||||
return GetConcatInputArgument(inputFiles, inputPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the concat input argument.
|
||||
/// </summary>
|
||||
/// <param name="inputFiles">The input files.</param>
|
||||
/// <param name="inputPrefix">The input prefix.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string GetConcatInputArgument(IReadOnlyList<string> inputFiles, string inputPrefix)
|
||||
{
|
||||
// Get all streams
|
||||
// If there's more than one we'll need to use the concat command
|
||||
if (inputFiles.Count > 1)
|
||||
{
|
||||
var files = string.Join('|', inputFiles.Select(NormalizePath));
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files);
|
||||
}
|
||||
|
||||
// Determine the input path for video files
|
||||
return GetFileInputArgument(inputFiles[0], inputPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file input argument.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="inputPrefix">The path prefix.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string GetFileInputArgument(string path, string inputPrefix)
|
||||
{
|
||||
if (path.Contains("://", StringComparison.Ordinal))
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path);
|
||||
}
|
||||
|
||||
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
|
||||
path = NormalizePath(path);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "{1}:\"{0}\"", path, inputPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string NormalizePath(string path)
|
||||
{
|
||||
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
|
||||
return path.Replace("\"", "\\\"", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{960295EE-4AF4-4440-A525-B4C295B01A61}</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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="AsyncKeyedLock" />
|
||||
<PackageReference Include="BDInfo" />
|
||||
<PackageReference Include="libse" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="UTF.Unknown" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="IDisposableAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace MediaBrowser.MediaEncoding.Probing;
|
||||
|
||||
/// <summary>
|
||||
/// FFmpeg Codec Type.
|
||||
/// </summary>
|
||||
public enum CodecType
|
||||
{
|
||||
/// <summary>
|
||||
/// Video.
|
||||
/// </summary>
|
||||
Video,
|
||||
|
||||
/// <summary>
|
||||
/// Audio.
|
||||
/// </summary>
|
||||
Audio,
|
||||
|
||||
/// <summary>
|
||||
/// Opaque data information usually continuous.
|
||||
/// </summary>
|
||||
Data,
|
||||
|
||||
/// <summary>
|
||||
/// Subtitles.
|
||||
/// </summary>
|
||||
Subtitle,
|
||||
|
||||
/// <summary>
|
||||
/// Opaque data information usually sparse.
|
||||
/// </summary>
|
||||
Attachment
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class containing helper methods for working with FFprobe output.
|
||||
/// </summary>
|
||||
public static class FFProbeHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes the FF probe result.
|
||||
/// </summary>
|
||||
/// <param name="result">The result.</param>
|
||||
public static void NormalizeFFProbeResult(InternalMediaInfoResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
if (result.Format?.Tags is not null)
|
||||
{
|
||||
result.Format.Tags = ConvertDictionaryToCaseInsensitive(result.Format.Tags);
|
||||
}
|
||||
|
||||
if (result.Streams is not null)
|
||||
{
|
||||
// Convert all dictionaries to case-insensitive
|
||||
foreach (var stream in result.Streams)
|
||||
{
|
||||
if (stream.Tags is not null)
|
||||
{
|
||||
stream.Tags = ConvertDictionaryToCaseInsensitive(stream.Tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an int from an FFProbeResult tags dictionary.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
public static int? GetDictionaryNumericValue(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags.TryGetValue(key, out var val) && int.TryParse(val, out var i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a DateTime from an FFProbeResult tags dictionary.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.Nullable{DateTime}.</returns>
|
||||
public static DateTime? GetDictionaryDateTime(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags.TryGetValue(key, out var val)
|
||||
&& (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime)
|
||||
|| DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out dateTime)))
|
||||
{
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a dictionary to case-insensitive.
|
||||
/// </summary>
|
||||
/// <param name="dict">The dict.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict)
|
||||
{
|
||||
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaInfoResult.
|
||||
/// </summary>
|
||||
public class InternalMediaInfoResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the streams.
|
||||
/// </summary>
|
||||
/// <value>The streams.</value>
|
||||
[JsonPropertyName("streams")]
|
||||
public IReadOnlyList<MediaStreamInfo> Streams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format.
|
||||
/// </summary>
|
||||
/// <value>The format.</value>
|
||||
[JsonPropertyName("format")]
|
||||
public MediaFormatInfo Format { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the chapters.
|
||||
/// </summary>
|
||||
/// <value>The chapters.</value>
|
||||
[JsonPropertyName("chapters")]
|
||||
public IReadOnlyList<MediaChapter> Chapters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the frames.
|
||||
/// </summary>
|
||||
/// <value>The streams.</value>
|
||||
[JsonPropertyName("frames")]
|
||||
public IReadOnlyList<MediaFrameInfo> Frames { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaChapter.
|
||||
/// </summary>
|
||||
public class MediaChapter
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long 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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#nullable disable
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing;
|
||||
|
||||
/// <summary>
|
||||
/// Class MediaFrameInfo.
|
||||
/// </summary>
|
||||
public class MediaFrameInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the media type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("media_type")]
|
||||
public string? MediaType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the StreamIndex.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stream_index")]
|
||||
public int? StreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the KeyFrame.
|
||||
/// </summary>
|
||||
[JsonPropertyName("key_frame")]
|
||||
public int? KeyFrame { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Pts.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pts")]
|
||||
public long? Pts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PtsTime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pts_time")]
|
||||
public string? PtsTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BestEffortTimestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("best_effort_timestamp")]
|
||||
public long BestEffortTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BestEffortTimestampTime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("best_effort_timestamp_time")]
|
||||
public string? BestEffortTimestampTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Duration.
|
||||
/// </summary>
|
||||
[JsonPropertyName("duration")]
|
||||
public int Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DurationTime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("duration_time")]
|
||||
public string? DurationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PktPos.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pkt_pos")]
|
||||
public string? PktPos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PktSize.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pkt_size")]
|
||||
public string? PktSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Width.
|
||||
/// </summary>
|
||||
[JsonPropertyName("width")]
|
||||
public int? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Height.
|
||||
/// </summary>
|
||||
[JsonPropertyName("height")]
|
||||
public int? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CropTop.
|
||||
/// </summary>
|
||||
[JsonPropertyName("crop_top")]
|
||||
public int? CropTop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CropBottom.
|
||||
/// </summary>
|
||||
[JsonPropertyName("crop_bottom")]
|
||||
public int? CropBottom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CropLeft.
|
||||
/// </summary>
|
||||
[JsonPropertyName("crop_left")]
|
||||
public int? CropLeft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CropRight.
|
||||
/// </summary>
|
||||
[JsonPropertyName("crop_right")]
|
||||
public int? CropRight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PixFmt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pix_fmt")]
|
||||
public string? PixFmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SampleAspectRatio.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sample_aspect_ratio")]
|
||||
public string? SampleAspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PictType.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pict_type")]
|
||||
public string? PictType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the InterlacedFrame.
|
||||
/// </summary>
|
||||
[JsonPropertyName("interlaced_frame")]
|
||||
public int? InterlacedFrame { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TopFieldFirst.
|
||||
/// </summary>
|
||||
[JsonPropertyName("top_field_first")]
|
||||
public int? TopFieldFirst { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the RepeatPict.
|
||||
/// </summary>
|
||||
[JsonPropertyName("repeat_pict")]
|
||||
public int? RepeatPict { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ColorRange.
|
||||
/// </summary>
|
||||
[JsonPropertyName("color_range")]
|
||||
public string? ColorRange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ColorSpace.
|
||||
/// </summary>
|
||||
[JsonPropertyName("color_space")]
|
||||
public string? ColorSpace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ColorPrimaries.
|
||||
/// </summary>
|
||||
[JsonPropertyName("color_primaries")]
|
||||
public string? ColorPrimaries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ColorTransfer.
|
||||
/// </summary>
|
||||
[JsonPropertyName("color_transfer")]
|
||||
public string? ColorTransfer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ChromaLocation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("chroma_location")]
|
||||
public string? ChromaLocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SideDataList.
|
||||
/// </summary>
|
||||
[JsonPropertyName("side_data_list")]
|
||||
public IReadOnlyList<MediaFrameSideDataInfo>? SideDataList { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing;
|
||||
|
||||
/// <summary>
|
||||
/// Class MediaFrameSideDataInfo.
|
||||
/// Currently only records the SideDataType for HDR10+ detection.
|
||||
/// </summary>
|
||||
public class MediaFrameSideDataInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SideDataType.
|
||||
/// </summary>
|
||||
[JsonPropertyName("side_data_type")]
|
||||
public string? SideDataType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
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 CodecType 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")]
|
||||
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 long StartPts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the stream is AVC.
|
||||
/// </summary>
|
||||
/// <value>The is_avc.</value>
|
||||
[JsonPropertyName("is_avc")]
|
||||
public bool 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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the field_order.
|
||||
/// </summary>
|
||||
/// <value>The field_order.</value>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color range.
|
||||
/// </summary>
|
||||
/// <value>The color range.</value>
|
||||
[JsonPropertyName("color_range")]
|
||||
public string ColorRange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color space.
|
||||
/// </summary>
|
||||
/// <value>The color space.</value>
|
||||
[JsonPropertyName("color_space")]
|
||||
public string ColorSpace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color transfer.
|
||||
/// </summary>
|
||||
/// <value>The color transfer.</value>
|
||||
[JsonPropertyName("color_transfer")]
|
||||
public string ColorTransfer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color primaries.
|
||||
/// </summary>
|
||||
/// <value>The color primaries.</value>
|
||||
[JsonPropertyName("color_primaries")]
|
||||
public string ColorPrimaries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the side_data_list.
|
||||
/// </summary>
|
||||
/// <value>The side_data_list.</value>
|
||||
[JsonPropertyName("side_data_list")]
|
||||
public IReadOnlyList<MediaStreamInfoSideData> SideDataList { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaStreamInfoSideData.
|
||||
/// </summary>
|
||||
public class MediaStreamInfoSideData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SideDataType.
|
||||
/// </summary>
|
||||
/// <value>The SideDataType.</value>
|
||||
[JsonPropertyName("side_data_type")]
|
||||
public string? SideDataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvVersionMajor.
|
||||
/// </summary>
|
||||
/// <value>The DvVersionMajor.</value>
|
||||
[JsonPropertyName("dv_version_major")]
|
||||
public int? DvVersionMajor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvVersionMinor.
|
||||
/// </summary>
|
||||
/// <value>The DvVersionMinor.</value>
|
||||
[JsonPropertyName("dv_version_minor")]
|
||||
public int? DvVersionMinor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvProfile.
|
||||
/// </summary>
|
||||
/// <value>The DvProfile.</value>
|
||||
[JsonPropertyName("dv_profile")]
|
||||
public int? DvProfile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvLevel.
|
||||
/// </summary>
|
||||
/// <value>The DvLevel.</value>
|
||||
[JsonPropertyName("dv_level")]
|
||||
public int? DvLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the RpuPresentFlag.
|
||||
/// </summary>
|
||||
/// <value>The RpuPresentFlag.</value>
|
||||
[JsonPropertyName("rpu_present_flag")]
|
||||
public int? RpuPresentFlag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ElPresentFlag.
|
||||
/// </summary>
|
||||
/// <value>The ElPresentFlag.</value>
|
||||
[JsonPropertyName("el_present_flag")]
|
||||
public int? ElPresentFlag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BlPresentFlag.
|
||||
/// </summary>
|
||||
/// <value>The BlPresentFlag.</value>
|
||||
[JsonPropertyName("bl_present_flag")]
|
||||
public int? BlPresentFlag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvBlSignalCompatibilityId.
|
||||
/// </summary>
|
||||
/// <value>The DvBlSignalCompatibilityId.</value>
|
||||
[JsonPropertyName("dv_bl_signal_compatibility_id")]
|
||||
public int? DvBlSignalCompatibilityId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Rotation in degrees.
|
||||
/// </summary>
|
||||
/// <value>The Rotation.</value>
|
||||
[JsonPropertyName("rotation")]
|
||||
public int? Rotation { get; set; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MediaBrowser.MediaEncoding")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Jellyfin Project")]
|
||||
[assembly: AssemblyProduct("Jellyfin Server")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: NeutralResourcesLanguage("en")]
|
||||
[assembly: InternalsVisibleTo("Jellyfin.MediaEncoding.Tests")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// ASS subtitle writer.
|
||||
/// </summary>
|
||||
public partial class AssWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewLineRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
var trackEvents = info.TrackEvents;
|
||||
var timeFormat = @"hh\:mm\:ss\.ff";
|
||||
|
||||
// Write ASS header
|
||||
writer.WriteLine("[Script Info]");
|
||||
writer.WriteLine("Title: Jellyfin transcoded ASS subtitle");
|
||||
writer.WriteLine("ScriptType: v4.00+");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("[V4+ Styles]");
|
||||
writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
|
||||
writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H910E0807,0,0,0,0,100,100,0,0,0,1,0,2,10,10,10,1");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("[Events]");
|
||||
writer.WriteLine("Format: Layer, Start, End, Style, Text");
|
||||
|
||||
for (int i = 0; i < trackEvents.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var trackEvent = trackEvents[i];
|
||||
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var text = NewLineRegex().Replace(trackEvent.Text, "\\n");
|
||||
|
||||
writer.WriteLine(
|
||||
"Dialogue: 0,{0},{1},Default,{2}",
|
||||
startTime,
|
||||
endTime,
|
||||
text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
public interface ISubtitleParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the specified stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="fileExtension">The file extension.</param>
|
||||
/// <returns>SubtitleTrackInfo.</returns>
|
||||
SubtitleTrackInfo Parse(Stream stream, string fileExtension);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the file extension is supported by the parser.
|
||||
/// </summary>
|
||||
/// <param name="fileExtension">The file extension.</param>
|
||||
/// <returns>A value indicating whether the file extension is supported.</returns>
|
||||
bool SupportsFileExtension(string fileExtension);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface ISubtitleWriter.
|
||||
/// </summary>
|
||||
public interface ISubtitleWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes the specified information.
|
||||
/// </summary>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON subtitle writer.
|
||||
/// </summary>
|
||||
public class JsonWriter : ISubtitleWriter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
var trackevents = info.TrackEvents;
|
||||
writer.WriteStartObject();
|
||||
writer.WriteStartArray("TrackEvents");
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// SRT subtitle writer.
|
||||
/// </summary>
|
||||
public partial class SrtWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewLineEscapedRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
var trackEvents = info.TrackEvents;
|
||||
|
||||
for (int i = 0; i < trackEvents.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
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;
|
||||
|
||||
// TODO: Not sure how to handle these
|
||||
text = NewLineEscapedRegex().Replace(text, " ");
|
||||
|
||||
writer.WriteLine(text);
|
||||
writer.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// SSA subtitle writer.
|
||||
/// </summary>
|
||||
public partial class SsaWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewLineRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
var trackEvents = info.TrackEvents;
|
||||
var timeFormat = @"hh\:mm\:ss\.ff";
|
||||
|
||||
// Write SSA header
|
||||
writer.WriteLine("[Script Info]");
|
||||
writer.WriteLine("Title: Jellyfin transcoded SSA subtitle");
|
||||
writer.WriteLine("ScriptType: v4.00");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("[V4 Styles]");
|
||||
writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding");
|
||||
writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H19333333,0,0,0,1,0,2,10,10,10,0,1");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("[Events]");
|
||||
writer.WriteLine("Format: Layer, Start, End, Style, Text");
|
||||
|
||||
for (int i = 0; i < trackEvents.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var trackEvent = trackEvents[i];
|
||||
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var text = NewLineRegex().Replace(trackEvent.Text, "\\n");
|
||||
|
||||
writer.WriteLine(
|
||||
"Dialogue: 0,{0},{1},Default,{2}",
|
||||
startTime,
|
||||
endTime,
|
||||
text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nikse.SubtitleEdit.Core.Common;
|
||||
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// SubStation Alpha subtitle parser.
|
||||
/// </summary>
|
||||
public class SubtitleEditParser : ISubtitleParser
|
||||
{
|
||||
private readonly ILogger<SubtitleEditParser> _logger;
|
||||
private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_subtitleFormatTypes = GetSubtitleFormatTypes();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SubtitleTrackInfo Parse(Stream stream, string fileExtension)
|
||||
{
|
||||
var subtitle = new Subtitle();
|
||||
var lines = stream.ReadAllLines().ToList();
|
||||
|
||||
if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension))
|
||||
{
|
||||
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
|
||||
}
|
||||
|
||||
foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
|
||||
{
|
||||
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
|
||||
_logger.LogDebug(
|
||||
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
||||
fileExtension,
|
||||
subtitleFormat.Name);
|
||||
subtitleFormat.LoadSubtitle(subtitle, lines, fileExtension);
|
||||
if (subtitleFormat.ErrorCount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (subtitleFormat.TryGetErrors(out var errors))
|
||||
{
|
||||
_logger.LogError(
|
||||
"{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser, errors: {Errors}",
|
||||
subtitleFormat.ErrorCount,
|
||||
fileExtension,
|
||||
subtitleFormat.Name,
|
||||
errors);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
"{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
||||
subtitleFormat.ErrorCount,
|
||||
fileExtension,
|
||||
subtitleFormat.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (subtitle.Paragraphs.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Unsupported format: " + fileExtension);
|
||||
}
|
||||
|
||||
var trackInfo = new SubtitleTrackInfo();
|
||||
int len = subtitle.Paragraphs.Count;
|
||||
var trackEvents = new SubtitleTrackEvent[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var p = subtitle.Paragraphs[i];
|
||||
trackEvents[i] = new SubtitleTrackEvent(p.Number.ToString(CultureInfo.InvariantCulture), p.Text)
|
||||
{
|
||||
StartPositionTicks = p.StartTime.TimeSpan.Ticks,
|
||||
EndPositionTicks = p.EndTime.TimeSpan.Ticks
|
||||
};
|
||||
}
|
||||
|
||||
trackInfo.TrackEvents = trackEvents;
|
||||
return trackInfo;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsFileExtension(string fileExtension)
|
||||
=> _subtitleFormatTypes.ContainsKey(fileExtension);
|
||||
|
||||
private Dictionary<string, List<Type>> GetSubtitleFormatTypes()
|
||||
{
|
||||
var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase);
|
||||
var assembly = typeof(SubtitleFormat).Assembly;
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
if (!type.IsSubclassOf(typeof(SubtitleFormat)) || type.IsAbstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
|
||||
var extension = tempInstance.Extension.TrimStart('.');
|
||||
if (!string.IsNullOrEmpty(extension))
|
||||
{
|
||||
// Store only the type, we will instantiate from it later
|
||||
if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension))
|
||||
{
|
||||
subtitleFormatTypes[extension] = [type];
|
||||
}
|
||||
else
|
||||
{
|
||||
subtitleFormatTypesForExtension.Add(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create instance of the subtitle format {SubtitleFormatType}", type.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return subtitleFormatTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Nikse.SubtitleEdit.Core.SubtitleFormats;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles;
|
||||
|
||||
internal static class SubtitleFormatExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Will try to find errors if supported by provider.
|
||||
/// </summary>
|
||||
/// <param name="format">The subtitle format.</param>
|
||||
/// <param name="errors">The out errors value.</param>
|
||||
/// <returns>True if errors are available for given format.</returns>
|
||||
public static bool TryGetErrors(this SubtitleFormat format, [NotNullWhen(true)] out string? errors)
|
||||
{
|
||||
errors = format switch
|
||||
{
|
||||
SubStationAlpha ssa => ssa.Errors,
|
||||
AdvancedSubStationAlpha assa => assa.Errors,
|
||||
SubRip subRip => subRip.Errors,
|
||||
MicroDvd microDvd => microDvd.Errors,
|
||||
DCinemaSmpte2007 smpte2007 => smpte2007.Errors,
|
||||
DCinemaSmpte2010 smpte2010 => smpte2010.Errors,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return !string.IsNullOrWhiteSpace(errors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// TTML subtitle writer.
|
||||
/// </summary>
|
||||
public partial class TtmlWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewLineEscapeRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
// Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml
|
||||
// Parser example: https://github.com/mozilla/popcorn-js/blob/master/parsers/parserTTML/popcorn.parserTTML.js
|
||||
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
|
||||
writer.WriteLine("<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:tts=\"http://www.w3.org/2006/04/ttaf1#styling\" lang=\"no\">");
|
||||
|
||||
writer.WriteLine("<head>");
|
||||
writer.WriteLine("<styling>");
|
||||
writer.WriteLine("<style id=\"italic\" tts:fontStyle=\"italic\" />");
|
||||
writer.WriteLine("<style id=\"left\" tts:textAlign=\"left\" />");
|
||||
writer.WriteLine("<style id=\"center\" tts:textAlign=\"center\" />");
|
||||
writer.WriteLine("<style id=\"right\" tts:textAlign=\"right\" />");
|
||||
writer.WriteLine("</styling>");
|
||||
writer.WriteLine("</head>");
|
||||
|
||||
writer.WriteLine("<body>");
|
||||
writer.WriteLine("<div>");
|
||||
|
||||
foreach (var trackEvent in info.TrackEvents)
|
||||
{
|
||||
var text = trackEvent.Text;
|
||||
|
||||
text = NewLineEscapeRegex().Replace(text, "<br/>");
|
||||
|
||||
writer.WriteLine(
|
||||
"<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
|
||||
trackEvent.StartPositionTicks,
|
||||
trackEvent.EndPositionTicks - trackEvent.StartPositionTicks,
|
||||
text);
|
||||
}
|
||||
|
||||
writer.WriteLine("</div>");
|
||||
writer.WriteLine("</body>");
|
||||
|
||||
writer.WriteLine("</tt>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// Subtitle writer for the WebVTT format.
|
||||
/// </summary>
|
||||
public partial class VttWriter : ISubtitleWriter
|
||||
{
|
||||
[GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex NewlineEscapeRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
writer.WriteLine("WEBVTT");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("Region: id:subtitle width:80% lines:3 regionanchor:50%,100% viewportanchor:50%,90%");
|
||||
writer.WriteLine();
|
||||
foreach (var trackEvent in info.TrackEvents)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks);
|
||||
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks);
|
||||
|
||||
// make sure the start and end times are different and sequential
|
||||
if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds)
|
||||
{
|
||||
endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
|
||||
}
|
||||
|
||||
writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle line:90%", startTime, endTime);
|
||||
|
||||
var text = trackEvent.Text;
|
||||
|
||||
// TODO: Not sure how to handle these
|
||||
text = NewlineEscapeRegex().Replace(text, " ");
|
||||
|
||||
writer.WriteLine(text);
|
||||
writer.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,757 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Controller.Streaming;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Transcoding;
|
||||
|
||||
/// <inheritdoc cref="ITranscodeManager"/>
|
||||
public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger<TranscodeManager> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly EncodingHelper _encodingHelper;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IAttachmentExtractor _attachmentExtractor;
|
||||
|
||||
private readonly List<TranscodingJob> _activeTranscodingJobs = new();
|
||||
private readonly AsyncKeyedLocker<string> _transcodingLocks = new(o =>
|
||||
{
|
||||
o.PoolSize = 20;
|
||||
o.PoolInitialFill = 1;
|
||||
});
|
||||
|
||||
private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
|
||||
/// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
|
||||
/// <param name="serverConfigurationManager">The <see cref="IServerConfigurationManager"/>.</param>
|
||||
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
|
||||
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
|
||||
/// <param name="encodingHelper">The <see cref="EncodingHelper"/>.</param>
|
||||
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
|
||||
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
|
||||
/// <param name="attachmentExtractor">The <see cref="IAttachmentExtractor"/>.</param>
|
||||
public TranscodeManager(
|
||||
ILoggerFactory loggerFactory,
|
||||
IFileSystem fileSystem,
|
||||
IApplicationPaths appPaths,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IUserManager userManager,
|
||||
ISessionManager sessionManager,
|
||||
EncodingHelper encodingHelper,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IAttachmentExtractor attachmentExtractor)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_userManager = userManager;
|
||||
_sessionManager = sessionManager;
|
||||
_encodingHelper = encodingHelper;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_attachmentExtractor = attachmentExtractor;
|
||||
|
||||
_logger = loggerFactory.CreateLogger<TranscodeManager>();
|
||||
DeleteEncodedMediaCache();
|
||||
_sessionManager.PlaybackProgress += OnPlaybackProgress;
|
||||
_sessionManager.PlaybackStart += OnPlaybackProgress;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TranscodingJob? GetTranscodingJob(string playSessionId)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(playSessionId);
|
||||
|
||||
_logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
|
||||
|
||||
List<TranscodingJob> jobs;
|
||||
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
// This is really only needed for HLS.
|
||||
// Progressive streams can stop on their own reliably.
|
||||
jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
if (isUserPaused.HasValue)
|
||||
{
|
||||
_logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
|
||||
job.IsUserPaused = isUserPaused.Value;
|
||||
}
|
||||
|
||||
PingTimer(job, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
|
||||
{
|
||||
if (job.HasExited)
|
||||
{
|
||||
job.StopKillTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
var timerDuration = 10000;
|
||||
|
||||
if (job.Type != TranscodingJobType.Progressive)
|
||||
{
|
||||
timerDuration = 60000;
|
||||
}
|
||||
|
||||
job.PingTimeout = timerDuration;
|
||||
job.LastPingDate = DateTime.UtcNow;
|
||||
|
||||
// Don't start the timer for playback checkins with progressive streaming
|
||||
if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
|
||||
{
|
||||
job.StartKillTimer(OnTranscodeKillTimerStopped);
|
||||
}
|
||||
else
|
||||
{
|
||||
job.ChangeKillTimerIfStarted();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnTranscodeKillTimerStopped(object? state)
|
||||
{
|
||||
var job = state as TranscodingJob ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJob)}", nameof(state));
|
||||
if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
|
||||
{
|
||||
var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
|
||||
|
||||
if (timeSinceLastPing < job.PingTimeout)
|
||||
{
|
||||
job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
||||
|
||||
await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
|
||||
{
|
||||
var jobs = new List<TranscodingJob>();
|
||||
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
// This is really only needed for HLS.
|
||||
// Progressive streams can stop on their own reliably.
|
||||
jobs.AddRange(_activeTranscodingJobs.Where(j => string.IsNullOrWhiteSpace(playSessionId)
|
||||
? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
|
||||
: string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
return Task.WhenAll(GetKillJobs());
|
||||
|
||||
IEnumerable<Task> GetKillJobs()
|
||||
{
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
yield return KillTranscodingJob(job, false, deleteFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
|
||||
{
|
||||
job.DisposeKillTimer();
|
||||
|
||||
_logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
||||
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
_activeTranscodingJobs.Remove(job);
|
||||
|
||||
if (job.CancellationTokenSource?.IsCancellationRequested == false)
|
||||
{
|
||||
#pragma warning disable CA1849 // Can't await in lock block
|
||||
job.CancellationTokenSource.Cancel();
|
||||
#pragma warning restore CA1849
|
||||
}
|
||||
}
|
||||
|
||||
job.Stop();
|
||||
|
||||
if (delete(job.Path!))
|
||||
{
|
||||
await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
|
||||
{
|
||||
await _sessionManager.CloseLiveStreamIfNeededAsync(job.LiveStreamId, job.PlaySessionId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
|
||||
{
|
||||
if (retryCount >= 10)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Deleting partial stream file(s) {Path}", path);
|
||||
|
||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (jobType == TranscodingJobType.Progressive)
|
||||
{
|
||||
DeleteProgressivePartialStreamFiles(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
DeleteHlsPartialStreamFiles(path);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
|
||||
|
||||
await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteProgressivePartialStreamFiles(string outputFilePath)
|
||||
{
|
||||
if (File.Exists(outputFilePath))
|
||||
{
|
||||
_fileSystem.DeleteFile(outputFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteHlsPartialStreamFiles(string outputFilePath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(outputFilePath)
|
||||
?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(outputFilePath);
|
||||
|
||||
var filesToDelete = _fileSystem.GetFilePaths(directory)
|
||||
.Where(f => f.Contains(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
List<Exception>? exs = null;
|
||||
foreach (var file in filesToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Deleting HLS file {0}", file);
|
||||
_fileSystem.DeleteFile(file);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
(exs ??= new List<Exception>()).Add(ex);
|
||||
_logger.LogError(ex, "Error deleting HLS file {Path}", file);
|
||||
}
|
||||
}
|
||||
|
||||
if (exs is not null)
|
||||
{
|
||||
throw new AggregateException("Error deleting HLS files", exs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReportTranscodingProgress(
|
||||
TranscodingJob job,
|
||||
StreamState state,
|
||||
TimeSpan? transcodingPosition,
|
||||
float? framerate,
|
||||
double? percentComplete,
|
||||
long? bytesTranscoded,
|
||||
int? bitRate)
|
||||
{
|
||||
var ticks = transcodingPosition?.Ticks;
|
||||
|
||||
if (job is not null)
|
||||
{
|
||||
job.Framerate = framerate;
|
||||
job.CompletionPercentage = percentComplete;
|
||||
job.TranscodingPositionTicks = ticks;
|
||||
job.BytesTranscoded = bytesTranscoded;
|
||||
job.BitRate = bitRate;
|
||||
}
|
||||
|
||||
var deviceId = state.Request.DeviceId;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
var audioCodec = state.ActualOutputAudioCodec;
|
||||
var videoCodec = state.ActualOutputVideoCodec;
|
||||
var hardwareAccelerationType = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
|
||||
|
||||
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
|
||||
{
|
||||
Bitrate = bitRate ?? state.TotalOutputBitrate,
|
||||
AudioCodec = audioCodec,
|
||||
VideoCodec = videoCodec,
|
||||
Container = state.OutputContainer,
|
||||
Framerate = framerate,
|
||||
CompletionPercentage = percentComplete,
|
||||
Width = state.OutputWidth,
|
||||
Height = state.OutputHeight,
|
||||
AudioChannels = state.OutputAudioChannels,
|
||||
IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
|
||||
IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
|
||||
HardwareAccelerationType = hardwareAccelerationType,
|
||||
TranscodeReasons = state.TranscodeReasons
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TranscodingJob> StartFfMpeg(
|
||||
StreamState state,
|
||||
string outputPath,
|
||||
string commandLineArguments,
|
||||
Guid userId,
|
||||
TranscodingJobType transcodingJobType,
|
||||
CancellationTokenSource cancellationTokenSource,
|
||||
string? workingDirectory = null)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
|
||||
|
||||
if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
{
|
||||
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
|
||||
if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
|
||||
{
|
||||
OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
|
||||
throw new ArgumentException("User does not have access to video transcoding.");
|
||||
}
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
|
||||
|
||||
// If subtitles get burned in fonts may need to be extracted from the media file
|
||||
if (state.SubtitleStream is not null && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode || state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding))
|
||||
{
|
||||
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
|
||||
{
|
||||
var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat");
|
||||
await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await _attachmentExtractor.ExtractAllAttachments(state.SubtitleStream.Path, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
|
||||
// Must consume both stdout and stderr or deadlocks may occur
|
||||
// RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = commandLineArguments,
|
||||
WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
var transcodingJob = OnTranscodeBeginning(
|
||||
outputPath,
|
||||
state.Request.PlaySessionId,
|
||||
state.MediaSource.LiveStreamId,
|
||||
Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
|
||||
transcodingJobType,
|
||||
process,
|
||||
state.Request.DeviceId,
|
||||
state,
|
||||
cancellationTokenSource);
|
||||
|
||||
_logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
var logFilePrefix = "FFmpeg.Transcode-";
|
||||
if (state.VideoRequest is not null
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
{
|
||||
logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
|
||||
? "FFmpeg.Remux-"
|
||||
: "FFmpeg.DirectStream-";
|
||||
}
|
||||
|
||||
if (state.VideoRequest is null && EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
|
||||
{
|
||||
logFilePrefix = "FFmpeg.Remux-";
|
||||
}
|
||||
|
||||
var logFilePath = Path.Combine(
|
||||
_serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
|
||||
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
|
||||
|
||||
// FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
||||
Stream logStream = new FileStream(
|
||||
logFilePath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.Read,
|
||||
IODefaults.FileStreamBufferSize,
|
||||
FileOptions.Asynchronous);
|
||||
|
||||
await JsonSerializer.SerializeAsync(logStream, state.MediaSource, cancellationToken: cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(
|
||||
Environment.NewLine
|
||||
+ Environment.NewLine
|
||||
+ process.StartInfo.FileName + " " + process.StartInfo.Arguments
|
||||
+ Environment.NewLine
|
||||
+ Environment.NewLine);
|
||||
|
||||
await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
process.Exited += (_, _) => OnFfMpegProcessExited(process, transcodingJob, state);
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting FFmpeg");
|
||||
OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Launched FFmpeg process");
|
||||
state.TranscodingJob = transcodingJob;
|
||||
|
||||
// Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
|
||||
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
|
||||
|
||||
// Wait for the file to exist before proceeding
|
||||
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
|
||||
_logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
|
||||
while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
|
||||
{
|
||||
await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
|
||||
|
||||
if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
|
||||
{
|
||||
await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
|
||||
{
|
||||
await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!transcodingJob.HasExited)
|
||||
{
|
||||
StartThrottler(state, transcodingJob);
|
||||
StartSegmentCleaner(state, transcodingJob);
|
||||
}
|
||||
else if (transcodingJob.ExitCode != 0)
|
||||
{
|
||||
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
|
||||
}
|
||||
|
||||
_logger.LogDebug("StartFfMpeg() finished successfully");
|
||||
|
||||
return transcodingJob;
|
||||
}
|
||||
|
||||
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
|
||||
{
|
||||
if (EnableThrottling(state)
|
||||
&& (_mediaEncoder.IsPkeyPauseSupported
|
||||
|| _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
|
||||
{
|
||||
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||
transcodingJob.TranscodingThrottler.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EnableThrottling(StreamState state)
|
||||
=> state.InputProtocol == MediaProtocol.File
|
||||
&& state.RunTimeTicks.HasValue
|
||||
&& state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks
|
||||
&& state.IsInputVideo
|
||||
&& state.VideoType == VideoType.VideoFile;
|
||||
|
||||
private void StartSegmentCleaner(StreamState state, TranscodingJob transcodingJob)
|
||||
{
|
||||
if (EnableSegmentCleaning(state))
|
||||
{
|
||||
transcodingJob.TranscodingSegmentCleaner = new TranscodingSegmentCleaner(transcodingJob, _loggerFactory.CreateLogger<TranscodingSegmentCleaner>(), _serverConfigurationManager, _fileSystem, _mediaEncoder, state.SegmentLength);
|
||||
transcodingJob.TranscodingSegmentCleaner.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EnableSegmentCleaning(StreamState state)
|
||||
=> state.InputProtocol is MediaProtocol.File or MediaProtocol.Http
|
||||
&& state.IsInputVideo
|
||||
&& state.TranscodingType == TranscodingJobType.Hls
|
||||
&& state.RunTimeTicks.HasValue
|
||||
&& state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks;
|
||||
|
||||
private TranscodingJob OnTranscodeBeginning(
|
||||
string path,
|
||||
string? playSessionId,
|
||||
string? liveStreamId,
|
||||
string transcodingJobId,
|
||||
TranscodingJobType type,
|
||||
Process process,
|
||||
string? deviceId,
|
||||
StreamState state,
|
||||
CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
var job = new TranscodingJob(_loggerFactory.CreateLogger<TranscodingJob>())
|
||||
{
|
||||
Type = type,
|
||||
Path = path,
|
||||
Process = process,
|
||||
ActiveRequestCount = 1,
|
||||
DeviceId = deviceId,
|
||||
CancellationTokenSource = cancellationTokenSource,
|
||||
Id = transcodingJobId,
|
||||
PlaySessionId = playSessionId,
|
||||
LiveStreamId = liveStreamId,
|
||||
MediaSource = state.MediaSource
|
||||
};
|
||||
|
||||
_activeTranscodingJobs.Add(job);
|
||||
|
||||
ReportTranscodingProgress(job, state, null, null, null, null, null);
|
||||
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnTranscodeEndRequest(TranscodingJob job)
|
||||
{
|
||||
job.ActiveRequestCount--;
|
||||
_logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
|
||||
if (job.ActiveRequestCount <= 0)
|
||||
{
|
||||
PingTimer(job, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (job is not null)
|
||||
{
|
||||
_activeTranscodingJobs.Remove(job);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
|
||||
{
|
||||
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
|
||||
{
|
||||
job.HasExited = true;
|
||||
job.ExitCode = process.ExitCode;
|
||||
|
||||
ReportTranscodingProgress(job, state, null, null, null, null, null);
|
||||
|
||||
_logger.LogDebug("Disposing stream resources");
|
||||
state.Dispose();
|
||||
|
||||
if (process.ExitCode == 0)
|
||||
{
|
||||
_logger.LogInformation("FFmpeg exited with code 0");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
|
||||
}
|
||||
|
||||
job.Dispose();
|
||||
}
|
||||
|
||||
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
|
||||
{
|
||||
var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
|
||||
new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
_encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
|
||||
|
||||
if (state.VideoRequest is not null)
|
||||
{
|
||||
_encodingHelper.TryStreamCopy(state, encodingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.MediaSource.BufferMs.HasValue)
|
||||
{
|
||||
await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type)
|
||||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
var job = _activeTranscodingJobs
|
||||
.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (job is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
job.ActiveRequestCount++;
|
||||
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
|
||||
{
|
||||
job.StopKillTimer();
|
||||
}
|
||||
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
|
||||
{
|
||||
PingTranscodingJob(e.PlaySessionId, e.IsPaused);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteEncodedMediaCache()
|
||||
{
|
||||
var path = _serverConfigurationManager.GetTranscodePath();
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var file in _fileSystem.GetFilePaths(path, true))
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(file);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting encoded media cache file {Path}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transcoding lock.
|
||||
/// </summary>
|
||||
/// <param name="outputPath">The output path of the transcoded file.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>An <see cref="IDisposable"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
|
||||
{
|
||||
return _transcodingLocks.LockAsync(outputPath, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
|
||||
_sessionManager.PlaybackStart -= OnPlaybackProgress;
|
||||
_transcodingLocks.Dispose();
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net10.0
|
||||
build_property.TargetFramework = net10.0
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v10.0
|
||||
build_property.TargetFrameworkVersion = v10.0
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = MediaBrowser.MediaEncoding
|
||||
build_property.ProjectDir = /srv/common_drive/Projects/pgsql-jellyfin/MediaBrowser.MediaEncoding/
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.EffectiveAnalysisLevelStyle = 10.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/wjones/.nuget/packages/</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/wjones/.nuget/packages/</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="/home/wjones/.nuget/packages/" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<None Include="$(NuGetPackageRoot)libse/4.0.12/contentFiles/any/netstandard2.1/Icon.png" Condition="Exists('$(NuGetPackageRoot)libse/4.0.12/contentFiles/any/netstandard2.1/Icon.png')">
|
||||
<NuGetPackageId>libse</NuGetPackageId>
|
||||
<NuGetPackageVersion>4.0.12</NuGetPackageVersion>
|
||||
<NuGetItemType>None</NuGetItemType>
|
||||
<Pack>false</Pack>
|
||||
<Private>False</Private>
|
||||
<Link>Icon.png</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.entityframeworkcore/10.0.3/buildTransitive/net10.0/Microsoft.EntityFrameworkCore.props" Condition="Exists('$(NuGetPackageRoot)microsoft.entityframeworkcore/10.0.3/buildTransitive/net10.0/Microsoft.EntityFrameworkCore.props')" />
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.props" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.props')" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<PkgStyleCop_Analyzers_Unstable Condition=" '$(PkgStyleCop_Analyzers_Unstable)' == '' ">/home/wjones/.nuget/packages/stylecop.analyzers.unstable/1.2.0.556</PkgStyleCop_Analyzers_Unstable>
|
||||
<PkgSmartAnalyzers_MultithreadingAnalyzer Condition=" '$(PkgSmartAnalyzers_MultithreadingAnalyzer)' == '' ">/home/wjones/.nuget/packages/smartanalyzers.multithreadinganalyzer/1.1.31</PkgSmartAnalyzers_MultithreadingAnalyzer>
|
||||
<PkgSerilogAnalyzer Condition=" '$(PkgSerilogAnalyzer)' == '' ">/home/wjones/.nuget/packages/seriloganalyzer/0.15.0</PkgSerilogAnalyzer>
|
||||
<PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers Condition=" '$(PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers)' == '' ">/home/wjones/.nuget/packages/microsoft.codeanalysis.bannedapianalyzers/4.14.0</PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers>
|
||||
<PkgIDisposableAnalyzers Condition=" '$(PkgIDisposableAnalyzers)' == '' ">/home/wjones/.nuget/packages/idisposableanalyzers/4.0.8</PkgIDisposableAnalyzers>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.options/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Options.targets')" />
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.binder/10.0.3/buildTransitive/netstandard2.0/Microsoft.Extensions.Configuration.Binder.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.binder/10.0.3/buildTransitive/netstandard2.0/Microsoft.Extensions.Configuration.Binder.targets')" />
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets')" />
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.targets')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "aR+NNh1PRbs=",
|
||||
"success": true,
|
||||
"projectFilePath": "/srv/common_drive/Projects/pgsql-jellyfin/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"/home/wjones/.nuget/packages/asynckeyedlock/8.0.2/asynckeyedlock.8.0.2.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/bdinfo/0.8.0/bdinfo.0.8.0.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/bitfaster.caching/2.5.4/bitfaster.caching.2.5.4.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/diacritics/4.1.4/diacritics.4.1.4.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/icu4n/60.1.0-alpha.356/icu4n.60.1.0-alpha.356.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/icu4n.transliterator/60.1.0-alpha.356/icu4n.transliterator.60.1.0-alpha.356.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/idisposableanalyzers/4.0.8/idisposableanalyzers.4.0.8.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/j2n/2.0.0/j2n.2.0.0.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/libse/4.0.12/libse.4.0.12.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.codeanalysis.bannedapianalyzers/4.14.0/microsoft.codeanalysis.bannedapianalyzers.4.14.0.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore/10.0.3/microsoft.entityframeworkcore.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.abstractions/10.0.3/microsoft.entityframeworkcore.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.analyzers/10.0.3/microsoft.entityframeworkcore.analyzers.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.relational/10.0.3/microsoft.entityframeworkcore.relational.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.caching.abstractions/10.0.3/microsoft.extensions.caching.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.caching.memory/10.0.3/microsoft.extensions.caching.memory.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.configuration/10.0.3/microsoft.extensions.configuration.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.abstractions/10.0.3/microsoft.extensions.configuration.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.binder/10.0.3/microsoft.extensions.configuration.binder.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.dependencyinjection/10.0.3/microsoft.extensions.dependencyinjection.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/10.0.3/microsoft.extensions.dependencyinjection.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.diagnostics/10.0.3/microsoft.extensions.diagnostics.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.diagnostics.abstractions/10.0.3/microsoft.extensions.diagnostics.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.http/10.0.3/microsoft.extensions.http.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.logging/10.0.3/microsoft.extensions.logging.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.logging.abstractions/10.0.3/microsoft.extensions.logging.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.options/10.0.3/microsoft.extensions.options.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.options.configurationextensions/10.0.3/microsoft.extensions.options.configurationextensions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.primitives/10.0.3/microsoft.extensions.primitives.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.win32.systemevents/9.0.2/microsoft.win32.systemevents.9.0.2.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/nebml/1.1.0.5/nebml.1.1.0.5.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/polly/8.6.5/polly.8.6.5.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/polly.core/8.6.5/polly.core.8.6.5.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/seriloganalyzer/0.15.0/seriloganalyzer.0.15.0.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/smartanalyzers.multithreadinganalyzer/1.1.31/smartanalyzers.multithreadinganalyzer.1.1.31.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/stylecop.analyzers/1.2.0-beta.556/stylecop.analyzers.1.2.0-beta.556.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/stylecop.analyzers.unstable/1.2.0.556/stylecop.analyzers.unstable.1.2.0.556.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/system.drawing.common/9.0.2/system.drawing.common.9.0.2.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/utf.unknown/2.6.0/utf.unknown.2.6.0.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/zlib.net-mutliplatform/1.0.8/zlib.net-mutliplatform.1.0.8.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
17714532087600000
|
||||
@@ -0,0 +1 @@
|
||||
17715044211800000
|
||||
Reference in New Issue
Block a user