mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
actually remove MediaBrowser.Api
This commit is contained in:
parent
9e00aa3014
commit
e64924f4d3
@ -1,678 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Api.Playback;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Session;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class ServerEntryPoint.
|
|
||||||
/// </summary>
|
|
||||||
public class ApiEntryPoint : IServerEntryPoint
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The instance.
|
|
||||||
/// </summary>
|
|
||||||
public static ApiEntryPoint Instance;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The logger.
|
|
||||||
/// </summary>
|
|
||||||
private ILogger<ApiEntryPoint> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The configuration manager.
|
|
||||||
/// </summary>
|
|
||||||
private IServerConfigurationManager _serverConfigurationManager;
|
|
||||||
|
|
||||||
private readonly ISessionManager _sessionManager;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The active transcoding jobs.
|
|
||||||
/// </summary>
|
|
||||||
private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
|
|
||||||
|
|
||||||
private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
|
|
||||||
new Dictionary<string, SemaphoreSlim>();
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="sessionManager">The session manager.</param>
|
|
||||||
/// <param name="config">The configuration.</param>
|
|
||||||
/// <param name="fileSystem">The file system.</param>
|
|
||||||
/// <param name="mediaSourceManager">The media source manager.</param>
|
|
||||||
public ApiEntryPoint(
|
|
||||||
ILogger<ApiEntryPoint> logger,
|
|
||||||
ISessionManager sessionManager,
|
|
||||||
IServerConfigurationManager config,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IMediaSourceManager mediaSourceManager)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_sessionManager = sessionManager;
|
|
||||||
_serverConfigurationManager = config;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
|
|
||||||
_sessionManager.PlaybackProgress += OnPlaybackProgress;
|
|
||||||
_sessionManager.PlaybackStart += OnPlaybackStart;
|
|
||||||
|
|
||||||
Instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string[] Split(string value, char separator, bool removeEmpty)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return Array.Empty<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return removeEmpty
|
|
||||||
? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
: value.Split(separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SemaphoreSlim GetTranscodingLock(string outputPath)
|
|
||||||
{
|
|
||||||
lock (_transcodingLocks)
|
|
||||||
{
|
|
||||||
if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
|
|
||||||
{
|
|
||||||
result = new SemaphoreSlim(1, 1);
|
|
||||||
_transcodingLocks[outputPath] = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
|
|
||||||
{
|
|
||||||
PingTranscodingJob(e.PlaySessionId, e.IsPaused);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
|
|
||||||
{
|
|
||||||
PingTranscodingJob(e.PlaySessionId, e.IsPaused);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs this instance.
|
|
||||||
/// </summary>
|
|
||||||
public Task RunAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DeleteEncodedMediaCache();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting encoded media cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the encoded media cache.
|
|
||||||
/// </summary>
|
|
||||||
private void DeleteEncodedMediaCache()
|
|
||||||
{
|
|
||||||
var path = _serverConfigurationManager.GetTranscodePath();
|
|
||||||
if (!Directory.Exists(path))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in _fileSystem.GetFilePaths(path, true))
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool dispose)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dispose)
|
|
||||||
{
|
|
||||||
// TODO: dispose
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobs = _activeTranscodingJobs.ToList();
|
|
||||||
var jobCount = jobs.Count;
|
|
||||||
|
|
||||||
IEnumerable<Task> GetKillJobs()
|
|
||||||
{
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
yield return KillTranscodingJob(job, false, path => true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all processes to be killed
|
|
||||||
if (jobCount > 0)
|
|
||||||
{
|
|
||||||
Task.WaitAll(GetKillJobs().ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
_activeTranscodingJobs.Clear();
|
|
||||||
_transcodingLocks.Clear();
|
|
||||||
|
|
||||||
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
|
|
||||||
_sessionManager.PlaybackStart -= OnPlaybackStart;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when [transcode beginning].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="playSessionId">The play session identifier.</param>
|
|
||||||
/// <param name="liveStreamId">The live stream identifier.</param>
|
|
||||||
/// <param name="transcodingJobId">The transcoding job identifier.</param>
|
|
||||||
/// <param name="type">The type.</param>
|
|
||||||
/// <param name="process">The process.</param>
|
|
||||||
/// <param name="deviceId">The device id.</param>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
|
||||||
/// <returns>TranscodingJob.</returns>
|
|
||||||
public TranscodingJob OnTranscodeBeginning(
|
|
||||||
string path,
|
|
||||||
string playSessionId,
|
|
||||||
string liveStreamId,
|
|
||||||
string transcodingJobId,
|
|
||||||
TranscodingJobType type,
|
|
||||||
Process process,
|
|
||||||
string deviceId,
|
|
||||||
StreamState state,
|
|
||||||
CancellationTokenSource cancellationTokenSource)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
var job = new TranscodingJob(_logger)
|
|
||||||
{
|
|
||||||
Type = type,
|
|
||||||
Path = path,
|
|
||||||
Process = process,
|
|
||||||
ActiveRequestCount = 1,
|
|
||||||
DeviceId = deviceId,
|
|
||||||
CancellationTokenSource = cancellationTokenSource,
|
|
||||||
Id = transcodingJobId,
|
|
||||||
PlaySessionId = playSessionId,
|
|
||||||
LiveStreamId = liveStreamId,
|
|
||||||
MediaSource = state.MediaSource
|
|
||||||
};
|
|
||||||
|
|
||||||
_activeTranscodingJobs.Add(job);
|
|
||||||
|
|
||||||
ReportTranscodingProgress(job, state, null, null, null, null, null);
|
|
||||||
|
|
||||||
return job;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <summary>
|
|
||||||
/// The progressive.
|
|
||||||
/// </summary>
|
|
||||||
/// Called when [transcode failed to start].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="type">The type.</param>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (job != null)
|
|
||||||
{
|
|
||||||
_activeTranscodingJobs.Remove(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_transcodingLocks)
|
|
||||||
{
|
|
||||||
_transcodingLocks.Remove(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
|
|
||||||
{
|
|
||||||
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether [has active transcoding job] [the specified path].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="type">The type.</param>
|
|
||||||
/// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
|
|
||||||
public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
|
|
||||||
{
|
|
||||||
return GetTranscodingJob(path, type) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TranscodingJob GetTranscodingJob(string playSessionId)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when [transcode begin request].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="type">The type.</param>
|
|
||||||
public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (job == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnTranscodeBeginRequest(job);
|
|
||||||
|
|
||||||
return job;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnTranscodeBeginRequest(TranscodingJob job)
|
|
||||||
{
|
|
||||||
job.ActiveRequestCount++;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
|
|
||||||
{
|
|
||||||
job.StopKillTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnTranscodeEndRequest(TranscodingJob job)
|
|
||||||
{
|
|
||||||
job.ActiveRequestCount--;
|
|
||||||
_logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
|
|
||||||
if (job.ActiveRequestCount <= 0)
|
|
||||||
{
|
|
||||||
PingTimer(job, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(playSessionId))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(playSessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
|
|
||||||
|
|
||||||
List<TranscodingJob> jobs;
|
|
||||||
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
// This is really only needed for HLS.
|
|
||||||
// Progressive streams can stop on their own reliably
|
|
||||||
jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
if (isUserPaused.HasValue)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
|
|
||||||
job.IsUserPaused = isUserPaused.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
PingTimer(job, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
|
|
||||||
{
|
|
||||||
if (job.HasExited)
|
|
||||||
{
|
|
||||||
job.StopKillTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var timerDuration = 10000;
|
|
||||||
|
|
||||||
if (job.Type != TranscodingJobType.Progressive)
|
|
||||||
{
|
|
||||||
timerDuration = 60000;
|
|
||||||
}
|
|
||||||
|
|
||||||
job.PingTimeout = timerDuration;
|
|
||||||
job.LastPingDate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
// Don't start the timer for playback checkins with progressive streaming
|
|
||||||
if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
|
|
||||||
{
|
|
||||||
job.StartKillTimer(OnTranscodeKillTimerStopped);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
job.ChangeKillTimerIfStarted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when [transcode kill timer stopped].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
private async void OnTranscodeKillTimerStopped(object state)
|
|
||||||
{
|
|
||||||
var job = (TranscodingJob)state;
|
|
||||||
|
|
||||||
if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
|
|
||||||
{
|
|
||||||
var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
|
|
||||||
|
|
||||||
if (timeSinceLastPing < job.PingTimeout)
|
|
||||||
{
|
|
||||||
job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
|
||||||
|
|
||||||
await KillTranscodingJob(job, true, path => true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kills the single transcoding job.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="deviceId">The device id.</param>
|
|
||||||
/// <param name="playSessionId">The play session identifier.</param>
|
|
||||||
/// <param name="deleteFiles">The delete files.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
|
|
||||||
{
|
|
||||||
return KillTranscodingJobs(j => string.IsNullOrWhiteSpace(playSessionId)
|
|
||||||
? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
|
|
||||||
: string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kills the transcoding jobs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="killJob">The kill job.</param>
|
|
||||||
/// <param name="deleteFiles">The delete files.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
private Task KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
|
|
||||||
{
|
|
||||||
var jobs = new List<TranscodingJob>();
|
|
||||||
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
// This is really only needed for HLS.
|
|
||||||
// Progressive streams can stop on their own reliably
|
|
||||||
jobs.AddRange(_activeTranscodingJobs.Where(killJob));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jobs.Count == 0)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<Task> GetKillJobs()
|
|
||||||
{
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
yield return KillTranscodingJob(job, false, deleteFiles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.WhenAll(GetKillJobs());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kills the transcoding job.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="job">The job.</param>
|
|
||||||
/// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
|
|
||||||
/// <param name="delete">The delete.</param>
|
|
||||||
private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
|
|
||||||
{
|
|
||||||
job.DisposeKillTimer();
|
|
||||||
|
|
||||||
_logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
|
||||||
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
_activeTranscodingJobs.Remove(job);
|
|
||||||
|
|
||||||
if (!job.CancellationTokenSource.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
job.CancellationTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_transcodingLocks)
|
|
||||||
{
|
|
||||||
_transcodingLocks.Remove(job.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (job.ProcessLock)
|
|
||||||
{
|
|
||||||
job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
var process = job.Process;
|
|
||||||
|
|
||||||
var hasExited = job.HasExited;
|
|
||||||
|
|
||||||
if (!hasExited)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
|
|
||||||
|
|
||||||
process.StandardInput.WriteLine("q");
|
|
||||||
|
|
||||||
// Need to wait because killing is asynchronous
|
|
||||||
if (!process.WaitForExit(5000))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
|
|
||||||
process.Kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delete(job.Path))
|
|
||||||
{
|
|
||||||
await DeletePartialStreamFiles(job.Path, job.Type, 0, 1500).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
|
|
||||||
{
|
|
||||||
if (retryCount >= 10)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Deleting partial stream file(s) {Path}", path);
|
|
||||||
|
|
||||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (jobType == TranscodingJobType.Progressive)
|
|
||||||
{
|
|
||||||
DeleteProgressivePartialStreamFiles(path);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DeleteHlsPartialStreamFiles(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
|
|
||||||
|
|
||||||
await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the progressive partial stream files.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputFilePath">The output file path.</param>
|
|
||||||
private void DeleteProgressivePartialStreamFiles(string outputFilePath)
|
|
||||||
{
|
|
||||||
if (File.Exists(outputFilePath))
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(outputFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the HLS partial stream files.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputFilePath">The output file path.</param>
|
|
||||||
private void DeleteHlsPartialStreamFiles(string outputFilePath)
|
|
||||||
{
|
|
||||||
var directory = Path.GetDirectoryName(outputFilePath);
|
|
||||||
var name = Path.GetFileNameWithoutExtension(outputFilePath);
|
|
||||||
|
|
||||||
var filesToDelete = _fileSystem.GetFilePaths(directory)
|
|
||||||
.Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
|
|
||||||
|
|
||||||
List<Exception> exs = null;
|
|
||||||
foreach (var file in filesToDelete)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Deleting HLS file {0}", file);
|
|
||||||
_fileSystem.DeleteFile(file);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
(exs ??= new List<Exception>(4)).Add(ex);
|
|
||||||
_logger.LogError(ex, "Error deleting HLS file {Path}", file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exs != null)
|
|
||||||
{
|
|
||||||
throw new AggregateException("Error deleting HLS files", exs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,416 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Dto;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class BaseApiService.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseApiService : IService, IRequiresRequest
|
|
||||||
{
|
|
||||||
public BaseApiService(
|
|
||||||
ILogger<BaseApiService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
ServerConfigurationManager = serverConfigurationManager;
|
|
||||||
ResultFactory = httpResultFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the logger.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The logger.</value>
|
|
||||||
protected ILogger<BaseApiService> Logger { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the server configuration manager.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The server configuration manager.</value>
|
|
||||||
protected IServerConfigurationManager ServerConfigurationManager { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the HTTP result factory.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The HTTP result factory.</value>
|
|
||||||
protected IHttpResultFactory ResultFactory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the request context.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The request context.</value>
|
|
||||||
public IRequest Request { get; set; }
|
|
||||||
|
|
||||||
public string GetHeader(string name) => Request.Headers[name];
|
|
||||||
|
|
||||||
public static string[] SplitValue(string value, char delim)
|
|
||||||
{
|
|
||||||
return value == null
|
|
||||||
? Array.Empty<string>()
|
|
||||||
: value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Guid[] GetGuids(string value)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
return Array.Empty<Guid>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(i => new Guid(i))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// To the optimized result.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="result">The result.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
protected object ToOptimizedResult<T>(T result)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return ResultFactory.GetResult(Request, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void AssertCanUpdateUser(IAuthorizationContext authContext, IUserManager userManager, Guid userId, bool restrictUserPreferences)
|
|
||||||
{
|
|
||||||
var auth = authContext.GetAuthorizationInfo(Request);
|
|
||||||
|
|
||||||
var authenticatedUser = auth.User;
|
|
||||||
|
|
||||||
// If they're going to update the record of another user, they must be an administrator
|
|
||||||
if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
|
|
||||||
|| (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
|
|
||||||
{
|
|
||||||
throw new SecurityException("Unauthorized access.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the session.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>SessionInfo.</returns>
|
|
||||||
protected SessionInfo GetSession(ISessionContext sessionContext)
|
|
||||||
{
|
|
||||||
var session = sessionContext.GetSession(Request);
|
|
||||||
|
|
||||||
if (session == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Session not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected DtoOptions GetDtoOptions(IAuthorizationContext authContext, object request)
|
|
||||||
{
|
|
||||||
var options = new DtoOptions();
|
|
||||||
|
|
||||||
if (request is IHasItemFields hasFields)
|
|
||||||
{
|
|
||||||
options.Fields = hasFields.GetItemFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.ContainsField(ItemFields.RecursiveItemCount)
|
|
||||||
|| !options.ContainsField(ItemFields.ChildCount))
|
|
||||||
{
|
|
||||||
var client = authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
|
|
||||||
if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
int oldLen = options.Fields.Length;
|
|
||||||
var arr = new ItemFields[oldLen + 1];
|
|
||||||
options.Fields.CopyTo(arr, 0);
|
|
||||||
arr[oldLen] = ItemFields.RecursiveItemCount;
|
|
||||||
options.Fields = arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
|
|
||||||
int oldLen = options.Fields.Length;
|
|
||||||
var arr = new ItemFields[oldLen + 1];
|
|
||||||
options.Fields.CopyTo(arr, 0);
|
|
||||||
arr[oldLen] = ItemFields.ChildCount;
|
|
||||||
options.Fields = arr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request is IHasDtoOptions hasDtoOptions)
|
|
||||||
{
|
|
||||||
options.EnableImages = hasDtoOptions.EnableImages ?? true;
|
|
||||||
|
|
||||||
if (hasDtoOptions.ImageTypeLimit.HasValue)
|
|
||||||
{
|
|
||||||
options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasDtoOptions.EnableUserData.HasValue)
|
|
||||||
{
|
|
||||||
options.EnableUserData = hasDtoOptions.EnableUserData.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
|
|
||||||
{
|
|
||||||
options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MusicArtist GetArtist(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<MusicArtist>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetArtist(name, dtoOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Studio GetStudio(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<Studio>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetStudio(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Genre GetGenre(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<Genre>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetGenre(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MusicGenre GetMusicGenre(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<MusicGenre>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetMusicGenre(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Person GetPerson(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<Person>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetPerson(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
|
|
||||||
where T : BaseItem, new()
|
|
||||||
{
|
|
||||||
var result = libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '&'),
|
|
||||||
IncludeItemTypes = new[] { typeof(T).Name },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '/'),
|
|
||||||
IncludeItemTypes = new[] { typeof(T).Name },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '?'),
|
|
||||||
IncludeItemTypes = new[] { typeof(T).Name },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the path segment at the specified index.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The index of the path segment.</param>
|
|
||||||
/// <returns>The path segment at the specified index.</returns>
|
|
||||||
/// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
|
|
||||||
/// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
|
|
||||||
protected internal ReadOnlySpan<char> GetPathValue(int index)
|
|
||||||
{
|
|
||||||
static void ThrowIndexOutOfRangeException()
|
|
||||||
=> throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
|
|
||||||
|
|
||||||
static void ThrowInvalidDataException()
|
|
||||||
=> throw new InvalidDataException("Path doesn't start with the base url.");
|
|
||||||
|
|
||||||
ReadOnlySpan<char> path = Request.PathInfo;
|
|
||||||
|
|
||||||
// Remove the protocol part from the url
|
|
||||||
int pos = path.LastIndexOf("://");
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(pos + 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the query string
|
|
||||||
pos = path.LastIndexOf('?');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the domain
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove base url
|
|
||||||
string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
|
|
||||||
int baseUrlLen = baseUrl.Length;
|
|
||||||
if (baseUrlLen != 0)
|
|
||||||
{
|
|
||||||
if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(baseUrlLen);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// The path doesn't start with the base url,
|
|
||||||
// how did we get here?
|
|
||||||
ThrowInvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove leading /
|
|
||||||
path = path.Slice(1);
|
|
||||||
|
|
||||||
// Backwards compatibility
|
|
||||||
const string Emby = "emby/";
|
|
||||||
if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(Emby.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const string MediaBrowser = "mediabrowser/";
|
|
||||||
if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(MediaBrowser.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip segments until we are at the right index
|
|
||||||
for (int i = 0; i < index; i++)
|
|
||||||
{
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos == -1)
|
|
||||||
{
|
|
||||||
ThrowIndexOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
path = path.Slice(pos + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the rest
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the name of the item by.
|
|
||||||
/// </summary>
|
|
||||||
protected BaseItem GetItemByName(string name, string type, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (type.Equals("Person", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetPerson(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("Artist", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetArtist(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("Genre", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetGenre(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("MusicGenre", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetMusicGenre(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("Studio", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetStudio(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("Year", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return libraryManager.GetYear(int.Parse(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException("Invalid type", nameof(type));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
public interface IHasDtoOptions : IHasItemFields
|
|
||||||
{
|
|
||||||
bool? EnableImages { get; set; }
|
|
||||||
|
|
||||||
bool? EnableUserData { get; set; }
|
|
||||||
|
|
||||||
int? ImageTypeLimit { get; set; }
|
|
||||||
|
|
||||||
string EnableImageTypes { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interface IHasItemFields.
|
|
||||||
/// </summary>
|
|
||||||
public interface IHasItemFields
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the fields.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The fields.</value>
|
|
||||||
string Fields { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Class ItemFieldsExtensions.
|
|
||||||
/// </summary>
|
|
||||||
public static class ItemFieldsExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the item fields.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>IEnumerable{ItemFields}.</returns>
|
|
||||||
public static ItemFields[] GetItemFields(this IHasItemFields request)
|
|
||||||
{
|
|
||||||
var val = request.Fields;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(val))
|
|
||||||
{
|
|
||||||
return Array.Empty<ItemFields>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return val.Split(',').Select(v =>
|
|
||||||
{
|
|
||||||
if (Enum.TryParse(v, true, out ItemFields value))
|
|
||||||
{
|
|
||||||
return (ItemFields?)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}).Where(i => i.HasValue).Select(i => i.Value).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="..\SharedVersion.cs" />
|
|
||||||
<Compile Remove="Images\ImageService.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Sessions" />
|
|
||||||
<Folder Include="System" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
File diff suppressed because it is too large
Load Diff
@ -1,344 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
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.Configuration;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Hls
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class BaseHlsService.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseHlsService : BaseStreamingService
|
|
||||||
{
|
|
||||||
public BaseHlsService(
|
|
||||||
ILogger<BaseHlsService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
EncodingHelper encodingHelper)
|
|
||||||
: base(
|
|
||||||
logger,
|
|
||||||
serverConfigurationManager,
|
|
||||||
httpResultFactory,
|
|
||||||
userManager,
|
|
||||||
libraryManager,
|
|
||||||
isoManager,
|
|
||||||
mediaEncoder,
|
|
||||||
fileSystem,
|
|
||||||
dlnaManager,
|
|
||||||
deviceManager,
|
|
||||||
mediaSourceManager,
|
|
||||||
jsonSerializer,
|
|
||||||
authorizationContext,
|
|
||||||
encodingHelper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the audio arguments.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract string GetAudioArguments(StreamState state, EncodingOptions encodingOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the video arguments.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract string GetVideoArguments(StreamState state, EncodingOptions encodingOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the segment file extension.
|
|
||||||
/// </summary>
|
|
||||||
protected string GetSegmentFileExtension(StreamRequest request)
|
|
||||||
{
|
|
||||||
var segmentContainer = request.SegmentContainer;
|
|
||||||
if (!string.IsNullOrWhiteSpace(segmentContainer))
|
|
||||||
{
|
|
||||||
return "." + segmentContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ".ts";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the type of the transcoding job.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The type of the transcoding job.</value>
|
|
||||||
protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Hls;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes the request async.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <param name="isLive">if set to <c>true</c> [is live].</param>
|
|
||||||
/// <returns>Task{System.Object}.</returns>
|
|
||||||
/// <exception cref="ArgumentException">A video bitrate is required
|
|
||||||
/// or
|
|
||||||
/// An audio bitrate is required</exception>
|
|
||||||
protected async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
|
|
||||||
{
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
TranscodingJob job = null;
|
|
||||||
var playlist = state.OutputFilePath;
|
|
||||||
|
|
||||||
if (!File.Exists(playlist))
|
|
||||||
{
|
|
||||||
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist);
|
|
||||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(playlist))
|
|
||||||
{
|
|
||||||
// If the playlist doesn't already exist, startup ffmpeg
|
|
||||||
try
|
|
||||||
{
|
|
||||||
job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
|
|
||||||
job.IsLiveOutput = isLive;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
state.Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
var minSegments = state.MinSegments;
|
|
||||||
if (minSegments > 0)
|
|
||||||
{
|
|
||||||
await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
transcodingLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLive)
|
|
||||||
{
|
|
||||||
job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
|
||||||
|
|
||||||
if (job != null)
|
|
||||||
{
|
|
||||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioBitrate = state.OutputAudioBitrate ?? 0;
|
|
||||||
var videoBitrate = state.OutputVideoBitrate ?? 0;
|
|
||||||
|
|
||||||
var baselineStreamBitrate = 64000;
|
|
||||||
|
|
||||||
var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate);
|
|
||||||
|
|
||||||
job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
|
||||||
|
|
||||||
if (job != null)
|
|
||||||
{
|
|
||||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLivePlaylistText(string path, int segmentLength)
|
|
||||||
{
|
|
||||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
|
|
||||||
var text = reader.ReadToEnd();
|
|
||||||
|
|
||||||
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
|
|
||||||
|
|
||||||
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
|
||||||
// text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
|
|
||||||
builder.AppendLine("#EXTM3U");
|
|
||||||
|
|
||||||
// Pad a little to satisfy the apple hls validator
|
|
||||||
var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
|
|
||||||
|
|
||||||
// Main stream
|
|
||||||
builder.Append("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=")
|
|
||||||
.AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture));
|
|
||||||
var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
|
|
||||||
builder.AppendLine(playlistUrl);
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
|
|
||||||
var fileStream = GetPlaylistFileStream(playlist);
|
|
||||||
await using (fileStream.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(fileStream);
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
while (!reader.EndOfStream)
|
|
||||||
{
|
|
||||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
if (count >= segmentCount)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
// May get an error if the file is locked
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Stream GetPlaylistFileStream(string path)
|
|
||||||
{
|
|
||||||
return new FileStream(
|
|
||||||
path,
|
|
||||||
FileMode.Open,
|
|
||||||
FileAccess.Read,
|
|
||||||
FileShare.ReadWrite,
|
|
||||||
IODefaults.FileStreamBufferSize,
|
|
||||||
FileOptions.SequentialScan);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
|
|
||||||
{
|
|
||||||
var itsOffsetMs = 0;
|
|
||||||
|
|
||||||
var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
|
|
||||||
|
|
||||||
var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
|
|
||||||
|
|
||||||
var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
|
|
||||||
|
|
||||||
// If isEncoding is true we're actually starting ffmpeg
|
|
||||||
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(CultureInfo.InvariantCulture) : "0";
|
|
||||||
|
|
||||||
var baseUrlParam = string.Empty;
|
|
||||||
|
|
||||||
if (state.Request is GetLiveHlsStream)
|
|
||||||
{
|
|
||||||
baseUrlParam = string.Format(" -hls_base_url \"{0}/\"",
|
|
||||||
"hls/" + Path.GetFileNameWithoutExtension(outputPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
var useGenericSegmenter = true;
|
|
||||||
if (useGenericSegmenter)
|
|
||||||
{
|
|
||||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
|
|
||||||
|
|
||||||
var timeDeltaParam = string.Empty;
|
|
||||||
|
|
||||||
var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
|
|
||||||
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
segmentFormat = "mpegts";
|
|
||||||
}
|
|
||||||
|
|
||||||
baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath));
|
|
||||||
|
|
||||||
return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
|
|
||||||
inputModifier,
|
|
||||||
EncodingHelper.GetInputArgument(state, encodingOptions),
|
|
||||||
threads,
|
|
||||||
EncodingHelper.GetMapArgs(state),
|
|
||||||
GetVideoArguments(state, encodingOptions),
|
|
||||||
GetAudioArguments(state, encodingOptions),
|
|
||||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
|
||||||
startNumberParam,
|
|
||||||
outputPath,
|
|
||||||
outputTsArg,
|
|
||||||
timeDeltaParam,
|
|
||||||
segmentFormat,
|
|
||||||
baseUrlParam
|
|
||||||
).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// add when stream copying?
|
|
||||||
// -avoid_negative_ts make_zero -fflags +genpts
|
|
||||||
|
|
||||||
var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
|
|
||||||
itsOffset,
|
|
||||||
inputModifier,
|
|
||||||
EncodingHelper.GetInputArgument(state, encodingOptions),
|
|
||||||
threads,
|
|
||||||
EncodingHelper.GetMapArgs(state),
|
|
||||||
GetVideoArguments(state, encodingOptions),
|
|
||||||
GetAudioArguments(state, encodingOptions),
|
|
||||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
|
||||||
startNumberParam,
|
|
||||||
state.HlsListSize.ToString(CultureInfo.InvariantCulture),
|
|
||||||
baseUrlParam,
|
|
||||||
outputPath
|
|
||||||
).Trim();
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetDefaultEncoderPreset()
|
|
||||||
{
|
|
||||||
return "veryfast";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual int GetStartNumber(StreamState state)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,126 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get various codec strings for use in HLS playlists.
|
|
||||||
/// </summary>
|
|
||||||
static class HlsCodecStringFactory
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a MP3 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>MP3 codec string.</returns>
|
|
||||||
public static string GetMP3String()
|
|
||||||
{
|
|
||||||
return "mp4a.40.34";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an AAC codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">AAC profile.</param>
|
|
||||||
/// <returns>AAC codec string.</returns>
|
|
||||||
public static string GetAACString(string profile)
|
|
||||||
{
|
|
||||||
StringBuilder result = new StringBuilder("mp4a", 9);
|
|
||||||
|
|
||||||
if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".40.5");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default to LC if profile is invalid
|
|
||||||
result.Append(".40.2");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a H.264 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">H.264 profile.</param>
|
|
||||||
/// <param name="level">H.264 level.</param>
|
|
||||||
/// <returns>H.264 string.</returns>
|
|
||||||
public static string GetH264String(string profile, int level)
|
|
||||||
{
|
|
||||||
StringBuilder result = new StringBuilder("avc1", 11);
|
|
||||||
|
|
||||||
if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".6400");
|
|
||||||
}
|
|
||||||
else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".4D40");
|
|
||||||
}
|
|
||||||
else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".42E0");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default to constrained baseline if profile is invalid
|
|
||||||
result.Append(".4240");
|
|
||||||
}
|
|
||||||
|
|
||||||
string levelHex = level.ToString("X2");
|
|
||||||
result.Append(levelHex);
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a H.265 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">H.265 profile.</param>
|
|
||||||
/// <param name="level">H.265 level.</param>
|
|
||||||
/// <returns>H.265 string.</returns>
|
|
||||||
public static string GetH265String(string profile, int level)
|
|
||||||
{
|
|
||||||
// The h265 syntax is a bit of a mystery at the time this comment was written.
|
|
||||||
// This is what I've found through various sources:
|
|
||||||
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
|
|
||||||
StringBuilder result = new StringBuilder("hev1", 16);
|
|
||||||
|
|
||||||
if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".2.6");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default to main if profile is invalid
|
|
||||||
result.Append(".1.6");
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Append(".L")
|
|
||||||
.Append(level * 3)
|
|
||||||
.Append(".B0");
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an AC-3 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>AC-3 codec string.</returns>
|
|
||||||
public static string GetAC3String()
|
|
||||||
{
|
|
||||||
return "mp4a.a5";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an E-AC-3 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>E-AC-3 codec string.</returns>
|
|
||||||
public static string GetEAC3String()
|
|
||||||
{
|
|
||||||
return "mp4a.a6";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace MediaBrowser.Api.Playback.Hls
|
|
||||||
{
|
|
||||||
public class GetLiveHlsStream : VideoStreamRequest
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,442 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
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.IO;
|
|
||||||
using MediaBrowser.Model.MediaInfo;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Progressive
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class BaseProgressiveStreamingService.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseProgressiveStreamingService : BaseStreamingService
|
|
||||||
{
|
|
||||||
protected IHttpClient HttpClient { get; private set; }
|
|
||||||
|
|
||||||
public BaseProgressiveStreamingService(
|
|
||||||
ILogger<BaseProgressiveStreamingService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IHttpClient httpClient,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
EncodingHelper encodingHelper)
|
|
||||||
: base(
|
|
||||||
logger,
|
|
||||||
serverConfigurationManager,
|
|
||||||
httpResultFactory,
|
|
||||||
userManager,
|
|
||||||
libraryManager,
|
|
||||||
isoManager,
|
|
||||||
mediaEncoder,
|
|
||||||
fileSystem,
|
|
||||||
dlnaManager,
|
|
||||||
deviceManager,
|
|
||||||
mediaSourceManager,
|
|
||||||
jsonSerializer,
|
|
||||||
authorizationContext,
|
|
||||||
encodingHelper)
|
|
||||||
{
|
|
||||||
HttpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the output file extension.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
protected override string GetOutputFileExtension(StreamState state)
|
|
||||||
{
|
|
||||||
var ext = base.GetOutputFileExtension(state);
|
|
||||||
|
|
||||||
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 type of the transcoding job.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The type of the transcoding job.</value>
|
|
||||||
protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Progressive;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes the request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
protected async Task<object> ProcessRequest(StreamRequest request, bool isHeadRequest)
|
|
||||||
{
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var responseHeaders = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
if (request.Static && state.DirectStreamProvider != null)
|
|
||||||
{
|
|
||||||
AddDlnaHeaders(state, responseHeaders, true);
|
|
||||||
|
|
||||||
using (state)
|
|
||||||
{
|
|
||||||
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// TODO: Don't hardcode this
|
|
||||||
outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts");
|
|
||||||
|
|
||||||
return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, CancellationToken.None)
|
|
||||||
{
|
|
||||||
AllowEndOfFile = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static remote stream
|
|
||||||
if (request.Static && state.InputProtocol == MediaProtocol.Http)
|
|
||||||
{
|
|
||||||
AddDlnaHeaders(state, responseHeaders, true);
|
|
||||||
|
|
||||||
using (state)
|
|
||||||
{
|
|
||||||
return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Static && state.InputProtocol != MediaProtocol.File)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(string.Format("Input protocol {0} cannot be streamed statically.", state.InputProtocol));
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputPath = state.OutputFilePath;
|
|
||||||
var outputPathExists = File.Exists(outputPath);
|
|
||||||
|
|
||||||
var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
|
|
||||||
var isTranscodeCached = outputPathExists && transcodingJob != null;
|
|
||||||
|
|
||||||
AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached);
|
|
||||||
|
|
||||||
// Static stream
|
|
||||||
if (request.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(request.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the static remote stream result.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <param name="responseHeaders">The response headers.</param>
|
|
||||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
|
||||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
|
||||||
/// <returns>Task{System.Object}.</returns>
|
|
||||||
private async Task<object> GetStaticRemoteStreamResult(
|
|
||||||
StreamState state,
|
|
||||||
Dictionary<string, string> responseHeaders,
|
|
||||||
bool isHeadRequest,
|
|
||||||
CancellationTokenSource cancellationTokenSource)
|
|
||||||
{
|
|
||||||
var options = new HttpRequestOptions
|
|
||||||
{
|
|
||||||
Url = state.MediaPath,
|
|
||||||
BufferContent = false,
|
|
||||||
CancellationToken = cancellationTokenSource.Token
|
|
||||||
};
|
|
||||||
|
|
||||||
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
|
|
||||||
{
|
|
||||||
options.UserAgent = useragent;
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
|
|
||||||
|
|
||||||
responseHeaders[HeaderNames.AcceptRanges] = "none";
|
|
||||||
|
|
||||||
// Seeing cases of -1 here
|
|
||||||
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
|
|
||||||
{
|
|
||||||
responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
|
||||||
using (response)
|
|
||||||
{
|
|
||||||
return ResultFactory.GetResult(null, Array.Empty<byte>(), response.ContentType, responseHeaders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new StaticRemoteStreamWriter(response);
|
|
||||||
|
|
||||||
result.Headers[HeaderNames.ContentType] = response.ContentType;
|
|
||||||
|
|
||||||
// Add the response headers to the result object
|
|
||||||
foreach (var header in responseHeaders)
|
|
||||||
{
|
|
||||||
result.Headers[header.Key] = header.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the stream result.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <param name="responseHeaders">The response headers.</param>
|
|
||||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
|
||||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
|
||||||
/// <returns>Task{System.Object}.</returns>
|
|
||||||
private async Task<object> GetStreamResult(StreamRequest request, StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
|
|
||||||
{
|
|
||||||
// Use the command line args with a dummy playlist path
|
|
||||||
var outputPath = state.OutputFilePath;
|
|
||||||
|
|
||||||
responseHeaders[HeaderNames.AcceptRanges] = "none";
|
|
||||||
|
|
||||||
var contentType = state.GetMimeType(outputPath);
|
|
||||||
|
|
||||||
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
|
|
||||||
var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
|
|
||||||
|
|
||||||
if (contentLength.HasValue)
|
|
||||||
{
|
|
||||||
responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers only
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
|
||||||
var streamResult = ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
|
|
||||||
|
|
||||||
if (streamResult is IHasHeaders hasHeaders)
|
|
||||||
{
|
|
||||||
if (contentLength.HasValue)
|
|
||||||
{
|
|
||||||
hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hasHeaders.Headers.Remove(HeaderNames.ContentLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return streamResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
|
|
||||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
TranscodingJob job;
|
|
||||||
|
|
||||||
if (!File.Exists(outputPath))
|
|
||||||
{
|
|
||||||
job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
|
|
||||||
state.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
[HeaderNames.ContentType] = contentType
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Add the response headers to the result object
|
|
||||||
foreach (var item in responseHeaders)
|
|
||||||
{
|
|
||||||
outputHeaders[item.Key] = item.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
transcodingLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the length of the estimated content.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <returns>System.Nullable{System.Int64}.</returns>
|
|
||||||
private long? GetEstimatedContentLength(StreamState state)
|
|
||||||
{
|
|
||||||
var totalBitrate = state.TotalOutputBitrate ?? 0;
|
|
||||||
|
|
||||||
if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
|
|
||||||
{
|
|
||||||
return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,182 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using MediaBrowser.Model.System;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Progressive
|
|
||||||
{
|
|
||||||
public class ProgressiveFileCopier : IAsyncStreamWriter, IHasHeaders
|
|
||||||
{
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly TranscodingJob _job;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly string _path;
|
|
||||||
private readonly CancellationToken _cancellationToken;
|
|
||||||
private readonly Dictionary<string, string> _outputHeaders;
|
|
||||||
|
|
||||||
private long _bytesWritten = 0;
|
|
||||||
public long StartPosition { get; set; }
|
|
||||||
|
|
||||||
public bool AllowEndOfFile = true;
|
|
||||||
|
|
||||||
private readonly IDirectStreamProvider _directStreamProvider;
|
|
||||||
|
|
||||||
public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_path = path;
|
|
||||||
_outputHeaders = outputHeaders;
|
|
||||||
_job = job;
|
|
||||||
_logger = logger;
|
|
||||||
_cancellationToken = cancellationToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_directStreamProvider = directStreamProvider;
|
|
||||||
_outputHeaders = outputHeaders;
|
|
||||||
_job = job;
|
|
||||||
_logger = logger;
|
|
||||||
_cancellationToken = cancellationToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDictionary<string, string> Headers => _outputHeaders;
|
|
||||||
|
|
||||||
private Stream GetInputStream(bool allowAsyncFileRead)
|
|
||||||
{
|
|
||||||
var fileOptions = FileOptions.SequentialScan;
|
|
||||||
|
|
||||||
if (allowAsyncFileRead)
|
|
||||||
{
|
|
||||||
fileOptions |= FileOptions.Asynchronous;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_directStreamProvider != null)
|
|
||||||
{
|
|
||||||
await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var eofCount = 0;
|
|
||||||
|
|
||||||
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
|
|
||||||
var allowAsyncFileRead = OperatingSystem.Id != OperatingSystemId.Windows;
|
|
||||||
|
|
||||||
using (var inputStream = GetInputStream(allowAsyncFileRead))
|
|
||||||
{
|
|
||||||
if (StartPosition > 0)
|
|
||||||
{
|
|
||||||
inputStream.Position = StartPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (eofCount < 20 || !AllowEndOfFile)
|
|
||||||
{
|
|
||||||
int bytesRead;
|
|
||||||
if (allowAsyncFileRead)
|
|
||||||
{
|
|
||||||
bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// var position = fs.Position;
|
|
||||||
// _logger.LogDebug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
|
|
||||||
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
if (_job == null || _job.HasExited)
|
|
||||||
{
|
|
||||||
eofCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
eofCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (_job != null)
|
|
||||||
{
|
|
||||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(_job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var array = new byte[IODefaults.CopyToBufferSize];
|
|
||||||
int bytesRead;
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
|
|
||||||
while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
|
|
||||||
{
|
|
||||||
var bytesToWrite = bytesRead;
|
|
||||||
|
|
||||||
if (bytesToWrite > 0)
|
|
||||||
{
|
|
||||||
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_bytesWritten += bytesRead;
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
|
|
||||||
if (_job != null)
|
|
||||||
{
|
|
||||||
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var array = new byte[IODefaults.CopyToBufferSize];
|
|
||||||
int bytesRead;
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
|
|
||||||
while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
|
||||||
{
|
|
||||||
var bytesToWrite = bytesRead;
|
|
||||||
|
|
||||||
if (bytesToWrite > 0)
|
|
||||||
{
|
|
||||||
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_bytesWritten += bytesRead;
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
|
|
||||||
if (_job != null)
|
|
||||||
{
|
|
||||||
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBytesRead;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
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.Configuration;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Progressive
|
|
||||||
{
|
|
||||||
public class GetVideoStream : VideoStreamRequest
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Class VideoService.
|
|
||||||
/// </summary>
|
|
||||||
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
|
|
||||||
//[Authenticated]
|
|
||||||
public class VideoService : BaseProgressiveStreamingService
|
|
||||||
{
|
|
||||||
public VideoService(
|
|
||||||
ILogger<VideoService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IHttpClient httpClient,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
EncodingHelper encodingHelper)
|
|
||||||
: base(
|
|
||||||
logger,
|
|
||||||
serverConfigurationManager,
|
|
||||||
httpResultFactory,
|
|
||||||
httpClient,
|
|
||||||
userManager,
|
|
||||||
libraryManager,
|
|
||||||
isoManager,
|
|
||||||
mediaEncoder,
|
|
||||||
fileSystem,
|
|
||||||
dlnaManager,
|
|
||||||
deviceManager,
|
|
||||||
mediaSourceManager,
|
|
||||||
jsonSerializer,
|
|
||||||
authorizationContext,
|
|
||||||
encodingHelper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the specified request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
public Task<object> Get(GetVideoStream request)
|
|
||||||
{
|
|
||||||
return ProcessRequest(request, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Heads the specified request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
public Task<object> Head(GetVideoStream request)
|
|
||||||
{
|
|
||||||
return ProcessRequest(request, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
|
|
||||||
{
|
|
||||||
return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultEncoderPreset());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class StaticRemoteStreamWriter.
|
|
||||||
/// </summary>
|
|
||||||
public class StaticRemoteStreamWriter : IAsyncStreamWriter, IHasHeaders
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The _input stream.
|
|
||||||
/// </summary>
|
|
||||||
private readonly HttpResponseInfo _response;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _options.
|
|
||||||
/// </summary>
|
|
||||||
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
public StaticRemoteStreamWriter(HttpResponseInfo response)
|
|
||||||
{
|
|
||||||
_response = response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the options.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The options.</value>
|
|
||||||
public IDictionary<string, string> Headers => _options;
|
|
||||||
|
|
||||||
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using (_response)
|
|
||||||
{
|
|
||||||
await _response.Content.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class StreamRequest.
|
|
||||||
/// </summary>
|
|
||||||
public class StreamRequest : BaseEncodingJobOptions
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
|
||||||
public string DeviceProfileId { get; set; }
|
|
||||||
|
|
||||||
public string Params { get; set; }
|
|
||||||
|
|
||||||
public string PlaySessionId { get; set; }
|
|
||||||
|
|
||||||
public string Tag { get; set; }
|
|
||||||
|
|
||||||
public string SegmentContainer { get; set; }
|
|
||||||
|
|
||||||
public int? SegmentLength { get; set; }
|
|
||||||
|
|
||||||
public int? MinSegments { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class VideoStreamRequest : StreamRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether this instance has fixed resolution.
|
|
||||||
/// </summary>
|
|
||||||
/// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
|
|
||||||
public bool HasFixedResolution => Width.HasValue || Height.HasValue;
|
|
||||||
|
|
||||||
public bool EnableSubtitlesInManifest { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,143 +0,0 @@
|
|||||||
using System;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
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 TranscodingJob 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)
|
|
||||||
{
|
|
||||||
ApiEntryPoint.Instance.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Model.Configuration;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
public class TranscodingThrottler : IDisposable
|
|
||||||
{
|
|
||||||
private readonly TranscodingJob _job;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private Timer _timer;
|
|
||||||
private bool _isPaused;
|
|
||||||
private readonly IConfigurationManager _config;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
|
|
||||||
public TranscodingThrottler(TranscodingJob job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem)
|
|
||||||
{
|
|
||||||
_job = job;
|
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EncodingOptions GetOptions()
|
|
||||||
{
|
|
||||||
return _config.GetConfiguration<EncodingOptions>("encoding");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
_timer = new Timer(TimerCallback, null, 5000, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void TimerCallback(object state)
|
|
||||||
{
|
|
||||||
if (_job.HasExited)
|
|
||||||
{
|
|
||||||
DisposeTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = GetOptions();
|
|
||||||
|
|
||||||
if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
|
|
||||||
{
|
|
||||||
await PauseTranscoding();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await UnpauseTranscoding();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PauseTranscoding()
|
|
||||||
{
|
|
||||||
if (!_isPaused)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Sending pause command to ffmpeg");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _job.Process.StandardInput.WriteAsync("c");
|
|
||||||
_isPaused = true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error pausing transcoding");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnpauseTranscoding()
|
|
||||||
{
|
|
||||||
if (_isPaused)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Sending resume command to ffmpeg");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _job.Process.StandardInput.WriteLineAsync();
|
|
||||||
_isPaused = false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error resuming transcoding");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds)
|
|
||||||
{
|
|
||||||
var bytesDownloaded = job.BytesDownloaded ?? 0;
|
|
||||||
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
|
|
||||||
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
|
|
||||||
|
|
||||||
var path = job.Path;
|
|
||||||
var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
|
|
||||||
|
|
||||||
if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
|
|
||||||
{
|
|
||||||
// HLS - time-based consideration
|
|
||||||
|
|
||||||
var targetGap = gapLengthInTicks;
|
|
||||||
var gap = transcodingPositionTicks - downloadPositionTicks;
|
|
||||||
|
|
||||||
if (gap < targetGap)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
|
|
||||||
{
|
|
||||||
// Progressive Streaming - byte-based consideration
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length;
|
|
||||||
|
|
||||||
// Estimate the bytes the transcoder should be ahead
|
|
||||||
double gapFactor = gapLengthInTicks;
|
|
||||||
gapFactor /= transcodingPositionTicks;
|
|
||||||
var targetGap = bytesTranscoded * gapFactor;
|
|
||||||
|
|
||||||
var gap = bytesTranscoded - bytesDownloaded;
|
|
||||||
|
|
||||||
if (gap < targetGap)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error getting output size");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("No throttle data for " + path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Stop()
|
|
||||||
{
|
|
||||||
DisposeTimer();
|
|
||||||
await UnpauseTranscoding();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
DisposeTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisposeTimer()
|
|
||||||
{
|
|
||||||
if (_timer != null)
|
|
||||||
{
|
|
||||||
_timer.Dispose();
|
|
||||||
_timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Resources;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
|
||||||
// set of attributes. Change these attribute values to modify the information
|
|
||||||
// associated with an assembly.
|
|
||||||
[assembly: AssemblyTitle("MediaBrowser.Api")]
|
|
||||||
[assembly: AssemblyDescription("")]
|
|
||||||
[assembly: AssemblyConfiguration("")]
|
|
||||||
[assembly: AssemblyCompany("Jellyfin Project")]
|
|
||||||
[assembly: AssemblyProduct("Jellyfin Server")]
|
|
||||||
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
|
|
||||||
[assembly: AssemblyTrademark("")]
|
|
||||||
[assembly: AssemblyCulture("")]
|
|
||||||
[assembly: NeutralResourcesLanguage("en")]
|
|
||||||
[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")]
|
|
||||||
|
|
||||||
// Setting ComVisible to false makes the types in this assembly not visible
|
|
||||||
// to COM components. If you need to access a type in this assembly from
|
|
||||||
// COM, set the ComVisible attribute to true on that type.
|
|
||||||
[assembly: ComVisible(false)]
|
|
@ -1,26 +0,0 @@
|
|||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Service for testing path value.
|
|
||||||
/// </summary>
|
|
||||||
public class TestService : BaseApiService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Test service.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{TestService}"/> interface.</param>
|
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
|
||||||
/// <param name="httpResultFactory">Instance of the <see cref="IHttpResultFactory"/> interface.</param>
|
|
||||||
public TestService(
|
|
||||||
ILogger<TestService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory)
|
|
||||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,165 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
|
||||||
using MediaBrowser.Api.Playback;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class TranscodingJob.
|
|
||||||
/// </summary>
|
|
||||||
public class TranscodingJob
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the play session identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The play session identifier.</value>
|
|
||||||
public string PlaySessionId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the live stream identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The live stream identifier.</value>
|
|
||||||
public string LiveStreamId { get; set; }
|
|
||||||
|
|
||||||
public bool IsLiveOutput { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the path.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The path.</value>
|
|
||||||
public MediaSourceInfo MediaSource { get; set; }
|
|
||||||
|
|
||||||
public string Path { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the type.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The type.</value>
|
|
||||||
public TranscodingJobType Type { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the process.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The process.</value>
|
|
||||||
public Process Process { get; set; }
|
|
||||||
|
|
||||||
public ILogger Logger { get; private set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the active request count.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The active request count.</value>
|
|
||||||
public int ActiveRequestCount { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the kill timer.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The kill timer.</value>
|
|
||||||
private Timer KillTimer { get; set; }
|
|
||||||
|
|
||||||
public string DeviceId { get; set; }
|
|
||||||
|
|
||||||
public CancellationTokenSource CancellationTokenSource { get; set; }
|
|
||||||
|
|
||||||
public object ProcessLock = new object();
|
|
||||||
|
|
||||||
public bool HasExited { get; set; }
|
|
||||||
|
|
||||||
public bool IsUserPaused { get; set; }
|
|
||||||
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
public float? Framerate { get; set; }
|
|
||||||
|
|
||||||
public double? CompletionPercentage { get; set; }
|
|
||||||
|
|
||||||
public long? BytesDownloaded { get; set; }
|
|
||||||
|
|
||||||
public long? BytesTranscoded { get; set; }
|
|
||||||
|
|
||||||
public int? BitRate { get; set; }
|
|
||||||
|
|
||||||
public long? TranscodingPositionTicks { get; set; }
|
|
||||||
|
|
||||||
public long? DownloadPositionTicks { get; set; }
|
|
||||||
|
|
||||||
public TranscodingThrottler TranscodingThrottler { get; set; }
|
|
||||||
|
|
||||||
private readonly object _timerLock = new object();
|
|
||||||
|
|
||||||
public DateTime LastPingDate { get; set; }
|
|
||||||
|
|
||||||
public int PingTimeout { get; set; }
|
|
||||||
|
|
||||||
public TranscodingJob(ILogger logger)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopKillTimer()
|
|
||||||
{
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisposeKillTimer()
|
|
||||||
{
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
if (KillTimer != null)
|
|
||||||
{
|
|
||||||
KillTimer.Dispose();
|
|
||||||
KillTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartKillTimer(Action<object> callback)
|
|
||||||
{
|
|
||||||
StartKillTimer(callback, PingTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartKillTimer(Action<object> callback, int intervalMs)
|
|
||||||
{
|
|
||||||
if (HasExited)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
if (KillTimer == null)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
|
||||||
KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
|
||||||
KillTimer.Change(intervalMs, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeKillTimerIfStarted()
|
|
||||||
{
|
|
||||||
if (HasExited)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
if (KillTimer != null)
|
|
||||||
{
|
|
||||||
var intervalMs = PingTimeout;
|
|
||||||
|
|
||||||
Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
|
||||||
KillTimer.Change(intervalMs, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user