2
0
Эх сурвалжийг харах

Migrate AudioService to Jellyfin.Api

David 5 жил өмнө
parent
commit
2328ec59c9

+ 183 - 0
Jellyfin.Api/Controllers/AudioController.cs

@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Api.Controllers
+{
+
+    /// <summary>
+    /// The audio controller.
+    /// </summary>
+    public class AudioController : BaseJellyfinApiController
+    {
+        private readonly IDlnaManager _dlnaManager;
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AudioController"/> class.
+        /// </summary>
+        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+        /// <param name="logger">Instance of the <see cref="ILogger{AuidoController}"/> interface.</param>
+        public AudioController(IDlnaManager dlnaManager, ILogger<AudioController> logger)
+        {
+            _dlnaManager = dlnaManager;
+            _logger = logger;
+        }
+
+        [HttpGet("{id}/stream.{container}")]
+        [HttpGet("{id}/stream")]
+        [HttpHead("{id}/stream.{container}")]
+        [HttpGet("{id}/stream")]
+        public async Task<ActionResult> GetAudioStream(
+            [FromRoute] string id,
+            [FromRoute] string container,
+            [FromQuery] bool Static,
+            [FromQuery] string tag)
+        {
+            bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+
+            var cancellationTokenSource = new CancellationTokenSource();
+
+            var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
+
+            if (Static && state.DirectStreamProvider != null)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager);
+
+                using (state)
+                {
+                    var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+                    // TODO: Don't hardcode this
+                    outputHeaders[HeaderNames.ContentType] = MimeTypes.GetMimeType("file.ts");
+
+                    return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, _logger, CancellationToken.None)
+                    {
+                        AllowEndOfFile = false
+                    };
+                }
+            }
+
+            // Static remote stream
+            if (Static && state.InputProtocol == MediaProtocol.Http)
+            {
+                StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, Request, _dlnaManager);
+
+                using (state)
+                {
+                    return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
+                }
+            }
+
+            if (Static && state.InputProtocol != MediaProtocol.File)
+            {
+                throw new ArgumentException(string.Format($"Input protocol {state.InputProtocol} cannot be streamed statically."));
+            }
+
+            var outputPath = state.OutputFilePath;
+            var outputPathExists = File.Exists(outputPath);
+
+            var transcodingJob = TranscodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+            var isTranscodeCached = outputPathExists && transcodingJob != null;
+
+            StreamingHelpers.AddDlnaHeaders(state, Response.Headers, Static || isTranscodeCached, Request, _dlnaManager);
+
+            // Static stream
+            if (Static)
+            {
+                var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+
+                using (state)
+                {
+                    if (state.MediaSource.IsInfiniteStream)
+                    {
+                        var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+                        {
+                            [HeaderNames.ContentType] = contentType
+                        };
+
+
+                        return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, _logger, CancellationToken.None)
+                        {
+                            AllowEndOfFile = false
+                        };
+                    }
+
+                    TimeSpan? cacheDuration = null;
+
+                    if (!string.IsNullOrEmpty(tag))
+                    {
+                        cacheDuration = TimeSpan.FromDays(365);
+                    }
+
+                    return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+                    {
+                        ResponseHeaders = responseHeaders,
+                        ContentType = contentType,
+                        IsHeadRequest = isHeadRequest,
+                        Path = state.MediaPath,
+                        CacheDuration = cacheDuration
+
+                    }).ConfigureAwait(false);
+                }
+            }
+
+            //// Not static but transcode cache file exists
+            //if (isTranscodeCached && state.VideoRequest == null)
+            //{
+            //    var contentType = state.GetMimeType(outputPath);
+
+            //    try
+            //    {
+            //        if (transcodingJob != null)
+            //        {
+            //            ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
+            //        }
+
+            //        return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
+            //        {
+            //            ResponseHeaders = responseHeaders,
+            //            ContentType = contentType,
+            //            IsHeadRequest = isHeadRequest,
+            //            Path = outputPath,
+            //            FileShare = FileShare.ReadWrite,
+            //            OnComplete = () =>
+            //            {
+            //                if (transcodingJob != null)
+            //                {
+            //                    ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
+            //                }
+            //            }
+
+            //        }).ConfigureAwait(false);
+            //    }
+            //    finally
+            //    {
+            //        state.Dispose();
+            //    }
+            //}
+
+            // Need to start ffmpeg
+            try
+            {
+                return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
+            }
+            catch
+            {
+                state.Dispose();
+
+                throw;
+            }
+        }
+    }
+}

+ 194 - 0
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -0,0 +1,194 @@
+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
+{
+    /// <summary>
+    /// The streaming helpers
+    /// </summary>
+    public class StreamingHelpers
+    {
+        /// <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="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,
+            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);
+            }
+        }
+
+        /// <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);
+            }
+        }
+
+        /// <summary>
+        /// Parses the time seek header.
+        /// </summary>
+        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));
+        }
+    }
+}

+ 55 - 0
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -5,10 +5,12 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Api.Models;
 using Jellyfin.Api.Models.PlaybackDtos;
 using Jellyfin.Api.Models.PlaybackDtos;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Jellyfin.Api.Helpers
 namespace Jellyfin.Api.Helpers
@@ -61,6 +63,14 @@ namespace Jellyfin.Api.Helpers
             }
             }
         }
         }
 
 
+        public static TranscodingJobDto GetTranscodingJob(string path, TranscodingJobType type)
+        {
+            lock (_activeTranscodingJobs)
+            {
+                return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Ping transcoding job.
         /// Ping transcoding job.
         /// </summary>
         /// </summary>
@@ -350,5 +360,50 @@ namespace Jellyfin.Api.Helpers
                 throw new AggregateException("Error deleting HLS files", exs);
                 throw new AggregateException("Error deleting HLS files", exs);
             }
             }
         }
         }
+
+        public void ReportTranscodingProgress(
+        TranscodingJob job,
+        StreamState state,
+        TimeSpan? transcodingPosition,
+        float? framerate,
+        double? percentComplete,
+        long? bytesTranscoded,
+        int? bitRate)
+        {
+            var ticks = transcodingPosition?.Ticks;
+
+            if (job != null)
+            {
+                job.Framerate = framerate;
+                job.CompletionPercentage = percentComplete;
+                job.TranscodingPositionTicks = ticks;
+                job.BytesTranscoded = bytesTranscoded;
+                job.BitRate = bitRate;
+            }
+
+            var deviceId = state.Request.DeviceId;
+
+            if (!string.IsNullOrWhiteSpace(deviceId))
+            {
+                var audioCodec = state.ActualOutputAudioCodec;
+                var videoCodec = state.ActualOutputVideoCodec;
+
+                _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
+                {
+                    Bitrate = bitRate ?? state.TotalOutputBitrate,
+                    AudioCodec = audioCodec,
+                    VideoCodec = videoCodec,
+                    Container = state.OutputContainer,
+                    Framerate = framerate,
+                    CompletionPercentage = percentComplete,
+                    Width = state.OutputWidth,
+                    Height = state.OutputHeight,
+                    AudioChannels = state.OutputAudioChannels,
+                    IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
+                    IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
+                    TranscodeReasons = state.TranscodeReasons
+                });
+            }
+        }
     }
     }
 }
 }

+ 145 - 0
Jellyfin.Api/Models/StreamState.cs

@@ -0,0 +1,145 @@
+using System;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.PlaybackDtos;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+
+namespace Jellyfin.Api.Models
+{
+    public class StreamState : EncodingJobInfo, IDisposable
+    {
+        private readonly IMediaSourceManager _mediaSourceManager;
+        private bool _disposed = false;
+
+        public string RequestedUrl { get; set; }
+
+        public StreamRequest Request
+        {
+            get => (StreamRequest)BaseRequest;
+            set
+            {
+                BaseRequest = value;
+
+                IsVideoRequest = VideoRequest != null;
+            }
+        }
+
+        public TranscodingThrottler TranscodingThrottler { get; set; }
+
+        public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
+
+        public IDirectStreamProvider DirectStreamProvider { get; set; }
+
+        public string WaitForPath { get; set; }
+
+        public bool IsOutputVideo => Request is VideoStreamRequest;
+
+        public int SegmentLength
+        {
+            get
+            {
+                if (Request.SegmentLength.HasValue)
+                {
+                    return Request.SegmentLength.Value;
+                }
+
+                if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
+                {
+                    var userAgent = UserAgent ?? string.Empty;
+
+                    if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
+                        userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
+                    {
+                        if (IsSegmentedLiveStream)
+                        {
+                            return 6;
+                        }
+
+                        return 6;
+                    }
+
+                    if (IsSegmentedLiveStream)
+                    {
+                        return 3;
+                    }
+
+                    return 6;
+                }
+
+                return 3;
+            }
+        }
+
+        public int MinSegments
+        {
+            get
+            {
+                if (Request.MinSegments.HasValue)
+                {
+                    return Request.MinSegments.Value;
+                }
+
+                return SegmentLength >= 10 ? 2 : 3;
+            }
+        }
+
+        public string UserAgent { get; set; }
+
+        public bool EstimateContentLength { get; set; }
+
+        public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+
+        public bool EnableDlnaHeaders { get; set; }
+
+        public DeviceProfile DeviceProfile { get; set; }
+
+        public TranscodingJobDto TranscodingJob { get; set; }
+
+        public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType)
+            : base(transcodingType)
+        {
+            _mediaSourceManager = mediaSourceManager;
+        }
+
+        public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
+        {
+            TranscodingJobHelper.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            if (disposing)
+            {
+                // REVIEW: Is this the right place for this?
+                if (MediaSource.RequiresClosing
+                    && string.IsNullOrWhiteSpace(Request.LiveStreamId)
+                    && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
+                {
+                    _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
+                }
+
+                TranscodingThrottler?.Dispose();
+            }
+
+            TranscodingThrottler = null;
+            TranscodingJob = null;
+
+            _disposed = true;
+        }
+    }
+}