using System; using System.Collections.Generic; using System.Globalization; using Jellyfin.Data.Enums; 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. /// A querystring representation of this object. public string ToUrl(string baseUrl, string? accessToken) { ArgumentException.ThrowIfNullOrEmpty(baseUrl); List list = []; foreach (NameValuePair pair in BuildParams(this, accessToken)) { if (string.IsNullOrEmpty(pair.Value)) { continue; } // 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; } if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) { continue; } if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) { continue; } var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); } string queryString = string.Join('&', list); return GetUrl(baseUrl, queryString); } private string GetUrl(string baseUrl, string queryString) { ArgumentException.ThrowIfNullOrEmpty(baseUrl); string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; baseUrl = baseUrl.TrimEnd('/'); if (MediaType == DlnaProfileType.Audio) { if (SubProtocol == MediaStreamProtocol.hls) { return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } if (SubProtocol == MediaStreamProtocol.hls) { return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } private static List BuildParams(StreamInfo item, string? accessToken) { List list = []; string audioCodecs = item.AudioCodecs.Count == 0 ? string.Empty : string.Join(',', item.AudioCodecs); string videoCodecs = item.VideoCodecs.Count == 0 ? string.Empty : string.Join(',', item.VideoCodecs); 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)); 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)); long startPositionTicks = item.StartPositionTicks; if (item.SubProtocol == MediaStreamProtocol.hls) { list.Add(new NameValuePair("StartTimeTicks", string.Empty)); } else { list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); } list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); string? liveStreamId = item.MediaSource?.LiveStreamId; list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); if (!item.IsDirectStream) { if (item.RequireNonAnamorphic) { list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); if (item.EnableSubtitlesInManifest) { list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } if (item.EnableMpegtsM2TsMode) { list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } if (item.EstimateContentLength) { list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) { list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); } if (item.CopyTimestamps) { list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); list.Add(new NameValuePair("EnableAudioVbrEncoding", item.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) { list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); if (item.SegmentLength.HasValue) { list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); } if (item.MinSegments.HasValue) { list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); } list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); } foreach (var pair in item.StreamOptions) { if (string.IsNullOrEmpty(pair.Value)) { continue; } // strip spaces to avoid having to encode h264 profile names list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); } if (!item.IsDirectStream) { list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); } return list; } /// /// 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 += "?api_key=" + 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; } }