mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-03 19:17:24 -05:00 
			
		
		
		
	bring back support for byte ranged requests
This commit is contained in:
		
							parent
							
								
									c7d2b3a407
								
							
						
					
					
						commit
						e5592bd220
					
				@ -18,8 +18,8 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [Route("/Audio/{Id}/segments/{SegmentId}.mp3", "GET")]
 | 
					    [Route("/Audio/{Id}/segments/{SegmentId}/stream.mp3", "GET")]
 | 
				
			||||||
    [Route("/Audio/{Id}/segments/{SegmentId}.aac", "GET")]
 | 
					    [Route("/Audio/{Id}/segments/{SegmentId}/stream.aac", "GET")]
 | 
				
			||||||
    [ServiceStack.ServiceHost.Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
 | 
					    [ServiceStack.ServiceHost.Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
 | 
				
			||||||
    public class GetHlsAudioSegment
 | 
					    public class GetHlsAudioSegment
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
				
			|||||||
@ -127,7 +127,7 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
				
			|||||||
            // The segement paths within the playlist are phsyical, so strip that out to make it relative
 | 
					            // The segement paths within the playlist are phsyical, so strip that out to make it relative
 | 
				
			||||||
            fileText = fileText.Replace(Path.GetDirectoryName(playlist) + Path.DirectorySeparatorChar, string.Empty);
 | 
					            fileText = fileText.Replace(Path.GetDirectoryName(playlist) + Path.DirectorySeparatorChar, string.Empty);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            fileText = fileText.Replace(SegmentFilePrefix, "segments/");
 | 
					            fileText = fileText.Replace(SegmentFilePrefix, "segments/").Replace(".ts", "/stream.ts").Replace(".aac", "/stream.aac").Replace(".mp3", "/stream.mp3");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Even though we specify target duration of 9, ffmpeg seems unable to keep all segments under that amount
 | 
					            // Even though we specify target duration of 9, ffmpeg seems unable to keep all segments under that amount
 | 
				
			||||||
            fileText = fileText.Replace("#EXT-X-TARGETDURATION:9", "#EXT-X-TARGETDURATION:10");
 | 
					            fileText = fileText.Replace("#EXT-X-TARGETDURATION:9", "#EXT-X-TARGETDURATION:10");
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [Route("/Videos/{Id}/segments/{SegmentId}.ts", "GET")]
 | 
					    [Route("/Videos/{Id}/segments/{SegmentId}/stream.ts", "GET")]
 | 
				
			||||||
    [ServiceStack.ServiceHost.Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
 | 
					    [ServiceStack.ServiceHost.Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
 | 
				
			||||||
    public class GetHlsVideoSegment
 | 
					    public class GetHlsVideoSegment
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@ -35,6 +35,7 @@ namespace MediaBrowser.Api.Playback.Hls
 | 
				
			|||||||
            var file = SegmentFilePrefix + request.SegmentId + Path.GetExtension(Request.PathInfo);
 | 
					            var file = SegmentFilePrefix + request.SegmentId + Path.GetExtension(Request.PathInfo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            file = Path.Combine(ApplicationPaths.EncodedMediaCachePath, file);
 | 
					            file = Path.Combine(ApplicationPaths.EncodedMediaCachePath, file);
 | 
				
			||||||
 | 
					            Logger.Info(file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return ToStaticFileResult(file);
 | 
					            return ToStaticFileResult(file);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,4 @@
 | 
				
			|||||||
using System;
 | 
					using System.Windows;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					 | 
				
			||||||
using System.Configuration;
 | 
					 | 
				
			||||||
using System.Data;
 | 
					 | 
				
			||||||
using System.Linq;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
using System.Windows;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace MediaBrowser.Installer
 | 
					namespace MediaBrowser.Installer
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
				
			|||||||
@ -87,15 +87,11 @@
 | 
				
			|||||||
    </Reference>
 | 
					    </Reference>
 | 
				
			||||||
    <Reference Include="System" />
 | 
					    <Reference Include="System" />
 | 
				
			||||||
    <Reference Include="System.Configuration" />
 | 
					    <Reference Include="System.Configuration" />
 | 
				
			||||||
    <Reference Include="System.Data" />
 | 
					 | 
				
			||||||
    <Reference Include="System.Drawing" />
 | 
					    <Reference Include="System.Drawing" />
 | 
				
			||||||
    <Reference Include="System.Web" />
 | 
					 | 
				
			||||||
    <Reference Include="System.Windows.Forms" />
 | 
					    <Reference Include="System.Windows.Forms" />
 | 
				
			||||||
    <Reference Include="System.Xml" />
 | 
					    <Reference Include="System.Xml" />
 | 
				
			||||||
    <Reference Include="Microsoft.CSharp" />
 | 
					    <Reference Include="Microsoft.CSharp" />
 | 
				
			||||||
    <Reference Include="System.Core" />
 | 
					    <Reference Include="System.Core" />
 | 
				
			||||||
    <Reference Include="System.Xml.Linq" />
 | 
					 | 
				
			||||||
    <Reference Include="System.Data.DataSetExtensions" />
 | 
					 | 
				
			||||||
    <Reference Include="System.Xaml">
 | 
					    <Reference Include="System.Xaml">
 | 
				
			||||||
      <RequiredTargetFramework>4.0</RequiredTargetFramework>
 | 
					      <RequiredTargetFramework>4.0</RequiredTargetFramework>
 | 
				
			||||||
    </Reference>
 | 
					    </Reference>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
using MediaBrowser.Common.Extensions;
 | 
					using System.Net;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Extensions;
 | 
				
			||||||
using MediaBrowser.Common.IO;
 | 
					using MediaBrowser.Common.IO;
 | 
				
			||||||
using MediaBrowser.Common.Net;
 | 
					using MediaBrowser.Common.Net;
 | 
				
			||||||
using MediaBrowser.Model.Logging;
 | 
					using MediaBrowser.Model.Logging;
 | 
				
			||||||
@ -257,6 +258,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                var stream = await factoryFn().ConfigureAwait(false);
 | 
					                var stream = await factoryFn().ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var httpListenerResponse = (HttpListenerResponse) Response.OriginalResponse;
 | 
				
			||||||
 | 
					                httpListenerResponse.SendChunked = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (IsRangeRequest)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    return new RangeRequestWriter(Request.Headers, httpListenerResponse, stream);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                httpListenerResponse.ContentLength64 = stream.Length;
 | 
				
			||||||
                return new StreamWriter(stream);
 | 
					                return new StreamWriter(stream);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,172 @@
 | 
				
			|||||||
 | 
					using ServiceStack.Service;
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
					using System.Collections.Specialized;
 | 
				
			||||||
 | 
					using System.IO;
 | 
				
			||||||
 | 
					using System.Linq;
 | 
				
			||||||
 | 
					using System.Net;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace MediaBrowser.Server.Implementations.HttpServer
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public class RangeRequestWriter : IStreamWriter
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets or sets the source stream.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The source stream.</value>
 | 
				
			||||||
 | 
					        public Stream SourceStream { get; set; }
 | 
				
			||||||
 | 
					        public HttpListenerResponse Response { get; set; }
 | 
				
			||||||
 | 
					        public NameValueCollection RequestHeaders { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Initializes a new instance of the <see cref="StreamWriter" /> class.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="requestHeaders">The request headers.</param>
 | 
				
			||||||
 | 
					        /// <param name="response">The response.</param>
 | 
				
			||||||
 | 
					        /// <param name="source">The source.</param>
 | 
				
			||||||
 | 
					        public RangeRequestWriter(NameValueCollection requestHeaders, HttpListenerResponse response, Stream source)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            RequestHeaders = requestHeaders;
 | 
				
			||||||
 | 
					            Response = response;
 | 
				
			||||||
 | 
					            SourceStream = source;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// The _requested ranges
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        private List<KeyValuePair<long, long?>> _requestedRanges;
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Gets the requested ranges.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <value>The requested ranges.</value>
 | 
				
			||||||
 | 
					        protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            get
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if (_requestedRanges == null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    _requestedRanges = new List<KeyValuePair<long, long?>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Example: bytes=0-,32-63
 | 
				
			||||||
 | 
					                    var ranges = RequestHeaders["Range"].Split('=')[1].Split(',');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    foreach (var range in ranges)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        var vals = range.Split('-');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        long start = 0;
 | 
				
			||||||
 | 
					                        long? end = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (!string.IsNullOrEmpty(vals[0]))
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            start = long.Parse(vals[0]);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (!string.IsNullOrEmpty(vals[1]))
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            end = long.Parse(vals[1]);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return _requestedRanges;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Writes to.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="responseStream">The response stream.</param>
 | 
				
			||||||
 | 
					        public void WriteTo(Stream responseStream)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Response.Headers["Accept-Ranges"] = "bytes";
 | 
				
			||||||
 | 
					            Response.StatusCode = 206;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            var task = WriteToAsync(responseStream);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Task.WaitAll(task);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Writes to async.
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="responseStream">The response stream.</param>
 | 
				
			||||||
 | 
					        /// <returns>Task.</returns>
 | 
				
			||||||
 | 
					        private Task WriteToAsync(Stream responseStream)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var requestedRange = RequestedRanges.First();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var totalLength = SourceStream.Length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If the requested range is "0-", we can optimize by just doing a stream copy
 | 
				
			||||||
 | 
					            if (!requestedRange.Value.HasValue)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return ServeCompleteRangeRequest(requestedRange, responseStream, totalLength);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // This will have to buffer a portion of the content into memory
 | 
				
			||||||
 | 
					            return ServePartialRangeRequest(requestedRange.Key, requestedRange.Value.Value, responseStream, totalLength);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Handles a range request of "bytes=0-"
 | 
				
			||||||
 | 
					        /// This will serve the complete content and add the content-range header
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="requestedRange">The requested range.</param>
 | 
				
			||||||
 | 
					        /// <param name="responseStream">The response stream.</param>
 | 
				
			||||||
 | 
					        /// <param name="totalContentLength">Total length of the content.</param>
 | 
				
			||||||
 | 
					        /// <returns>Task.</returns>
 | 
				
			||||||
 | 
					        private Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream, long totalContentLength)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var rangeStart = requestedRange.Key;
 | 
				
			||||||
 | 
					            var rangeEnd = totalContentLength - 1;
 | 
				
			||||||
 | 
					            var rangeLength = 1 + rangeEnd - rangeStart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Content-Length is the length of what we're serving, not the original content
 | 
				
			||||||
 | 
					            Response.ContentLength64 = rangeLength;
 | 
				
			||||||
 | 
					            Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (rangeStart > 0)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                SourceStream.Position = rangeStart;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return SourceStream.CopyToAsync(responseStream);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Serves a partial range request
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="rangeStart">The range start.</param>
 | 
				
			||||||
 | 
					        /// <param name="rangeEnd">The range end.</param>
 | 
				
			||||||
 | 
					        /// <param name="responseStream">The response stream.</param>
 | 
				
			||||||
 | 
					        /// <param name="totalContentLength">Total length of the content.</param>
 | 
				
			||||||
 | 
					        /// <returns>Task.</returns>
 | 
				
			||||||
 | 
					        private async Task ServePartialRangeRequest(long rangeStart, long rangeEnd, Stream responseStream, long totalContentLength)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var rangeLength = 1 + rangeEnd - rangeStart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Content-Length is the length of what we're serving, not the original content
 | 
				
			||||||
 | 
					            Response.ContentLength64 = rangeLength;
 | 
				
			||||||
 | 
					            Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            SourceStream.Position = rangeStart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Fast track to just copy the stream to the end
 | 
				
			||||||
 | 
					            if (rangeEnd == totalContentLength - 1)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                await SourceStream.CopyToAsync(responseStream).ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // Read the bytes we need
 | 
				
			||||||
 | 
					                var buffer = new byte[Convert.ToInt32(rangeLength)];
 | 
				
			||||||
 | 
					                await SourceStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                await responseStream.WriteAsync(buffer, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -109,6 +109,7 @@
 | 
				
			|||||||
    <Compile Include="HttpServer\HttpResultFactory.cs" />
 | 
					    <Compile Include="HttpServer\HttpResultFactory.cs" />
 | 
				
			||||||
    <Compile Include="HttpServer\HttpServer.cs" />
 | 
					    <Compile Include="HttpServer\HttpServer.cs" />
 | 
				
			||||||
    <Compile Include="HttpServer\NativeWebSocket.cs" />
 | 
					    <Compile Include="HttpServer\NativeWebSocket.cs" />
 | 
				
			||||||
 | 
					    <Compile Include="HttpServer\RangeRequestWriter.cs" />
 | 
				
			||||||
    <Compile Include="HttpServer\ServerFactory.cs" />
 | 
					    <Compile Include="HttpServer\ServerFactory.cs" />
 | 
				
			||||||
    <Compile Include="HttpServer\StreamWriter.cs" />
 | 
					    <Compile Include="HttpServer\StreamWriter.cs" />
 | 
				
			||||||
    <Compile Include="HttpServer\SwaggerService.cs" />
 | 
					    <Compile Include="HttpServer\SwaggerService.cs" />
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user