#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; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; namespace MediaBrowser.Model.Dlna; /// /// Class holding information on a stream. /// public class StreamInfo { /// /// Initializes a new instance of the class. /// public StreamInfo() { AudioCodecs = []; VideoCodecs = []; SubtitleCodecs = []; StreamOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); } /// /// Gets or sets the item id. /// /// The item id. public Guid ItemId { get; set; } /// /// Gets or sets the play method. /// /// The play method. public PlayMethod PlayMethod { get; set; } /// /// Gets or sets the encoding context. /// /// The encoding context. public EncodingContext Context { get; set; } /// /// Gets or sets the media type. /// /// The media type. public DlnaProfileType MediaType { get; set; } /// /// Gets or sets the container. /// /// The container. public string? Container { get; set; } /// /// Gets or sets the sub protocol. /// /// The sub protocol. public MediaStreamProtocol SubProtocol { get; set; } /// /// Gets or sets the start position ticks. /// /// The start position ticks. public long StartPositionTicks { get; set; } /// /// Gets or sets the segment length. /// /// The segment length. public int? SegmentLength { get; set; } /// /// Gets or sets the minimum segments count. /// /// The minimum segments count. public int? MinSegments { get; set; } /// /// Gets or sets a value indicating whether the stream can be broken on non-keyframes. /// public bool BreakOnNonKeyFrames { get; set; } /// /// Gets or sets a value indicating whether the stream requires AVC. /// public bool RequireAvc { get; set; } /// /// Gets or sets a value indicating whether the stream requires AVC. /// public bool RequireNonAnamorphic { get; set; } /// /// Gets or sets a value indicating whether timestamps should be copied. /// public bool CopyTimestamps { get; set; } /// /// Gets or sets a value indicating whether timestamps should be copied. /// public bool EnableMpegtsM2TsMode { get; set; } /// /// Gets or sets a value indicating whether the subtitle manifest is enabled. /// public bool EnableSubtitlesInManifest { get; set; } /// /// Gets or sets the audio codecs. /// /// The audio codecs. public IReadOnlyList AudioCodecs { get; set; } /// /// Gets or sets the video codecs. /// /// The video codecs. public IReadOnlyList VideoCodecs { get; set; } /// /// Gets or sets the audio stream index. /// /// The audio stream index. public int? AudioStreamIndex { get; set; } /// /// Gets or sets the video stream index. /// /// The subtitle stream index. public int? SubtitleStreamIndex { get; set; } /// /// Gets or sets the maximum transcoding audio channels. /// /// The maximum transcoding audio channels. public int? TranscodingMaxAudioChannels { get; set; } /// /// Gets or sets the global maximum audio channels. /// /// The global maximum audio channels. public int? GlobalMaxAudioChannels { get; set; } /// /// Gets or sets the audio bitrate. /// /// The audio bitrate. public int? AudioBitrate { get; set; } /// /// Gets or sets the audio sample rate. /// /// The audio sample rate. public int? AudioSampleRate { get; set; } /// /// Gets or sets the video bitrate. /// /// The video bitrate. public int? VideoBitrate { get; set; } /// /// Gets or sets the maximum output width. /// /// The output width. public int? MaxWidth { get; set; } /// /// Gets or sets the maximum output height. /// /// The maximum output height. public int? MaxHeight { get; set; } /// /// Gets or sets the maximum framerate. /// /// The maximum framerate. public float? MaxFramerate { get; set; } /// /// Gets or sets the device profile. /// /// The device profile. public required DeviceProfile DeviceProfile { get; set; } /// /// Gets or sets the device profile id. /// /// The device profile id. public string? DeviceProfileId { get; set; } /// /// Gets or sets the device id. /// /// The device id. public string? DeviceId { get; set; } /// /// Gets or sets the runtime ticks. /// /// The runtime ticks. public long? RunTimeTicks { get; set; } /// /// Gets or sets the transcode seek info. /// /// The transcode seek info. public TranscodeSeekInfo TranscodeSeekInfo { get; set; } /// /// Gets or sets a value indicating whether content length should be estimated. /// public bool EstimateContentLength { get; set; } /// /// Gets or sets the media source info. /// /// The media source info. public MediaSourceInfo? MediaSource { get; set; } /// /// Gets or sets the subtitle codecs. /// /// The subtitle codecs. public IReadOnlyList SubtitleCodecs { get; set; } /// /// Gets or sets the subtitle delivery method. /// /// The subtitle delivery method. public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } /// /// Gets or sets the subtitle format. /// /// The subtitle format. public string? SubtitleFormat { get; set; } /// /// Gets or sets the play session id. /// /// The play session id. public string? PlaySessionId { get; set; } /// /// Gets or sets the transcode reasons. /// /// The transcode reasons. public TranscodeReason TranscodeReasons { get; set; } /// /// Gets the stream options. /// /// The stream options. public Dictionary StreamOptions { get; private set; } /// /// Gets the media source id. /// /// The media source id. public string? MediaSourceId => MediaSource?.Id; /// /// Gets or sets a value indicating whether audio VBR encoding is enabled. /// public bool EnableAudioVbrEncoding { get; set; } /// /// Gets or sets a value indicating whether always burn in subtitles when transcoding. /// public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } /// /// Gets a value indicating whether the stream is direct. /// public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; /// /// Gets the audio stream that will be used in the output stream. /// /// The audio stream. public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); /// /// Gets the video stream that will be used in the output stream. /// /// The video stream. public MediaStream? TargetVideoStream => MediaSource?.VideoStream; /// /// Gets the audio sample rate that will be in the output stream. /// /// The target audio sample rate. public int? TargetAudioSampleRate { get { var stream = TargetAudioStream; return AudioSampleRate.HasValue && !IsDirectStream ? AudioSampleRate : stream?.SampleRate; } } /// /// Gets the audio bit depth that will be in the output stream. /// /// The target bit depth. public int? TargetAudioBitDepth { get { if (IsDirectStream) { return TargetAudioStream?.BitDepth; } var targetAudioCodecs = TargetAudioCodec; var audioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; if (!string.IsNullOrEmpty(audioCodec)) { return GetTargetAudioBitDepth(audioCodec); } return TargetAudioStream?.BitDepth; } } /// /// Gets the video bit depth that will be in the output stream. /// /// The target video bit depth. public int? TargetVideoBitDepth { get { if (IsDirectStream) { return TargetVideoStream?.BitDepth; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; if (!string.IsNullOrEmpty(videoCodec)) { return GetTargetVideoBitDepth(videoCodec); } return TargetVideoStream?.BitDepth; } } /// /// Gets the target reference frames that will be in the output stream. /// /// The target reference frames. public int? TargetRefFrames { get { if (IsDirectStream) { return TargetVideoStream?.RefFrames; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; if (!string.IsNullOrEmpty(videoCodec)) { return GetTargetRefFrames(videoCodec); } return TargetVideoStream?.RefFrames; } } /// /// Gets the target framerate that will be in the output stream. /// /// The target framerate. public float? TargetFramerate { get { var stream = TargetVideoStream; return MaxFramerate.HasValue && !IsDirectStream ? MaxFramerate : stream?.ReferenceFrameRate; } } /// /// Gets the target video level that will be in the output stream. /// /// The target video level. public double? TargetVideoLevel { get { if (IsDirectStream) { return TargetVideoStream?.Level; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; if (!string.IsNullOrEmpty(videoCodec)) { return GetTargetVideoLevel(videoCodec); } return TargetVideoStream?.Level; } } /// /// Gets the target packet length that will be in the output stream. /// /// The target packet length. public int? TargetPacketLength { get { var stream = TargetVideoStream; return !IsDirectStream ? null : stream?.PacketLength; } } /// /// Gets the target video profile that will be in the output stream. /// /// The target video profile. public string? TargetVideoProfile { get { if (IsDirectStream) { return TargetVideoStream?.Profile; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; if (!string.IsNullOrEmpty(videoCodec)) { return GetOption(videoCodec, "profile"); } return TargetVideoStream?.Profile; } } /// /// Gets the target video range type that will be in the output stream. /// /// The video range type. public VideoRangeType TargetVideoRangeType { get { if (IsDirectStream) { return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; if (!string.IsNullOrEmpty(videoCodec) && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) { return videoRangeType; } return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } } /// /// Gets the target video codec tag. /// /// The video codec tag. public string? TargetVideoCodecTag { get { var stream = TargetVideoStream; return !IsDirectStream ? null : stream?.CodecTag; } } /// /// Gets the audio bitrate that will be in the output stream. /// /// The audio bitrate. public int? TargetAudioBitrate { get { var stream = TargetAudioStream; return AudioBitrate.HasValue && !IsDirectStream ? AudioBitrate : stream?.BitRate; } } /// /// Gets the amount of audio channels that will be in the output stream. /// /// The target audio channels. public int? TargetAudioChannels { get { if (IsDirectStream) { return TargetAudioStream?.Channels; } var targetAudioCodecs = TargetAudioCodec; var codec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; if (!string.IsNullOrEmpty(codec)) { return GetTargetRefFrames(codec); } return TargetAudioStream?.Channels; } } /// /// Gets the audio codec that will be in the output stream. /// /// The audio codec. public IReadOnlyList TargetAudioCodec { get { var stream = TargetAudioStream; string? inputCodec = stream?.Codec; if (IsDirectStream) { return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec]; } foreach (string codec in AudioCodecs) { if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { return string.IsNullOrEmpty(codec) ? [] : [codec]; } } return AudioCodecs; } } /// /// Gets the video codec that will be in the output stream. /// /// The target video codec. public IReadOnlyList TargetVideoCodec { get { var stream = TargetVideoStream; string? inputCodec = stream?.Codec; if (IsDirectStream) { return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec]; } foreach (string codec in VideoCodecs) { if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { return string.IsNullOrEmpty(codec) ? [] : [codec]; } } return VideoCodecs; } } /// /// Gets the target size of the output stream. /// /// The target size. public long? TargetSize { get { if (IsDirectStream) { return MediaSource?.Size; } if (RunTimeTicks.HasValue) { int? totalBitrate = TargetTotalBitrate; double totalSeconds = RunTimeTicks.Value; // Convert to ms totalSeconds /= 10000; // Convert to seconds totalSeconds /= 1000; return totalBitrate.HasValue ? Convert.ToInt64(totalBitrate.Value * totalSeconds) : null; } return null; } } /// /// Gets the target video bitrate of the output stream. /// /// The video bitrate. public int? TargetVideoBitrate { get { var stream = TargetVideoStream; return VideoBitrate.HasValue && !IsDirectStream ? VideoBitrate : stream?.BitRate; } } /// /// Gets the target timestamp of the output stream. /// /// The target timestamp. public TransportStreamTimestamp TargetTimestamp { get { var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) ? TransportStreamTimestamp.Valid : TransportStreamTimestamp.None; return !IsDirectStream ? defaultValue : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; } } /// /// Gets the target total bitrate of the output stream. /// /// The target total bitrate. public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); /// /// Gets a value indicating whether the output stream is anamorphic. /// public bool? IsTargetAnamorphic { get { if (IsDirectStream) { return TargetVideoStream?.IsAnamorphic; } return false; } } /// /// Gets a value indicating whether the output stream is interlaced. /// public bool? IsTargetInterlaced { get { if (IsDirectStream) { return TargetVideoStream?.IsInterlaced; } var targetVideoCodecs = TargetVideoCodec; var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; if (!string.IsNullOrEmpty(videoCodec)) { if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) { return false; } } return TargetVideoStream?.IsInterlaced; } } /// /// Gets a value indicating whether the output stream is AVC. /// public bool? IsTargetAVC { get { if (IsDirectStream) { return TargetVideoStream?.IsAVC; } return true; } } /// /// Gets the target width of the output stream. /// /// The target width. public int? TargetWidth { get { var videoStream = TargetVideoStream; if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) { ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); return size.Width; } return MaxWidth; } } /// /// Gets the target height of the output stream. /// /// The target height. public int? TargetHeight { get { var videoStream = TargetVideoStream; if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) { ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); return size.Height; } return MaxHeight; } } /// /// Gets the target video stream count of the output stream. /// /// The target video stream count. public int? TargetVideoStreamCount { get { if (IsDirectStream) { return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); } return GetMediaStreamCount(MediaStreamType.Video, 1); } } /// /// Gets the target audio stream count of the output stream. /// /// The target audio stream count. public int? TargetAudioStreamCount { get { if (IsDirectStream) { return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); } return GetMediaStreamCount(MediaStreamType.Audio, 1); } } /// /// Sets a stream option. /// /// The qualifier. /// The name. /// The value. public void SetOption(string? qualifier, string name, string value) { if (string.IsNullOrEmpty(qualifier)) { SetOption(name, value); } else { SetOption(qualifier + "-" + name, value); } } /// /// Sets a stream option. /// /// The name. /// The value. public void SetOption(string name, string value) { StreamOptions[name] = value; } /// /// Gets a stream option. /// /// The qualifier. /// The name. /// The value. public string? GetOption(string? qualifier, string name) { var value = GetOption(qualifier + "-" + name); if (string.IsNullOrEmpty(value)) { value = GetOption(name); } return value; } /// /// Gets a stream option. /// /// The name. /// The value. public string? GetOption(string name) { if (StreamOptions.TryGetValue(name, out var value)) { return value; } return null; } /// /// Returns this output stream URL for this class. /// /// The base Url. /// The access Token. /// Optional extra query. /// A querystring representation of this object. public string ToUrl(string? baseUrl, string? accessToken, string? query) { var sb = new StringBuilder(); if (!string.IsNullOrEmpty(baseUrl)) { sb.Append(baseUrl.TrimEnd('/')); } if (MediaType == DlnaProfileType.Audio) { sb.Append("/audio/"); } else { sb.Append("/videos/"); } sb.Append(ItemId); if (SubProtocol == MediaStreamProtocol.hls) { sb.Append("/master.m3u8?"); } else { sb.Append("/stream"); if (!string.IsNullOrEmpty(Container)) { sb.Append('.'); sb.Append(Container); } sb.Append('?'); } if (!string.IsNullOrEmpty(DeviceProfileId)) { sb.Append("&DeviceProfileId="); sb.Append(DeviceProfileId); } if (!string.IsNullOrEmpty(DeviceId)) { sb.Append("&DeviceId="); sb.Append(DeviceId); } if (!string.IsNullOrEmpty(MediaSourceId)) { sb.Append("&MediaSourceId="); sb.Append(MediaSourceId); } // default true so don't store. if (IsDirectStream) { sb.Append("&Static=true"); } if (VideoCodecs.Count != 0) { sb.Append("&VideoCodec="); sb.AppendJoin(',', VideoCodecs); } if (AudioCodecs.Count != 0) { sb.Append("&AudioCodec="); sb.AppendJoin(',', AudioCodecs); } if (AudioStreamIndex.HasValue) { sb.Append("&AudioStreamIndex="); sb.Append(AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture)); } if (SubtitleStreamIndex.HasValue && (AlwaysBurnInSubtitleWhenTranscoding || SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) && SubtitleStreamIndex != -1) { sb.Append("&SubtitleStreamIndex="); sb.Append(SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture)); } if (VideoBitrate.HasValue) { sb.Append("&VideoBitrate="); sb.Append(VideoBitrate.Value.ToString(CultureInfo.InvariantCulture)); } if (AudioBitrate.HasValue) { sb.Append("&AudioBitrate="); sb.Append(AudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); } if (AudioSampleRate.HasValue) { sb.Append("&AudioSampleRate="); sb.Append(AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); } if (MaxFramerate.HasValue) { sb.Append("&MaxFramerate="); sb.Append(MaxFramerate.Value.ToString(CultureInfo.InvariantCulture)); } if (MaxWidth.HasValue) { sb.Append("&MaxWidth="); sb.Append(MaxWidth.Value.ToString(CultureInfo.InvariantCulture)); } 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); } if (SegmentLength.HasValue) { sb.Append("&SegmentLength="); sb.Append(SegmentLength.Value.ToString(CultureInfo.InvariantCulture)); } if (MinSegments.HasValue) { sb.Append("&MinSegments="); sb.Append(MinSegments.Value.ToString(CultureInfo.InvariantCulture)); } sb.Append("&BreakOnNonKeyFrames="); sb.Append(BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)); } else { if (StartPositionTicks != 0) { sb.Append("&StartTimeTicks="); sb.Append(StartPositionTicks.ToString(CultureInfo.InvariantCulture)); } } if (!string.IsNullOrEmpty(PlaySessionId)) { sb.Append("&PlaySessionId="); sb.Append(PlaySessionId); } if (!string.IsNullOrEmpty(accessToken)) { sb.Append("&ApiKey="); sb.Append(accessToken); } var liveStreamId = MediaSource?.LiveStreamId; if (!string.IsNullOrEmpty(liveStreamId)) { sb.Append("&LiveStreamId="); sb.Append(liveStreamId); } if (!IsDirectStream) { if (RequireNonAnamorphic) { sb.Append("&RequireNonAnamorphic="); sb.Append(RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture)); } if (TranscodingMaxAudioChannels.HasValue) { sb.Append("&TranscodingMaxAudioChannels="); sb.Append(TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); } if (EnableSubtitlesInManifest) { sb.Append("&EnableSubtitlesInManifest="); sb.Append(EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture)); } if (EnableMpegtsM2TsMode) { sb.Append("&EnableMpegtsM2TsMode="); sb.Append(EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture)); } if (EstimateContentLength) { sb.Append("&EstimateContentLength="); sb.Append(EstimateContentLength.ToString(CultureInfo.InvariantCulture)); } if (TranscodeSeekInfo != TranscodeSeekInfo.Auto) { sb.Append("&TranscodeSeekInfo="); sb.Append(TranscodeSeekInfo.ToString()); } if (CopyTimestamps) { sb.Append("&CopyTimestamps="); sb.Append(CopyTimestamps.ToString(CultureInfo.InvariantCulture)); } sb.Append("&RequireAvc="); sb.Append(RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); sb.Append("&EnableAudioVbrEncoding="); sb.Append(EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); } var etag = MediaSource?.ETag; if (!string.IsNullOrEmpty(etag)) { sb.Append("&Tag="); sb.Append(etag); } if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) { sb.Append("&SubtitleMethod="); sb.Append(SubtitleDeliveryMethod); } if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed && SubtitleCodecs.Count != 0) { sb.Append("&SubtitleCodec="); sb.AppendJoin(',', SubtitleCodecs); } foreach (var pair in StreamOptions) { // 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)); } var transcodeReasonsValues = TranscodeReasons.GetUniqueFlags().ToArray(); if (!IsDirectStream && transcodeReasonsValues.Length > 0) { sb.Append("&TranscodeReasons="); sb.AppendJoin(',', transcodeReasonsValues); } if (!string.IsNullOrEmpty(query)) { sb.Append(query); } return sb.ToString(); } /// /// Gets the subtitle profiles. /// /// The transcoder support. /// If only the selected track should be included. /// The base URL. /// The access token. /// The of the profiles. public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) { return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); } /// /// Gets the subtitle profiles. /// /// The transcoder support. /// If only the selected track should be included. /// If all profiles are enabled. /// The base URL. /// The access token. /// The of the profiles. public IEnumerable GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) { if (MediaSource is null) { return []; } List list = []; // HLS will preserve timestamps so we can just grab the full subtitle stream long startPositionTicks = SubProtocol == MediaStreamProtocol.hls ? 0 : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); // First add the selected track if (SubtitleStreamIndex.HasValue) { foreach (var stream in MediaSource.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) { AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); } } } if (!includeSelectedTrackOnly) { foreach (var stream in MediaSource.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) { AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); } } } return list; } private void AddSubtitleProfiles(List list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) { if (enableAllProfiles) { foreach (var profile in DeviceProfile.SubtitleProfiles) { var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); if (info is not null) { list.Add(info); } } } else { var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); if (info is not null) { list.Add(info); } } } private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) { if (MediaSource is null) { return null; } var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); var info = new SubtitleStreamInfo { IsForced = stream.IsForced, Language = stream.Language, Name = stream.Language ?? "Unknown", Format = subtitleProfile.Format, Index = stream.Index, DeliveryMethod = subtitleProfile.Method, DisplayTitle = stream.DisplayTitle }; if (info.DeliveryMethod == SubtitleDeliveryMethod.External) { if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) { info.Url = string.Format( CultureInfo.InvariantCulture, "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", baseUrl, ItemId, MediaSourceId, stream.Index.ToString(CultureInfo.InvariantCulture), startPositionTicks.ToString(CultureInfo.InvariantCulture), subtitleProfile.Format); if (!string.IsNullOrEmpty(accessToken)) { info.Url += "?ApiKey=" + accessToken; } info.IsExternalUrl = false; } else { info.Url = stream.Path; info.IsExternalUrl = true; } } return info; } /// /// Gets the target video bit depth. /// /// The codec. /// The target video bit depth. public int? GetTargetVideoBitDepth(string? codec) { var value = GetOption(codec, "videobitdepth"); if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } return null; } /// /// Gets the target audio bit depth. /// /// The codec. /// The target audio bit depth. public int? GetTargetAudioBitDepth(string? codec) { var value = GetOption(codec, "audiobitdepth"); if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } return null; } /// /// Gets the target video level. /// /// The codec. /// The target video level. public double? GetTargetVideoLevel(string? codec) { var value = GetOption(codec, "level"); if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } return null; } /// /// Gets the target reference frames. /// /// The codec. /// The target reference frames. public int? GetTargetRefFrames(string? codec) { var value = GetOption(codec, "maxrefframes"); if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { return result; } return null; } /// /// Gets the target audio channels. /// /// The codec. /// The target audio channels. public int? GetTargetAudioChannels(string? codec) { var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; var value = GetOption(codec, "audiochannels"); if (string.IsNullOrEmpty(value)) { return defaultValue; } if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return Math.Min(result, defaultValue ?? result); } return defaultValue; } /// /// Gets the media stream count. /// /// The type. /// The limit. /// The media stream count. private int? GetMediaStreamCount(MediaStreamType type, int limit) { var count = MediaSource?.GetStreamCount(type); if (count.HasValue) { count = Math.Min(count.Value, limit); } return count; } }