diff options
| -rw-r--r-- | Jellyfin.Api/Controllers/AudioController.cs | 51 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/LiveTvController.cs | 19 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/MediaInfoController.cs | 7 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/VideosController.cs | 321 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 17 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 143 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 28 | ||||
| -rw-r--r-- | Jellyfin.Api/Jellyfin.Api.csproj | 1 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs | 15 | ||||
| -rw-r--r-- | Jellyfin.Server/Startup.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Progressive/VideoService.cs | 43 |
11 files changed, 519 insertions, 128 deletions
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 7405c26fb..d9afbd910 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -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, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index bc5446510..9144d6f28 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -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)); } diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index da400f510..c2c02c02c 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -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, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index e2a44427b..d1ef817eb 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -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); + } } } diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 636f47f5f..a463783e0 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -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 diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index e8e6966f4..432df9708 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -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,35 +15,54 @@ 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> /// <param name="outputStream">Output stream.</param> @@ -48,37 +70,106 @@ namespace Jellyfin.Api.Helpers /// <returns>A <see cref="Task"/>.</returns> public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken) { - if (_directStreamProvider != null) + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token; + + try + { + if (_directStreamProvider != null) + { + await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); + return; + } + + var fileOptions = FileOptions.SequentialScan; + var allowAsyncFileRead = false; + + // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fileOptions |= FileOptions.Asynchronous; + allowAsyncFileRead = true; + } + + await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions); + + var eofCount = 0; + const int EmptyReadLimit = 20; + if (StartPosition > 0) + { + 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 + { + eofCount = 0; + } + } + } + finally { - await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); - return; + if (_job != null) + { + _transcodingJobHelper.OnTranscodeEndRequest(_job); + } } + } - var fileOptions = FileOptions.SequentialScan; + 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; - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (Environment.OSVersion.Platform != PlatformID.Win32NT) + if (readAsync) + { + bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); + } + else { - fileOptions |= FileOptions.Asynchronous; + bytesRead = source.Read(array, 0, array.Length); } - await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, fileOptions); - const int emptyReadLimit = 100; - var eofCount = 0; - while (eofCount < emptyReadLimit) + while (bytesRead != 0) { - var bytesRead = await _streamHelper.CopyToAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false); + var bytesToWrite = bytesRead; - if (bytesRead == 0) + if (bytesToWrite > 0) { - eofCount++; - await Task.Delay(100, cancellationToken).ConfigureAwait(false); + 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 { - eofCount = 0; + bytesRead = source.Read(array, 0, array.Length); } } + + return totalBytesRead; } } } diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 76f7c8fde..fc38eacaf 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -681,6 +681,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 /// </summary> @@ -713,20 +727,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> /// <param name="process">The process.</param> diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 572cb1af2..a52b234d4 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -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> diff --git a/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs new file mode 100644 index 000000000..db55dc34b --- /dev/null +++ b/Jellyfin.Api/Models/VideoDtos/DeviceProfileDto.cs @@ -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; } + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index edf023fa2..108d8f881 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -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> diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index c3f6b905c..5bc85f42d 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -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 { } |
