Migrate AudioService to Jellyfin.Api

This commit is contained in:
David 2020-07-11 11:14:23 +02:00
parent 2eef7d4913
commit 2328ec59c9
4 changed files with 577 additions and 0 deletions

View File

@ -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;
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -5,10 +5,12 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models;
using Jellyfin.Api.Models.PlaybackDtos;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
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>
/// Ping transcoding job.
/// </summary>
@ -350,5 +360,50 @@ namespace Jellyfin.Api.Helpers
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
});
}
}
}
}

View File

@ -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;
}
}
}