using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Api.Models; using MediaBrowser.Controller.Dlna; using MediaBrowser.Model.Dlna; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; namespace Jellyfin.Api.Helpers { /// /// The streaming helpers /// public class StreamingHelpers { /// /// Adds the dlna headers. /// /// The state. /// The response headers. /// if set to true [is statically streamed]. /// The . /// Instance of the interface. public static void AddDlnaHeaders( StreamState state, IHeaderDictionary responseHeaders, bool isStaticallyStreamed, HttpRequest request, IDlnaManager dlnaManager) { if (!state.EnableDlnaHeaders) { return; } var profile = state.DeviceProfile; StringValues transferMode = request.Headers["transferMode.dlna.org"]; responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString()); responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"); if (state.RunTimeTicks.HasValue) { if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase)) { var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds; responseHeaders.Add("MediaInfo.sec", string.Format( CultureInfo.InvariantCulture, "SEC_Duration={0};", Convert.ToInt32(ms))); } if (!isStaticallyStreamed && profile != null) { AddTimeSeekResponseHeaders(state, responseHeaders); } } if (profile == null) { profile = dlnaManager.GetDefaultProfile(); } var audioCodec = state.ActualOutputAudioCodec; if (state.VideoRequest == null) { responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildAudioHeader( state.OutputContainer, audioCodec, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioChannels, state.OutputAudioBitDepth, isStaticallyStreamed, state.RunTimeTicks, state.TranscodeSeekInfo)); } else { var videoCodec = state.ActualOutputVideoCodec; responseHeaders.Add("contentFeatures.dlna.org", new ContentFeatureBuilder(profile).BuildVideoHeader( state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); } } /// /// Parses the dlna headers. /// /// The start time ticks. /// The . public void ParseDlnaHeaders(long? startTimeTicks, HttpRequest request) { if (!startTimeTicks.HasValue) { var timeSeek = request.Headers["TimeSeekRange.dlna.org"]; startTimeTicks = ParseTimeSeekHeader(timeSeek); } } /// /// Parses the time seek header. /// public long? ParseTimeSeekHeader(string value) { if (string.IsNullOrWhiteSpace(value)) { return null; } const string Npt = "npt="; if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Invalid timeseek header"); } int index = value.IndexOf('-'); value = index == -1 ? value.Substring(Npt.Length) : value.Substring(Npt.Length, index - Npt.Length); if (value.IndexOf(':') == -1) { // Parses npt times in the format of '417.33' if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds)) { return TimeSpan.FromSeconds(seconds).Ticks; } throw new ArgumentException("Invalid timeseek header"); } // Parses npt times in the format of '10:19:25.7' var tokens = value.Split(new[] { ':' }, 3); double secondsSum = 0; var timeFactor = 3600; foreach (var time in tokens) { if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit)) { secondsSum += digit * timeFactor; } else { throw new ArgumentException("Invalid timeseek header"); } timeFactor /= 60; } return TimeSpan.FromSeconds(secondsSum).Ticks; } public void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders) { var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); responseHeaders.Add("TimeSeekRange.dlna.org", string.Format( CultureInfo.InvariantCulture, "npt={0}-{1}/{1}", startSeconds, runtimeSeconds)); responseHeaders.Add("X-AvailableSeekRange", string.Format( CultureInfo.InvariantCulture, "1 npt={0}-{1}", startSeconds, runtimeSeconds)); } } }