5e779f20ee
Issue: https://github.com/jellyfin/jellyfin/issues/6450 Enable DirectPlay responses Rewrite DirectPlay and DirectStream resolution Prefer copy transcode video codec options Enhance condition processor Support DirectStream and Transcode with parity Rework audio stream selection and add tests for ExternalAudio Update MediaInfoHelper to only call StreamBuilder once
696 lines
19 KiB
C#
696 lines
19 KiB
C#
#nullable disable
|
|
|
|
#pragma warning disable CS1591, SA1401
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text.Json.Serialization;
|
|
using Jellyfin.Data.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
|
|
{
|
|
public int? OutputAudioBitrate;
|
|
public int? OutputAudioChannels;
|
|
|
|
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 TranscodeReason[] TranscodeReasons => TranscodeReason.ToArray();
|
|
|
|
[JsonIgnore]
|
|
public TranscodeReason TranscodeReason
|
|
{
|
|
get
|
|
{
|
|
if (!_transcodeReasons.HasValue)
|
|
{
|
|
if (BaseRequest.TranscodeReasons == null)
|
|
{
|
|
_transcodeReasons = TranscodeReason.None;
|
|
return TranscodeReason.None;
|
|
}
|
|
|
|
_ = 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 != 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 != 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 != 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 != 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 (!string.IsNullOrEmpty(level)
|
|
&& double.TryParse(level, NumberStyles.Any, 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 == null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
|
{
|
|
return VideoStream.Codec;
|
|
}
|
|
|
|
return OutputVideoCodec;
|
|
}
|
|
}
|
|
|
|
public string ActualOutputAudioCodec
|
|
{
|
|
get
|
|
{
|
|
if (AudioStream == 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 int HlsListSize => 0;
|
|
|
|
public bool EnableBreakOnNonKeyFrames(string videoCodec)
|
|
{
|
|
if (TranscodingType != TranscodingJobType.Progressive)
|
|
{
|
|
if (IsSegmentedLiveStream)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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 != 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(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(codec))
|
|
{
|
|
var profile = BaseRequest.GetOption(codec, "profile");
|
|
|
|
if (!string.IsNullOrEmpty(profile))
|
|
{
|
|
return profile.Split(new[] { '|', ',' }, 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 (!string.IsNullOrEmpty(value)
|
|
&& int.TryParse(value, NumberStyles.Any, 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 (!string.IsNullOrEmpty(value)
|
|
&& int.TryParse(value, NumberStyles.Any, 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 (!string.IsNullOrEmpty(value)
|
|
&& int.TryParse(value, NumberStyles.Any, 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 (!string.IsNullOrEmpty(value)
|
|
&& int.TryParse(value, NumberStyles.Any, 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);
|
|
}
|
|
}
|
|
}
|