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…
Reference in New Issue
Block a user