aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Model/Dlna
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Model/Dlna')
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs17
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs2
-rw-r--r--MediaBrowser.Model/Dlna/ProfileConditionValue.cs3
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs376
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs319
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs29
6 files changed, 497 insertions, 249 deletions
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index af0787990d..1b61bfe155 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -25,9 +25,10 @@ namespace MediaBrowser.Model.Dlna
/// <param name="videoFramerate">The framerate.</param>
/// <param name="packetLength">The packet length.</param>
/// <param name="timestamp">The <see cref="TransportStreamTimestamp"/>.</param>
- /// <param name="isAnamorphic">A value indicating whether tthe video is anamorphic.</param>
- /// <param name="isInterlaced">A value indicating whether tthe video is interlaced.</param>
+ /// <param name="isAnamorphic">A value indicating whether the video is anamorphic.</param>
+ /// <param name="isInterlaced">A value indicating whether the video is interlaced.</param>
/// <param name="refFrames">The reference frames.</param>
+ /// <param name="numStreams">The number of streams.</param>
/// <param name="numVideoStreams">The number of video streams.</param>
/// <param name="numAudioStreams">The number of audio streams.</param>
/// <param name="videoCodecTag">The video codec tag.</param>
@@ -48,6 +49,7 @@ namespace MediaBrowser.Model.Dlna
bool? isAnamorphic,
bool? isInterlaced,
int? refFrames,
+ int numStreams,
int? numVideoStreams,
int? numAudioStreams,
string? videoCodecTag,
@@ -83,6 +85,8 @@ namespace MediaBrowser.Model.Dlna
return IsConditionSatisfied(condition, width);
case ProfileConditionValue.RefFrames:
return IsConditionSatisfied(condition, refFrames);
+ case ProfileConditionValue.NumStreams:
+ return IsConditionSatisfied(condition, numStreams);
case ProfileConditionValue.NumAudioStreams:
return IsConditionSatisfied(condition, numAudioStreams);
case ProfileConditionValue.NumVideoStreams:
@@ -341,6 +345,15 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired;
}
+ // Special case: HDR10 also satisfies if the video is HDR10Plus
+ if (currentValue.Value == VideoRangeType.HDR10Plus)
+ {
+ if (IsConditionSatisfied(condition, VideoRangeType.HDR10))
+ {
+ return true;
+ }
+ }
+
var conditionType = condition.Condition;
if (conditionType == ProfileConditionType.EqualsAny)
{
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
index 438df34415..553ccfc64b 100644
--- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -59,7 +59,7 @@ public class DirectPlayProfile
/// <returns>True if supported.</returns>
public bool SupportsAudioCodec(string? codec)
{
- // Video profiles can have audio codec restrictions too, therefore incude Video as valid type.
+ // Video profiles can have audio codec restrictions too, therefore include Video as valid type.
return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerHelper.ContainsContainer(AudioCodec, codec);
}
}
diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
index a32433e185..b66a15840b 100644
--- a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
+++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
@@ -27,6 +27,7 @@ namespace MediaBrowser.Model.Dlna
IsInterlaced = 21,
AudioSampleRate = 22,
AudioBitDepth = 23,
- VideoRangeType = 24
+ VideoRangeType = 24,
+ NumStreams = 25
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 767e012029..61e04a8134 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -30,7 +30,7 @@ namespace MediaBrowser.Model.Dlna
private readonly ITranscoderSupport _transcoderSupport;
private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
- private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
+ private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"];
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
@@ -101,21 +101,16 @@ namespace MediaBrowser.Model.Dlna
MediaStream audioStream = item.GetDefaultAudioStream(null);
+ ArgumentNullException.ThrowIfNull(audioStream);
+
var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
var directPlayMethod = directPlayInfo.PlayMethod;
var transcodeReasons = directPlayInfo.TranscodeReasons;
- var inputAudioChannels = audioStream?.Channels;
- var inputAudioBitrate = audioStream?.BitRate;
- var inputAudioSampleRate = audioStream?.SampleRate;
- var inputAudioBitDepth = audioStream?.BitDepth;
-
if (directPlayMethod is PlayMethod.DirectPlay)
{
- var profile = options.Profile;
- var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
- var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions);
+ var audioFailureReasons = GetCompatibilityAudioCodec(options, item, item.Container, audioStream, null, false, false);
transcodeReasons |= audioFailureReasons;
if (audioFailureReasons == 0)
@@ -188,6 +183,11 @@ namespace MediaBrowser.Model.Dlna
SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
+ var inputAudioChannels = audioStream.Channels;
+ var inputAudioBitrate = audioStream.BitRate;
+ var inputAudioSampleRate = audioStream.SampleRate;
+ var inputAudioBitDepth = audioStream.BitDepth;
+
var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
@@ -338,6 +338,9 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.IsSecondaryAudio:
return TranscodeReason.SecondaryAudioNotSupported;
+ case ProfileConditionValue.NumStreams:
+ return TranscodeReason.StreamCountExceedsLimit;
+
case ProfileConditionValue.NumAudioStreams:
// TODO
return 0;
@@ -662,15 +665,39 @@ namespace MediaBrowser.Model.Dlna
// Collect candidate audio streams
ICollection<MediaStream> candidateAudioStreams = audioStream is null ? [] : [audioStream];
- if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0)
+ // When the index is explicitly required by client or the default is specified by user, don't do any stream reselection.
+ if (!item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.User) && (options.AudioStreamIndex is null or < 0))
{
- if (audioStream?.IsDefault == true)
+ // When user has no preferences allow stream selection on all streams.
+ if (item.DefaultAudioIndexSource == AudioIndexSource.None && audioStream is not null)
{
- candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray();
+ candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio).ToArray();
+ if (audioStream.IsDefault)
+ {
+ // If default is picked, only allow selection within default streams.
+ candidateAudioStreams = candidateAudioStreams.Where(stream => stream.IsDefault).ToArray();
+ }
}
- else
+
+ if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Language))
{
+ // If user has language preference, only allow stream selection within the same language.
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language).ToArray();
+ if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default))
+ {
+ var defaultStreamsInPreferredLanguage = candidateAudioStreams.Where(stream => stream.IsDefault).ToArray();
+
+ // If the user also prefers default streams, try limit selection within default tracks in the same language.
+ // If there is no default stream in the preferred language, allow selection on all default streams to match the "Play default audio track regardless of language" setting.
+ candidateAudioStreams = defaultStreamsInPreferredLanguage.Length > 0
+ ? defaultStreamsInPreferredLanguage
+ : item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray();
+ }
+ }
+ else if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default))
+ {
+ // If user prefers default streams, only allow stream selection on default streams.
+ candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray();
}
}
@@ -797,7 +824,7 @@ namespace MediaBrowser.Model.Dlna
options.SubtitleStreamIndex,
playlistItem.PlayMethod,
playlistItem.TranscodeReasons,
- playlistItem.ToUrl("media:", "<token>"));
+ playlistItem.ToUrl("media:", "<token>", null));
item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
return playlistItem;
@@ -810,6 +837,10 @@ namespace MediaBrowser.Model.Dlna
MediaStream? audioStream,
StreamInfo playlistItem)
{
+ var mediaSource = playlistItem.MediaSource;
+
+ ArgumentNullException.ThrowIfNull(mediaSource);
+
if (!(item.SupportsTranscoding || item.SupportsDirectStream))
{
return (null, null);
@@ -824,17 +855,7 @@ namespace MediaBrowser.Model.Dlna
}
var videoCodec = videoStream?.Codec;
- float videoFramerate = videoStream?.ReferenceFrameRate ?? 0;
- TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
- int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
- int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
-
var audioCodec = audioStream?.Codec;
- var audioProfile = audioStream?.Profile;
- var audioChannels = audioStream?.Channels;
- var audioBitrate = audioStream?.BitRate;
- var audioSampleRate = audioStream?.SampleRate;
- var audioBitDepth = audioStream?.BitDepth;
var analyzedProfiles = transcodingProfiles
.Select(transcodingProfile =>
@@ -843,37 +864,40 @@ namespace MediaBrowser.Model.Dlna
var container = transcodingProfile.Container;
- if (options.AllowVideoStreamCopy)
+ if (videoStream is not null
+ && options.AllowVideoStreamCopy
+ && ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec))
{
- if (ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec))
- {
- var appliedVideoConditions = options.Profile.CodecProfiles
- .Where(i => i.Type == CodecType.Video &&
- i.ContainsAnyCodec(videoCodec, container) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)))
- .Select(i =>
- i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
-
- // An empty appliedVideoConditions means that the codec has no conditions for the current video stream
- var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
- rank.Video = conditionsSatisfied ? 1 : 2;
- }
+ var failures = GetCompatibilityVideoCodec(options, mediaSource, container, videoStream);
+ rank.Video = failures == 0 ? 1 : 2;
}
- if (options.AllowAudioStreamCopy)
+ if (audioStream is not null
+ && options.AllowAudioStreamCopy)
{
- if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
+ // For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies
+ // the transcoding conditions, then the one does not satisfy the transcoding conditions.
+ // For example: A client can support both aac and flac, but flac only supports 2 channels while aac supports 6.
+ // When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of down-mix to 2 channel flac.
+ var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
+
+ foreach (var transcodingAudioCodec in transcodingAudioCodecs)
{
- var appliedVideoConditions = options.Profile.CodecProfiles
- .Where(i => i.Type == CodecType.VideoAudio &&
- i.ContainsAnyCodec(audioCodec, container) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
- .Select(i =>
- i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
-
- // An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
- var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
- rank.Audio = conditionsSatisfied ? 1 : 2;
+ var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, transcodingAudioCodec, true, false);
+
+ var rankAudio = 3;
+
+ if (failures == 0)
+ {
+ rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ? 1 : 2;
+ }
+
+ rank.Audio = Math.Min(rank.Audio, rankAudio);
+
+ if (rank.Audio == 1)
+ {
+ break;
+ }
}
}
@@ -963,9 +987,18 @@ namespace MediaBrowser.Model.Dlna
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
- var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
+ var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channels > (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue);
+
+ var directAudioFailures = audioStreamWithSupportedCodec is null ? default : GetCompatibilityAudioCodec(options, item, container ?? string.Empty, audioStreamWithSupportedCodec, null, true, false);
+
+ playlistItem.TranscodeReasons |= directAudioFailures;
+
+ var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
+ && directAudioFailures == 0;
+
+ directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit);
- var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
+ var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
{
@@ -1013,6 +1046,7 @@ namespace MediaBrowser.Model.Dlna
int? packetLength = videoStream?.PacketLength;
int? refFrames = videoStream?.RefFrames;
+ int numStreams = item.MediaStreams.Count;
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
@@ -1021,7 +1055,7 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
.Reverse();
foreach (var condition in appliedVideoConditions)
@@ -1087,12 +1121,12 @@ namespace MediaBrowser.Model.Dlna
_logger.LogDebug(
"Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {AudioStreamIndex}, SubtitleStreamIndex: {SubtitleStreamIndex}, Reasons: {TranscodeReason}",
- options.Profile?.Name ?? "Anonymous Profile",
+ options.Profile.Name ?? "Anonymous Profile",
item.Path ?? "Unknown path",
- playlistItem?.PlayMethod,
+ playlistItem.PlayMethod,
audioStream?.Index,
- playlistItem?.SubtitleStreamIndex,
- playlistItem?.TranscodeReasons);
+ playlistItem.SubtitleStreamIndex,
+ playlistItem.TranscodeReasons);
}
private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels)
@@ -1251,52 +1285,14 @@ namespace MediaBrowser.Model.Dlna
DeviceProfile profile = options.Profile;
string container = mediaSource.Container;
- // Video
- int? width = videoStream?.Width;
- int? height = videoStream?.Height;
- int? bitDepth = videoStream?.BitDepth;
- int? videoBitrate = videoStream?.BitRate;
- double? videoLevel = videoStream?.Level;
- string? videoProfile = videoStream?.Profile;
- VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
- float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
- bool? isAnamorphic = videoStream?.IsAnamorphic;
- bool? isInterlaced = videoStream?.IsInterlaced;
- string? videoCodecTag = videoStream?.CodecTag;
- bool? isAvc = videoStream?.IsAVC;
-
- TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
- int? packetLength = videoStream?.PacketLength;
- int? refFrames = videoStream?.RefFrames;
-
- int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
- int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
-
- var checkVideoConditions = (ProfileCondition[] conditions) =>
- conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
-
// Check container conditions
- var containerProfileReasons = AggregateFailureConditions(
- mediaSource,
- profile,
- "VideoCodecProfile",
- profile.ContainerProfiles
- .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.ContainsContainer(container))
- .SelectMany(containerProfile => checkVideoConditions(containerProfile.Conditions)));
+ var containerProfileReasons = GetCompatibilityContainer(options, mediaSource, container, videoStream);
// Check video conditions
- var videoCodecProfileReasons = AggregateFailureConditions(
- mediaSource,
- profile,
- "VideoCodecProfile",
- profile.CodecProfiles
- .Where(codecProfile => codecProfile.Type == CodecType.Video &&
- codecProfile.ContainsAnyCodec(videoStream?.Codec, container) &&
- !checkVideoConditions(codecProfile.ApplyConditions).Any())
- .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
+ var videoCodecProfileReasons = videoStream is null ? default : GetCompatibilityVideoCodec(options, mediaSource, container, videoStream);
// Check audio candidates profile conditions
- var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream));
+ var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => GetCompatibilityAudioCodecDirect(options, mediaSource, container, audioStream, true, mediaSource.IsSecondaryAudio(audioStream) ?? false));
TranscodeReason subtitleProfileReasons = 0;
if (subtitleStream is not null)
@@ -1409,20 +1405,6 @@ namespace MediaBrowser.Model.Dlna
return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
}
- private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream)
- {
- var profile = options.Profile;
- var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
-
- var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions);
- if (audioStream.IsExternal == true)
- {
- audioStreamFailureReasons |= TranscodeReason.AudioIsExternal;
- }
-
- return audioStreamFailureReasons;
- }
-
private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string type, IEnumerable<ProfileCondition> conditions)
{
return conditions.Aggregate<ProfileCondition, TranscodeReason>(0, (reasons, i) =>
@@ -1896,6 +1878,7 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.AudioProfile:
case ProfileConditionValue.Has64BitOffsets:
case ProfileConditionValue.PacketLength:
+ case ProfileConditionValue.NumStreams:
case ProfileConditionValue.NumAudioStreams:
case ProfileConditionValue.NumVideoStreams:
case ProfileConditionValue.IsSecondaryAudio:
@@ -2213,7 +2196,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+ private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
{
// Check container type
if (!profile.SupportsContainer(item.Container))
@@ -2221,6 +2204,20 @@ namespace MediaBrowser.Model.Dlna
return false;
}
+ // Never direct play audio in matroska when the device only declare support for webm.
+ // The first check is not enough because mkv is assumed can be webm.
+ // See https://github.com/jellyfin/jellyfin/issues/13344
+ return !ContainerHelper.ContainsContainer("mkv", item.Container)
+ || profile.SupportsContainer("mkv");
+ }
+
+ private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+ {
+ if (!IsAudioContainerSupported(profile, item))
+ {
+ return false;
+ }
+
// Check audio codec
string? audioCodec = audioStream?.Codec;
if (!profile.SupportsAudioCodec(audioCodec))
@@ -2235,19 +2232,16 @@ namespace MediaBrowser.Model.Dlna
{
// Check container type, this should NOT be supported
// If the container is supported, the file should be directly played
- if (!profile.SupportsContainer(item.Container))
+ if (IsAudioContainerSupported(profile, item))
{
- // Check audio codec, we cannot use the SupportsAudioCodec here
- // Because that one assumes empty container supports all codec, which is just useless
- string? audioCodec = audioStream?.Codec;
- if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
+ return false;
}
- return false;
+ // Check audio codec, we cannot use the SupportsAudioCodec here
+ // Because that one assumes empty container supports all codec, which is just useless
+ string? audioCodec = audioStream?.Codec;
+ return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
}
private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
@@ -2266,5 +2260,141 @@ namespace MediaBrowser.Model.Dlna
return index;
}
+
+ /// <summary>
+ /// Check the profile conditions.
+ /// </summary>
+ /// <param name="conditions">Profile conditions.</param>
+ /// <param name="mediaSource">Media source.</param>
+ /// <param name="videoStream">Video stream.</param>
+ /// <returns>Failed profile conditions.</returns>
+ private IEnumerable<ProfileCondition> CheckVideoConditions(ProfileCondition[] conditions, MediaSourceInfo mediaSource, MediaStream? videoStream)
+ {
+ int? width = videoStream?.Width;
+ int? height = videoStream?.Height;
+ int? bitDepth = videoStream?.BitDepth;
+ int? videoBitrate = videoStream?.BitRate;
+ double? videoLevel = videoStream?.Level;
+ string? videoProfile = videoStream?.Profile;
+ VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
+ float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
+ bool? isAnamorphic = videoStream?.IsAnamorphic;
+ bool? isInterlaced = videoStream?.IsInterlaced;
+ string? videoCodecTag = videoStream?.CodecTag;
+ bool? isAvc = videoStream?.IsAVC;
+
+ TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
+ int? packetLength = videoStream?.PacketLength;
+ int? refFrames = videoStream?.RefFrames;
+
+ int numStreams = mediaSource.MediaStreams.Count;
+ int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
+ int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
+
+ return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
+ }
+
+ /// <summary>
+ /// Check the compatibility of the container.
+ /// </summary>
+ /// <param name="options">Media options.</param>
+ /// <param name="mediaSource">Media source.</param>
+ /// <param name="container">Container.</param>
+ /// <param name="videoStream">Video stream.</param>
+ /// <returns>Transcode reasons if the container is not fully compatible.</returns>
+ private TranscodeReason GetCompatibilityContainer(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream? videoStream)
+ {
+ var profile = options.Profile;
+
+ var failures = AggregateFailureConditions(
+ mediaSource,
+ profile,
+ "VideoCodecProfile",
+ profile.ContainerProfiles
+ .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.ContainsContainer(container))
+ .SelectMany(containerProfile => CheckVideoConditions(containerProfile.Conditions, mediaSource, videoStream)));
+
+ return failures;
+ }
+
+ /// <summary>
+ /// Check the compatibility of the video codec.
+ /// </summary>
+ /// <param name="options">Media options.</param>
+ /// <param name="mediaSource">Media source.</param>
+ /// <param name="container">Container.</param>
+ /// <param name="videoStream">Video stream.</param>
+ /// <returns>Transcode reasons if the video stream is not fully compatible.</returns>
+ private TranscodeReason GetCompatibilityVideoCodec(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream videoStream)
+ {
+ var profile = options.Profile;
+
+ string videoCodec = videoStream.Codec;
+
+ var failures = AggregateFailureConditions(
+ mediaSource,
+ profile,
+ "VideoCodecProfile",
+ profile.CodecProfiles
+ .Where(codecProfile => codecProfile.Type == CodecType.Video &&
+ codecProfile.ContainsAnyCodec(videoCodec, container) &&
+ !CheckVideoConditions(codecProfile.ApplyConditions, mediaSource, videoStream).Any())
+ .SelectMany(codecProfile => CheckVideoConditions(codecProfile.Conditions, mediaSource, videoStream)));
+
+ return failures;
+ }
+
+ /// <summary>
+ /// Check the compatibility of the audio codec.
+ /// </summary>
+ /// <param name="options">Media options.</param>
+ /// <param name="mediaSource">Media source.</param>
+ /// <param name="container">Container.</param>
+ /// <param name="audioStream">Audio stream.</param>
+ /// <param name="transcodingAudioCodec">Override audio codec.</param>
+ /// <param name="isVideo">The media source is video.</param>
+ /// <param name="isSecondaryAudio">The audio stream is secondary.</param>
+ /// <returns>Transcode reasons if the audio stream is not fully compatible.</returns>
+ private TranscodeReason GetCompatibilityAudioCodec(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string? transcodingAudioCodec, bool isVideo, bool isSecondaryAudio)
+ {
+ var profile = options.Profile;
+
+ var audioCodec = transcodingAudioCodec ?? audioStream.Codec;
+ var audioProfile = audioStream.Profile;
+ var audioChannels = audioStream.Channels;
+ var audioBitrate = audioStream.BitRate;
+ var audioSampleRate = audioStream.SampleRate;
+ var audioBitDepth = audioStream.BitDepth;
+
+ var audioFailureConditions = isVideo
+ ? GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)
+ : GetProfileConditionsForAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, true);
+
+ var failures = AggregateFailureConditions(mediaSource, profile, "AudioCodecProfile", audioFailureConditions);
+
+ return failures;
+ }
+
+ /// <summary>
+ /// Check the compatibility of the audio codec for direct playback.
+ /// </summary>
+ /// <param name="options">Media options.</param>
+ /// <param name="mediaSource">Media source.</param>
+ /// <param name="container">Container.</param>
+ /// <param name="audioStream">Audio stream.</param>
+ /// <param name="isVideo">The media source is video.</param>
+ /// <param name="isSecondaryAudio">The audio stream is secondary.</param>
+ /// <returns>Transcode reasons if the audio stream is not fully compatible for direct playback.</returns>
+ private TranscodeReason GetCompatibilityAudioCodecDirect(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, bool isVideo, bool isSecondaryAudio)
+ {
+ var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, null, isVideo, isSecondaryAudio);
+
+ if (audioStream.IsExternal)
+ {
+ failures |= TranscodeReason.AudioIsExternal;
+ }
+
+ return failures;
+ }
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 1ae4e1962d..13acd15a3f 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -1,7 +1,13 @@
+#pragma warning disable CA1819 // Properties should not return arrays
+
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Globalization;
+using System.Linq;
+using System.Text;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -871,202 +877,271 @@ public class StreamInfo
/// </summary>
/// <param name="baseUrl">The base Url.</param>
/// <param name="accessToken">The access Token.</param>
+ /// <param name="query">Optional extra query.</param>
/// <returns>A querystring representation of this object.</returns>
- public string ToUrl(string baseUrl, string? accessToken)
+ public string ToUrl(string? baseUrl, string? accessToken, string? query)
{
- ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+ var sb = new StringBuilder();
+ if (!string.IsNullOrEmpty(baseUrl))
+ {
+ sb.Append(baseUrl.TrimEnd('/'));
+ }
- List<string> list = [];
- foreach (NameValuePair pair in BuildParams(this, accessToken))
+ if (MediaType == DlnaProfileType.Audio)
{
- if (string.IsNullOrEmpty(pair.Value))
- {
- continue;
- }
+ sb.Append("/audio/");
+ }
+ else
+ {
+ sb.Append("/videos/");
+ }
- // Try to keep the url clean by omitting defaults
- if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
+ sb.Append(ItemId);
- if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
+ if (SubProtocol == MediaStreamProtocol.hls)
+ {
+ sb.Append("/master.m3u8?");
+ }
+ else
+ {
+ sb.Append("/stream");
- if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(Container))
{
- continue;
+ sb.Append('.');
+ sb.Append(Container);
}
- var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
+ sb.Append('?');
+ }
- list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
+ if (!string.IsNullOrEmpty(DeviceProfileId))
+ {
+ sb.Append("&DeviceProfileId=");
+ sb.Append(DeviceProfileId);
}
- string queryString = string.Join('&', list);
+ if (!string.IsNullOrEmpty(DeviceId))
+ {
+ sb.Append("&DeviceId=");
+ sb.Append(DeviceId);
+ }
- return GetUrl(baseUrl, queryString);
- }
+ if (!string.IsNullOrEmpty(MediaSourceId))
+ {
+ sb.Append("&MediaSourceId=");
+ sb.Append(MediaSourceId);
+ }
- private string GetUrl(string baseUrl, string queryString)
- {
- ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+ // default true so don't store.
+ if (IsDirectStream)
+ {
+ sb.Append("&Static=true");
+ }
- string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
+ if (VideoCodecs.Count != 0)
+ {
+ sb.Append("&VideoCodec=");
+ sb.AppendJoin(',', VideoCodecs);
+ }
- baseUrl = baseUrl.TrimEnd('/');
+ if (AudioCodecs.Count != 0)
+ {
+ sb.Append("&AudioCodec=");
+ sb.AppendJoin(',', AudioCodecs);
+ }
- if (MediaType == DlnaProfileType.Audio)
+ if (AudioStreamIndex.HasValue)
{
- if (SubProtocol == MediaStreamProtocol.hls)
- {
- return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
- }
+ sb.Append("&AudioStreamIndex=");
+ sb.Append(AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture));
+ }
- return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ if (SubtitleStreamIndex.HasValue && (AlwaysBurnInSubtitleWhenTranscoding || SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) && SubtitleStreamIndex != -1)
+ {
+ sb.Append("&SubtitleStreamIndex=");
+ sb.Append(SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture));
}
- if (SubProtocol == MediaStreamProtocol.hls)
+ if (VideoBitrate.HasValue)
{
- return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ sb.Append("&VideoBitrate=");
+ sb.Append(VideoBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
- return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
- }
+ if (AudioBitrate.HasValue)
+ {
+ sb.Append("&AudioBitrate=");
+ sb.Append(AudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
+ }
- private static List<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
- {
- List<NameValuePair> list = [];
+ if (AudioSampleRate.HasValue)
+ {
+ sb.Append("&AudioSampleRate=");
+ sb.Append(AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
+ }
- string audioCodecs = item.AudioCodecs.Count == 0 ?
- string.Empty :
- string.Join(',', item.AudioCodecs);
+ if (MaxFramerate.HasValue)
+ {
+ sb.Append("&MaxFramerate=");
+ sb.Append(MaxFramerate.Value.ToString(CultureInfo.InvariantCulture));
+ }
- string videoCodecs = item.VideoCodecs.Count == 0 ?
- string.Empty :
- string.Join(',', item.VideoCodecs);
+ if (MaxWidth.HasValue)
+ {
+ sb.Append("&MaxWidth=");
+ sb.Append(MaxWidth.Value.ToString(CultureInfo.InvariantCulture));
+ }
- list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
- list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
- list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
- list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- list.Add(new NameValuePair("VideoCodec", videoCodecs));
- list.Add(new NameValuePair("AudioCodec", audioCodecs));
- list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ if (MaxHeight.HasValue)
+ {
+ sb.Append("&MaxHeight=");
+ sb.Append(MaxHeight.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (SubProtocol == MediaStreamProtocol.hls)
+ {
+ if (!string.IsNullOrEmpty(Container))
+ {
+ sb.Append("&SegmentContainer=");
+ sb.Append(Container);
+ }
- list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ if (SegmentLength.HasValue)
+ {
+ sb.Append("&SegmentLength=");
+ sb.Append(SegmentLength.Value.ToString(CultureInfo.InvariantCulture));
+ }
- long startPositionTicks = item.StartPositionTicks;
+ if (MinSegments.HasValue)
+ {
+ sb.Append("&MinSegments=");
+ sb.Append(MinSegments.Value.ToString(CultureInfo.InvariantCulture));
+ }
- if (item.SubProtocol == MediaStreamProtocol.hls)
- {
- list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+ sb.Append("&BreakOnNonKeyFrames=");
+ sb.Append(BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture));
}
else
{
- list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+ if (StartPositionTicks != 0)
+ {
+ sb.Append("&StartTimeTicks=");
+ sb.Append(StartPositionTicks.ToString(CultureInfo.InvariantCulture));
+ }
}
- list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
- list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
+ if (!string.IsNullOrEmpty(PlaySessionId))
+ {
+ sb.Append("&PlaySessionId=");
+ sb.Append(PlaySessionId);
+ }
- string? liveStreamId = item.MediaSource?.LiveStreamId;
- list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
+ if (!string.IsNullOrEmpty(accessToken))
+ {
+ sb.Append("&ApiKey=");
+ sb.Append(accessToken);
+ }
- list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
+ var liveStreamId = MediaSource?.LiveStreamId;
+ if (!string.IsNullOrEmpty(liveStreamId))
+ {
+ sb.Append("&LiveStreamId=");
+ sb.Append(liveStreamId);
+ }
- if (!item.IsDirectStream)
+ if (!IsDirectStream)
{
- if (item.RequireNonAnamorphic)
+ if (RequireNonAnamorphic)
{
- list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&RequireNonAnamorphic=");
+ sb.Append(RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture));
}
- list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ if (TranscodingMaxAudioChannels.HasValue)
+ {
+ sb.Append("&TranscodingMaxAudioChannels=");
+ sb.Append(TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
+ }
- if (item.EnableSubtitlesInManifest)
+ if (EnableSubtitlesInManifest)
{
- list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&EnableSubtitlesInManifest=");
+ sb.Append(EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture));
}
- if (item.EnableMpegtsM2TsMode)
+ if (EnableMpegtsM2TsMode)
{
- list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&EnableMpegtsM2TsMode=");
+ sb.Append(EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture));
}
- if (item.EstimateContentLength)
+ if (EstimateContentLength)
{
- list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&EstimateContentLength=");
+ sb.Append(EstimateContentLength.ToString(CultureInfo.InvariantCulture));
}
- if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+ if (TranscodeSeekInfo != TranscodeSeekInfo.Auto)
{
- list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
+ sb.Append("&TranscodeSeekInfo=");
+ sb.Append(TranscodeSeekInfo.ToString());
}
- if (item.CopyTimestamps)
+ if (CopyTimestamps)
{
- list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&CopyTimestamps=");
+ sb.Append(CopyTimestamps.ToString(CultureInfo.InvariantCulture));
}
- list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&RequireAvc=");
+ sb.Append(RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
- list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&EnableAudioVbrEncoding=");
+ sb.Append(EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
}
- list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
-
- string subtitleCodecs = item.SubtitleCodecs.Count == 0 ?
- string.Empty :
- string.Join(",", item.SubtitleCodecs);
-
- list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
-
- if (item.SubProtocol == MediaStreamProtocol.hls)
+ var etag = MediaSource?.ETag;
+ if (!string.IsNullOrEmpty(etag))
{
- list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
-
- if (item.SegmentLength.HasValue)
- {
- list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
- }
+ sb.Append("&Tag=");
+ sb.Append(etag);
+ }
- if (item.MinSegments.HasValue)
- {
- list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
- }
+ if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod != SubtitleDeliveryMethod.External)
+ {
+ sb.Append("&SubtitleMethod=");
+ sb.Append(SubtitleDeliveryMethod);
+ }
- list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
+ if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed && SubtitleCodecs.Count != 0)
+ {
+ sb.Append("&SubtitleCodec=");
+ sb.AppendJoin(',', SubtitleCodecs);
}
- foreach (var pair in item.StreamOptions)
+ foreach (var pair in StreamOptions)
{
- if (string.IsNullOrEmpty(pair.Value))
- {
- continue;
- }
+ // Strip spaces to avoid having to encode h264 profile names
+ sb.Append('&');
+ sb.Append(pair.Key);
+ sb.Append('=');
+ sb.Append(pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal));
+ }
- // strip spaces to avoid having to encode h264 profile names
- list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
+ var transcodeReasonsValues = TranscodeReasons.GetUniqueFlags().ToArray();
+ if (!IsDirectStream && transcodeReasonsValues.Length > 0)
+ {
+ sb.Append("&TranscodeReasons=");
+ sb.AppendJoin(',', transcodeReasonsValues);
}
- if (!item.IsDirectStream)
+ if (!string.IsNullOrEmpty(query))
{
- list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
+ sb.Append(query);
}
- return list;
+ return sb.ToString();
}
/// <summary>
@@ -1189,7 +1264,7 @@ public class StreamInfo
if (!string.IsNullOrEmpty(accessToken))
{
- info.Url += "?api_key=" + accessToken;
+ info.Url += "?ApiKey=" + accessToken;
}
info.IsExternalUrl = false;
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index 5a9fa22ae4..5797d42506 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -1,3 +1,4 @@
+using System;
using System.ComponentModel;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
@@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna;
/// <summary>
/// A class for transcoding profile information.
+/// Note for client developers: Conditions defined in <see cref="CodecProfile"/> has higher priority and can override values defined here.
/// </summary>
public class TranscodingProfile
{
@@ -18,6 +20,33 @@ public class TranscodingProfile
}
/// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingProfile" /> class copying the values from another instance.
+ /// </summary>
+ /// <param name="other">Another instance of <see cref="TranscodingProfile" /> to be copied.</param>
+ public TranscodingProfile(TranscodingProfile other)
+ {
+ ArgumentNullException.ThrowIfNull(other);
+
+ Container = other.Container;
+ Type = other.Type;
+ VideoCodec = other.VideoCodec;
+ AudioCodec = other.AudioCodec;
+ Protocol = other.Protocol;
+ EstimateContentLength = other.EstimateContentLength;
+ EnableMpegtsM2TsMode = other.EnableMpegtsM2TsMode;
+ TranscodeSeekInfo = other.TranscodeSeekInfo;
+ CopyTimestamps = other.CopyTimestamps;
+ Context = other.Context;
+ EnableSubtitlesInManifest = other.EnableSubtitlesInManifest;
+ MaxAudioChannels = other.MaxAudioChannels;
+ MinSegments = other.MinSegments;
+ SegmentLength = other.SegmentLength;
+ BreakOnNonKeyFrames = other.BreakOnNonKeyFrames;
+ Conditions = other.Conditions;
+ EnableAudioVbrEncoding = other.EnableAudioVbrEncoding;
+ }
+
+ /// <summary>
/// Gets or sets the container.
/// </summary>
[XmlAttribute("container")]