repo creation with initial code after cloning public repo

This commit is contained in:
2026-02-19 07:36:25 -05:00
commit 460884fea3
2860 changed files with 650825 additions and 0 deletions
@@ -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();
}
}
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
@@ -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 =
@@ -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