repo creation with initial code after cloning public repo
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public class BaseEncodingJobOptions
|
||||
{
|
||||
public BaseEncodingJobOptions()
|
||||
{
|
||||
EnableAutoStreamCopy = true;
|
||||
AllowVideoStreamCopy = true;
|
||||
AllowAudioStreamCopy = true;
|
||||
Context = EncodingContext.Streaming;
|
||||
StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
/// <value>The id.</value>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
public string Container { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the audio codec.
|
||||
/// </summary>
|
||||
/// <value>The audio codec.</value>
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
public bool EnableAutoStreamCopy { get; set; }
|
||||
|
||||
public bool AllowVideoStreamCopy { get; set; }
|
||||
|
||||
public bool AllowAudioStreamCopy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the audio sample rate.
|
||||
/// </summary>
|
||||
/// <value>The audio sample rate.</value>
|
||||
public int? AudioSampleRate { get; set; }
|
||||
|
||||
public int? MaxAudioBitDepth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the audio bit rate.
|
||||
/// </summary>
|
||||
/// <value>The audio bit rate.</value>
|
||||
public int? AudioBitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the audio channels.
|
||||
/// </summary>
|
||||
/// <value>The audio channels.</value>
|
||||
public int? AudioChannels { get; set; }
|
||||
|
||||
public int? MaxAudioChannels { get; set; }
|
||||
|
||||
public bool Static { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile.
|
||||
/// </summary>
|
||||
/// <value>The profile.</value>
|
||||
public string Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video range type.
|
||||
/// </summary>
|
||||
/// <value>The video range type.</value>
|
||||
public string VideoRangeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
/// <value>The level.</value>
|
||||
public string Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec tag.
|
||||
/// </summary>
|
||||
/// <value>The codec tag.</value>
|
||||
public string CodecTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the framerate.
|
||||
/// </summary>
|
||||
/// <value>The framerate.</value>
|
||||
public float? Framerate { get; set; }
|
||||
|
||||
public float? MaxFramerate { get; set; }
|
||||
|
||||
public bool CopyTimestamps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start time ticks.
|
||||
/// </summary>
|
||||
/// <value>The start time ticks.</value>
|
||||
public long? StartTimeTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
public int? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
public int? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the max.
|
||||
/// </summary>
|
||||
/// <value>The width of the max.</value>
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height of the max.
|
||||
/// </summary>
|
||||
/// <value>The height of the max.</value>
|
||||
public int? MaxHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video bit rate.
|
||||
/// </summary>
|
||||
/// <value>The video bit rate.</value>
|
||||
public int? VideoBitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the subtitle stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the subtitle stream.</value>
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
|
||||
|
||||
public int? MaxRefFrames { get; set; }
|
||||
|
||||
public int? MaxVideoBitDepth { get; set; }
|
||||
|
||||
public bool RequireAvc { get; set; }
|
||||
|
||||
public bool DeInterlace { get; set; }
|
||||
|
||||
public bool RequireNonAnamorphic { get; set; }
|
||||
|
||||
public int? TranscodingMaxAudioChannels { get; set; }
|
||||
|
||||
public int? CpuCoreLimit { get; set; }
|
||||
|
||||
public string LiveStreamId { get; set; }
|
||||
|
||||
public bool EnableMpegtsM2TsMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video codec.
|
||||
/// </summary>
|
||||
/// <value>The video codec.</value>
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
public string SubtitleCodec { get; set; }
|
||||
|
||||
public string TranscodeReasons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the audio stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the audio stream.</value>
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the video stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the video stream.</value>
|
||||
public int? VideoStreamIndex { get; set; }
|
||||
|
||||
public EncodingContext Context { get; set; }
|
||||
|
||||
public Dictionary<string, string> StreamOptions { get; set; }
|
||||
|
||||
public bool EnableAudioVbrEncoding { get; set; }
|
||||
|
||||
public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; }
|
||||
|
||||
public string GetOption(string qualifier, string name)
|
||||
{
|
||||
var value = GetOption(qualifier + "-" + name);
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
value = GetOption(name);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public string GetOption(string name)
|
||||
{
|
||||
if (StreamOptions.TryGetValue(name, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace MediaBrowser.Controller.MediaEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Enum BitStreamFilterOptionType.
|
||||
/// </summary>
|
||||
public enum BitStreamFilterOptionType
|
||||
{
|
||||
/// <summary>
|
||||
/// hevc_metadata bsf with remove_dovi option.
|
||||
/// </summary>
|
||||
HevcMetadataRemoveDovi = 0,
|
||||
|
||||
/// <summary>
|
||||
/// hevc_metadata bsf with remove_hdr10plus option.
|
||||
/// </summary>
|
||||
HevcMetadataRemoveHdr10Plus = 1,
|
||||
|
||||
/// <summary>
|
||||
/// av1_metadata bsf with remove_dovi option.
|
||||
/// </summary>
|
||||
Av1MetadataRemoveDovi = 2,
|
||||
|
||||
/// <summary>
|
||||
/// av1_metadata bsf with remove_hdr10plus option.
|
||||
/// </summary>
|
||||
Av1MetadataRemoveHdr10Plus = 3,
|
||||
|
||||
/// <summary>
|
||||
/// dovi_rpu bsf with strip option.
|
||||
/// </summary>
|
||||
DoviRpuStrip = 4,
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the downmix algorithms capabilities.
|
||||
/// </summary>
|
||||
public static class DownMixAlgorithmsHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The filter string of the DownMixStereoAlgorithms.
|
||||
/// The index is the tuple of (algorithm, layout).
|
||||
/// </summary>
|
||||
public static readonly Dictionary<(DownMixStereoAlgorithms, string), string> AlgorithmFilterStrings = new()
|
||||
{
|
||||
{ (DownMixStereoAlgorithms.Dave750, "5.1"), "pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3" },
|
||||
// Use AC-4 algorithm to downmix 7.1 inputs to 5.1 first
|
||||
{ (DownMixStereoAlgorithms.Dave750, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3" },
|
||||
{ (DownMixStereoAlgorithms.NightmodeDialogue, "5.1"), "pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5" },
|
||||
// Use AC-4 algorithm to downmix 7.1 inputs to 5.1 first
|
||||
{ (DownMixStereoAlgorithms.NightmodeDialogue, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5" },
|
||||
{ (DownMixStereoAlgorithms.Rfc7845, "3.0"), "pan=stereo|c0=0.414214*c2+0.585786*c0|c1=0.414214*c2+0.585786*c1" },
|
||||
{ (DownMixStereoAlgorithms.Rfc7845, "quad"), "pan=stereo|c0=0.422650*c0+0.366025*c2+0.211325*c3|c1=0.422650*c1+0.366025*c3+0.211325*c2" },
|
||||
{ (DownMixStereoAlgorithms.Rfc7845, "5.0"), "pan=stereo|c0=0.460186*c2+0.650802*c0+0.563611*c3+0.325401*c4|c1=0.460186*c2+0.650802*c1+0.563611*c4+0.325401*c3" },
|
||||
{ (DownMixStereoAlgorithms.Rfc7845, "5.1"), "pan=stereo|c0=0.374107*c2+0.529067*c0+0.458186*c4+0.264534*c5+0.374107*c3|c1=0.374107*c2+0.529067*c1+0.458186*c5+0.264534*c4+0.374107*c3" },
|
||||
{ (DownMixStereoAlgorithms.Rfc7845, "6.1"), "pan=stereo|c0=0.321953*c2+0.455310*c0+0.394310*c5+0.227655*c6+0.278819*c4+0.321953*c3|c1=0.321953*c2+0.455310*c1+0.394310*c6+0.227655*c5+0.278819*c4+0.321953*c3" },
|
||||
{ (DownMixStereoAlgorithms.Rfc7845, "7.1"), "pan=stereo|c0=0.274804*c2+0.388631*c0+0.336565*c6+0.194316*c7+0.336565*c4+0.194316*c5+0.274804*c3|c1=0.274804*c2+0.388631*c1+0.336565*c7+0.194316*c6+0.336565*c5+0.194316*c4+0.274804*c3" },
|
||||
{ (DownMixStereoAlgorithms.Ac4, "3.0"), "pan=stereo|c0=c0+0.707*c2|c1=c1+0.707*c2" },
|
||||
{ (DownMixStereoAlgorithms.Ac4, "5.0"), "pan=stereo|c0=c0+0.707*c2+0.707*c3|c1=c1+0.707*c2+0.707*c4" },
|
||||
{ (DownMixStereoAlgorithms.Ac4, "5.1"), "pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5" },
|
||||
{ (DownMixStereoAlgorithms.Ac4, "7.0"), "pan=5.0(side)|c0=c0|c1=c1|c2=c2|c3=0.707*c3+0.707*c5|c4=0.707*c4+0.707*c6,pan=stereo|c0=c0+0.707*c2+0.707*c3|c1=c1+0.707*c2+0.707*c4" },
|
||||
{ (DownMixStereoAlgorithms.Ac4, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get the audio channel layout string from the audio stream
|
||||
/// If the input audio string does not have a valid layout string, guess from channel count.
|
||||
/// </summary>
|
||||
/// <param name="audioStream">The audio stream to get layout.</param>
|
||||
/// <returns>Channel Layout string.</returns>
|
||||
public static string InferChannelLayout(MediaStream audioStream)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(audioStream.ChannelLayout))
|
||||
{
|
||||
// Note: BDMVs do not derive this string from ffmpeg, which would cause ambiguity with 4-channel audio
|
||||
// "quad" => 2 front and 2 rear, "4.0" => 3 front and 1 rear
|
||||
// BDMV will always use "4.0" in this case
|
||||
// Because the quad layout is super rare in BDs, we will use "4.0" as is here
|
||||
return audioStream.ChannelLayout;
|
||||
}
|
||||
|
||||
if (audioStream.Channels is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// When we don't have definitive channel layout, we have to guess from the channel count
|
||||
// Guessing is not always correct, but for most videos we don't have to guess like this as the definitive layout is recorded during scan
|
||||
var inferredLayout = audioStream.Channels.Value switch
|
||||
{
|
||||
1 => "mono",
|
||||
2 => "stereo",
|
||||
3 => "2.1", // Could also be 3.0, prefer 2.1
|
||||
4 => "4.0", // Could also be quad (with rear left and rear right) and 3.1 with LFE. prefer 4.0 with front center and back center
|
||||
5 => "5.0",
|
||||
6 => "5.1", // Could also be 6.0 or hexagonal, prefer 5.1
|
||||
7 => "6.1", // Could also be 7.0, prefer 6.1
|
||||
8 => "7.1", // Could also be 8.0, prefer 7.1
|
||||
_ => string.Empty // Return empty string for not supported layout
|
||||
};
|
||||
return inferredLayout;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,738 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591, SA1401
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Session;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
// For now, a common base class until the API and MediaEncoding classes are unified
|
||||
public class EncodingJobInfo
|
||||
{
|
||||
private static readonly char[] _separators = ['|', ','];
|
||||
|
||||
private TranscodeReason? _transcodeReasons = null;
|
||||
|
||||
public EncodingJobInfo(TranscodingJobType jobType)
|
||||
{
|
||||
TranscodingType = jobType;
|
||||
RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
SupportedAudioCodecs = Array.Empty<string>();
|
||||
SupportedVideoCodecs = Array.Empty<string>();
|
||||
SupportedSubtitleCodecs = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public int? OutputAudioBitrate { get; set; }
|
||||
|
||||
public int? OutputAudioChannels { get; set; }
|
||||
|
||||
public TranscodeReason TranscodeReasons
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_transcodeReasons.HasValue)
|
||||
{
|
||||
if (BaseRequest.TranscodeReasons is null)
|
||||
{
|
||||
_transcodeReasons = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
_ = Enum.TryParse<TranscodeReason>(BaseRequest.TranscodeReasons, out var reason);
|
||||
_transcodeReasons = reason;
|
||||
}
|
||||
|
||||
return _transcodeReasons.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public IProgress<double> Progress { get; set; }
|
||||
|
||||
public MediaStream VideoStream { get; set; }
|
||||
|
||||
public VideoType VideoType { get; set; }
|
||||
|
||||
public Dictionary<string, string> RemoteHttpHeaders { get; set; }
|
||||
|
||||
public string OutputVideoCodec { get; set; }
|
||||
|
||||
public MediaProtocol InputProtocol { get; set; }
|
||||
|
||||
public string MediaPath { get; set; }
|
||||
|
||||
public bool IsInputVideo { get; set; }
|
||||
|
||||
public string OutputAudioCodec { get; set; }
|
||||
|
||||
public int? OutputVideoBitrate { get; set; }
|
||||
|
||||
public MediaStream SubtitleStream { get; set; }
|
||||
|
||||
public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
|
||||
|
||||
public string[] SupportedSubtitleCodecs { get; set; }
|
||||
|
||||
public int InternalSubtitleStreamOffset { get; set; }
|
||||
|
||||
public MediaSourceInfo MediaSource { get; set; }
|
||||
|
||||
public User User { get; set; }
|
||||
|
||||
public long? RunTimeTicks { get; set; }
|
||||
|
||||
public bool ReadInputAtNativeFramerate { get; set; }
|
||||
|
||||
public string OutputFilePath { get; set; }
|
||||
|
||||
public string MimeType { get; set; }
|
||||
|
||||
public bool IgnoreInputDts => MediaSource.IgnoreDts;
|
||||
|
||||
public bool IgnoreInputIndex => MediaSource.IgnoreIndex;
|
||||
|
||||
public bool GenPtsInput => MediaSource.GenPtsInput;
|
||||
|
||||
public bool DiscardCorruptFramesInput => false;
|
||||
|
||||
public bool EnableFastSeekInput => false;
|
||||
|
||||
public bool GenPtsOutput => false;
|
||||
|
||||
public string OutputContainer { get; set; }
|
||||
|
||||
public string OutputVideoSync { get; set; }
|
||||
|
||||
public string AlbumCoverPath { get; set; }
|
||||
|
||||
public string InputAudioSync { get; set; }
|
||||
|
||||
public string InputVideoSync { get; set; }
|
||||
|
||||
public TransportStreamTimestamp InputTimestamp { get; set; }
|
||||
|
||||
public MediaStream AudioStream { get; set; }
|
||||
|
||||
public string[] SupportedAudioCodecs { get; set; }
|
||||
|
||||
public string[] SupportedVideoCodecs { get; set; }
|
||||
|
||||
public string InputContainer { get; set; }
|
||||
|
||||
public IsoType? IsoType { get; set; }
|
||||
|
||||
public BaseEncodingJobOptions BaseRequest { get; set; }
|
||||
|
||||
public bool IsVideoRequest { get; set; }
|
||||
|
||||
public TranscodingJobType TranscodingType { get; set; }
|
||||
|
||||
public long? StartTimeTicks => BaseRequest.StartTimeTicks;
|
||||
|
||||
public bool CopyTimestamps => BaseRequest.CopyTimestamps;
|
||||
|
||||
public bool IsSegmentedLiveStream
|
||||
=> TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue;
|
||||
|
||||
public int? TotalOutputBitrate => (OutputAudioBitrate ?? 0) + (OutputVideoBitrate ?? 0);
|
||||
|
||||
public int? OutputWidth
|
||||
{
|
||||
get
|
||||
{
|
||||
if (VideoStream is not null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
|
||||
{
|
||||
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
|
||||
|
||||
var newSize = DrawingUtils.Resize(
|
||||
size,
|
||||
BaseRequest.Width ?? 0,
|
||||
BaseRequest.Height ?? 0,
|
||||
BaseRequest.MaxWidth ?? 0,
|
||||
BaseRequest.MaxHeight ?? 0);
|
||||
|
||||
return newSize.Width;
|
||||
}
|
||||
|
||||
if (!IsVideoRequest)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return BaseRequest.MaxWidth ?? BaseRequest.Width;
|
||||
}
|
||||
}
|
||||
|
||||
public int? OutputHeight
|
||||
{
|
||||
get
|
||||
{
|
||||
if (VideoStream is not null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
|
||||
{
|
||||
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
|
||||
|
||||
var newSize = DrawingUtils.Resize(
|
||||
size,
|
||||
BaseRequest.Width ?? 0,
|
||||
BaseRequest.Height ?? 0,
|
||||
BaseRequest.MaxWidth ?? 0,
|
||||
BaseRequest.MaxHeight ?? 0);
|
||||
|
||||
return newSize.Height;
|
||||
}
|
||||
|
||||
if (!IsVideoRequest)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return BaseRequest.MaxHeight ?? BaseRequest.Height;
|
||||
}
|
||||
}
|
||||
|
||||
public int? OutputAudioSampleRate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static
|
||||
|| EncodingHelper.IsCopyCodec(OutputAudioCodec))
|
||||
{
|
||||
if (AudioStream is not null)
|
||||
{
|
||||
return AudioStream.SampleRate;
|
||||
}
|
||||
}
|
||||
else if (BaseRequest.AudioSampleRate.HasValue)
|
||||
{
|
||||
// Don't exceed what the encoder supports
|
||||
// Seeing issues of attempting to encode to 88200
|
||||
return BaseRequest.AudioSampleRate.Value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int? OutputAudioBitDepth
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static
|
||||
|| EncodingHelper.IsCopyCodec(OutputAudioCodec))
|
||||
{
|
||||
if (AudioStream is not null)
|
||||
{
|
||||
return AudioStream.BitDepth;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target video level.
|
||||
/// </summary>
|
||||
public double? TargetVideoLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.Level;
|
||||
}
|
||||
|
||||
var level = GetRequestedLevel(ActualOutputVideoCodec);
|
||||
if (double.TryParse(level, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target video bit depth.
|
||||
/// </summary>
|
||||
public int? TargetVideoBitDepth
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static
|
||||
|| EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.BitDepth;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target reference frames.
|
||||
/// </summary>
|
||||
/// <value>The target reference frames.</value>
|
||||
public int? TargetRefFrames
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static
|
||||
|| EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.RefFrames;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target framerate.
|
||||
/// </summary>
|
||||
public float? TargetFramerate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static
|
||||
|| EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.ReferenceFrameRate;
|
||||
}
|
||||
|
||||
return BaseRequest.MaxFramerate ?? BaseRequest.Framerate;
|
||||
}
|
||||
}
|
||||
|
||||
public TransportStreamTimestamp TargetTimestamp
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static)
|
||||
{
|
||||
return InputTimestamp;
|
||||
}
|
||||
|
||||
return string.Equals(OutputContainer, "m2ts", StringComparison.OrdinalIgnoreCase) ?
|
||||
TransportStreamTimestamp.Valid :
|
||||
TransportStreamTimestamp.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target packet length.
|
||||
/// </summary>
|
||||
public int? TargetPacketLength
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.PacketLength;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target video profile.
|
||||
/// </summary>
|
||||
public string TargetVideoProfile
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.Profile;
|
||||
}
|
||||
|
||||
var requestedProfile = GetRequestedProfiles(ActualOutputVideoCodec).FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(requestedProfile))
|
||||
{
|
||||
return requestedProfile;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target video range type.
|
||||
/// </summary>
|
||||
public VideoRangeType TargetVideoRangeType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
|
||||
}
|
||||
|
||||
if (Enum.TryParse(GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault() ?? "Unknown", true, out VideoRangeType requestedRangeType))
|
||||
{
|
||||
return requestedRangeType;
|
||||
}
|
||||
|
||||
return VideoRangeType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public string TargetVideoCodecTag
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static
|
||||
|| EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.CodecTag;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool? IsTargetAnamorphic
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static
|
||||
|| EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.IsAnamorphic;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string ActualOutputVideoCodec
|
||||
{
|
||||
get
|
||||
{
|
||||
if (VideoStream is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream.Codec;
|
||||
}
|
||||
|
||||
return OutputVideoCodec;
|
||||
}
|
||||
}
|
||||
|
||||
public string ActualOutputAudioCodec
|
||||
{
|
||||
get
|
||||
{
|
||||
if (AudioStream is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(OutputAudioCodec))
|
||||
{
|
||||
return AudioStream.Codec;
|
||||
}
|
||||
|
||||
return OutputAudioCodec;
|
||||
}
|
||||
}
|
||||
|
||||
public bool? IsTargetInterlaced
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static
|
||||
|| EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.IsInterlaced;
|
||||
}
|
||||
|
||||
if (DeInterlace(ActualOutputVideoCodec, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return VideoStream?.IsInterlaced;
|
||||
}
|
||||
}
|
||||
|
||||
public bool? IsTargetAVC
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.IsAVC;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int? TargetVideoStreamCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static)
|
||||
{
|
||||
return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
|
||||
}
|
||||
|
||||
return GetMediaStreamCount(MediaStreamType.Video, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public int? TargetAudioStreamCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static)
|
||||
{
|
||||
return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
|
||||
}
|
||||
|
||||
return GetMediaStreamCount(MediaStreamType.Audio, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableAudioVbrEncoding => BaseRequest.EnableAudioVbrEncoding;
|
||||
|
||||
public int HlsListSize => 0;
|
||||
|
||||
private int? GetMediaStreamCount(MediaStreamType type, int limit)
|
||||
{
|
||||
var count = MediaSource.GetStreamCount(type);
|
||||
|
||||
if (count.HasValue)
|
||||
{
|
||||
count = Math.Min(count.Value, limit);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public string GetMimeType(string outputPath, bool enableStreamDefault = true)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(MimeType))
|
||||
{
|
||||
return MimeType;
|
||||
}
|
||||
|
||||
if (enableStreamDefault)
|
||||
{
|
||||
return MimeTypes.GetMimeType(outputPath);
|
||||
}
|
||||
|
||||
return MimeTypes.GetMimeType(outputPath, null);
|
||||
}
|
||||
|
||||
public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
|
||||
{
|
||||
var videoStream = VideoStream;
|
||||
var isInputInterlaced = videoStream is not null && videoStream.IsInterlaced;
|
||||
|
||||
if (!isInputInterlaced)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Support general param
|
||||
if (BaseRequest.DeInterlace)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(videoCodec))
|
||||
{
|
||||
if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return forceDeinterlaceIfSourceIsInterlaced;
|
||||
}
|
||||
|
||||
public string[] GetRequestedProfiles(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BaseRequest.Profile))
|
||||
{
|
||||
return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var profile = BaseRequest.GetOption(codec, "profile");
|
||||
|
||||
if (!string.IsNullOrEmpty(profile))
|
||||
{
|
||||
return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string[] GetRequestedRangeTypes(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
|
||||
{
|
||||
return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var rangetype = BaseRequest.GetOption(codec, "rangetype");
|
||||
|
||||
if (!string.IsNullOrEmpty(rangetype))
|
||||
{
|
||||
return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string[] GetRequestedCodecTags(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
|
||||
{
|
||||
return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var codectag = BaseRequest.GetOption(codec, "codectag");
|
||||
|
||||
if (!string.IsNullOrEmpty(codectag))
|
||||
{
|
||||
return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string GetRequestedLevel(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BaseRequest.Level))
|
||||
{
|
||||
return BaseRequest.Level;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
return BaseRequest.GetOption(codec, "level");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int? GetRequestedMaxRefFrames(string codec)
|
||||
{
|
||||
if (BaseRequest.MaxRefFrames.HasValue)
|
||||
{
|
||||
return BaseRequest.MaxRefFrames;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var value = BaseRequest.GetOption(codec, "maxrefframes");
|
||||
if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int? GetRequestedVideoBitDepth(string codec)
|
||||
{
|
||||
if (BaseRequest.MaxVideoBitDepth.HasValue)
|
||||
{
|
||||
return BaseRequest.MaxVideoBitDepth;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var value = BaseRequest.GetOption(codec, "videobitdepth");
|
||||
if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int? GetRequestedAudioBitDepth(string codec)
|
||||
{
|
||||
if (BaseRequest.MaxAudioBitDepth.HasValue)
|
||||
{
|
||||
return BaseRequest.MaxAudioBitDepth;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var value = BaseRequest.GetOption(codec, "audiobitdepth");
|
||||
if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int? GetRequestedAudioChannels(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var value = BaseRequest.GetOption(codec, "audiochannels");
|
||||
if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (BaseRequest.MaxAudioChannels.HasValue)
|
||||
{
|
||||
return BaseRequest.MaxAudioChannels;
|
||||
}
|
||||
|
||||
if (BaseRequest.AudioChannels.HasValue)
|
||||
{
|
||||
return BaseRequest.AudioChannels;
|
||||
}
|
||||
|
||||
if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
|
||||
{
|
||||
return BaseRequest.TranscodingMaxAudioChannels;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
|
||||
{
|
||||
Progress.Report(percentComplete.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum FilterOptionType.
|
||||
/// </summary>
|
||||
public enum FilterOptionType
|
||||
{
|
||||
/// <summary>
|
||||
/// The scale_cuda_format.
|
||||
/// </summary>
|
||||
ScaleCudaFormat = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The tonemap_cuda_name.
|
||||
/// </summary>
|
||||
TonemapCudaName = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The tonemap_opencl_bt2390.
|
||||
/// </summary>
|
||||
TonemapOpenclBt2390 = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The overlay_opencl_framesync.
|
||||
/// </summary>
|
||||
OverlayOpenclFrameSync = 3,
|
||||
|
||||
/// <summary>
|
||||
/// The overlay_vaapi_framesync.
|
||||
/// </summary>
|
||||
OverlayVaapiFrameSync = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The overlay_vulkan_framesync.
|
||||
/// </summary>
|
||||
OverlayVulkanFrameSync = 5,
|
||||
|
||||
/// <summary>
|
||||
/// The transpose_opencl_reversal.
|
||||
/// </summary>
|
||||
TransposeOpenclReversal = 6,
|
||||
|
||||
/// <summary>
|
||||
/// The overlay_opencl_alpha_format.
|
||||
/// </summary>
|
||||
OverlayOpenclAlphaFormat = 7,
|
||||
|
||||
/// <summary>
|
||||
/// The overlay_cuda_alpha_format.
|
||||
/// </summary>
|
||||
OverlayCudaAlphaFormat = 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding;
|
||||
|
||||
public interface IAttachmentExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path to the attachment file.
|
||||
/// </summary>
|
||||
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
||||
/// <param name="mediaSourceId">The media source id.</param>
|
||||
/// <param name="attachmentStreamIndex">The attachment index.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The async task.</returns>
|
||||
Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment(
|
||||
BaseItem item,
|
||||
string mediaSourceId,
|
||||
int attachmentStreamIndex,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the attachment file.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">The input file path.</param>
|
||||
/// <param name="mediaSource">The <see cref="MediaSourceInfo" /> source id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The async task.</returns>
|
||||
Task ExtractAllAttachments(
|
||||
string inputFile,
|
||||
MediaSourceInfo mediaSource,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IMediaEncoder.
|
||||
/// </summary>
|
||||
public interface IMediaEncoder : ITranscoderSupport
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the encoder path.
|
||||
/// </summary>
|
||||
/// <value>The encoder path.</value>
|
||||
string EncoderPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the probe path.
|
||||
/// </summary>
|
||||
/// <value>The probe path.</value>
|
||||
string ProbePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version of encoder.
|
||||
/// </summary>
|
||||
/// <returns>The version of encoder.</returns>
|
||||
Version EncoderVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether p key pausing is supported.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if p key pausing is supported, <c>false</c> otherwise.</value>
|
||||
bool IsPkeyPauseSupported { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the configured Vaapi device is from AMD(radeonsi/r600 Mesa driver).
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Vaapi device is an AMD(radeonsi/r600 Mesa driver) GPU, <c>false</c> otherwise.</value>
|
||||
bool IsVaapiDeviceAmd { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the configured Vaapi device is from Intel(iHD driver).
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Vaapi device is an Intel(iHD driver) GPU, <c>false</c> otherwise.</value>
|
||||
bool IsVaapiDeviceInteliHD { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the configured Vaapi device is from Intel(legacy i965 driver).
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Vaapi device is an Intel(legacy i965 driver) GPU, <c>false</c> otherwise.</value>
|
||||
bool IsVaapiDeviceInteli965 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Vaapi device supports vulkan drm format modifier, <c>false</c> otherwise.</value>
|
||||
bool IsVaapiDeviceSupportVulkanDrmModifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the configured Vaapi device supports vulkan drm interop via dma-buf.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Vaapi device supports vulkan drm interop, <c>false</c> otherwise.</value>
|
||||
bool IsVaapiDeviceSupportVulkanDrmInterop { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether av1 decoding is available via VideoToolbox.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the av1 is available via VideoToolbox, <c>false</c> otherwise.</value>
|
||||
bool IsVideoToolboxAv1DecodeAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether given encoder codec is supported.
|
||||
/// </summary>
|
||||
/// <param name="encoder">The encoder.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool SupportsEncoder(string encoder);
|
||||
|
||||
/// <summary>
|
||||
/// Whether given decoder codec is supported.
|
||||
/// </summary>
|
||||
/// <param name="decoder">The decoder.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool SupportsDecoder(string decoder);
|
||||
|
||||
/// <summary>
|
||||
/// Whether given hardware acceleration type is supported.
|
||||
/// </summary>
|
||||
/// <param name="hwaccel">The hwaccel.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool SupportsHwaccel(string hwaccel);
|
||||
|
||||
/// <summary>
|
||||
/// Whether given filter is supported.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter.</param>
|
||||
/// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
|
||||
bool SupportsFilter(string filter);
|
||||
|
||||
/// <summary>
|
||||
/// Whether filter is supported with the given option.
|
||||
/// </summary>
|
||||
/// <param name="option">The option.</param>
|
||||
/// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
|
||||
bool SupportsFilterWithOption(FilterOptionType option);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the bitstream filter is supported with the given option.
|
||||
/// </summary>
|
||||
/// <param name="option">The option.</param>
|
||||
/// <returns><c>true</c> if the bitstream filter is supported, <c>false</c> otherwise.</returns>
|
||||
bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the audio image.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="imageStreamIndex">Index of the image stream.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{Stream}.</returns>
|
||||
Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the video image.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">Input file.</param>
|
||||
/// <param name="container">Video container type.</param>
|
||||
/// <param name="mediaSource">Media source information.</param>
|
||||
/// <param name="videoStream">Media stream information.</param>
|
||||
/// <param name="threedFormat">Video 3D format.</param>
|
||||
/// <param name="offset">Time offset.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
|
||||
/// <returns>Location of video image.</returns>
|
||||
Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the video image.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">Input file.</param>
|
||||
/// <param name="container">Video container type.</param>
|
||||
/// <param name="mediaSource">Media source information.</param>
|
||||
/// <param name="imageStream">Media stream information.</param>
|
||||
/// <param name="imageStreamIndex">Index of the stream to extract from.</param>
|
||||
/// <param name="targetFormat">The format of the file to write.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
|
||||
/// <returns>Location of video image.</returns>
|
||||
Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the video images on interval.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">Input file.</param>
|
||||
/// <param name="container">Video container type.</param>
|
||||
/// <param name="mediaSource">Media source information.</param>
|
||||
/// <param name="imageStream">Media stream information.</param>
|
||||
/// <param name="maxWidth">The maximum width.</param>
|
||||
/// <param name="interval">The interval.</param>
|
||||
/// <param name="allowHwAccel">Allow for hardware acceleration.</param>
|
||||
/// <param name="enableHwEncoding">Use hardware mjpeg encoder.</param>
|
||||
/// <param name="threads">The input/output thread count for ffmpeg.</param>
|
||||
/// <param name="qualityScale">The qscale value for ffmpeg.</param>
|
||||
/// <param name="priority">The process priority for the ffmpeg process.</param>
|
||||
/// <param name="enableKeyFrameOnlyExtraction">Whether to only extract key frames.</param>
|
||||
/// <param name="encodingHelper">EncodingHelper instance.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Directory where images where extracted. A given image made before another will always be named with a lower number.</returns>
|
||||
Task<string> ExtractVideoImagesOnIntervalAccelerated(
|
||||
string inputFile,
|
||||
string container,
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaStream imageStream,
|
||||
int maxWidth,
|
||||
TimeSpan interval,
|
||||
bool allowHwAccel,
|
||||
bool enableHwEncoding,
|
||||
int? threads,
|
||||
int? qualityScale,
|
||||
ProcessPriorityClass? priority,
|
||||
bool enableKeyFrameOnlyExtraction,
|
||||
EncodingHelper encodingHelper,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media info.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input argument.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">The input file.</param>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetInputArgument(string inputFile, MediaSourceInfo mediaSource);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input argument.
|
||||
/// </summary>
|
||||
/// <param name="inputFiles">The input files.</param>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input argument for an external subtitle file.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">The input file.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetExternalSubtitleInputArgument(string inputFile);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time parameter.
|
||||
/// </summary>
|
||||
/// <param name="ticks">The ticks.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetTimeParameter(long ticks);
|
||||
|
||||
Task ConvertImage(string inputPath, string outputPath);
|
||||
|
||||
/// <summary>
|
||||
/// Escapes the subtitle filter path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string EscapeSubtitleFilterPath(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the path to find FFmpeg.
|
||||
/// </summary>
|
||||
/// <returns>bool indicates whether a valid ffmpeg is found.</returns>
|
||||
bool SetFFmpegPath();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary playlist of .vob files.
|
||||
/// </summary>
|
||||
/// <param name="path">The to the .vob files.</param>
|
||||
/// <param name="titleNumber">The title number to start with.</param>
|
||||
/// <returns>A playlist.</returns>
|
||||
IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary playlist of .m2ts files.
|
||||
/// </summary>
|
||||
/// <param name="path">The to the .m2ts files.</param>
|
||||
/// <returns>A playlist.</returns>
|
||||
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input path argument from <see cref="EncodingJobInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="state">The <see cref="EncodingJobInfo"/>.</param>
|
||||
/// <returns>The input path argument.</returns>
|
||||
string GetInputPathArgument(EncodingJobInfo state);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input path argument.
|
||||
/// </summary>
|
||||
/// <param name="path">The item path.</param>
|
||||
/// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
|
||||
/// <returns>The input path argument.</returns>
|
||||
string GetInputPathArgument(string path, MediaSourceInfo mediaSource);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a FFmpeg concat config for the source.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="MediaSourceInfo"/>.</param>
|
||||
/// <param name="concatFilePath">The path the config should be written to.</param>
|
||||
void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public interface ISubtitleEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the subtitles.
|
||||
/// </summary>
|
||||
/// <param name="item">Item to use.</param>
|
||||
/// <param name="mediaSourceId">Media source.</param>
|
||||
/// <param name="subtitleStreamIndex">Subtitle stream to use.</param>
|
||||
/// <param name="outputFormat">Output format to use.</param>
|
||||
/// <param name="startTimeTicks">Start time.</param>
|
||||
/// <param name="endTimeTicks">End time.</param>
|
||||
/// <param name="preserveOriginalTimestamps">Option to preserve original timestamps.</param>
|
||||
/// <param name="cancellationToken">The cancellation token for the operation.</param>
|
||||
/// <returns>Task{Stream}.</returns>
|
||||
Task<Stream> GetSubtitles(
|
||||
BaseItem item,
|
||||
string mediaSourceId,
|
||||
int subtitleStreamIndex,
|
||||
string outputFormat,
|
||||
long startTimeTicks,
|
||||
long endTimeTicks,
|
||||
bool preserveOriginalTimestamps,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the subtitle language encoding parameter.
|
||||
/// </summary>
|
||||
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||
/// <param name="language">The language.</param>
|
||||
/// <param name="mediaSource">The media source.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to a subtitle file.
|
||||
/// </summary>
|
||||
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||
/// <param name="mediaSource">The media source.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all extractable subtitles (text and pgs).
|
||||
/// </summary>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Streaming;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// A service for managing media transcoding.
|
||||
/// </summary>
|
||||
public interface ITranscodeManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Get transcoding job.
|
||||
/// </summary>
|
||||
/// <param name="playSessionId">Playback session id.</param>
|
||||
/// <returns>The transcoding job.</returns>
|
||||
public TranscodingJob? GetTranscodingJob(string playSessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Get transcoding job.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the transcoding file.</param>
|
||||
/// <param name="type">The <see cref="TranscodingJobType"/>.</param>
|
||||
/// <returns>The transcoding job.</returns>
|
||||
public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type);
|
||||
|
||||
/// <summary>
|
||||
/// Ping transcoding job.
|
||||
/// </summary>
|
||||
/// <param name="playSessionId">Play session id.</param>
|
||||
/// <param name="isUserPaused">Is user paused.</param>
|
||||
/// <exception cref="ArgumentNullException">Play session id is null.</exception>
|
||||
public void PingTranscodingJob(string playSessionId, bool? isUserPaused);
|
||||
|
||||
/// <summary>
|
||||
/// Kills the single transcoding job.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="playSessionId">The play session identifier.</param>
|
||||
/// <param name="deleteFiles">The delete files.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles);
|
||||
|
||||
/// <summary>
|
||||
/// Report the transcoding progress to the session manager.
|
||||
/// </summary>
|
||||
/// <param name="job">The <see cref="TranscodingJob"/> of which the progress will be reported.</param>
|
||||
/// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
|
||||
/// <param name="transcodingPosition">The current transcoding position.</param>
|
||||
/// <param name="framerate">The framerate of the transcoding job.</param>
|
||||
/// <param name="percentComplete">The completion percentage of the transcode.</param>
|
||||
/// <param name="bytesTranscoded">The number of bytes transcoded.</param>
|
||||
/// <param name="bitRate">The bitrate of the transcoding job.</param>
|
||||
public void ReportTranscodingProgress(
|
||||
TranscodingJob job,
|
||||
StreamState state,
|
||||
TimeSpan? transcodingPosition,
|
||||
float? framerate,
|
||||
double? percentComplete,
|
||||
long? bytesTranscoded,
|
||||
int? bitRate);
|
||||
|
||||
/// <summary>
|
||||
/// Starts FFMpeg.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
|
||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
||||
/// <param name="workingDirectory">The working directory.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task<TranscodingJob> StartFfMpeg(
|
||||
StreamState state,
|
||||
string outputPath,
|
||||
string commandLineArguments,
|
||||
Guid userId,
|
||||
TranscodingJobType transcodingJobType,
|
||||
CancellationTokenSource cancellationTokenSource,
|
||||
string? workingDirectory = null);
|
||||
|
||||
/// <summary>
|
||||
/// Called when [transcode begin request].
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>The <see cref="TranscodingJob"/>.</returns>
|
||||
public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type);
|
||||
|
||||
/// <summary>
|
||||
/// Called when [transcode end].
|
||||
/// </summary>
|
||||
/// <param name="job">The transcode job.</param>
|
||||
public void OnTranscodeEndRequest(TranscodingJob job);
|
||||
|
||||
/// <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>
|
||||
ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public class JobLogger
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public JobLogger(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartStreamingLog(EncodingJobInfo state, StreamReader reader, Stream target)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (target)
|
||||
using (reader)
|
||||
{
|
||||
string line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
while (line is not null && reader.BaseStream.CanRead)
|
||||
{
|
||||
ParseLogLine(line, state);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
|
||||
|
||||
// If ffmpeg process is closed, the state is disposed, so don't write to target in that case
|
||||
if (!target.CanWrite)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await target.WriteAsync(bytes).ConfigureAwait(false);
|
||||
|
||||
// Check again, the stream could have been closed
|
||||
if (!target.CanWrite)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await target.FlushAsync().ConfigureAwait(false);
|
||||
line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading ffmpeg log");
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseLogLine(string line, EncodingJobInfo state)
|
||||
{
|
||||
float? framerate = null;
|
||||
double? percent = null;
|
||||
TimeSpan? transcodingPosition = null;
|
||||
long? bytesTranscoded = null;
|
||||
int? bitRate = null;
|
||||
|
||||
var parts = line.Split(' ');
|
||||
|
||||
var totalMs = state.RunTimeTicks.HasValue
|
||||
? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
|
||||
: 0;
|
||||
|
||||
var startMs = state.BaseRequest.StartTimeTicks.HasValue
|
||||
? TimeSpan.FromTicks(state.BaseRequest.StartTimeTicks.Value).TotalMilliseconds
|
||||
: 0;
|
||||
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) &&
|
||||
(i + 1 < parts.Length))
|
||||
{
|
||||
var rate = parts[i + 1];
|
||||
|
||||
if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
framerate = val;
|
||||
}
|
||||
}
|
||||
else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var rate = part.Split('=', 2)[^1];
|
||||
|
||||
if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
framerate = val;
|
||||
}
|
||||
}
|
||||
else if (state.RunTimeTicks.HasValue &&
|
||||
part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var time = part.Split('=', 2)[^1];
|
||||
|
||||
if (TimeSpan.TryParse(time, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
var currentMs = startMs + val.TotalMilliseconds;
|
||||
|
||||
percent = 100.0 * currentMs / totalMs;
|
||||
|
||||
transcodingPosition = TimeSpan.FromMilliseconds(currentMs);
|
||||
}
|
||||
}
|
||||
else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var size = part.Split('=', 2)[^1];
|
||||
|
||||
int? scale = null;
|
||||
if (size.Contains("kb", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
scale = 1024;
|
||||
size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (scale.HasValue)
|
||||
{
|
||||
if (long.TryParse(size, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
bytesTranscoded = val * scale.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var rate = part.Split('=', 2)[^1];
|
||||
|
||||
int? scale = null;
|
||||
if (rate.Contains("kbits/s", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
scale = 1024;
|
||||
rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (scale.HasValue)
|
||||
{
|
||||
if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
bitRate = (int)Math.Ceiling(val * scale.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (framerate.HasValue || percent.HasValue)
|
||||
{
|
||||
state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public class MediaInfoRequest
|
||||
{
|
||||
public MediaSourceInfo MediaSource { get; set; }
|
||||
|
||||
public bool ExtractChapters { get; set; }
|
||||
|
||||
public DlnaProfileType MediaType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Class TranscodingJob.
|
||||
/// </summary>
|
||||
public sealed class TranscodingJob : IDisposable
|
||||
{
|
||||
private readonly ILogger<TranscodingJob> _logger;
|
||||
private readonly Lock _processLock = new();
|
||||
private readonly Lock _timerLock = new();
|
||||
|
||||
private Timer? _killTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodingJob"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobDto}"/> interface.</param>
|
||||
public TranscodingJob(ILogger<TranscodingJob> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the play session identifier.
|
||||
/// </summary>
|
||||
public string? PlaySessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the live stream identifier.
|
||||
/// </summary>
|
||||
public string? LiveStreamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether is live output.
|
||||
/// </summary>
|
||||
public bool IsLiveOutput { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
public MediaSourceInfo? MediaSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets path.
|
||||
/// </summary>
|
||||
public string? Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
/// </summary>
|
||||
public TranscodingJobType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the process.
|
||||
/// </summary>
|
||||
public Process? Process { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the active request count.
|
||||
/// </summary>
|
||||
public int ActiveRequestCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets device id.
|
||||
/// </summary>
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets cancellation token source.
|
||||
/// </summary>
|
||||
public CancellationTokenSource? CancellationTokenSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether has exited.
|
||||
/// </summary>
|
||||
public bool HasExited { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets exit code.
|
||||
/// </summary>
|
||||
public int ExitCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether is user paused.
|
||||
/// </summary>
|
||||
public bool IsUserPaused { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets id.
|
||||
/// </summary>
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets framerate.
|
||||
/// </summary>
|
||||
public float? Framerate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets completion percentage.
|
||||
/// </summary>
|
||||
public double? CompletionPercentage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets bytes downloaded.
|
||||
/// </summary>
|
||||
public long BytesDownloaded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets bytes transcoded.
|
||||
/// </summary>
|
||||
public long? BytesTranscoded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets bit rate.
|
||||
/// </summary>
|
||||
public int? BitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets transcoding position ticks.
|
||||
/// </summary>
|
||||
public long? TranscodingPositionTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets download position ticks.
|
||||
/// </summary>
|
||||
public long? DownloadPositionTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets transcoding throttler.
|
||||
/// </summary>
|
||||
public TranscodingThrottler? TranscodingThrottler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets transcoding segment cleaner.
|
||||
/// </summary>
|
||||
public TranscodingSegmentCleaner? TranscodingSegmentCleaner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets last ping date.
|
||||
/// </summary>
|
||||
public DateTime LastPingDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets ping timeout.
|
||||
/// </summary>
|
||||
public int PingTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stop kill timer.
|
||||
/// </summary>
|
||||
public void StopKillTimer()
|
||||
{
|
||||
lock (_timerLock)
|
||||
{
|
||||
_killTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose kill timer.
|
||||
/// </summary>
|
||||
public void DisposeKillTimer()
|
||||
{
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (_killTimer is not null)
|
||||
{
|
||||
_killTimer.Dispose();
|
||||
_killTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start kill timer.
|
||||
/// </summary>
|
||||
/// <param name="callback">Callback action.</param>
|
||||
public void StartKillTimer(Action<object?> callback)
|
||||
{
|
||||
StartKillTimer(callback, PingTimeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start kill timer.
|
||||
/// </summary>
|
||||
/// <param name="callback">Callback action.</param>
|
||||
/// <param name="intervalMs">Callback interval.</param>
|
||||
public void StartKillTimer(Action<object?> callback, int intervalMs)
|
||||
{
|
||||
if (HasExited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (_killTimer is null)
|
||||
{
|
||||
_logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||
_killTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||
_killTimer.Change(intervalMs, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change kill timer if started.
|
||||
/// </summary>
|
||||
public void ChangeKillTimerIfStarted()
|
||||
{
|
||||
if (HasExited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (_killTimer is not null)
|
||||
{
|
||||
var intervalMs = PingTimeout;
|
||||
|
||||
_logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||
_killTimer.Change(intervalMs, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the transcoding job.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
lock (_processLock)
|
||||
{
|
||||
#pragma warning disable CA1849 // Can't await in lock block
|
||||
TranscodingThrottler?.Stop().GetAwaiter().GetResult();
|
||||
TranscodingSegmentCleaner?.Stop();
|
||||
|
||||
var process = Process;
|
||||
|
||||
if (!HasExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Stopping ffmpeg process with q command for {Path}", Path);
|
||||
|
||||
process!.StandardInput.WriteLine("q");
|
||||
|
||||
// Need to wait because killing is asynchronous.
|
||||
if (!process.WaitForExit(5000))
|
||||
{
|
||||
_logger.LogInformation("Killing FFmpeg process for {Path}", Path);
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1849
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Process?.Dispose();
|
||||
Process = null;
|
||||
_killTimer?.Dispose();
|
||||
_killTimer = null;
|
||||
CancellationTokenSource?.Dispose();
|
||||
CancellationTokenSource = null;
|
||||
TranscodingThrottler?.Dispose();
|
||||
TranscodingThrottler = null;
|
||||
TranscodingSegmentCleaner?.Dispose();
|
||||
TranscodingSegmentCleaner = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum TranscodingJobType.
|
||||
/// </summary>
|
||||
public enum TranscodingJobType
|
||||
{
|
||||
/// <summary>
|
||||
/// The progressive.
|
||||
/// </summary>
|
||||
Progressive,
|
||||
|
||||
/// <summary>
|
||||
/// The HLS.
|
||||
/// </summary>
|
||||
Hls,
|
||||
|
||||
/// <summary>
|
||||
/// The dash.
|
||||
/// </summary>
|
||||
Dash
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Transcoding segment cleaner.
|
||||
/// </summary>
|
||||
public class TranscodingSegmentCleaner : IDisposable
|
||||
{
|
||||
private readonly TranscodingJob _job;
|
||||
private readonly ILogger<TranscodingSegmentCleaner> _logger;
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private Timer? _timer;
|
||||
private int _segmentLength;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodingSegmentCleaner"/> class.
|
||||
/// </summary>
|
||||
/// <param name="job">Transcoding job dto.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingSegmentCleaner}"/> interface.</param>
|
||||
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="segmentLength">The segment length of this transcoding job.</param>
|
||||
public TranscodingSegmentCleaner(TranscodingJob job, ILogger<TranscodingSegmentCleaner> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder, int segmentLength)
|
||||
{
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_segmentLength = segmentLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start timer.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_timer = new Timer(TimerCallback, null, 20000, 20000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop cleaner.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
DisposeTimer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose cleaner.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose cleaner.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Disposing.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
DisposeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private EncodingOptions GetOptions()
|
||||
{
|
||||
return _config.GetEncodingOptions();
|
||||
}
|
||||
|
||||
private async void TimerCallback(object? state)
|
||||
{
|
||||
if (_job.HasExited)
|
||||
{
|
||||
DisposeTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
var options = GetOptions();
|
||||
var enableSegmentDeletion = options.EnableSegmentDeletion;
|
||||
var segmentKeepSeconds = Math.Max(options.SegmentKeepSeconds, 20);
|
||||
|
||||
if (enableSegmentDeletion)
|
||||
{
|
||||
var downloadPositionTicks = _job.DownloadPositionTicks ?? 0;
|
||||
var downloadPositionSeconds = Convert.ToInt64(TimeSpan.FromTicks(downloadPositionTicks).TotalSeconds);
|
||||
|
||||
if (downloadPositionSeconds > 0 && segmentKeepSeconds > 0 && downloadPositionSeconds > segmentKeepSeconds)
|
||||
{
|
||||
var idxMaxToDelete = (downloadPositionSeconds - segmentKeepSeconds) / _segmentLength;
|
||||
|
||||
if (idxMaxToDelete > 0)
|
||||
{
|
||||
await DeleteSegmentFiles(_job, 0, idxMaxToDelete, 1500).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSegmentFiles(TranscodingJob job, long idxMin, long idxMax, int delayMs)
|
||||
{
|
||||
var path = job.Path ?? throw new ArgumentException("Path can't be null.");
|
||||
|
||||
_logger.LogDebug("Deleting segment file(s) index {Min} to {Max} from {Path}", idxMin, idxMax, path);
|
||||
|
||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (job.Type == TranscodingJobType.Hls)
|
||||
{
|
||||
DeleteHlsSegmentFiles(path, idxMin, idxMax);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Error deleting segment file(s) {Path}", path);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteHlsSegmentFiles(string outputFilePath, long idxMin, long idxMax)
|
||||
{
|
||||
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 => long.TryParse(Path.GetFileNameWithoutExtension(f).Replace(name, string.Empty, StringComparison.Ordinal), out var idx)
|
||||
&& (idx >= idxMin && idx <= idxMax));
|
||||
|
||||
List<Exception>? exs = null;
|
||||
foreach (var file in filesToDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Deleting HLS segment file {0}", file);
|
||||
_fileSystem.DeleteFile(file);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
(exs ??= new List<Exception>()).Add(ex);
|
||||
_logger.LogDebug(ex, "Error deleting HLS segment file {Path}", file);
|
||||
}
|
||||
}
|
||||
|
||||
if (exs is not null)
|
||||
{
|
||||
throw new AggregateException("Error deleting HLS segment files", exs);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeTimer()
|
||||
{
|
||||
if (_timer is not null)
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Transcoding throttler.
|
||||
/// </summary>
|
||||
public class TranscodingThrottler : IDisposable
|
||||
{
|
||||
private readonly TranscodingJob _job;
|
||||
private readonly ILogger<TranscodingThrottler> _logger;
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private Timer? _timer;
|
||||
private bool _isPaused;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodingThrottler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="job">Transcoding job dto.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
|
||||
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
public TranscodingThrottler(TranscodingJob job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start timer.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_timer = new Timer(TimerCallback, null, 5000, 5000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpause transcoding.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/>.</returns>
|
||||
public async Task UnpauseTranscoding()
|
||||
{
|
||||
if (_isPaused)
|
||||
{
|
||||
_logger.LogDebug("Sending resume command to ffmpeg");
|
||||
|
||||
try
|
||||
{
|
||||
var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine;
|
||||
await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false);
|
||||
_isPaused = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error resuming transcoding");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop throttler.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/>.</returns>
|
||||
public async Task Stop()
|
||||
{
|
||||
DisposeTimer();
|
||||
await UnpauseTranscoding().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose throttler.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose throttler.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Disposing.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
DisposeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private EncodingOptions GetOptions()
|
||||
{
|
||||
return _config.GetEncodingOptions();
|
||||
}
|
||||
|
||||
private async void TimerCallback(object? state)
|
||||
{
|
||||
if (_job.HasExited)
|
||||
{
|
||||
DisposeTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
var options = GetOptions();
|
||||
|
||||
if (options.EnableThrottling && IsThrottleAllowed(_job, Math.Max(options.ThrottleDelaySeconds, 60)))
|
||||
{
|
||||
await PauseTranscoding().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await UnpauseTranscoding().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PauseTranscoding()
|
||||
{
|
||||
if (!_isPaused)
|
||||
{
|
||||
var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c";
|
||||
|
||||
_logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey);
|
||||
|
||||
try
|
||||
{
|
||||
await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
|
||||
_isPaused = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error pausing transcoding");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds)
|
||||
{
|
||||
var bytesDownloaded = job.BytesDownloaded;
|
||||
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
|
||||
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
|
||||
|
||||
var path = job.Path ?? throw new ArgumentException("Path can't be null.");
|
||||
|
||||
var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
|
||||
|
||||
if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
|
||||
{
|
||||
// HLS - time-based consideration
|
||||
|
||||
var targetGap = gapLengthInTicks;
|
||||
var gap = transcodingPositionTicks - downloadPositionTicks;
|
||||
|
||||
if (gap < targetGap)
|
||||
{
|
||||
_logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
|
||||
{
|
||||
// Progressive Streaming - byte-based consideration
|
||||
|
||||
try
|
||||
{
|
||||
var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length;
|
||||
|
||||
// Estimate the bytes the transcoder should be ahead
|
||||
double gapFactor = gapLengthInTicks;
|
||||
gapFactor /= transcodingPositionTicks;
|
||||
var targetGap = bytesTranscoded * gapFactor;
|
||||
|
||||
var gap = bytesTranscoded - bytesDownloaded;
|
||||
|
||||
if (gap < targetGap)
|
||||
{
|
||||
_logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting output size");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("No throttle data for {Path}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DisposeTimer()
|
||||
{
|
||||
if (_timer is not null)
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user