Reputation: 1600
I've written a custom HTTP server that works fine for everything until the browser makes a request with a byte range. When attempting to load video (seemingly for files above a certain size because this doesn't happen every time), the browser makes a request for the video file with this header:
method: GET
/media/mp4/32.mp4
Connection - keep-alive
Accept - */*
Accept-Encoding - identity;q=1/*;q=0
Accept-Language - en-us/en;q=0.8
Host - localhost:20809
Referer - ...
Range - bytes=0-
User-Agent - Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.170 Safari/537.36
so the server sends the requested file... Then, immediately afterwards, it makes this request:
method: GET
/media/mp4/32.mp4
Connection - keep-alive
Accept - */*
Accept-Encoding - identity;q=1/*;q=0
Accept-Language - en-us/en;q=0.8
Host - localhost:20809
Referer - ...
Range - bytes=40-3689973
User-Agent - Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.170 Safari/537.36
So I write the requested bytes to the output stream but it always errors out on the second request with an error. It's almost like the server is still trying to send the file when the browser sends another request.
The I/O operation has been aborted because of either a thread exit or an application request
Here is the code that handles the range request:
public void StartServer()
{
_server = new HttpListener();
_server.Prefixes.Add("http://localhost:" + _port.ToString() + "/");
LogWebserver("Listening...");
_server.Start();
th = new Thread(new ThreadStart(startlistener));
th.Start();
}
private void startlistener()
{
while (true)
{
////blocks until a client has connected to the server
ProcessRequest();
}
}
private void ProcessRequest()
{
var result = _server.BeginGetContext(ListenerCallback, _server);
result.AsyncWaitHandle.WaitOne();
}
private void ListenerCallback(IAsyncResult result)
{
var context = _server.EndGetContext(result);
HandleContext(context);
}
private void HandleContext(HttpListenerContext context)
{
HttpListenerRequest req = context.Request;
...stuff...
using (HttpListenerResponse resp = context.Response)
{
.... stuff....
byte[] buffer = File.ReadAllBytes(localFile);
if (mime.ToString().Contains("video") || mime.ToString().Contains("audio"))
{
resp.StatusCode = 206;
resp.StatusDescription = "Partial Content";
int startByte = -1;
int endByte = -1;
int byteRange = -1;
if (req.Headers.GetValues("Range") != null)
{
string rangeHeader = req.Headers.GetValues("Range")[0].Replace("bytes=", "");
string[] range = rangeHeader.Split('-');
startByte = int.Parse(range[0]);
if (range[1].Trim().Length > 0) int.TryParse(range[1], out endByte);
if (endByte == -1) endByte = buffer.Length;
}
else
{
startByte = 0;
endByte = buffer.Length;
}
byteRange = endByte - startByte;
resp.ContentLength64 = byteRange;
resp.Headers.Add("Accept-Ranges", "bytes");
resp.Headers.Add("Content-Range", string.Format("bytes {0}-{1}/{2}", startByte, byteRange - 1, byteRange));
resp.Headers.Add("X-Content-Duration", "0.0");
resp.Headers.Add("Content-Duration", "0.0");
resp.OutputStream.Write(buffer, startByte, byteRange);/* this is where it gives the IO error */
resp.OutputStream.Close();
resp.Close();
}
else
{
resp.ContentLength64 = buffer.Length;
resp.OutputStream.Write(buffer, 0, buffer.Length);
resp.OutputStream.Close();
resp.Close();
}
}
}
I've tried simply ignoring the request with the range in it, but while no error is thrown, the browser throw an error because the video wasn't downloaded.
How do I handle these range requests and avoid the IO error?
Upvotes: 2
Views: 5148
Reputation: 51
I know its late, but i think there are some misstakes at your solution. Your code will not work with Safari.
Safari always request 2 bytes to check, unfortunatly without infos if failed. Other browsers are more tolerant.
Upvotes: 0
Reputation: 1600
I solved the problem, but it involved scrapping HttpListener
and using TcpListener
instead.
I used the code here as a basis for the server: http://www.codeproject.com/Articles/137979/Simple-HTTP-Server-in-C
And then I modified the handleGETRequest
function with this:
if (mime.ToString().Contains("video") || mime.ToString().Contains("audio"))
{
using (FileStream fs = new FileStream(localFile, FileMode.Open))
{
int startByte = -1;
int endByte = -1;
if (p.httpHeaders.Contains("Range"))
{
string rangeHeader = p.httpHeaders["Range"].ToString().Replace("bytes=", "");
string[] range = rangeHeader.Split('-');
startByte = int.Parse(range[0]);
if (range[1].Trim().Length > 0) int.TryParse(range[1], out endByte);
if (endByte == -1) endByte = (int)fs.Length;
}
else
{
startByte = 0;
endByte = (int)fs.Length;
}
byte[] buffer = new byte[endByte - startByte];
fs.Position = startByte;
int read = fs.Read(buffer,0, endByte-startByte);
fs.Flush();
fs.Close();
p.outputStream.AutoFlush = true;
p.outputStream.WriteLine("HTTP/1.0 206 Partial Content");
p.outputStream.WriteLine("Content-Type: " + mime);
p.outputStream.WriteLine("Accept-Ranges: bytes");
int totalCount = startByte + buffer.Length;
p.outputStream.WriteLine(string.Format("Content-Range: bytes {0}-{1}/{2}", startByte, totalCount - 1, totalCount));
p.outputStream.WriteLine("Content-Length: " + buffer.Length.ToString());
p.outputStream.WriteLine("Connection: keep-alive");
p.outputStream.WriteLine("");
p.outputStream.AutoFlush = false;
p.outputStream.BaseStream.Write(buffer, 0, buffer.Length);
p.outputStream.BaseStream.Flush();
}
}
else
{
byte[] buffer = File.ReadAllBytes(localFile);
p.outputStream.AutoFlush = true;
p.outputStream.WriteLine("HTTP/1.0 200 OK");
p.outputStream.WriteLine("Content-Type: " + mime);
p.outputStream.WriteLine("Connection: close");
p.outputStream.WriteLine("Content-Length: " + buffer.Length.ToString());
p.outputStream.WriteLine("");
p.outputStream.AutoFlush = false;
p.outputStream.BaseStream.Write(buffer, 0, buffer.Length);
p.outputStream.BaseStream.Flush();
}
Using TcpListener
avoids whatever stupid problem was causing the I/O error on the response stream.
Upvotes: 3