aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Api/Playback
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Api/Playback')
-rw-r--r--MediaBrowser.Api/Playback/BaseStreamingService.cs1003
-rw-r--r--MediaBrowser.Api/Playback/Hls/BaseHlsService.cs342
-rw-r--r--MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs1230
-rw-r--r--MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs126
-rw-r--r--MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs164
-rw-r--r--MediaBrowser.Api/Playback/Hls/VideoHlsService.cs173
-rw-r--r--MediaBrowser.Api/Playback/MediaInfoService.cs672
-rw-r--r--MediaBrowser.Api/Playback/Progressive/AudioService.cs95
-rw-r--r--MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs436
-rw-r--r--MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs180
-rw-r--r--MediaBrowser.Api/Playback/Progressive/VideoService.cs132
-rw-r--r--MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs44
-rw-r--r--MediaBrowser.Api/Playback/StreamRequest.cs33
-rw-r--r--MediaBrowser.Api/Playback/StreamState.cs143
-rw-r--r--MediaBrowser.Api/Playback/TranscodingThrottler.cs175
-rw-r--r--MediaBrowser.Api/Playback/UniversalAudioService.cs382
16 files changed, 0 insertions, 5330 deletions
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
deleted file mode 100644
index f796aa486..000000000
--- a/MediaBrowser.Api/Playback/BaseStreamingService.cs
+++ /dev/null
@@ -1,1003 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-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.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
- /// <summary>
- /// Class BaseStreamingService
- /// </summary>
- public abstract class BaseStreamingService : BaseApiService
- {
- protected virtual bool EnableOutputInSubFolder => false;
-
- /// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- protected IUserManager UserManager { get; private set; }
-
- /// <summary>
- /// Gets or sets the library manager.
- /// </summary>
- /// <value>The library manager.</value>
- protected ILibraryManager LibraryManager { get; private set; }
-
- /// <summary>
- /// Gets or sets the iso manager.
- /// </summary>
- /// <value>The iso manager.</value>
- protected IIsoManager IsoManager { get; private set; }
-
- /// <summary>
- /// Gets or sets the media encoder.
- /// </summary>
- /// <value>The media encoder.</value>
- protected IMediaEncoder MediaEncoder { get; private set; }
-
- protected IFileSystem FileSystem { get; private set; }
-
- protected IDlnaManager DlnaManager { get; private set; }
-
- protected IDeviceManager DeviceManager { get; private set; }
-
- protected IMediaSourceManager MediaSourceManager { get; private set; }
-
- protected IJsonSerializer JsonSerializer { get; private set; }
-
- protected IAuthorizationContext AuthorizationContext { get; private set; }
-
- protected EncodingHelper EncodingHelper { get; set; }
-
- /// <summary>
- /// Gets the type of the transcoding job.
- /// </summary>
- /// <value>The type of the transcoding job.</value>
- protected abstract TranscodingJobType TranscodingJobType { get; }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
- /// </summary>
- protected BaseStreamingService(
- ILogger<BaseStreamingService> 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 = userManager;
- LibraryManager = libraryManager;
- IsoManager = isoManager;
- MediaEncoder = mediaEncoder;
- FileSystem = fileSystem;
- DlnaManager = dlnaManager;
- DeviceManager = deviceManager;
- MediaSourceManager = mediaSourceManager;
- JsonSerializer = jsonSerializer;
- AuthorizationContext = authorizationContext;
-
- EncodingHelper = encodingHelper;
- }
-
- /// <summary>
- /// Gets the command line arguments.
- /// </summary>
- protected abstract string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding);
-
- /// <summary>
- /// Gets the output file extension.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <returns>System.String.</returns>
- protected virtual string GetOutputFileExtension(StreamState state)
- {
- return Path.GetExtension(state.RequestedUrl);
- }
-
- /// <summary>
- /// Gets the output file path.
- /// </summary>
- private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension)
- {
- var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}";
-
- var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- var ext = outputFileExtension?.ToLowerInvariant();
- var folder = ServerConfigurationManager.GetTranscodePath();
-
- return EnableOutputInSubFolder
- ? Path.Combine(folder, filename, filename + ext)
- : Path.Combine(folder, filename + ext);
- }
-
- protected virtual string GetDefaultEncoderPreset()
- {
- return "superfast";
- }
-
- private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
- {
- if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath))
- {
- state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false);
- }
-
- if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
- {
- var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
- {
- OpenToken = state.MediaSource.OpenToken
- }, cancellationTokenSource.Token).ConfigureAwait(false);
-
- EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
-
- if (state.VideoRequest != null)
- {
- EncodingHelper.TryStreamCopy(state);
- }
- }
-
- if (state.MediaSource.BufferMs.HasValue)
- {
- await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Starts the FFMPEG.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="outputPath">The output path.</param>
- /// <param name="cancellationTokenSource">The cancellation token source.</param>
- /// <param name="workingDirectory">The working directory.</param>
- /// <returns>Task.</returns>
- protected async Task<TranscodingJob> StartFfMpeg(
- StreamState state,
- string outputPath,
- CancellationTokenSource cancellationTokenSource,
- string workingDirectory = null)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
-
- await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
-
- if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- var auth = AuthorizationContext.GetAuthorizationInfo(Request);
- if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding)
- {
- ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
-
- throw new ArgumentException("User does not have access to video transcoding");
- }
- }
-
- var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
- var process = new Process()
- {
- StartInfo = new ProcessStartInfo()
- {
- WindowStyle = ProcessWindowStyle.Hidden,
- CreateNoWindow = true,
- UseShellExecute = false,
-
- // Must consume both stdout and stderr or deadlocks may occur
- //RedirectStandardOutput = true,
- RedirectStandardError = true,
- RedirectStandardInput = true,
-
- FileName = MediaEncoder.EncoderPath,
- Arguments = GetCommandLineArguments(outputPath, encodingOptions, state, true),
- WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
-
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- };
-
- var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
- state.Request.PlaySessionId,
- state.MediaSource.LiveStreamId,
- Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- TranscodingJobType,
- process,
- state.Request.DeviceId,
- state,
- cancellationTokenSource);
-
- var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
- Logger.LogInformation(commandLineLogMessage);
-
- var logFilePrefix = "ffmpeg-transcode";
- if (state.VideoRequest != null
- && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- logFilePrefix = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)
- ? "ffmpeg-remux" : "ffmpeg-directstream";
- }
-
- var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
-
- // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-
- var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
- await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
-
- process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
-
- try
- {
- process.Start();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error starting ffmpeg");
-
- ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
-
- throw;
- }
-
- Logger.LogDebug("Launched ffmpeg process");
- state.TranscodingJob = transcodingJob;
-
- // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
- _ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
-
- // Wait for the file to exist before proceeeding
- var ffmpegTargetFile = state.WaitForPath ?? outputPath;
- Logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
- while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
- {
- await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
- }
-
- Logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
-
- if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
- {
- await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
-
- if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
- {
- await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- }
-
- if (!transcodingJob.HasExited)
- {
- StartThrottler(state, transcodingJob);
- }
- Logger.LogDebug("StartFfMpeg() finished successfully");
-
- return transcodingJob;
- }
-
- private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
- {
- if (EnableThrottling(state))
- {
- transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager, FileSystem);
- state.TranscodingThrottler.Start();
- }
- }
-
- private bool EnableThrottling(StreamState state)
- {
- var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
- // enable throttling when NOT using hardware acceleration
- if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
- {
- return state.InputProtocol == MediaProtocol.File &&
- state.RunTimeTicks.HasValue &&
- state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
- state.IsInputVideo &&
- state.VideoType == VideoType.VideoFile &&
- !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase);
- }
-
- return false;
- }
-
- /// <summary>
- /// Processes the exited.
- /// </summary>
- /// <param name="process">The process.</param>
- /// <param name="job">The job.</param>
- /// <param name="state">The state.</param>
- private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
- {
- if (job != null)
- {
- job.HasExited = true;
- }
-
- Logger.LogDebug("Disposing stream resources");
- state.Dispose();
-
- if (process.ExitCode == 0)
- {
- Logger.LogInformation("FFMpeg exited with code 0");
- }
- else
- {
- Logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
- }
-
- process.Dispose();
- }
-
- /// <summary>
- /// Parses the parameters.
- /// </summary>
- /// <param name="request">The request.</param>
- private void ParseParams(StreamRequest request)
- {
- var vals = request.Params.Split(';');
-
- var videoRequest = request as VideoStreamRequest;
-
- for (var i = 0; i < vals.Length; i++)
- {
- var val = vals[i];
-
- if (string.IsNullOrWhiteSpace(val))
- {
- continue;
- }
-
- switch (i)
- {
- case 0:
- request.DeviceProfileId = val;
- break;
- case 1:
- request.DeviceId = val;
- break;
- case 2:
- request.MediaSourceId = val;
- break;
- case 3:
- request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- break;
- case 4:
- if (videoRequest != null)
- {
- videoRequest.VideoCodec = val;
- }
-
- break;
- case 5:
- request.AudioCodec = val;
- break;
- case 6:
- if (videoRequest != null)
- {
- videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 7:
- if (videoRequest != null)
- {
- videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 8:
- if (videoRequest != null)
- {
- videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 9:
- request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 10:
- request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 11:
- if (videoRequest != null)
- {
- videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 12:
- if (videoRequest != null)
- {
- videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 13:
- if (videoRequest != null)
- {
- videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 14:
- request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 15:
- if (videoRequest != null)
- {
- videoRequest.Level = val;
- }
-
- break;
- case 16:
- if (videoRequest != null)
- {
- videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 17:
- if (videoRequest != null)
- {
- videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
- }
-
- break;
- case 18:
- if (videoRequest != null)
- {
- videoRequest.Profile = val;
- }
-
- break;
- case 19:
- // cabac no longer used
- break;
- case 20:
- request.PlaySessionId = val;
- break;
- case 21:
- // api_key
- break;
- case 22:
- request.LiveStreamId = val;
- break;
- case 23:
- // Duplicating ItemId because of MediaMonkey
- break;
- case 24:
- if (videoRequest != null)
- {
- videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 25:
- if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
- {
- if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
- {
- videoRequest.SubtitleMethod = method;
- }
- }
-
- break;
- case 26:
- request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
- break;
- case 27:
- if (videoRequest != null)
- {
- videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 28:
- request.Tag = val;
- break;
- case 29:
- if (videoRequest != null)
- {
- videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 30:
- request.SubtitleCodec = val;
- break;
- case 31:
- if (videoRequest != null)
- {
- videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 32:
- if (videoRequest != null)
- {
- videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- case 33:
- request.TranscodeReasons = val;
- break;
- }
- }
- }
-
- /// <summary>
- /// Parses query parameters as StreamOptions.
- /// </summary>
- /// <param name="request">The stream request.</param>
- private void ParseStreamOptions(StreamRequest request)
- {
- foreach (var param in Request.QueryString)
- {
- if (char.IsLower(param.Key[0]))
- {
- // This was probably not parsed initially and should be a StreamOptions
- // TODO: This should be incorporated either in the lower framework for parsing requests
- // or the generated URL should correctly serialize it
- request.StreamOptions[param.Key] = param.Value;
- }
- }
- }
-
- /// <summary>
- /// Parses the dlna headers.
- /// </summary>
- /// <param name="request">The request.</param>
- private void ParseDlnaHeaders(StreamRequest request)
- {
- if (!request.StartTimeTicks.HasValue)
- {
- var timeSeek = GetHeader("TimeSeekRange.dlna.org");
-
- request.StartTimeTicks = ParseTimeSeekHeader(timeSeek);
- }
- }
-
- /// <summary>
- /// Parses the time seek header.
- /// </summary>
- private long? ParseTimeSeekHeader(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return null;
- }
-
- const string Npt = "npt=";
- if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase))
- {
- throw new ArgumentException("Invalid timeseek header");
- }
- int index = value.IndexOf('-');
- value = index == -1
- ? value.Substring(Npt.Length)
- : value.Substring(Npt.Length, index - Npt.Length);
-
- if (value.IndexOf(':') == -1)
- {
- // Parses npt times in the format of '417.33'
- if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
- {
- return TimeSpan.FromSeconds(seconds).Ticks;
- }
-
- throw new ArgumentException("Invalid timeseek header");
- }
-
- // Parses npt times in the format of '10:19:25.7'
- var tokens = value.Split(new[] { ':' }, 3);
- double secondsSum = 0;
- var timeFactor = 3600;
-
- foreach (var time in tokens)
- {
- if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit))
- {
- secondsSum += digit * timeFactor;
- }
- else
- {
- throw new ArgumentException("Invalid timeseek header");
- }
- timeFactor /= 60;
- }
- return TimeSpan.FromSeconds(secondsSum).Ticks;
- }
-
- /// <summary>
- /// Gets the state.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>StreamState.</returns>
- protected async Task<StreamState> GetState(StreamRequest request, CancellationToken cancellationToken)
- {
- ParseDlnaHeaders(request);
-
- if (!string.IsNullOrWhiteSpace(request.Params))
- {
- ParseParams(request);
- }
-
- ParseStreamOptions(request);
-
- var url = Request.PathInfo;
-
- if (string.IsNullOrEmpty(request.AudioCodec))
- {
- request.AudioCodec = EncodingHelper.InferAudioCodec(url);
- }
-
- var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) ||
- string.Equals(GetHeader("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase);
-
- var state = new StreamState(MediaSourceManager, TranscodingJobType)
- {
- Request = request,
- RequestedUrl = url,
- UserAgent = Request.UserAgent,
- EnableDlnaHeaders = enableDlnaHeaders
- };
-
- var auth = AuthorizationContext.GetAuthorizationInfo(Request);
- if (!auth.UserId.Equals(Guid.Empty))
- {
- state.User = UserManager.GetUserById(auth.UserId);
- }
-
- //if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
- // (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
- // (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
- //{
- // state.SegmentLength = 6;
- //}
-
- if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
- {
- state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
- state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
- }
-
- if (!string.IsNullOrWhiteSpace(request.AudioCodec))
- {
- state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
- state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToAudioCodec(i))
- ?? state.SupportedAudioCodecs.FirstOrDefault();
- }
-
- if (!string.IsNullOrWhiteSpace(request.SubtitleCodec))
- {
- state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
- state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToSubtitleCodec(i))
- ?? state.SupportedSubtitleCodecs.FirstOrDefault();
- }
-
- var item = LibraryManager.GetItemById(request.Id);
-
- state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
-
- //var primaryImage = item.GetImageInfo(ImageType.Primary, 0) ??
- // item.Parents.Select(i => i.GetImageInfo(ImageType.Primary, 0)).FirstOrDefault(i => i != null);
- //if (primaryImage != null)
- //{
- // state.AlbumCoverPath = primaryImage.Path;
- //}
-
- MediaSourceInfo mediaSource = null;
- if (string.IsNullOrWhiteSpace(request.LiveStreamId))
- {
- var currentJob = !string.IsNullOrWhiteSpace(request.PlaySessionId) ?
- ApiEntryPoint.Instance.GetTranscodingJob(request.PlaySessionId)
- : null;
-
- if (currentJob != null)
- {
- mediaSource = currentJob.MediaSource;
- }
-
- if (mediaSource == null)
- {
- var mediaSources = await MediaSourceManager.GetPlaybackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false);
-
- mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
- ? mediaSources[0]
- : mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId));
-
- if (mediaSource == null && Guid.Parse(request.MediaSourceId) == request.Id)
- {
- mediaSource = mediaSources[0];
- }
- }
- }
- else
- {
- var liveStreamInfo = await MediaSourceManager.GetLiveStreamWithDirectStreamProvider(request.LiveStreamId, cancellationToken).ConfigureAwait(false);
- mediaSource = liveStreamInfo.Item1;
- state.DirectStreamProvider = liveStreamInfo.Item2;
- }
-
- var videoRequest = request as VideoStreamRequest;
-
- EncodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
-
- var container = Path.GetExtension(state.RequestedUrl);
-
- if (string.IsNullOrEmpty(container))
- {
- container = request.Container;
- }
-
- if (string.IsNullOrEmpty(container))
- {
- container = request.Static ?
- StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, state.MediaPath, null, DlnaProfileType.Audio) :
- GetOutputFileExtension(state);
- }
-
- state.OutputContainer = (container ?? string.Empty).TrimStart('.');
-
- state.OutputAudioBitrate = EncodingHelper.GetAudioBitrateParam(state.Request, state.AudioStream);
-
- state.OutputAudioCodec = state.Request.AudioCodec;
-
- state.OutputAudioChannels = EncodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
-
- if (videoRequest != null)
- {
- state.OutputVideoCodec = state.VideoRequest.VideoCodec;
- state.OutputVideoBitrate = EncodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
-
- if (videoRequest != null)
- {
- EncodingHelper.TryStreamCopy(state);
- }
-
- if (state.OutputVideoBitrate.HasValue && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- var resolution = ResolutionNormalizer.Normalize(
- state.VideoStream?.BitRate,
- state.VideoStream?.Width,
- state.VideoStream?.Height,
- state.OutputVideoBitrate.Value,
- state.VideoStream?.Codec,
- state.OutputVideoCodec,
- videoRequest.MaxWidth,
- videoRequest.MaxHeight);
-
- videoRequest.MaxWidth = resolution.MaxWidth;
- videoRequest.MaxHeight = resolution.MaxHeight;
- }
- }
-
- ApplyDeviceProfileSettings(state);
-
- var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
- ? GetOutputFileExtension(state)
- : ('.' + state.OutputContainer);
-
- var encodingOptions = ServerConfigurationManager.GetEncodingOptions();
-
- state.OutputFilePath = GetOutputFilePath(state, encodingOptions, ext);
-
- return state;
- }
-
- private void ApplyDeviceProfileSettings(StreamState state)
- {
- var headers = Request.Headers;
-
- if (!string.IsNullOrWhiteSpace(state.Request.DeviceProfileId))
- {
- state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId);
- }
- else if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
- {
- var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
-
- state.DeviceProfile = caps == null ? DlnaManager.GetProfile(headers) : caps.DeviceProfile;
- }
-
- var profile = state.DeviceProfile;
-
- if (profile == null)
- {
- // Don't use settings from the default profile.
- // Only use a specific profile if it was requested.
- return;
- }
-
- var audioCodec = state.ActualOutputAudioCodec;
- var videoCodec = state.ActualOutputVideoCodec;
-
- var mediaProfile = state.VideoRequest == null ?
- profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth) :
- profile.GetVideoMediaProfile(state.OutputContainer,
- audioCodec,
- videoCodec,
- state.OutputWidth,
- state.OutputHeight,
- state.TargetVideoBitDepth,
- state.OutputVideoBitrate,
- state.TargetVideoProfile,
- state.TargetVideoLevel,
- state.TargetFramerate,
- state.TargetPacketLength,
- state.TargetTimestamp,
- state.IsTargetAnamorphic,
- state.IsTargetInterlaced,
- state.TargetRefFrames,
- state.TargetVideoStreamCount,
- state.TargetAudioStreamCount,
- state.TargetVideoCodecTag,
- state.IsTargetAVC);
-
- if (mediaProfile != null)
- {
- state.MimeType = mediaProfile.MimeType;
- }
-
- if (!state.Request.Static)
- {
- var transcodingProfile = state.VideoRequest == null ?
- profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) :
- profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);
-
- if (transcodingProfile != null)
- {
- state.EstimateContentLength = transcodingProfile.EstimateContentLength;
- //state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
- state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
-
- if (state.VideoRequest != null)
- {
- state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
- state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
- }
- }
- }
- }
-
- /// <summary>
- /// Adds the dlna headers.
- /// </summary>
- /// <param name="state">The state.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
- protected void AddDlnaHeaders(StreamState state, IDictionary<string, string> responseHeaders, bool isStaticallyStreamed)
- {
- if (!state.EnableDlnaHeaders)
- {
- return;
- }
-
- var profile = state.DeviceProfile;
-
- var transferMode = GetHeader("transferMode.dlna.org");
- responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode;
- responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*";
-
- if (state.RunTimeTicks.HasValue)
- {
- if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase))
- {
- var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
- responseHeaders["MediaInfo.sec"] = string.Format(
- CultureInfo.InvariantCulture,
- "SEC_Duration={0};",
- Convert.ToInt32(ms));
- }
-
- if (!isStaticallyStreamed && profile != null)
- {
- AddTimeSeekResponseHeaders(state, responseHeaders);
- }
- }
-
- if (profile == null)
- {
- profile = DlnaManager.GetDefaultProfile();
- }
-
- var audioCodec = state.ActualOutputAudioCodec;
-
- if (state.VideoRequest == null)
- {
- responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildAudioHeader(
- state.OutputContainer,
- audioCodec,
- state.OutputAudioBitrate,
- state.OutputAudioSampleRate,
- state.OutputAudioChannels,
- state.OutputAudioBitDepth,
- isStaticallyStreamed,
- state.RunTimeTicks,
- state.TranscodeSeekInfo);
- }
- else
- {
- var videoCodec = state.ActualOutputVideoCodec;
-
- responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildVideoHeader(
- state.OutputContainer,
- videoCodec,
- audioCodec,
- state.OutputWidth,
- state.OutputHeight,
- state.TargetVideoBitDepth,
- state.OutputVideoBitrate,
- state.TargetTimestamp,
- isStaticallyStreamed,
- state.RunTimeTicks,
- state.TargetVideoProfile,
- state.TargetVideoLevel,
- state.TargetFramerate,
- state.TargetPacketLength,
- state.TranscodeSeekInfo,
- state.IsTargetAnamorphic,
- state.IsTargetInterlaced,
- state.TargetRefFrames,
- state.TargetVideoStreamCount,
- state.TargetAudioStreamCount,
- state.TargetVideoCodecTag,
- state.IsTargetAVC).FirstOrDefault() ?? string.Empty;
- }
- }
-
- private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders)
- {
- var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
- var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
-
- responseHeaders["TimeSeekRange.dlna.org"] = string.Format(
- CultureInfo.InvariantCulture,
- "npt={0}-{1}/{1}",
- startSeconds,
- runtimeSeconds);
- responseHeaders["X-AvailableSeekRange"] = string.Format(
- CultureInfo.InvariantCulture,
- "1 npt={0}-{1}",
- startSeconds,
- runtimeSeconds);
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
deleted file mode 100644
index 627421aac..000000000
--- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs
+++ /dev/null
@@ -1,342 +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.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + 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;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
deleted file mode 100644
index b8ea9c6da..000000000
--- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
+++ /dev/null
@@ -1,1230 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-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.Configuration;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
- /// <summary>
- /// Options is needed for chromecast. Threw Head in there since it's related
- /// </summary>
- [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
- [Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
- public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest
- {
- public bool EnableAdaptiveBitrateStreaming { get; set; }
-
- public GetMasterHlsVideoPlaylist()
- {
- EnableAdaptiveBitrateStreaming = true;
- }
- }
-
- [Route("/Audio/{Id}/master.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
- [Route("/Audio/{Id}/master.m3u8", "HEAD", Summary = "Gets an audio stream using HTTP live streaming.")]
- public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest
- {
- public bool EnableAdaptiveBitrateStreaming { get; set; }
-
- public GetMasterHlsAudioPlaylist()
- {
- EnableAdaptiveBitrateStreaming = true;
- }
- }
-
- public interface IMasterHlsRequest
- {
- bool EnableAdaptiveBitrateStreaming { get; set; }
- }
-
- [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
- public class GetVariantHlsVideoPlaylist : VideoStreamRequest
- {
- }
-
- [Route("/Audio/{Id}/main.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
- public class GetVariantHlsAudioPlaylist : StreamRequest
- {
- }
-
- [Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
- public class GetHlsVideoSegment : VideoStreamRequest
- {
- public string PlaylistId { get; set; }
-
- /// <summary>
- /// Gets or sets the segment id.
- /// </summary>
- /// <value>The segment id.</value>
- public string SegmentId { get; set; }
- }
-
- [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
- public class GetHlsAudioSegment : StreamRequest
- {
- public string PlaylistId { get; set; }
-
- /// <summary>
- /// Gets or sets the segment id.
- /// </summary>
- /// <value>The segment id.</value>
- public string SegmentId { get; set; }
- }
-
- [Authenticated]
- public class DynamicHlsService : BaseHlsService
- {
- public DynamicHlsService(
- ILogger<DynamicHlsService> 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,
- INetworkManager networkManager,
- EncodingHelper encodingHelper)
- : base(
- logger,
- serverConfigurationManager,
- httpResultFactory,
- userManager,
- libraryManager,
- isoManager,
- mediaEncoder,
- fileSystem,
- dlnaManager,
- deviceManager,
- mediaSourceManager,
- jsonSerializer,
- authorizationContext,
- encodingHelper)
- {
- NetworkManager = networkManager;
- }
-
- protected INetworkManager NetworkManager { get; private set; }
-
- public Task<object> Get(GetMasterHlsVideoPlaylist request)
- {
- return GetMasterPlaylistInternal(request, "GET");
- }
-
- public Task<object> Head(GetMasterHlsVideoPlaylist request)
- {
- return GetMasterPlaylistInternal(request, "HEAD");
- }
-
- public Task<object> Get(GetMasterHlsAudioPlaylist request)
- {
- return GetMasterPlaylistInternal(request, "GET");
- }
-
- public Task<object> Head(GetMasterHlsAudioPlaylist request)
- {
- return GetMasterPlaylistInternal(request, "HEAD");
- }
-
- public Task<object> Get(GetVariantHlsVideoPlaylist request)
- {
- return GetVariantPlaylistInternal(request, true, "main");
- }
-
- public Task<object> Get(GetVariantHlsAudioPlaylist request)
- {
- return GetVariantPlaylistInternal(request, false, "main");
- }
-
- public Task<object> Get(GetHlsVideoSegment request)
- {
- return GetDynamicSegment(request, request.SegmentId);
- }
-
- public Task<object> Get(GetHlsAudioSegment request)
- {
- return GetDynamicSegment(request, request.SegmentId);
- }
-
- private async Task<object> GetDynamicSegment(StreamRequest request, string segmentId)
- {
- if ((request.StartTimeTicks ?? 0) > 0)
- {
- throw new ArgumentException("StartTimeTicks is not allowed.");
- }
-
- var cancellationTokenSource = new CancellationTokenSource();
- var cancellationToken = cancellationTokenSource.Token;
-
- var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
-
- var state = await GetState(request, cancellationToken).ConfigureAwait(false);
-
- var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
-
- var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
-
- var segmentExtension = GetSegmentFileExtension(state.Request);
-
- TranscodingJob job = null;
-
- if (File.Exists(segmentPath))
- {
- job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- Logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
- }
-
- var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
- var released = false;
- var startTranscoding = false;
-
- try
- {
- if (File.Exists(segmentPath))
- {
- job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- transcodingLock.Release();
- released = true;
- Logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
- var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
-
- if (currentTranscodingIndex == null)
- {
- Logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
- startTranscoding = true;
- }
- else if (requestedIndex < currentTranscodingIndex.Value)
- {
- Logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
- startTranscoding = true;
- }
- else if (requestedIndex - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
- {
- Logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", requestedIndex - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, requestedIndex);
- startTranscoding = true;
- }
- if (startTranscoding)
- {
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- await ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
-
- if (currentTranscodingIndex.HasValue)
- {
- DeleteLastFile(playlistPath, segmentExtension, 0);
- }
-
- request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex);
-
- state.WaitForPath = segmentPath;
- job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
- }
- catch
- {
- state.Dispose();
- throw;
- }
-
- //await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
- }
- else
- {
- job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- if (job.TranscodingThrottler != null)
- {
- await job.TranscodingThrottler.UnpauseTranscoding();
- }
- }
- }
- }
- finally
- {
- if (!released)
- {
- transcodingLock.Release();
- }
- }
-
- //Logger.LogInformation("waiting for {0}", segmentPath);
- //while (!File.Exists(segmentPath))
- //{
- // await Task.Delay(50, cancellationToken).ConfigureAwait(false);
- //}
-
- Logger.LogDebug("returning {0} [general case]", segmentPath);
- job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
- }
-
- private const int BufferSize = 81920;
-
- private long GetStartPositionTicks(StreamState state, int requestedIndex)
- {
- double startSeconds = 0;
- var lengths = GetSegmentLengths(state);
-
- if (requestedIndex >= lengths.Length)
- {
- var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
- throw new ArgumentException(msg);
- }
-
- for (var i = 0; i < requestedIndex; i++)
- {
- startSeconds += lengths[i];
- }
-
- var position = TimeSpan.FromSeconds(startSeconds).Ticks;
- return position;
- }
-
- private long GetEndPositionTicks(StreamState state, int requestedIndex)
- {
- double startSeconds = 0;
- var lengths = GetSegmentLengths(state);
-
- if (requestedIndex >= lengths.Length)
- {
- var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
- throw new ArgumentException(msg);
- }
-
- for (var i = 0; i <= requestedIndex; i++)
- {
- startSeconds += lengths[i];
- }
-
- var position = TimeSpan.FromSeconds(startSeconds).Ticks;
- return position;
- }
-
- private double[] GetSegmentLengths(StreamState state)
- {
- var result = new List<double>();
-
- var ticks = state.RunTimeTicks ?? 0;
-
- var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
-
- while (ticks > 0)
- {
- var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
-
- result.Add(TimeSpan.FromTicks(length).TotalSeconds);
-
- ticks -= length;
- }
-
- return result.ToArray();
- }
-
- public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
- {
- var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
-
- if (job == null || job.HasExited)
- {
- return null;
- }
-
- var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem);
-
- if (file == null)
- {
- return null;
- }
-
- var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
-
- var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
-
- return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
- }
-
- private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
- {
- var file = GetLastTranscodingFile(playlistPath, segmentExtension, FileSystem);
-
- if (file != null)
- {
- DeleteFile(file.FullName, retryCount);
- }
- }
-
- private void DeleteFile(string path, int retryCount)
- {
- if (retryCount >= 5)
- {
- return;
- }
-
- Logger.LogDebug("Deleting partial HLS file {path}", path);
-
- try
- {
- FileSystem.DeleteFile(path);
- }
- catch (IOException ex)
- {
- Logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
-
- var task = Task.Delay(100);
- Task.WaitAll(task);
- DeleteFile(path, retryCount + 1);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
- }
- }
-
- private static FileSystemMetadata GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
- {
- var folder = Path.GetDirectoryName(playlist);
-
- var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
-
- try
- {
- return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
- .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
- .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
- .FirstOrDefault();
- }
- catch (IOException)
- {
- return null;
- }
- }
-
- protected override int GetStartNumber(StreamState state)
- {
- return GetStartNumber(state.VideoRequest);
- }
-
- private int GetStartNumber(VideoStreamRequest request)
- {
- var segmentId = "0";
-
- if (request is GetHlsVideoSegment segmentRequest)
- {
- segmentId = segmentRequest.SegmentId;
- }
-
- return int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
- }
-
- private string GetSegmentPath(StreamState state, string playlist, int index)
- {
- var folder = Path.GetDirectoryName(playlist);
-
- var filename = Path.GetFileNameWithoutExtension(playlist);
-
- return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request));
- }
-
- private async Task<object> GetSegmentResult(StreamState state,
- string playlistPath,
- string segmentPath,
- string segmentExtension,
- int segmentIndex,
- TranscodingJob transcodingJob,
- CancellationToken cancellationToken)
- {
- var segmentExists = File.Exists(segmentPath);
- if (segmentExists)
- {
- if (transcodingJob != null && transcodingJob.HasExited)
- {
- // Transcoding job is over, so assume all existing files are ready
- Logger.LogDebug("serving up {0} as transcode is over", segmentPath);
- return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
- }
-
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
-
- // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
- if (segmentIndex < currentTranscodingIndex)
- {
- Logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
- return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
- }
- }
-
- var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
- if (transcodingJob != null)
- {
- while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
- {
- // To be considered ready, the segment file has to exist AND
- // either the transcoding job should be done or next segment should also exist
- if (segmentExists)
- {
- if (transcodingJob.HasExited || File.Exists(nextSegmentPath))
- {
- Logger.LogDebug("serving up {0} as it deemed ready", segmentPath);
- return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
- }
- }
- else
- {
- segmentExists = File.Exists(segmentPath);
- if (segmentExists)
- {
- continue; // avoid unnecessary waiting if segment just became available
- }
- }
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
-
- if (!File.Exists(segmentPath))
- {
- Logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
- }
- else
- {
- Logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
- }
- cancellationToken.ThrowIfCancellationRequested();
- }
- else
- {
- Logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
- }
-
- return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
- }
-
- private Task<object> GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJob transcodingJob)
- {
- var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
-
- return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- {
- Path = segmentPath,
- FileShare = FileShare.ReadWrite,
- OnComplete = () =>
- {
- Logger.LogDebug("finished serving {0}", segmentPath);
- if (transcodingJob != null)
- {
- transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
- ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
- }
- }
- });
- }
-
- private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method)
- {
- var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
-
- if (string.IsNullOrEmpty(request.MediaSourceId))
- {
- throw new ArgumentException("MediaSourceId is required");
- }
-
- var playlistText = string.Empty;
-
- if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
- {
- var audioBitrate = state.OutputAudioBitrate ?? 0;
- var videoBitrate = state.OutputVideoBitrate ?? 0;
-
- playlistText = GetMasterPlaylistFileText(state, videoBitrate + audioBitrate);
- }
-
- return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
- }
-
- private string GetMasterPlaylistFileText(StreamState state, int totalBitrate)
- {
- var builder = new StringBuilder();
-
- builder.AppendLine("#EXTM3U");
-
- var isLiveStream = state.IsSegmentedLiveStream;
-
- var queryStringIndex = Request.RawUrl.IndexOf('?');
- var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
-
- // from universal audio service
- if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
- {
- queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
- }
- // from universal audio service
- if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
- {
- queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
- }
-
- // Main stream
- var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
-
- playlistUrl += queryString;
-
- var request = state.Request;
-
- var subtitleStreams = state.MediaSource
- .MediaStreams
- .Where(i => i.IsTextSubtitleStream)
- .ToList();
-
- var subtitleGroup = subtitleStreams.Count > 0 &&
- request is GetMasterHlsVideoPlaylist &&
- (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest.EnableSubtitlesInManifest) ?
- "subs" :
- null;
-
- // If we're burning in subtitles then don't add additional subs to the manifest
- if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
- {
- subtitleGroup = null;
- }
-
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
- {
- AddSubtitles(state, subtitleStreams, builder);
- }
-
- AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
-
- if (EnableAdaptiveBitrateStreaming(state, isLiveStream))
- {
- var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
-
- // By default, vary by just 200k
- var variation = GetBitrateVariation(totalBitrate);
-
- var newBitrate = totalBitrate - variation;
- var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
-
- variation *= 2;
- newBitrate = totalBitrate - variation;
- variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
- }
-
- return builder.ToString();
- }
-
- private string ReplaceBitrate(string url, int oldValue, int newValue)
- {
- return url.Replace(
- "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
- "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
- StringComparison.OrdinalIgnoreCase);
- }
-
- private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
- {
- var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
-
- foreach (var stream in subtitles)
- {
- const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
-
- var name = stream.DisplayTitle;
-
- var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
- var isForced = stream.IsForced;
-
- var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
- state.Request.MediaSourceId,
- stream.Index.ToString(CultureInfo.InvariantCulture),
- 30.ToString(CultureInfo.InvariantCulture),
- AuthorizationContext.GetAuthorizationInfo(Request).Token);
-
- var line = string.Format(format,
- name,
- isDefault ? "YES" : "NO",
- isForced ? "YES" : "NO",
- url,
- stream.Language ?? "Unknown");
-
- builder.AppendLine(line);
- }
- }
-
- private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream)
- {
- // Within the local network this will likely do more harm than good.
- if (Request.IsLocal || NetworkManager.IsInLocalNetwork(Request.RemoteIp))
- {
- return false;
- }
-
- if (state.Request is IMasterHlsRequest request && !request.EnableAdaptiveBitrateStreaming)
- {
- return false;
- }
-
- if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
- {
- // Opening live streams is so slow it's not even worth it
- return false;
- }
-
- if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (!state.IsOutputVideo)
- {
- return false;
- }
-
- // Having problems in android
- return false;
- //return state.VideoRequest.VideoBitRate.HasValue;
- }
-
- /// <summary>
- /// Get the H.26X level of the output video stream.
- /// </summary>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>H.26X level of the output video stream.</returns>
- private int? GetOutputVideoCodecLevel(StreamState state)
- {
- string levelString;
- if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)
- && state.VideoStream.Level.HasValue)
- {
- levelString = state.VideoStream?.Level.ToString();
- }
- else
- {
- levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
- }
-
- if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
- {
- return parsedLevel;
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets a formatted string of the output audio codec, for use in the CODECS field.
- /// </summary>
- /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
- /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>Formatted audio codec string.</returns>
- private string GetPlaylistAudioCodecs(StreamState state)
- {
-
- if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- string profile = state.GetRequestedProfiles("aac").FirstOrDefault();
-
- return HlsCodecStringFactory.GetAACString(profile);
- }
- else if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringFactory.GetMP3String();
- }
- else if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringFactory.GetAC3String();
- }
- else if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringFactory.GetEAC3String();
- }
-
- return string.Empty;
- }
-
- /// <summary>
- /// Gets a formatted string of the output video codec, for use in the CODECS field.
- /// </summary>
- /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
- /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>Formatted video codec string.</returns>
- private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
- {
- if (level == 0)
- {
- // This is 0 when there's no requested H.26X level in the device profile
- // and the source is not encoded in H.26X
- Logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
- return string.Empty;
- }
-
- if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
-
- return HlsCodecStringFactory.GetH264String(profile, level);
- }
- else if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
-
- return HlsCodecStringFactory.GetH265String(profile, level);
- }
-
- return string.Empty;
- }
-
- /// <summary>
- /// Appends a CODECS field containing formatted strings of
- /// the active streams output video and audio codecs.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
- /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
- {
- // Video
- string videoCodecs = string.Empty;
- int? videoCodecLevel = GetOutputVideoCodecLevel(state);
- if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
- {
- videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
- }
-
- // Audio
- string audioCodecs = string.Empty;
- if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
- {
- audioCodecs = GetPlaylistAudioCodecs(state);
- }
-
- StringBuilder codecs = new StringBuilder();
-
- codecs.Append(videoCodecs)
- .Append(',')
- .Append(audioCodecs);
-
- if (codecs.Length > 1)
- {
- builder.Append(",CODECS=\"")
- .Append(codecs)
- .Append('"');
- }
- }
-
- /// <summary>
- /// Appends a FRAME-RATE field containing the framerate of the output stream.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
- {
- double? framerate = null;
- if (state.TargetFramerate.HasValue)
- {
- framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
- }
- else if (state.VideoStream?.RealFrameRate != null)
- {
- framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
- }
-
- if (framerate.HasValue)
- {
- builder.Append(",FRAME-RATE=\"")
- .Append(framerate.Value)
- .Append('"');
- }
- }
-
- /// <summary>
- /// Appends a RESOLUTION field containing the resolution of the output stream.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
- {
- if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
- {
- builder.Append(",RESOLUTION=\"")
- .Append(state.OutputWidth.GetValueOrDefault())
- .Append('x')
- .Append(state.OutputHeight.GetValueOrDefault())
- .Append('"');
- }
- }
-
- private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
- {
- builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture))
- .Append(",AVERAGE-BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture));
-
- AppendPlaylistCodecsField(builder, state);
-
- AppendPlaylistResolutionField(builder, state);
-
- AppendPlaylistFramerateField(builder, state);
-
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
- {
- builder.Append(",SUBTITLES=\"")
- .Append(subtitleGroup)
- .Append('"');
- }
-
- builder.Append(Environment.NewLine);
- builder.AppendLine(url);
- }
-
- private int GetBitrateVariation(int bitrate)
- {
- // By default, vary by just 50k
- var variation = 50000;
-
- if (bitrate >= 10000000)
- {
- variation = 2000000;
- }
- else if (bitrate >= 5000000)
- {
- variation = 1500000;
- }
- else if (bitrate >= 3000000)
- {
- variation = 1000000;
- }
- else if (bitrate >= 2000000)
- {
- variation = 500000;
- }
- else if (bitrate >= 1000000)
- {
- variation = 300000;
- }
- else if (bitrate >= 600000)
- {
- variation = 200000;
- }
- else if (bitrate >= 400000)
- {
- variation = 100000;
- }
-
- return variation;
- }
-
- private async Task<object> GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name)
- {
- var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
-
- var segmentLengths = GetSegmentLengths(state);
-
- var builder = new StringBuilder();
-
- builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
- builder.AppendLine("#EXT-X-VERSION:3");
- builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
- builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
-
- var queryStringIndex = Request.RawUrl.IndexOf('?');
- var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
-
- //if ((Request.UserAgent ?? string.Empty).IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1)
- //{
- // queryString = string.Empty;
- //}
-
- var index = 0;
-
- foreach (var length in segmentLengths)
- {
- builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
-
- builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
-
- name,
- index.ToString(CultureInfo.InvariantCulture),
- GetSegmentFileExtension(request),
- queryString));
-
- index++;
- }
-
- builder.AppendLine("#EXT-X-ENDLIST");
-
- var playlistText = builder.ToString();
-
- return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
- }
-
- protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
- {
- var audioCodec = EncodingHelper.GetAudioEncoder(state);
-
- if (!state.IsOutputVideo)
- {
- if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- return "-acodec copy";
- }
-
- var audioTranscodeParams = new List<string>();
-
- audioTranscodeParams.Add("-acodec " + audioCodec);
-
- if (state.OutputAudioBitrate.HasValue)
- {
- audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- if (state.OutputAudioChannels.HasValue)
- {
- audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- if (state.OutputAudioSampleRate.HasValue)
- {
- audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- audioTranscodeParams.Add("-vn");
- return string.Join(" ", audioTranscodeParams.ToArray());
- }
-
- if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.EnableBreakOnNonKeyFrames(videoCodec))
- {
- return "-codec:a:0 copy -copypriorss:a:0 0";
- }
-
- return "-codec:a:0 copy";
- }
-
- var args = "-codec:a:0 " + audioCodec;
-
- var channels = state.OutputAudioChannels;
-
- if (channels.HasValue)
- {
- args += " -ac " + channels.Value;
- }
-
- var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
- {
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- if (state.OutputAudioSampleRate.HasValue)
- {
- args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
-
- return args;
- }
-
- protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
- {
- if (!state.IsOutputVideo)
- {
- return string.Empty;
- }
-
- var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var args = "-codec:v:0 " + codec;
-
- // if (state.EnableMpegtsM2TsMode)
- // {
- // args += " -mpegts_m2ts_mode 1";
- // }
-
- // See if we can save come cpu cycles by avoiding encoding
- if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
- {
- string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
- if (!string.IsNullOrEmpty(bitStreamArgs))
- {
- args += " " + bitStreamArgs;
- }
- }
-
- //args += " -flags -global_header";
- }
- else
- {
- var gopArg = string.Empty;
- var keyFrameArg = string.Format(
- CultureInfo.InvariantCulture,
- " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
- GetStartNumber(state) * state.SegmentLength,
- state.SegmentLength);
-
- var framerate = state.VideoStream?.RealFrameRate;
-
- if (framerate.HasValue)
- {
- // This is to make sure keyframe interval is limited to our segment,
- // as forcing keyframes is not enough.
- // Example: we encoded half of desired length, then codec detected
- // scene cut and inserted a keyframe; next forced keyframe would
- // be created outside of segment, which breaks seeking
- // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
- gopArg = string.Format(
- CultureInfo.InvariantCulture,
- " -g {0} -keyint_min {0} -sc_threshold 0",
- Math.Ceiling(state.SegmentLength * framerate.Value)
- );
- }
-
- args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset());
-
- // Unable to force key frames using these hw encoders, set key frames by GOP
- if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
- {
- args += " " + gopArg;
- }
- else
- {
- args += " " + keyFrameArg + gopArg;
- }
-
- //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
-
- var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
- // This is for graphical subs
- if (hasGraphicalSubs)
- {
- args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
- }
- // Add resolution params, if specified
- else
- {
- args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
- }
-
- // -start_at_zero is necessary to use with -ss when seeking,
- // otherwise the target position cannot be determined.
- if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
- {
- args += " -start_at_zero";
- }
-
- //args += " -flags -global_header";
- }
-
- if (!string.IsNullOrEmpty(state.OutputVideoSync))
- {
- args += " -vsync " + state.OutputVideoSync;
- }
-
- args += EncodingHelper.GetOutputFFlags(state);
-
- return args;
- }
-
- protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
- {
- var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
-
- if (state.BaseRequest.BreakOnNonKeyFrames)
- {
- // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
- // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
- // to produce a missing part of video stream before first keyframe is encountered, which may lead to
- // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
- Logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
- state.BaseRequest.BreakOnNonKeyFrames = false;
- }
-
- var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
-
- // If isEncoding is true we're actually starting ffmpeg
- var startNumber = GetStartNumber(state);
- var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
-
- var mapArgs = state.IsOutputVideo ? EncodingHelper.GetMapArgs(state) : string.Empty;
-
- var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
-
- var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
- if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
- {
- segmentFormat = "mpegts";
- }
-
- return string.Format(
- "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
- inputModifier,
- EncodingHelper.GetInputArgument(state, encodingOptions),
- threads,
- mapArgs,
- GetVideoArguments(state, encodingOptions),
- GetAudioArguments(state, encodingOptions),
- state.SegmentLength.ToString(CultureInfo.InvariantCulture),
- segmentFormat,
- startNumberParam,
- outputTsArg,
- outputPath
- ).Trim();
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs b/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs
deleted file mode 100644
index 3bbb77a65..000000000
--- a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs
+++ /dev/null
@@ -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";
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
deleted file mode 100644
index 87ccde2e0..000000000
--- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
- /// <summary>
- /// Class GetHlsAudioSegment
- /// </summary>
- // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
- //[Authenticated]
- [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")]
- [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")]
- public class GetHlsAudioSegmentLegacy
- {
- // TODO: Deprecate with new iOS app
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the segment id.
- /// </summary>
- /// <value>The segment id.</value>
- public string SegmentId { get; set; }
- }
-
- /// <summary>
- /// Class GetHlsVideoSegment
- /// </summary>
- [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")]
- [Authenticated]
- public class GetHlsPlaylistLegacy
- {
- // TODO: Deprecate with new iOS app
-
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- public string Id { get; set; }
-
- public string PlaylistId { get; set; }
- }
-
- [Route("/Videos/ActiveEncodings", "DELETE")]
- [Authenticated]
- public class StopEncodingProcess
- {
- [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string DeviceId { get; set; }
-
- [ApiMember(Name = "PlaySessionId", Description = "The play session id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
- public string PlaySessionId { get; set; }
- }
-
- /// <summary>
- /// Class GetHlsVideoSegment
- /// </summary>
- // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
- //[Authenticated]
- [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
- public class GetHlsVideoSegmentLegacy : VideoStreamRequest
- {
- public string PlaylistId { get; set; }
-
- /// <summary>
- /// Gets or sets the segment id.
- /// </summary>
- /// <value>The segment id.</value>
- public string SegmentId { get; set; }
- }
-
- public class HlsSegmentService : BaseApiService
- {
- private readonly IFileSystem _fileSystem;
-
- public HlsSegmentService(
- ILogger<HlsSegmentService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IFileSystem fileSystem)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _fileSystem = fileSystem;
- }
-
- public Task<object> Get(GetHlsPlaylistLegacy request)
- {
- var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
- file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file);
-
- return GetFileResult(file, file);
- }
-
- public Task Delete(StopEncodingProcess request)
- {
- return ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, path => true);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Get(GetHlsVideoSegmentLegacy request)
- {
- var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
- var transcodeFolderPath = ServerConfigurationManager.GetTranscodePath();
-
- file = Path.Combine(transcodeFolderPath, file);
-
- var normalizedPlaylistId = request.PlaylistId;
-
- var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
- .FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
-
- return GetFileResult(file, playlistPath);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Get(GetHlsAudioSegmentLegacy request)
- {
- // TODO: Deprecate with new iOS app
- var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
- file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file);
-
- return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite);
- }
-
- private Task<object> GetFileResult(string path, string playlistPath)
- {
- var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
-
- return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
- {
- Path = path,
- FileShare = FileShare.ReadWrite,
- OnComplete = () =>
- {
- if (transcodingJob != null)
- {
- ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
- }
- }
- });
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
deleted file mode 100644
index d1c53c1c1..000000000
--- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
+++ /dev/null
@@ -1,173 +0,0 @@
-using System;
-using System.Globalization;
-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.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback.Hls
-{
- [Route("/Videos/{Id}/live.m3u8", "GET")]
- public class GetLiveHlsStream : VideoStreamRequest
- {
- }
-
- /// <summary>
- /// Class VideoHlsService
- /// </summary>
- [Authenticated]
- public class VideoHlsService : BaseHlsService
- {
- public VideoHlsService(
- ILogger<VideoHlsService> 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)
- {
- }
-
- public Task<object> Get(GetLiveHlsStream request)
- {
- return ProcessRequestAsync(request, true);
- }
-
- /// <summary>
- /// Gets the audio arguments.
- /// </summary>
- protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
- {
- var codec = EncodingHelper.GetAudioEncoder(state);
-
- if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- return "-codec:a:0 copy";
- }
-
- var args = "-codec:a:0 " + codec;
-
- var channels = state.OutputAudioChannels;
-
- if (channels.HasValue)
- {
- args += " -ac " + channels.Value;
- }
-
- var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
- {
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- if (state.OutputAudioSampleRate.HasValue)
- {
- args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
-
- return args;
- }
-
- /// <summary>
- /// Gets the video arguments.
- /// </summary>
- protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
- {
- if (!state.IsOutputVideo)
- {
- return string.Empty;
- }
-
- var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var args = "-codec:v:0 " + codec;
-
- // if (state.EnableMpegtsM2TsMode)
- // {
- // args += " -mpegts_m2ts_mode 1";
- // }
-
- // See if we can save come cpu cycles by avoiding encoding
- if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
- {
- // if h264_mp4toannexb is ever added, do not use it for live tv
- if (state.VideoStream != null &&
- !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
- {
- string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
- if (!string.IsNullOrEmpty(bitStreamArgs))
- {
- args += " " + bitStreamArgs;
- }
- }
- }
- else
- {
- var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
- state.SegmentLength.ToString(CultureInfo.InvariantCulture));
-
- var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
-
- args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()) + keyFrameArg;
-
- // Add resolution params, if specified
- if (!hasGraphicalSubs)
- {
- args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
- }
-
- // This is for internal graphical subs
- if (hasGraphicalSubs)
- {
- args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
- }
- }
-
- args += " -flags -global_header";
-
- if (!string.IsNullOrEmpty(state.OutputVideoSync))
- {
- args += " -vsync " + state.OutputVideoSync;
- }
-
- args += EncodingHelper.GetOutputFFlags(state);
-
- return args;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs
deleted file mode 100644
index e2d771ec6..000000000
--- a/MediaBrowser.Api/Playback/MediaInfoService.cs
+++ /dev/null
@@ -1,672 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable SA1402
-#pragma warning disable SA1649
-
-using System;
-using System.Buffers;
-using System.Globalization;
-using System.Linq;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.Session;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
- [Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
- public class GetPlaybackInfo : IReturn<PlaybackInfoResponse>
- {
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public Guid UserId { get; set; }
- }
-
- [Route("/Items/{Id}/PlaybackInfo", "POST", Summary = "Gets live playback media info for an item")]
- public class GetPostedPlaybackInfo : PlaybackInfoRequest, IReturn<PlaybackInfoResponse>
- {
- }
-
- [Route("/LiveStreams/Open", "POST", Summary = "Opens a media source")]
- public class OpenMediaSource : LiveStreamRequest, IReturn<LiveStreamResponse>
- {
- }
-
- [Route("/LiveStreams/Close", "POST", Summary = "Closes a media source")]
- public class CloseMediaSource : IReturnVoid
- {
- [ApiMember(Name = "LiveStreamId", Description = "LiveStreamId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
- public string LiveStreamId { get; set; }
- }
-
- [Route("/Playback/BitrateTest", "GET")]
- public class GetBitrateTestBytes
- {
- [ApiMember(Name = "Size", Description = "Size", IsRequired = true, DataType = "int", ParameterType = "query", Verb = "GET")]
- public int Size { get; set; }
-
- public GetBitrateTestBytes()
- {
- // 100k
- Size = 102400;
- }
- }
-
- [Authenticated]
- public class MediaInfoService : BaseApiService
- {
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IDeviceManager _deviceManager;
- private readonly ILibraryManager _libraryManager;
- private readonly INetworkManager _networkManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IUserManager _userManager;
- private readonly IAuthorizationContext _authContext;
-
- public MediaInfoService(
- ILogger<MediaInfoService> logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- IMediaSourceManager mediaSourceManager,
- IDeviceManager deviceManager,
- ILibraryManager libraryManager,
- INetworkManager networkManager,
- IMediaEncoder mediaEncoder,
- IUserManager userManager,
- IAuthorizationContext authContext)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _mediaSourceManager = mediaSourceManager;
- _deviceManager = deviceManager;
- _libraryManager = libraryManager;
- _networkManager = networkManager;
- _mediaEncoder = mediaEncoder;
- _userManager = userManager;
- _authContext = authContext;
- }
-
- public object Get(GetBitrateTestBytes request)
- {
- const int MaxSize = 10_000_000;
-
- var size = request.Size;
-
- if (size <= 0)
- {
- throw new ArgumentException($"The requested size ({size}) is equal to or smaller than 0.", nameof(request));
- }
-
- if (size > MaxSize)
- {
- throw new ArgumentException($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).", nameof(request));
- }
-
- byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
- try
- {
- new Random().NextBytes(buffer);
- return ResultFactory.GetResult(null, buffer, "application/octet-stream");
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(buffer);
- }
- }
-
- public async Task<object> Get(GetPlaybackInfo request)
- {
- var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
- return ToOptimizedResult(result);
- }
-
- public async Task<object> Post(OpenMediaSource request)
- {
- var result = await OpenMediaSource(request).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- private async Task<LiveStreamResponse> OpenMediaSource(OpenMediaSource request)
- {
- var authInfo = _authContext.GetAuthorizationInfo(Request);
-
- var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
-
- var profile = request.DeviceProfile;
- if (profile == null)
- {
- var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
- if (caps != null)
- {
- profile = caps.DeviceProfile;
- }
- }
-
- if (profile != null)
- {
- var item = _libraryManager.GetItemById(request.ItemId);
-
- SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
- request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
- request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, true, true, true);
- }
- else
- {
- if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
- {
- result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
- }
- }
-
- if (result.MediaSource != null)
- {
- NormalizeMediaSourceContainer(result.MediaSource, profile, DlnaProfileType.Video);
- }
-
- return result;
- }
-
- public void Post(CloseMediaSource request)
- {
- _mediaSourceManager.CloseLiveStream(request.LiveStreamId).GetAwaiter().GetResult();
- }
-
- public async Task<PlaybackInfoResponse> GetPlaybackInfo(GetPostedPlaybackInfo request)
- {
- var authInfo = _authContext.GetAuthorizationInfo(Request);
-
- var profile = request.DeviceProfile;
-
- Logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
-
- if (profile == null)
- {
- var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
- if (caps != null)
- {
- profile = caps.DeviceProfile;
- }
- }
-
- var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
-
- if (profile != null)
- {
- var mediaSourceId = request.MediaSourceId;
-
- SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, true, request.EnableDirectStream, request.EnableTranscoding, request.AllowVideoStreamCopy, request.AllowAudioStreamCopy);
- }
-
- if (request.AutoOpenLiveStream)
- {
- var mediaSource = string.IsNullOrWhiteSpace(request.MediaSourceId) ? info.MediaSources.FirstOrDefault() : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId, StringComparison.Ordinal));
-
- if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
- {
- var openStreamResult = await OpenMediaSource(new OpenMediaSource
- {
- AudioStreamIndex = request.AudioStreamIndex,
- DeviceProfile = request.DeviceProfile,
- EnableDirectPlay = request.EnableDirectPlay,
- EnableDirectStream = request.EnableDirectStream,
- ItemId = request.Id,
- MaxAudioChannels = request.MaxAudioChannels,
- MaxStreamingBitrate = request.MaxStreamingBitrate,
- PlaySessionId = info.PlaySessionId,
- StartTimeTicks = request.StartTimeTicks,
- SubtitleStreamIndex = request.SubtitleStreamIndex,
- UserId = request.UserId,
- OpenToken = mediaSource.OpenToken
- }).ConfigureAwait(false);
-
- info.MediaSources = new[] { openStreamResult.MediaSource };
- }
- }
-
- if (info.MediaSources != null)
- {
- foreach (var mediaSource in info.MediaSources)
- {
- NormalizeMediaSourceContainer(mediaSource, profile, DlnaProfileType.Video);
- }
- }
-
- return info;
- }
-
- private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
- {
- mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
- }
-
- public async Task<object> Post(GetPostedPlaybackInfo request)
- {
- var result = await GetPlaybackInfo(request).ConfigureAwait(false);
-
- return ToOptimizedResult(result);
- }
-
- private async Task<PlaybackInfoResponse> GetPlaybackInfo(Guid id, Guid userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
- {
- var user = _userManager.GetUserById(userId);
- var item = _libraryManager.GetItemById(id);
- var result = new PlaybackInfoResponse();
-
- MediaSourceInfo[] mediaSources;
- if (string.IsNullOrWhiteSpace(liveStreamId))
- {
-
- // TODO handle supportedLiveMediaTypes?
- var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
-
- if (string.IsNullOrWhiteSpace(mediaSourceId))
- {
- mediaSources = mediaSourcesList.ToArray();
- }
- else
- {
- mediaSources = mediaSourcesList
- .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
- .ToArray();
- }
- }
- else
- {
- var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
-
- mediaSources = new[] { mediaSource };
- }
-
- if (mediaSources.Length == 0)
- {
- result.MediaSources = Array.Empty<MediaSourceInfo>();
-
- if (!result.ErrorCode.HasValue)
- {
- result.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
- }
- }
- else
- {
- // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
- // Should we move this directly into MediaSourceManager?
- result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
-
- result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- }
-
- return result;
- }
-
- private void SetDeviceSpecificData(
- Guid itemId,
- PlaybackInfoResponse result,
- DeviceProfile profile,
- AuthorizationInfo auth,
- long? maxBitrate,
- long startTimeTicks,
- string mediaSourceId,
- int? audioStreamIndex,
- int? subtitleStreamIndex,
- int? maxAudioChannels,
- Guid userId,
- bool enableDirectPlay,
- bool forceDirectPlayRemoteMediaSource,
- bool enableDirectStream,
- bool enableTranscoding,
- bool allowVideoStreamCopy,
- bool allowAudioStreamCopy)
- {
- var item = _libraryManager.GetItemById(itemId);
-
- foreach (var mediaSource in result.MediaSources)
- {
- SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding, allowVideoStreamCopy, allowAudioStreamCopy);
- }
-
- SortMediaSources(result, maxBitrate);
- }
-
- private void SetDeviceSpecificData(
- BaseItem item,
- MediaSourceInfo mediaSource,
- DeviceProfile profile,
- AuthorizationInfo auth,
- long? maxBitrate,
- long startTimeTicks,
- string mediaSourceId,
- int? audioStreamIndex,
- int? subtitleStreamIndex,
- int? maxAudioChannels,
- string playSessionId,
- Guid userId,
- bool enableDirectPlay,
- bool forceDirectPlayRemoteMediaSource,
- bool enableDirectStream,
- bool enableTranscoding,
- bool allowVideoStreamCopy,
- bool allowAudioStreamCopy)
- {
- var streamBuilder = new StreamBuilder(_mediaEncoder, Logger);
-
- var options = new VideoOptions
- {
- MediaSources = new[] { mediaSource },
- Context = EncodingContext.Streaming,
- DeviceId = auth.DeviceId,
- ItemId = item.Id,
- Profile = profile,
- MaxAudioChannels = maxAudioChannels
- };
-
- if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
- {
- options.MediaSourceId = mediaSourceId;
- options.AudioStreamIndex = audioStreamIndex;
- options.SubtitleStreamIndex = subtitleStreamIndex;
- }
-
- var user = _userManager.GetUserById(userId);
-
- if (!enableDirectPlay)
- {
- mediaSource.SupportsDirectPlay = false;
- }
-
- if (!enableDirectStream)
- {
- mediaSource.SupportsDirectStream = false;
- }
-
- if (!enableTranscoding)
- {
- mediaSource.SupportsTranscoding = false;
- }
-
- if (item is Audio)
- {
- Logger.LogInformation("User policy for {0}. EnableAudioPlaybackTranscoding: {1}", user.Name, user.Policy.EnableAudioPlaybackTranscoding);
- }
- else
- {
- Logger.LogInformation("User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
- user.Name,
- user.Policy.EnablePlaybackRemuxing,
- user.Policy.EnableVideoPlaybackTranscoding,
- user.Policy.EnableAudioPlaybackTranscoding);
- }
-
- // Beginning of Playback Determination: Attempt DirectPlay first
- if (mediaSource.SupportsDirectPlay)
- {
- if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
- {
- mediaSource.SupportsDirectPlay = false;
- }
- else
- {
- var supportsDirectStream = mediaSource.SupportsDirectStream;
-
- // Dummy this up to fool StreamBuilder
- mediaSource.SupportsDirectStream = true;
- options.MaxBitrate = maxBitrate;
-
- if (item is Audio)
- {
- if (!user.Policy.EnableAudioPlaybackTranscoding)
- {
- options.ForceDirectPlay = true;
- }
- }
- else if (item is Video)
- {
- if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
- {
- options.ForceDirectPlay = true;
- }
- }
-
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
-
- if (streamInfo == null || !streamInfo.IsDirectStream)
- {
- mediaSource.SupportsDirectPlay = false;
- }
-
- // Set this back to what it was
- mediaSource.SupportsDirectStream = supportsDirectStream;
-
- if (streamInfo != null)
- {
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- }
- }
- }
-
- if (mediaSource.SupportsDirectStream)
- {
- if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
- {
- mediaSource.SupportsDirectStream = false;
- }
- else
- {
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
- if (item is Audio)
- {
- if (!user.Policy.EnableAudioPlaybackTranscoding)
- {
- options.ForceDirectStream = true;
- }
- }
- else if (item is Video)
- {
- if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
- {
- options.ForceDirectStream = true;
- }
- }
-
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
-
- if (streamInfo == null || !streamInfo.IsDirectStream)
- {
- mediaSource.SupportsDirectStream = false;
- }
-
- if (streamInfo != null)
- {
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- }
- }
- }
-
- if (mediaSource.SupportsTranscoding)
- {
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
-
- if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
- {
- if (streamInfo != null)
- {
- streamInfo.PlaySessionId = playSessionId;
- streamInfo.StartPositionTicks = startTimeTicks;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
- // Do this after the above so that StartPositionTicks is set
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- }
- }
- else
- {
- if (streamInfo != null)
- {
- streamInfo.PlaySessionId = playSessionId;
-
- if (streamInfo.PlayMethod == PlayMethod.Transcode)
- {
- streamInfo.StartPositionTicks = startTimeTicks;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-
- if (!allowVideoStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- }
- if (!allowAudioStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- }
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
- }
-
- if (!allowAudioStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- }
-
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
- // Do this after the above so that StartPositionTicks is set
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- }
- }
- }
-
- foreach (var attachment in mediaSource.MediaAttachments)
- {
- attachment.DeliveryUrl = string.Format(
- CultureInfo.InvariantCulture,
- "/Videos/{0}/{1}/Attachments/{2}",
- item.Id,
- mediaSource.Id,
- attachment.Index);
- }
- }
-
- private long? GetMaxBitrate(long? clientMaxBitrate, User user)
- {
- var maxBitrate = clientMaxBitrate;
- var remoteClientMaxBitrate = user?.Policy.RemoteClientBitrateLimit ?? 0;
-
- if (remoteClientMaxBitrate <= 0)
- {
- remoteClientMaxBitrate = ServerConfigurationManager.Configuration.RemoteClientBitrateLimit;
- }
-
- if (remoteClientMaxBitrate > 0)
- {
- var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.RemoteIp);
-
- Logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.RemoteIp, isInLocalNetwork);
- if (!isInLocalNetwork)
- {
- maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
- }
- }
-
- return maxBitrate;
- }
-
- private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
- {
- var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
- mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
-
- mediaSource.TranscodeReasons = info.TranscodeReasons;
-
- foreach (var profile in profiles)
- {
- foreach (var stream in mediaSource.MediaStreams)
- {
- if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
- {
- stream.DeliveryMethod = profile.DeliveryMethod;
-
- if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
- {
- stream.DeliveryUrl = profile.Url.TrimStart('-');
- stream.IsExternalUrl = profile.IsExternalUrl;
- }
- }
- }
- }
- }
-
- private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
- {
- var originalList = result.MediaSources.ToList();
-
- result.MediaSources = result.MediaSources.OrderBy(i =>
- {
- // Nothing beats direct playing a file
- if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
- {
- return 0;
- }
-
- return 1;
-
- }).ThenBy(i =>
- {
- // Let's assume direct streaming a file is just as desirable as direct playing a remote url
- if (i.SupportsDirectPlay || i.SupportsDirectStream)
- {
- return 0;
- }
-
- return 1;
-
- }).ThenBy(i =>
- {
- return i.Protocol switch
- {
- MediaProtocol.File => 0,
- _ => 1,
- };
- }).ThenBy(i =>
- {
- if (maxBitrate.HasValue && i.Bitrate.HasValue)
- {
- return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
- }
-
- return 1;
-
- }).ThenBy(originalList.IndexOf)
- .ToArray();
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
deleted file mode 100644
index 34c7986ca..000000000
--- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs
+++ /dev/null
@@ -1,95 +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
-{
- /// <summary>
- /// Class GetAudioStream
- /// </summary>
- [Route("/Audio/{Id}/stream.{Container}", "GET", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/stream", "GET", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/stream.{Container}", "HEAD", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")]
- public class GetAudioStream : StreamRequest
- {
- }
-
- /// <summary>
- /// Class AudioService
- /// </summary>
- // TODO: In order to autheneticate this in the future, Dlna playback will require updating
- //[Authenticated]
- public class AudioService : BaseProgressiveStreamingService
- {
- public AudioService(
- ILogger<AudioService> 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(GetAudioStream request)
- {
- return ProcessRequest(request, false);
- }
-
- /// <summary>
- /// Gets the specified request.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>System.Object.</returns>
- public Task<object> Head(GetAudioStream request)
- {
- return ProcessRequest(request, true);
- }
-
- protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
- {
- return EncodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
deleted file mode 100644
index c7bf055fb..000000000
--- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
+++ /dev/null
@@ -1,436 +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;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
deleted file mode 100644
index a53b848f9..000000000
--- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
+++ /dev/null
@@ -1,180 +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;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
deleted file mode 100644
index 4de81655c..000000000
--- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs
+++ /dev/null
@@ -1,132 +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
-{
- /// <summary>
- /// Class GetVideoStream
- /// </summary>
- [Route("/Videos/{Id}/stream.mpegts", "GET")]
- [Route("/Videos/{Id}/stream.ts", "GET")]
- [Route("/Videos/{Id}/stream.webm", "GET")]
- [Route("/Videos/{Id}/stream.asf", "GET")]
- [Route("/Videos/{Id}/stream.wmv", "GET")]
- [Route("/Videos/{Id}/stream.ogv", "GET")]
- [Route("/Videos/{Id}/stream.mp4", "GET")]
- [Route("/Videos/{Id}/stream.m4v", "GET")]
- [Route("/Videos/{Id}/stream.mkv", "GET")]
- [Route("/Videos/{Id}/stream.mpeg", "GET")]
- [Route("/Videos/{Id}/stream.mpg", "GET")]
- [Route("/Videos/{Id}/stream.avi", "GET")]
- [Route("/Videos/{Id}/stream.m2ts", "GET")]
- [Route("/Videos/{Id}/stream.3gp", "GET")]
- [Route("/Videos/{Id}/stream.wmv", "GET")]
- [Route("/Videos/{Id}/stream.wtv", "GET")]
- [Route("/Videos/{Id}/stream.mov", "GET")]
- [Route("/Videos/{Id}/stream.iso", "GET")]
- [Route("/Videos/{Id}/stream.flv", "GET")]
- [Route("/Videos/{Id}/stream.rm", "GET")]
- [Route("/Videos/{Id}/stream", "GET")]
- [Route("/Videos/{Id}/stream.ts", "HEAD")]
- [Route("/Videos/{Id}/stream.webm", "HEAD")]
- [Route("/Videos/{Id}/stream.asf", "HEAD")]
- [Route("/Videos/{Id}/stream.wmv", "HEAD")]
- [Route("/Videos/{Id}/stream.ogv", "HEAD")]
- [Route("/Videos/{Id}/stream.mp4", "HEAD")]
- [Route("/Videos/{Id}/stream.m4v", "HEAD")]
- [Route("/Videos/{Id}/stream.mkv", "HEAD")]
- [Route("/Videos/{Id}/stream.mpeg", "HEAD")]
- [Route("/Videos/{Id}/stream.mpg", "HEAD")]
- [Route("/Videos/{Id}/stream.avi", "HEAD")]
- [Route("/Videos/{Id}/stream.3gp", "HEAD")]
- [Route("/Videos/{Id}/stream.wmv", "HEAD")]
- [Route("/Videos/{Id}/stream.wtv", "HEAD")]
- [Route("/Videos/{Id}/stream.m2ts", "HEAD")]
- [Route("/Videos/{Id}/stream.mov", "HEAD")]
- [Route("/Videos/{Id}/stream.iso", "HEAD")]
- [Route("/Videos/{Id}/stream.flv", "HEAD")]
- [Route("/Videos/{Id}/stream", "HEAD")]
- 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());
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs
deleted file mode 100644
index 3b8b29995..000000000
--- a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs
+++ /dev/null
@@ -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);
- }
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs
deleted file mode 100644
index 9ba8eda91..000000000
--- a/MediaBrowser.Api/Playback/StreamRequest.cs
+++ /dev/null
@@ -1,33 +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; }
- }
-}
diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs
deleted file mode 100644
index d5d2f58c0..000000000
--- a/MediaBrowser.Api/Playback/StreamState.cs
+++ /dev/null
@@ -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 (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
- {
- 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;
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/TranscodingThrottler.cs b/MediaBrowser.Api/Playback/TranscodingThrottler.cs
deleted file mode 100644
index 0e73d77ef..000000000
--- a/MediaBrowser.Api/Playback/TranscodingThrottler.cs
+++ /dev/null
@@ -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;
- }
- }
- }
-}
diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs
deleted file mode 100644
index a3b319d44..000000000
--- a/MediaBrowser.Api/Playback/UniversalAudioService.cs
+++ /dev/null
@@ -1,382 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Api.Playback.Hls;
-using MediaBrowser.Api.Playback.Progressive;
-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.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.Playback
-{
- public class BaseUniversalRequest
- {
- /// <summary>
- /// Gets or sets the id.
- /// </summary>
- /// <value>The id.</value>
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid Id { get; set; }
-
- [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string MediaSourceId { get; set; }
-
- [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string DeviceId { get; set; }
-
- public Guid UserId { get; set; }
- public string AudioCodec { get; set; }
- public string Container { get; set; }
-
- public int? MaxAudioChannels { get; set; }
- public int? TranscodingAudioChannels { get; set; }
-
- public long? MaxStreamingBitrate { get; set; }
-
- [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
- public long? StartTimeTicks { get; set; }
-
- public string TranscodingContainer { get; set; }
- public string TranscodingProtocol { get; set; }
- public int? MaxAudioSampleRate { get; set; }
- public int? MaxAudioBitDepth { get; set; }
-
- public bool EnableRedirection { get; set; }
- public bool EnableRemoteMedia { get; set; }
- public bool BreakOnNonKeyFrames { get; set; }
-
- public BaseUniversalRequest()
- {
- EnableRedirection = true;
- }
- }
-
- [Route("/Audio/{Id}/universal.{Container}", "GET", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/universal", "GET", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/universal.{Container}", "HEAD", Summary = "Gets an audio stream")]
- [Route("/Audio/{Id}/universal", "HEAD", Summary = "Gets an audio stream")]
- public class GetUniversalAudioStream : BaseUniversalRequest
- {
- }
-
- [Authenticated]
- public class UniversalAudioService : BaseApiService
- {
- private readonly EncodingHelper _encodingHelper;
- private readonly ILoggerFactory _loggerFactory;
-
- public UniversalAudioService(
- ILogger<UniversalAudioService> logger,
- ILoggerFactory loggerFactory,
- 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,
- INetworkManager networkManager,
- EncodingHelper encodingHelper)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- HttpClient = httpClient;
- UserManager = userManager;
- LibraryManager = libraryManager;
- IsoManager = isoManager;
- MediaEncoder = mediaEncoder;
- FileSystem = fileSystem;
- DlnaManager = dlnaManager;
- DeviceManager = deviceManager;
- MediaSourceManager = mediaSourceManager;
- JsonSerializer = jsonSerializer;
- AuthorizationContext = authorizationContext;
- NetworkManager = networkManager;
- _encodingHelper = encodingHelper;
- _loggerFactory = loggerFactory;
- }
-
- protected IHttpClient HttpClient { get; private set; }
- protected IUserManager UserManager { get; private set; }
- protected ILibraryManager LibraryManager { get; private set; }
- protected IIsoManager IsoManager { get; private set; }
- protected IMediaEncoder MediaEncoder { get; private set; }
- protected IFileSystem FileSystem { get; private set; }
- protected IDlnaManager DlnaManager { get; private set; }
- protected IDeviceManager DeviceManager { get; private set; }
- protected IMediaSourceManager MediaSourceManager { get; private set; }
- protected IJsonSerializer JsonSerializer { get; private set; }
- protected IAuthorizationContext AuthorizationContext { get; private set; }
- protected INetworkManager NetworkManager { get; private set; }
-
- public Task<object> Get(GetUniversalAudioStream request)
- {
- return GetUniversalStream(request, false);
- }
-
- public Task<object> Head(GetUniversalAudioStream request)
- {
- return GetUniversalStream(request, true);
- }
-
- private DeviceProfile GetDeviceProfile(GetUniversalAudioStream request)
- {
- var deviceProfile = new DeviceProfile();
-
- var directPlayProfiles = new List<DirectPlayProfile>();
-
- var containers = (request.Container ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
-
- foreach (var container in containers)
- {
- var parts = container.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
-
- var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
-
- directPlayProfiles.Add(new DirectPlayProfile
- {
- Type = DlnaProfileType.Audio,
- Container = parts[0],
- AudioCodec = audioCodecs
- });
- }
-
- deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
-
- deviceProfile.TranscodingProfiles = new[]
- {
- new TranscodingProfile
- {
- Type = DlnaProfileType.Audio,
- Context = EncodingContext.Streaming,
- Container = request.TranscodingContainer,
- AudioCodec = request.AudioCodec,
- Protocol = request.TranscodingProtocol,
- BreakOnNonKeyFrames = request.BreakOnNonKeyFrames,
- MaxAudioChannels = request.TranscodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
- }
- };
-
- var codecProfiles = new List<CodecProfile>();
- var conditions = new List<ProfileCondition>();
-
- if (request.MaxAudioSampleRate.HasValue)
- {
- // codec profile
- conditions.Add(new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioSampleRate,
- Value = request.MaxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
- });
- }
-
- if (request.MaxAudioBitDepth.HasValue)
- {
- // codec profile
- conditions.Add(new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioBitDepth,
- Value = request.MaxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
- });
- }
-
- if (request.MaxAudioChannels.HasValue)
- {
- // codec profile
- conditions.Add(new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioChannels,
- Value = request.MaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
- });
- }
-
- if (conditions.Count > 0)
- {
- // codec profile
- codecProfiles.Add(new CodecProfile
- {
- Type = CodecType.Audio,
- Container = request.Container,
- Conditions = conditions.ToArray()
- });
- }
-
- deviceProfile.CodecProfiles = codecProfiles.ToArray();
-
- return deviceProfile;
- }
-
- private async Task<object> GetUniversalStream(GetUniversalAudioStream request, bool isHeadRequest)
- {
- var deviceProfile = GetDeviceProfile(request);
-
- AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
-
- var mediaInfoService = new MediaInfoService(
- _loggerFactory.CreateLogger<MediaInfoService>(),
- ServerConfigurationManager,
- ResultFactory,
- MediaSourceManager,
- DeviceManager,
- LibraryManager,
- NetworkManager,
- MediaEncoder,
- UserManager,
- AuthorizationContext)
- {
- Request = Request
- };
-
- var playbackInfoResult = await mediaInfoService.GetPlaybackInfo(new GetPostedPlaybackInfo
- {
- Id = request.Id,
- MaxAudioChannels = request.MaxAudioChannels,
- MaxStreamingBitrate = request.MaxStreamingBitrate,
- StartTimeTicks = request.StartTimeTicks,
- UserId = request.UserId,
- DeviceProfile = deviceProfile,
- MediaSourceId = request.MediaSourceId
-
- }).ConfigureAwait(false);
-
- var mediaSource = playbackInfoResult.MediaSources[0];
-
- if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
- {
- if (request.EnableRedirection)
- {
- if (mediaSource.IsRemote && request.EnableRemoteMedia)
- {
- return ResultFactory.GetRedirectResult(mediaSource.Path);
- }
- }
- }
-
- var isStatic = mediaSource.SupportsDirectStream;
-
- if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
- {
- var service = new DynamicHlsService(
- _loggerFactory.CreateLogger<DynamicHlsService>(),
- ServerConfigurationManager,
- ResultFactory,
- UserManager,
- LibraryManager,
- IsoManager,
- MediaEncoder,
- FileSystem,
- DlnaManager,
- DeviceManager,
- MediaSourceManager,
- JsonSerializer,
- AuthorizationContext,
- NetworkManager,
- _encodingHelper)
- {
- Request = Request
- };
-
- var transcodingProfile = deviceProfile.TranscodingProfiles[0];
-
- // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
- // TODO: remove this when we switch back to the segment muxer
- var supportedHLSContainers = new[] { "mpegts", "fmp4" };
-
- var newRequest = new GetMasterHlsAudioPlaylist
- {
- AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
- AudioCodec = transcodingProfile.AudioCodec,
- Container = ".m3u8",
- DeviceId = request.DeviceId,
- Id = request.Id,
- MaxAudioChannels = request.MaxAudioChannels,
- MediaSourceId = mediaSource.Id,
- PlaySessionId = playbackInfoResult.PlaySessionId,
- StartTimeTicks = request.StartTimeTicks,
- Static = isStatic,
- // fallback to mpegts if device reports some weird value unsupported by hls
- SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
- AudioSampleRate = request.MaxAudioSampleRate,
- MaxAudioBitDepth = request.MaxAudioBitDepth,
- BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
- };
-
- if (isHeadRequest)
- {
- return await service.Head(newRequest).ConfigureAwait(false);
- }
- return await service.Get(newRequest).ConfigureAwait(false);
- }
- else
- {
- var service = new AudioService(
- _loggerFactory.CreateLogger<AudioService>(),
- ServerConfigurationManager,
- ResultFactory,
- HttpClient,
- UserManager,
- LibraryManager,
- IsoManager,
- MediaEncoder,
- FileSystem,
- DlnaManager,
- DeviceManager,
- MediaSourceManager,
- JsonSerializer,
- AuthorizationContext,
- _encodingHelper)
- {
- Request = Request
- };
-
- var newRequest = new GetAudioStream
- {
- AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
- AudioCodec = request.AudioCodec,
- Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
- DeviceId = request.DeviceId,
- Id = request.Id,
- MaxAudioChannels = request.MaxAudioChannels,
- MediaSourceId = mediaSource.Id,
- PlaySessionId = playbackInfoResult.PlaySessionId,
- StartTimeTicks = request.StartTimeTicks,
- Static = isStatic,
- AudioSampleRate = request.MaxAudioSampleRate,
- MaxAudioBitDepth = request.MaxAudioBitDepth,
- TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
- };
-
- if (isHeadRequest)
- {
- return await service.Head(newRequest).ConfigureAwait(false);
- }
-
- return await service.Get(newRequest).ConfigureAwait(false);
- }
- }
- }
-}