mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-15 10:43:30 +01:00
Port MediaEncoding and Api.Playback from 10e57ce8d21b4516733894075001819f3cd6db6b
This commit is contained in:
67
MediaBrowser.Api/Playback/Progressive/AudioService.cs
Normal file
67
MediaBrowser.Api/Playback/Progressive/AudioService.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
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(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, IEnvironmentInfo environmentInfo) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext, imageProcessor, environmentInfo)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Progressive
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BaseProgressiveStreamingService
|
||||
/// </summary>
|
||||
public abstract class BaseProgressiveStreamingService : BaseStreamingService
|
||||
{
|
||||
protected readonly IImageProcessor ImageProcessor;
|
||||
protected readonly IEnvironmentInfo EnvironmentInfo;
|
||||
|
||||
public BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, IEnvironmentInfo environmentInfo) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext)
|
||||
{
|
||||
ImageProcessor = imageProcessor;
|
||||
EnvironmentInfo = environmentInfo;
|
||||
}
|
||||
|
||||
/// <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))
|
||||
{
|
||||
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
|
||||
{
|
||||
get { return 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["Content-Type"] = MediaBrowser.Model.Net.MimeTypes.GetMimeType("file.ts");
|
||||
|
||||
return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, EnvironmentInfo, 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 = FileSystem.FileExists(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);
|
||||
|
||||
outputHeaders["Content-Type"] = contentType;
|
||||
|
||||
return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, EnvironmentInfo, 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 = FileShareMode.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)
|
||||
{
|
||||
string useragent = null;
|
||||
state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
|
||||
|
||||
var trySupportSeek = false;
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = state.MediaPath,
|
||||
UserAgent = useragent,
|
||||
BufferContent = false,
|
||||
CancellationToken = cancellationTokenSource.Token
|
||||
};
|
||||
|
||||
if (trySupportSeek)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Request.QueryString["Range"]))
|
||||
{
|
||||
options.RequestHeaders["Range"] = Request.QueryString["Range"];
|
||||
}
|
||||
}
|
||||
var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
|
||||
|
||||
if (trySupportSeek)
|
||||
{
|
||||
foreach (var name in new[] { "Content-Range", "Accept-Ranges" })
|
||||
{
|
||||
var val = response.Headers[name];
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
responseHeaders[name] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
responseHeaders["Accept-Ranges"] = "none";
|
||||
}
|
||||
|
||||
// Seeing cases of -1 here
|
||||
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
|
||||
{
|
||||
responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture);
|
||||
}
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
using (response)
|
||||
{
|
||||
return ResultFactory.GetResult(null, new byte[] { }, response.ContentType, responseHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
var result = new StaticRemoteStreamWriter(response);
|
||||
|
||||
result.Headers["Content-Type"] = 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["Accept-Ranges"] = "none";
|
||||
|
||||
var contentType = state.GetMimeType(outputPath);
|
||||
|
||||
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
|
||||
// What we really want to do is hunt that down and remove that
|
||||
var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
|
||||
|
||||
if (contentLength.HasValue)
|
||||
{
|
||||
responseHeaders["Content-Length"] = contentLength.Value.ToString(UsCulture);
|
||||
}
|
||||
|
||||
// Headers only
|
||||
if (isHeadRequest)
|
||||
{
|
||||
var streamResult = ResultFactory.GetResult(null, new byte[] { }, contentType, responseHeaders);
|
||||
|
||||
var hasHeaders = streamResult as IHasHeaders;
|
||||
if (hasHeaders != null)
|
||||
{
|
||||
if (contentLength.HasValue)
|
||||
{
|
||||
hasHeaders.Headers["Content-Length"] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasHeaders.Headers.ContainsKey("Content-Length"))
|
||||
{
|
||||
hasHeaders.Headers.Remove("Content-Length");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return streamResult;
|
||||
}
|
||||
|
||||
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
|
||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
TranscodingJob job;
|
||||
|
||||
if (!FileSystem.FileExists(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);
|
||||
|
||||
outputHeaders["Content-Type"] = 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, EnvironmentInfo, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
193
MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
Normal file
193
MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
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;
|
||||
|
||||
const int StreamCopyToBufferSize = 81920;
|
||||
|
||||
private long _bytesWritten = 0;
|
||||
public long StartPosition { get; set; }
|
||||
public bool AllowEndOfFile = true;
|
||||
|
||||
private readonly IDirectStreamProvider _directStreamProvider;
|
||||
private readonly IEnvironmentInfo _environment;
|
||||
|
||||
public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, IEnvironmentInfo environment, CancellationToken cancellationToken)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_path = path;
|
||||
_outputHeaders = outputHeaders;
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_cancellationToken = cancellationToken;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, IEnvironmentInfo environment, CancellationToken cancellationToken)
|
||||
{
|
||||
_directStreamProvider = directStreamProvider;
|
||||
_outputHeaders = outputHeaders;
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_cancellationToken = cancellationToken;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Headers
|
||||
{
|
||||
get
|
||||
{
|
||||
return _outputHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
private Stream GetInputStream(bool allowAsyncFileRead)
|
||||
{
|
||||
var fileOpenOptions = FileOpenOptions.SequentialScan;
|
||||
|
||||
if (allowAsyncFileRead)
|
||||
{
|
||||
fileOpenOptions |= FileOpenOptions.Asynchronous;
|
||||
}
|
||||
|
||||
return _fileSystem.GetFileStream(_path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions);
|
||||
}
|
||||
|
||||
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 = _environment.OperatingSystem != Model.System.OperatingSystem.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.Debug("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[StreamCopyToBufferSize];
|
||||
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[StreamCopyToBufferSize];
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
MediaBrowser.Api/Playback/Progressive/VideoService.cs
Normal file
100
MediaBrowser.Api/Playback/Progressive/VideoService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Services;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
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", "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(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, IImageProcessor imageProcessor, IEnvironmentInfo environmentInfo) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext, imageProcessor, environmentInfo)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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, GetDefaultH264Preset());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user