|
@@ -1,32 +1,255 @@
|
|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
using System.Globalization;
|
|
|
+using System.IO;
|
|
|
using System.Linq;
|
|
|
-using Jellyfin.Api.Models;
|
|
|
+using System.Threading;
|
|
|
+using System.Threading.Tasks;
|
|
|
+using Jellyfin.Api.Models.StreamingDtos;
|
|
|
+using MediaBrowser.Common.Configuration;
|
|
|
+using MediaBrowser.Common.Extensions;
|
|
|
+using MediaBrowser.Controller.Configuration;
|
|
|
+using MediaBrowser.Controller.Devices;
|
|
|
using MediaBrowser.Controller.Dlna;
|
|
|
+using MediaBrowser.Controller.Library;
|
|
|
+using MediaBrowser.Controller.MediaEncoding;
|
|
|
+using MediaBrowser.Controller.Net;
|
|
|
using MediaBrowser.Model.Dlna;
|
|
|
+using MediaBrowser.Model.Dto;
|
|
|
+using MediaBrowser.Model.Entities;
|
|
|
+using MediaBrowser.Model.IO;
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
+using Microsoft.Extensions.Configuration;
|
|
|
using Microsoft.Extensions.Primitives;
|
|
|
+using Microsoft.Net.Http.Headers;
|
|
|
|
|
|
namespace Jellyfin.Api.Helpers
|
|
|
{
|
|
|
/// <summary>
|
|
|
- /// The streaming helpers
|
|
|
+ /// The streaming helpers.
|
|
|
/// </summary>
|
|
|
- public class StreamingHelpers
|
|
|
+ public static class StreamingHelpers
|
|
|
{
|
|
|
+ public static async Task<StreamState> GetStreamingState(
|
|
|
+ Guid itemId,
|
|
|
+ long? startTimeTicks,
|
|
|
+ string? audioCodec,
|
|
|
+ string? subtitleCodec,
|
|
|
+ string? videoCodec,
|
|
|
+ string? @params,
|
|
|
+ bool? @static,
|
|
|
+ string? container,
|
|
|
+ string? liveStreamId,
|
|
|
+ string? playSessionId,
|
|
|
+ string? mediaSourceId,
|
|
|
+ string? deviceId,
|
|
|
+ string? deviceProfileId,
|
|
|
+ int? audioBitRate,
|
|
|
+ HttpRequest request,
|
|
|
+ IAuthorizationContext authorizationContext,
|
|
|
+ IMediaSourceManager mediaSourceManager,
|
|
|
+ IUserManager userManager,
|
|
|
+ ILibraryManager libraryManager,
|
|
|
+ IServerConfigurationManager serverConfigurationManager,
|
|
|
+ IMediaEncoder mediaEncoder,
|
|
|
+ IFileSystem fileSystem,
|
|
|
+ ISubtitleEncoder subtitleEncoder,
|
|
|
+ IConfiguration configuration,
|
|
|
+ IDlnaManager dlnaManager,
|
|
|
+ IDeviceManager deviceManager,
|
|
|
+ TranscodingJobHelper transcodingJobHelper,
|
|
|
+ TranscodingJobType transcodingJobType,
|
|
|
+ bool isVideoRequest,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ EncodingHelper encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
|
|
|
+ // Parse the DLNA time seek header
|
|
|
+ if (!startTimeTicks.HasValue)
|
|
|
+ {
|
|
|
+ var timeSeek = request.Headers["TimeSeekRange.dlna.org"];
|
|
|
+
|
|
|
+ startTimeTicks = ParseTimeSeekHeader(timeSeek);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!string.IsNullOrWhiteSpace(@params))
|
|
|
+ {
|
|
|
+ // What is this?
|
|
|
+ ParseParams(request);
|
|
|
+ }
|
|
|
+
|
|
|
+ var streamOptions = ParseStreamOptions(request.Query);
|
|
|
+
|
|
|
+ var url = request.Path.Value.Split('.').Last();
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(audioCodec))
|
|
|
+ {
|
|
|
+ audioCodec = encodingHelper.InferAudioCodec(url);
|
|
|
+ }
|
|
|
+
|
|
|
+ var enableDlnaHeaders = !string.IsNullOrWhiteSpace(@params) ||
|
|
|
+ string.Equals(request.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
|
|
|
+
|
|
|
+ var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
|
|
|
+ {
|
|
|
+ // TODO request was the StreamingRequest living in MediaBrowser.Api.Playback.Progressive
|
|
|
+ Request = request,
|
|
|
+ RequestedUrl = url,
|
|
|
+ UserAgent = request.Headers[HeaderNames.UserAgent],
|
|
|
+ EnableDlnaHeaders = enableDlnaHeaders
|
|
|
+ };
|
|
|
+
|
|
|
+ var auth = authorizationContext.GetAuthorizationInfo(request);
|
|
|
+ if (!auth.UserId.Equals(Guid.Empty))
|
|
|
+ {
|
|
|
+ state.User = userManager.GetUserById(auth.UserId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
|
+ (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
|
+ (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
|
|
|
+ {
|
|
|
+ state.SegmentLength = 6;
|
|
|
+ }
|
|
|
+ */
|
|
|
+
|
|
|
+ if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
|
|
|
+ {
|
|
|
+ state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
|
|
|
+ state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!string.IsNullOrWhiteSpace(audioCodec))
|
|
|
+ {
|
|
|
+ state.SupportedAudioCodecs = audioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
|
|
|
+ state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToAudioCodec(i))
|
|
|
+ ?? state.SupportedAudioCodecs.FirstOrDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!string.IsNullOrWhiteSpace(subtitleCodec))
|
|
|
+ {
|
|
|
+ state.SupportedSubtitleCodecs = subtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
|
|
|
+ state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => mediaEncoder.CanEncodeToSubtitleCodec(i))
|
|
|
+ ?? state.SupportedSubtitleCodecs.FirstOrDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ var item = libraryManager.GetItemById(itemId);
|
|
|
+
|
|
|
+ state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
|
|
+
|
|
|
+ /*
|
|
|
+ var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
|
|
|
+ item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
|
|
|
+ if (primaryImage != null)
|
|
|
+ {
|
|
|
+ state.AlbumCoverPath = primaryImage.Path;
|
|
|
+ }
|
|
|
+ */
|
|
|
+
|
|
|
+ MediaSourceInfo? mediaSource = null;
|
|
|
+ if (string.IsNullOrWhiteSpace(liveStreamId))
|
|
|
+ {
|
|
|
+ var currentJob = !string.IsNullOrWhiteSpace(playSessionId)
|
|
|
+ ? transcodingJobHelper.GetTranscodingJob(playSessionId)
|
|
|
+ : null;
|
|
|
+
|
|
|
+ if (currentJob != null)
|
|
|
+ {
|
|
|
+ mediaSource = currentJob.MediaSource;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mediaSource == null)
|
|
|
+ {
|
|
|
+ var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(itemId), null, false, false, cancellationToken).ConfigureAwait(false);
|
|
|
+
|
|
|
+ mediaSource = string.IsNullOrEmpty(mediaSourceId)
|
|
|
+ ? mediaSources[0]
|
|
|
+ : mediaSources.Find(i => string.Equals(i.Id, mediaSourceId, StringComparison.InvariantCulture));
|
|
|
+
|
|
|
+ if (mediaSource == null && Guid.Parse(mediaSourceId) == itemId)
|
|
|
+ {
|
|
|
+ mediaSource = mediaSources[0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(liveStreamId, cancellationToken).ConfigureAwait(false);
|
|
|
+ mediaSource = liveStreamInfo.Item1;
|
|
|
+ state.DirectStreamProvider = liveStreamInfo.Item2;
|
|
|
+ }
|
|
|
+
|
|
|
+ encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
|
|
|
+
|
|
|
+ var containerInternal = Path.GetExtension(state.RequestedUrl);
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(container))
|
|
|
+ {
|
|
|
+ containerInternal = container;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(containerInternal))
|
|
|
+ {
|
|
|
+ containerInternal = (@static.HasValue && @static.Value) ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) : GetOutputFileExtension(state);
|
|
|
+ }
|
|
|
+
|
|
|
+ state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
|
|
+
|
|
|
+ state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(audioBitRate, state.AudioStream);
|
|
|
+
|
|
|
+ state.OutputAudioCodec = audioCodec;
|
|
|
+
|
|
|
+ state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
|
|
+
|
|
|
+ if (isVideoRequest)
|
|
|
+ {
|
|
|
+ state.OutputVideoCodec = state.VideoRequest.VideoCodec;
|
|
|
+ state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
|
|
+
|
|
|
+ encodingHelper.TryStreamCopy(state);
|
|
|
+
|
|
|
+ if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
|
|
+ {
|
|
|
+ var resolution = ResolutionNormalizer.Normalize(
|
|
|
+ state.VideoStream?.BitRate,
|
|
|
+ state.VideoStream?.Width,
|
|
|
+ state.VideoStream?.Height,
|
|
|
+ state.OutputVideoBitrate.Value,
|
|
|
+ state.VideoStream?.Codec,
|
|
|
+ state.OutputVideoCodec,
|
|
|
+ videoRequest.MaxWidth,
|
|
|
+ videoRequest.MaxHeight);
|
|
|
+
|
|
|
+ videoRequest.MaxWidth = resolution.MaxWidth;
|
|
|
+ videoRequest.MaxHeight = resolution.MaxHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, request, deviceProfileId, @static);
|
|
|
+
|
|
|
+ var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
|
|
+ ? GetOutputFileExtension(state)
|
|
|
+ : ('.' + state.OutputContainer);
|
|
|
+
|
|
|
+ state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, deviceId, playSessionId);
|
|
|
+
|
|
|
+ return state;
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// Adds the dlna headers.
|
|
|
/// </summary>
|
|
|
/// <param name="state">The state.</param>
|
|
|
/// <param name="responseHeaders">The response headers.</param>
|
|
|
/// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
|
|
|
+ /// <param name="startTimeTicks">The start time in ticks.</param>
|
|
|
/// <param name="request">The <see cref="HttpRequest"/>.</param>
|
|
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
|
|
public static void AddDlnaHeaders(
|
|
|
StreamState state,
|
|
|
IHeaderDictionary responseHeaders,
|
|
|
bool isStaticallyStreamed,
|
|
|
+ long? startTimeTicks,
|
|
|
HttpRequest request,
|
|
|
IDlnaManager dlnaManager)
|
|
|
{
|
|
@@ -54,7 +277,7 @@ namespace Jellyfin.Api.Helpers
|
|
|
|
|
|
if (!isStaticallyStreamed && profile != null)
|
|
|
{
|
|
|
- AddTimeSeekResponseHeaders(state, responseHeaders);
|
|
|
+ AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -82,51 +305,18 @@ namespace Jellyfin.Api.Helpers
|
|
|
{
|
|
|
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);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Parses the dlna headers.
|
|
|
- /// </summary>
|
|
|
- /// <param name="startTimeTicks">The start time ticks.</param>
|
|
|
- /// <param name="request">The <see cref="HttpRequest"/>.</param>
|
|
|
- public void ParseDlnaHeaders(long? startTimeTicks, HttpRequest request)
|
|
|
- {
|
|
|
- if (!startTimeTicks.HasValue)
|
|
|
- {
|
|
|
- var timeSeek = request.Headers["TimeSeekRange.dlna.org"];
|
|
|
-
|
|
|
- startTimeTicks = ParseTimeSeekHeader(timeSeek);
|
|
|
+ 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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// Parses the time seek header.
|
|
|
/// </summary>
|
|
|
- public long? ParseTimeSeekHeader(string value)
|
|
|
+ /// <param name="value">The time seek header string.</param>
|
|
|
+ /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>
|
|
|
+ public static long? ParseTimeSeekHeader(string value)
|
|
|
{
|
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
|
{
|
|
@@ -138,12 +328,13 @@ namespace Jellyfin.Api.Helpers
|
|
|
{
|
|
|
throw new ArgumentException("Invalid timeseek header");
|
|
|
}
|
|
|
- int index = value.IndexOf('-');
|
|
|
+
|
|
|
+ int index = value.IndexOf('-', StringComparison.InvariantCulture);
|
|
|
value = index == -1
|
|
|
? value.Substring(Npt.Length)
|
|
|
: value.Substring(Npt.Length, index - Npt.Length);
|
|
|
|
|
|
- if (value.IndexOf(':') == -1)
|
|
|
+ if (value.IndexOf(':', StringComparison.InvariantCulture) == -1)
|
|
|
{
|
|
|
// Parses npt times in the format of '417.33'
|
|
|
if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
|
|
@@ -169,15 +360,45 @@ namespace Jellyfin.Api.Helpers
|
|
|
{
|
|
|
throw new ArgumentException("Invalid timeseek header");
|
|
|
}
|
|
|
+
|
|
|
timeFactor /= 60;
|
|
|
}
|
|
|
+
|
|
|
return TimeSpan.FromSeconds(secondsSum).Ticks;
|
|
|
}
|
|
|
|
|
|
- public void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders)
|
|
|
+ /// <summary>
|
|
|
+ /// Parses query parameters as StreamOptions.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="queryString">The query string.</param>
|
|
|
+ /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
|
|
|
+ public static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)
|
|
|
{
|
|
|
- var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
|
|
|
- var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
|
|
|
+ Dictionary<string, string> streamOptions = new Dictionary<string, string>();
|
|
|
+ foreach (var param in queryString)
|
|
|
+ {
|
|
|
+ if (char.IsLower(param.Key[0]))
|
|
|
+ {
|
|
|
+ // This was probably not parsed initially and should be a StreamOptions
|
|
|
+ // or the generated URL should correctly serialize it
|
|
|
+ // TODO: This should be incorporated either in the lower framework for parsing requests
|
|
|
+ streamOptions[param.Key] = param.Value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return streamOptions;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Adds the dlna time seek headers to the response.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="state">The current <see cref="StreamState"/>.</param>
|
|
|
+ /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>
|
|
|
+ /// <param name="startTimeTicks">The start time in ticks.</param>
|
|
|
+ public static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)
|
|
|
+ {
|
|
|
+ var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
|
|
|
+ var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
|
responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(
|
|
|
CultureInfo.InvariantCulture,
|
|
@@ -190,5 +411,369 @@ namespace Jellyfin.Api.Helpers
|
|
|
startSeconds,
|
|
|
runtimeSeconds));
|
|
|
}
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets the output file extension.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="state">The state.</param>
|
|
|
+ /// <returns>System.String.</returns>
|
|
|
+ public static string? GetOutputFileExtension(StreamState state)
|
|
|
+ {
|
|
|
+ var ext = Path.GetExtension(state.RequestedUrl);
|
|
|
+
|
|
|
+ if (!string.IsNullOrEmpty(ext))
|
|
|
+ {
|
|
|
+ return ext;
|
|
|
+ }
|
|
|
+
|
|
|
+ var isVideoRequest = state.VideoRequest != null;
|
|
|
+
|
|
|
+ // Try to infer based on the desired video codec
|
|
|
+ if (isVideoRequest)
|
|
|
+ {
|
|
|
+ var videoCodec = state.VideoRequest.VideoCodec;
|
|
|
+
|
|
|
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
|
|
+ string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return ".ts";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return ".ogv";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return ".webm";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return ".asf";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try to infer based on the desired audio codec
|
|
|
+ if (!isVideoRequest)
|
|
|
+ {
|
|
|
+ var audioCodec = state.Request.AudioCodec;
|
|
|
+
|
|
|
+ if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return ".aac";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return ".mp3";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return ".ogg";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return ".wma";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets the output file path for transcoding.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="state">The current <see cref="StreamState"/>.</param>
|
|
|
+ /// <param name="outputFileExtension">The file extension of the output file.</param>
|
|
|
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
|
|
+ /// <param name="deviceId">The device id.</param>
|
|
|
+ /// <param name="playSessionId">The play session id.</param>
|
|
|
+ /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
|
|
|
+ private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
|
|
|
+ {
|
|
|
+ var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
|
|
+
|
|
|
+ var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
|
|
+ var ext = outputFileExtension?.ToLowerInvariant();
|
|
|
+ var folder = serverConfigurationManager.GetTranscodePath();
|
|
|
+
|
|
|
+ return Path.Combine(folder, filename + ext);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
|
|
|
+ {
|
|
|
+ var headers = request.Headers;
|
|
|
+
|
|
|
+ if (!string.IsNullOrWhiteSpace(deviceProfileId))
|
|
|
+ {
|
|
|
+ state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
|
|
|
+ }
|
|
|
+ else if (!string.IsNullOrWhiteSpace(deviceProfileId))
|
|
|
+ {
|
|
|
+ var caps = deviceManager.GetCapabilities(deviceProfileId);
|
|
|
+
|
|
|
+ state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
|
|
|
+ }
|
|
|
+
|
|
|
+ var profile = state.DeviceProfile;
|
|
|
+
|
|
|
+ if (profile == null)
|
|
|
+ {
|
|
|
+ // Don't use settings from the default profile.
|
|
|
+ // Only use a specific profile if it was requested.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var audioCodec = state.ActualOutputAudioCodec;
|
|
|
+ var videoCodec = state.ActualOutputVideoCodec;
|
|
|
+
|
|
|
+ var mediaProfile = state.VideoRequest == null
|
|
|
+ ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)
|
|
|
+ : profile.GetVideoMediaProfile(
|
|
|
+ state.OutputContainer,
|
|
|
+ audioCodec,
|
|
|
+ videoCodec,
|
|
|
+ state.OutputWidth,
|
|
|
+ state.OutputHeight,
|
|
|
+ state.TargetVideoBitDepth,
|
|
|
+ state.OutputVideoBitrate,
|
|
|
+ state.TargetVideoProfile,
|
|
|
+ state.TargetVideoLevel,
|
|
|
+ state.TargetFramerate,
|
|
|
+ state.TargetPacketLength,
|
|
|
+ state.TargetTimestamp,
|
|
|
+ state.IsTargetAnamorphic,
|
|
|
+ state.IsTargetInterlaced,
|
|
|
+ state.TargetRefFrames,
|
|
|
+ state.TargetVideoStreamCount,
|
|
|
+ state.TargetAudioStreamCount,
|
|
|
+ state.TargetVideoCodecTag,
|
|
|
+ state.IsTargetAVC);
|
|
|
+
|
|
|
+ if (mediaProfile != null)
|
|
|
+ {
|
|
|
+ state.MimeType = mediaProfile.MimeType;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!(@static.HasValue && @static.Value))
|
|
|
+ {
|
|
|
+ var transcodingProfile = state.VideoRequest == null ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
|
|
|
+
|
|
|
+ if (transcodingProfile != null)
|
|
|
+ {
|
|
|
+ state.EstimateContentLength = transcodingProfile.EstimateContentLength;
|
|
|
+ // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
|
|
|
+ state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
|
|
|
+
|
|
|
+ if (state.VideoRequest != null)
|
|
|
+ {
|
|
|
+ state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
|
|
|
+ state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Parses the parameters.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="request">The request.</param>
|
|
|
+ private void ParseParams(StreamRequest request)
|
|
|
+ {
|
|
|
+ var vals = request.Params.Split(';');
|
|
|
+
|
|
|
+ var videoRequest = request as VideoStreamRequest;
|
|
|
+
|
|
|
+ for (var i = 0; i < vals.Length; i++)
|
|
|
+ {
|
|
|
+ var val = vals[i];
|
|
|
+
|
|
|
+ if (string.IsNullOrWhiteSpace(val))
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (i)
|
|
|
+ {
|
|
|
+ case 0:
|
|
|
+ request.DeviceProfileId = val;
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ request.DeviceId = val;
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ request.MediaSourceId = val;
|
|
|
+ break;
|
|
|
+ case 3:
|
|
|
+ request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
|
|
+ break;
|
|
|
+ case 4:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.VideoCodec = val;
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 5:
|
|
|
+ request.AudioCodec = val;
|
|
|
+ break;
|
|
|
+ case 6:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 7:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 8:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 9:
|
|
|
+ request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ break;
|
|
|
+ case 10:
|
|
|
+ request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ break;
|
|
|
+ case 11:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 12:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 13:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 14:
|
|
|
+ request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ break;
|
|
|
+ case 15:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.Level = val;
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 16:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 17:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 18:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.Profile = val;
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 19:
|
|
|
+ // cabac no longer used
|
|
|
+ break;
|
|
|
+ case 20:
|
|
|
+ request.PlaySessionId = val;
|
|
|
+ break;
|
|
|
+ case 21:
|
|
|
+ // api_key
|
|
|
+ break;
|
|
|
+ case 22:
|
|
|
+ request.LiveStreamId = val;
|
|
|
+ break;
|
|
|
+ case 23:
|
|
|
+ // Duplicating ItemId because of MediaMonkey
|
|
|
+ break;
|
|
|
+ case 24:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 25:
|
|
|
+ if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
|
|
|
+ {
|
|
|
+ if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
|
|
|
+ {
|
|
|
+ videoRequest.SubtitleMethod = method;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 26:
|
|
|
+ request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
+ break;
|
|
|
+ case 27:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 28:
|
|
|
+ request.Tag = val;
|
|
|
+ break;
|
|
|
+ case 29:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 30:
|
|
|
+ request.SubtitleCodec = val;
|
|
|
+ break;
|
|
|
+ case 31:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 32:
|
|
|
+ if (videoRequest != null)
|
|
|
+ {
|
|
|
+ videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ case 33:
|
|
|
+ request.TranscodeReasons = val;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|