Merge pull request #3691 from crobibero/api-video

move VideoService.cs to Jellyfin.Api
This commit is contained in:
Cody Robibero 2020-07-31 09:27:31 -06:00 committed by GitHub
commit cb31aba5dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 528 additions and 137 deletions

View File

@ -35,13 +35,12 @@ namespace Jellyfin.Api.Controllers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IStreamHelper _streamHelper;
private readonly IFileSystem _fileSystem;
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly HttpClient _httpClient;
private readonly IHttpClientFactory _httpClientFactory;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
@ -55,13 +54,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
/// <param name="httpClient">Instance of the <see cref="HttpClient"/>.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
public AudioController(
IDlnaManager dlnaManager,
IUserManager userManger,
@ -70,13 +68,12 @@ namespace Jellyfin.Api.Controllers
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IStreamHelper streamHelper,
IFileSystem fileSystem,
ISubtitleEncoder subtitleEncoder,
IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
HttpClient httpClient)
IHttpClientFactory httpClientFactory)
{
_dlnaManager = dlnaManager;
_authContext = authorizationContext;
@ -85,13 +82,12 @@ namespace Jellyfin.Api.Controllers
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_streamHelper = streamHelper;
_fileSystem = fileSystem;
_subtitleEncoder = subtitleEncoder;
_configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClient = httpClient;
_httpClientFactory = httpClientFactory;
}
/// <summary>
@ -146,6 +142,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/{stream=stream}.{container?}")]
[HttpGet("{itemId}/stream")]
@ -211,7 +208,7 @@ namespace Jellyfin.Api.Controllers
{
Id = itemId,
Container = container,
Static = @static.HasValue ? @static.Value : true,
Static = @static ?? true,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
@ -222,10 +219,10 @@ namespace Jellyfin.Api.Controllers
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
AudioCodec = audioCodec,
EnableAutoStreamCopy = enableAutoStreamCopy.HasValue ? enableAutoStreamCopy.Value : true,
AllowAudioStreamCopy = allowAudioStreamCopy.HasValue ? allowAudioStreamCopy.Value : true,
AllowVideoStreamCopy = allowVideoStreamCopy.HasValue ? allowVideoStreamCopy.Value : true,
BreakOnNonKeyFrames = breakOnNonKeyFrames.HasValue ? breakOnNonKeyFrames.Value : false,
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
@ -235,7 +232,7 @@ namespace Jellyfin.Api.Controllers
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps.HasValue ? copyTimestamps.Value : true,
CopyTimestamps = copyTimestamps ?? true,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
@ -244,13 +241,13 @@ namespace Jellyfin.Api.Controllers
SubtitleMethod = subtitleMethod,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc.HasValue ? requireAvc.Value : true,
DeInterlace = deInterlace.HasValue ? deInterlace.Value : true,
RequireNonAnamorphic = requireNonAnamorphic.HasValue ? requireNonAnamorphic.Value : true,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode.HasValue ? enableMpegtsM2TsMode.Value : true,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons,
@ -283,8 +280,11 @@ namespace Jellyfin.Api.Controllers
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
// TODO AllowEndOfFile = false
await new ProgressiveFileCopier(_streamHelper, state.DirectStreamProvider).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
{
AllowEndOfFile = false
}.WriteToAsync(Response.Body, CancellationToken.None)
.ConfigureAwait(false);
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
@ -295,7 +295,8 @@ namespace Jellyfin.Api.Controllers
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, _httpClient).ConfigureAwait(false);
using var httpClient = _httpClientFactory.CreateClient();
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
}
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
@ -318,8 +319,11 @@ namespace Jellyfin.Api.Controllers
if (state.MediaSource.IsInfiniteStream)
{
// TODO AllowEndOfFile = false
await new ProgressiveFileCopier(_streamHelper, state.MediaPath).WriteToAsync(Response.Body, CancellationToken.None).ConfigureAwait(false);
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
{
AllowEndOfFile = false
}.WriteToAsync(Response.Body, CancellationToken.None)
.ConfigureAwait(false);
return File(Response.Body, contentType);
}
@ -338,7 +342,6 @@ namespace Jellyfin.Api.Controllers
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
_streamHelper,
this,
_transcodingJobHelper,
ffmpegCommandLineArguments,

View File

@ -24,7 +24,6 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
@ -45,9 +44,9 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly ISessionContext _sessionContext;
private readonly IStreamHelper _streamHelper;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
/// <summary>
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
@ -58,9 +57,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="sessionContext">Instance of the <see cref="ISessionContext"/> interface.</param>
/// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
public LiveTvController(
ILiveTvManager liveTvManager,
IUserManager userManager,
@ -68,9 +67,9 @@ namespace Jellyfin.Api.Controllers
ILibraryManager libraryManager,
IDtoService dtoService,
ISessionContext sessionContext,
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager)
IConfigurationManager configurationManager,
TranscodingJobHelper transcodingJobHelper)
{
_liveTvManager = liveTvManager;
_userManager = userManager;
@ -78,9 +77,9 @@ namespace Jellyfin.Api.Controllers
_libraryManager = libraryManager;
_dtoService = dtoService;
_sessionContext = sessionContext;
_streamHelper = streamHelper;
_mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager;
_transcodingJobHelper = transcodingJobHelper;
}
/// <summary>
@ -1187,7 +1186,9 @@ namespace Jellyfin.Api.Controllers
}
await using var memoryStream = new MemoryStream();
await new ProgressiveFileCopier(_streamHelper, path).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
await new ProgressiveFileCopier(path, null, _transcodingJobHelper, CancellationToken.None)
.WriteToAsync(memoryStream, CancellationToken.None)
.ConfigureAwait(false);
return File(memoryStream, MimeTypes.GetMimeType(path));
}
@ -1214,7 +1215,9 @@ namespace Jellyfin.Api.Controllers
}
await using var memoryStream = new MemoryStream();
await new ProgressiveFileCopier(_streamHelper, liveStreamInfo).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
.WriteToAsync(memoryStream, CancellationToken.None)
.ConfigureAwait(false);
return File(memoryStream, MimeTypes.GetMimeType("file." + container));
}

View File

@ -7,6 +7,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.VideoDtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxAudioChannels,
[FromQuery] string? mediaSourceId,
[FromQuery] string? liveStreamId,
[FromQuery] DeviceProfile? deviceProfile,
[FromBody] DeviceProfileDto? deviceProfile,
[FromQuery] bool autoOpenLiveStream = false,
[FromQuery] bool enableDirectPlay = true,
[FromQuery] bool enableDirectStream = true,
@ -136,7 +137,7 @@ namespace Jellyfin.Api.Controllers
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
var profile = deviceProfile;
var profile = deviceProfile?.DeviceProfile;
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
@ -190,7 +191,7 @@ namespace Jellyfin.Api.Controllers
var openStreamResult = await OpenMediaSource(new LiveStreamRequest
{
AudioStreamIndex = audioStreamIndex,
DeviceProfile = deviceProfile,
DeviceProfile = deviceProfile?.DeviceProfile,
EnableDirectPlay = enableDirectPlay,
EnableDirectStream = enableDirectStream,
ItemId = itemId,

View File

@ -1,19 +1,34 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
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.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Controllers
{
@ -26,6 +41,19 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
private readonly IDlnaManager _dlnaManager;
private readonly IAuthorizationContext _authContext;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly IHttpClientFactory _httpClientFactory;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
/// <summary>
/// Initializes a new instance of the <see cref="VideosController"/> class.
@ -33,14 +61,47 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
public VideosController(
ILibraryManager libraryManager,
IUserManager userManager,
IDtoService dtoService)
IDtoService dtoService,
IDlnaManager dlnaManager,
IAuthorizationContext authContext,
IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
ISubtitleEncoder subtitleEncoder,
IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
IHttpClientFactory httpClientFactory)
{
_libraryManager = libraryManager;
_userManager = userManager;
_dtoService = dtoService;
_dlnaManager = dlnaManager;
_authContext = authContext;
_mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
_subtitleEncoder = subtitleEncoder;
_configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
_httpClientFactory = httpClientFactory;
}
/// <summary>
@ -200,5 +261,263 @@ namespace Jellyfin.Api.Controllers
primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Gets a video stream.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
/// <param name="maxRefFrames">Optional.</param>
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodingReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/{stream=stream}.{container?}")]
[HttpGet("{itemId}/stream")]
[HttpHead("{itemId}/{stream=stream}.{container?}")]
[HttpHead("{itemId}/stream")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetVideoStream(
[FromRoute] Guid itemId,
[FromRoute] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
[FromQuery] long? startTimeTicks,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
[FromQuery] bool? deInterlace,
[FromQuery] bool? requireNonAnamorphic,
[FromQuery] int? transcodingMaxAudioChannels,
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] string? transcodingReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
{
Id = itemId,
Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
MinSegments = minSegments,
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
AudioCodec = audioCodec,
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
StreamOptions = streamOptions
};
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
Request,
_authContext,
_mediaSourceManager,
_userManager,
_libraryManager,
_serverConfigurationManager,
_mediaEncoder,
_fileSystem,
_subtitleEncoder,
_configuration,
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
_transcodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
{
AllowEndOfFile = false
}.WriteToAsync(Response.Body, CancellationToken.None)
.ConfigureAwait(false);
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
}
// Static remote stream
if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
using var httpClient = _httpClientFactory.CreateClient();
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
}
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
{
return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
}
var outputPath = state.OutputFilePath;
var outputPathExists = System.IO.File.Exists(outputPath);
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var isTranscodeCached = outputPathExists && transcodingJob != null;
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
// Static stream
if (@static.HasValue && @static.Value)
{
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
if (state.MediaSource.IsInfiniteStream)
{
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
{
AllowEndOfFile = false
}.WriteToAsync(Response.Body, CancellationToken.None)
.ConfigureAwait(false);
return File(Response.Body, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
state.MediaPath,
contentType,
isHeadRequest,
this);
}
// Need to start ffmpeg (because media can't be returned directly)
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
this,
_transcodingJobHelper,
ffmpegCommandLineArguments,
Request,
_transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
}
}

View File

@ -3,9 +3,9 @@ using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
@ -71,8 +71,7 @@ namespace Jellyfin.Api.Helpers
return controller.NoContent();
}
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
return controller.File(stream, contentType);
return controller.PhysicalFile(path, contentType);
}
/// <summary>
@ -80,7 +79,6 @@ namespace Jellyfin.Api.Helpers
/// </summary>
/// <param name="state">The current <see cref="StreamState"/>.</param>
/// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
/// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
/// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
/// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
@ -91,7 +89,6 @@ namespace Jellyfin.Api.Helpers
public static async Task<ActionResult> GetTranscodedFile(
StreamState state,
bool isHeadRequest,
IStreamHelper streamHelper,
ControllerBase controller,
TranscodingJobHelper transcodingJobHelper,
string ffmpegCommandLineArguments,
@ -116,18 +113,20 @@ namespace Jellyfin.Api.Helpers
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
TranscodingJobDto? job;
if (!File.Exists(outputPath))
{
await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
}
else
{
transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
state.Dispose();
}
await using var memoryStream = new MemoryStream();
await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
var memoryStream = new MemoryStream();
await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false);
memoryStream.Position = 0;
return controller.File(memoryStream, contentType);
}
finally

View File

@ -1,7 +1,10 @@
using System;
using System.Buffers;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
@ -12,34 +15,53 @@ namespace Jellyfin.Api.Helpers
/// </summary>
public class ProgressiveFileCopier
{
private readonly TranscodingJobDto? _job;
private readonly string? _path;
private readonly CancellationToken _cancellationToken;
private readonly IDirectStreamProvider? _directStreamProvider;
private readonly IStreamHelper _streamHelper;
private readonly TranscodingJobHelper _transcodingJobHelper;
private long _bytesWritten;
/// <summary>
/// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
/// </summary>
/// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
/// <param name="path">Filepath to stream from.</param>
public ProgressiveFileCopier(IStreamHelper streamHelper, string path)
/// <param name="path">The path to copy from.</param>
/// <param name="job">The transcoding job.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
{
_path = path;
_streamHelper = streamHelper;
_directStreamProvider = null;
_job = job;
_cancellationToken = cancellationToken;
_transcodingJobHelper = transcodingJobHelper;
}
/// <summary>
/// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
/// </summary>
/// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
/// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
public ProgressiveFileCopier(IStreamHelper streamHelper, IDirectStreamProvider directStreamProvider)
/// <param name="job">The transcoding job.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
{
_directStreamProvider = directStreamProvider;
_streamHelper = streamHelper;
_path = null;
_job = job;
_cancellationToken = cancellationToken;
_transcodingJobHelper = transcodingJobHelper;
}
/// <summary>
/// Gets or sets a value indicating whether allow read end of file.
/// </summary>
public bool AllowEndOfFile { get; set; } = true;
/// <summary>
/// Gets or sets copy start position.
/// </summary>
public long StartPosition { get; set; }
/// <summary>
/// Write source stream to output.
/// </summary>
@ -47,6 +69,10 @@ namespace Jellyfin.Api.Helpers
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A <see cref="Task"/>.</returns>
public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
{
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
try
{
if (_directStreamProvider != null)
{
@ -55,23 +81,35 @@ namespace Jellyfin.Api.Helpers
}
var fileOptions = FileOptions.SequentialScan;
var allowAsyncFileRead = false;
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
fileOptions |= FileOptions.Asynchronous;
allowAsyncFileRead = true;
}
await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, fileOptions);
const int emptyReadLimit = 100;
await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
var eofCount = 0;
while (eofCount < emptyReadLimit)
const int EmptyReadLimit = 20;
if (StartPosition > 0)
{
var bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
inputStream.Position = StartPosition;
}
while (eofCount < EmptyReadLimit || !AllowEndOfFile)
{
var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
if (bytesRead == 0)
{
if (_job == null || _job.HasExited)
{
eofCount++;
}
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
else
@ -80,5 +118,58 @@ namespace Jellyfin.Api.Helpers
}
}
}
finally
{
if (_job != null)
{
_transcodingJobHelper.OnTranscodeEndRequest(_job);
}
}
}
private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
{
var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
int bytesRead;
int totalBytesRead = 0;
if (readAsync)
{
bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
}
else
{
bytesRead = source.Read(array, 0, array.Length);
}
while (bytesRead != 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);
}
}
if (readAsync)
{
bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
}
else
{
bytesRead = source.Read(array, 0, array.Length);
}
}
return totalBytesRead;
}
}
}

View File

@ -680,6 +680,20 @@ namespace Jellyfin.Api.Helpers
}
}
/// <summary>
/// Called when [transcode end].
/// </summary>
/// <param name="job">The transcode job.</param>
public void OnTranscodeEndRequest(TranscodingJobDto job)
{
job.ActiveRequestCount--;
_logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
if (job.ActiveRequestCount <= 0)
{
PingTimer(job, false);
}
}
/// <summary>
/// <summary>
/// The progressive
@ -712,20 +726,6 @@ namespace Jellyfin.Api.Helpers
}
}
/// <summary>
/// Transcoding video finished. Decrement the active request counter.
/// </summary>
/// <param name="job">The <see cref="TranscodingJobDto"/> which ended.</param>
public void OnTranscodeEndRequest(TranscodingJobDto job)
{
job.ActiveRequestCount--;
_logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
if (job.ActiveRequestCount <= 0)
{
PingTimer(job, false);
}
}
/// <summary>
/// Processes the exited.
/// </summary>

View File

@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.3.3" />
</ItemGroup>

View File

@ -0,0 +1,15 @@
using MediaBrowser.Model.Dlna;
namespace Jellyfin.Api.Models.VideoDtos
{
/// <summary>
/// Device profile dto.
/// </summary>
public class DeviceProfileDto
{
/// <summary>
/// Gets or sets device profile.
/// </summary>
public DeviceProfile? DeviceProfile { get; set; }
}
}

View File

@ -1,3 +1,4 @@
using System.Net.Http;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Middleware;
using Jellyfin.Server.Models;
@ -43,6 +44,7 @@ namespace Jellyfin.Server
services.AddCustomAuthentication();
services.AddJellyfinApiAuthorization();
services.AddHttpClient();
}
/// <summary>

View File

@ -14,49 +14,6 @@ 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
{
}