Safari (old and new) was not serving video files
by Brett Andrew 23/02/2025
Our team were notified that Safari browsers were not serving Video content, this was a surprise to me so I've followed the solution here.
Our service is behind Azure Front Door (it has caching turned on for anything under the URL api/websiteCached/) and our system uses that URL to cache data.
So the Azure Front Door was working fine, its CDN capabilities are great, but our own service was not implementing "Range" (where browsers request to fast forward through a file). The issue was that if the file was not cached, Safari could not load it.
Turns out Safari requires the support of range as mandatory.
To implement range, this took several days to work out the complexities. Our final "ah ha!" moment was when we realised we are the middle man. The headers we are receiving, we need to pass onto the service and the headers we get back, we need to pass them back to.
See the below working code, this works with cached and non-cached files.
Hope this helps someone who is on their 30+ hours of working this out!
/// <summary>
/// Ultimately this is just the messenger, retrieving a file either in full or via ranges (bytes) from another service
/// What we had to do was pass the range request to the service and manage the content range and content lengths returned
/// Once we worked this out, the two services talked perfectly to each other and all browsers could stream content.
/// </summary>
/// <param name="BlobUri"></param>
/// <param name="headerfiletype"></param>
/// <param name="Response"></param>
/// <param name="filename"></param>
/// <param name="Cache"></param>
/// <param name="headers"></param>
/// <returns></returns>
public async Task<IActionResult> DownloadFileAsync(string BlobUri, string headerfiletype, HttpResponse Response, string filename, bool Cache = false, IDictionary<string, string> headers = null)
{
//download entire file, then send file to client
HttpClient xClient = httpClientFactory.CreateClient("DownloadFileAsync");
string RangeHeader = "";
// Pass through the range header if it exists (we are just the messenger)
if (headers != null && headers.TryGetValue("Range", out var rangeHeader))
{
//lets ignore this request
if (rangeHeader != "bytes=0-")
{
xClient.DefaultRequestHeaders.Add("Range", rangeHeader);
RangeHeader = rangeHeader;
}
}
//check the contentlength
var httpResponse = await xClient.GetAsync(BlobUri, HttpCompletionOption.ResponseHeadersRead);
//Response.Headers.Append("Data-Test", RangeHeader);
if (httpResponse.IsSuccessStatusCode)
{
//set headers on (need content type
Response.Headers.Append("Content-Type", httpResponse.Content.Headers.ContentType.ToString());
//Response.Headers.Append("Data-Test-StatusCode", httpResponse.StatusCode.ToString());
//check if the source accepts ranges
//Tell the client whether the source accepts ranges or not (dont ask us we are just the messenger)
if (httpResponse.Content.Headers.TryGetValues("Accept-Ranges", out var acceptRanges))
{
Response.Headers.Append("Accept-Ranges", string.Join(",", acceptRanges));
//Response.Headers.Append("Data-TestAccept-Ranges1", "bytes");
}
else
{
// Handle the case where the "Accept-Ranges" header is missing or null
if (BlobUri.Contains("blob.core.windows.net"))
{
Response.Headers.Append("Accept-Ranges", "bytes");
//Response.Headers.Append("Data-TestAccept-Ranges2", "bytes");
}
}
//full file, allow caching - if you are Caching this at Azure front door, you have to set the age here
if (httpResponse.Content.Headers != null && httpResponse.Content.Headers.ContentLength != null)
{
Response.Headers.Append("Content-Length", httpResponse.Content.Headers.ContentLength.ToString());
}
if (httpResponse.Content.Headers != null && httpResponse.Content.Headers.ContentRange != null)
{
Response.Headers.Append("Content-Range", httpResponse.Content.Headers.ContentRange.ToString());
}
if (httpResponse.Content.Headers != null && httpResponse.Content.Headers.ContentEncoding != null)
{
Response.Headers.Append("Content-Encoding", httpResponse.Content.Headers.ContentEncoding.ToString());
}
Response.StatusCode = (int)httpResponse.StatusCode;
//if cache is requested, lets only cache status ok
//in azure front door we cache all urls under /api/websiteCached/ but they only cache if the Cache-Control is set.
if ((int)httpResponse.StatusCode == (int)HttpStatusCode.OK)
{
if (Cache)
{
Response.Headers.Add("Cache-Control", "public, max-age=86400");
}
else
{
Response.Headers.Add("Cache-Control", "no-cache");
}
}
else
{
Response.Headers.Add("Cache-Control", "no-cache");
}
var stream = await httpResponse.Content.ReadAsStreamAsync();
return new FileStreamResult(stream, httpResponse.Content.Headers.ContentType.ToString())
{
FileDownloadName = filename,
EnableRangeProcessing = true
};
}
else
{
Response.Headers.Append("Data-Test-Error", httpResponse.Content.ToString());
Response.Headers.Append("Data-Test", RangeHeader);
Response.Headers.Append("Data-StatusCode", httpResponse.StatusCode.ToString());
Response.StatusCode = (int)httpResponse.StatusCode;
return null;
}
}
Powered by mition