Migrate AudioService to Jellyfin.Api
This commit is contained in:
parent
2eef7d4913
commit
2328ec59c9
183
Jellyfin.Api/Controllers/AudioController.cs
Normal file
183
Jellyfin.Api/Controllers/AudioController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
194
Jellyfin.Api/Helpers/StreamingHelpers.cs
Normal file
194
Jellyfin.Api/Helpers/StreamingHelpers.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
145
Jellyfin.Api/Models/StreamState.cs
Normal file
145
Jellyfin.Api/Models/StreamState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user