google-cloud-storageavplayerflutter-video-playergoogle-cloud-load-balancergoogle-cloud-cdn

Serving video files using Cloud Storage and Cloud CDN


Update: I've come up with something of a fix.

I added the following to the http headers I was sending

"If-None-Match": "1"

This forces a cache miss when AVPlayer sends it's initial byte range request to retrieve the first byte and thus the response has all the data it needs to start playback (Content-Length, Content-Type, Content-Range).

This is a hack, and causes cache misses on other requests as well. I will leave the question unanswered as it'd be great to get to an actual solution. One thing I think might help would be to set this on the URL requests in AVPlayer

NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData

However operation at the abstraction level of Flutter's VideoPlayer I could not find a way to do this. There are probably changes that could be made at the Cloud CDN level as well but I've yet to find one that works.


Update: I believe I know what is causing this behavior. When AVPlayer sends it's first request with Range: bytes=0-1 there are two cases:

  1. Server responds with 304 response code, and response headers containing only Age, Etag, Cache-Control and Date. In this case AVPlayer will send another request to download the entire mp4 before playing (presumably because it has no idea what kind of file/size of the file it has been asked to play)

  2. Server responds with a 206 response code and all the necessary headers populated. In this case the player will play the video basically instantly and send multiple requests to the server to pull down the video in small chunks until it is done playing. Here are screen captures to make it more concrete.

Response code 304 Case1

Response code 206 Case2

So I think this problem breaks down to, why is the server responding with 304 for some initial byte range requests and how can it respond with 206 and the needed content each time instead?


I am developing a Flutter app where users can view uploaded mp4 files from Cloud Storage. I have setup a Global external application load balancer with Cloud CDN and my Cloud Storage bucket as the backend origin. My flutter application requests files from cloud storage by sending HTTP requests to the external IP exposed by the load balancer.

On Android this works great. Video files load very fast. On iOS the device downloads the entire file before it starts playing. On a WIFI signal this results in a 1-5 second delay while on 5G it can be 10-100 seconds. I modified my code so that instead of going through the application load balancer it requests the files directly from Cloud Storage. Doing this the files load quickly (maybe 1 second delay) although not as quickly as when going thorough CDN and leveraging the cache.

I have inspected the network requests and it looks like when AvPlayer sends it's first byte range request (Range bytes=0-1) Cloud Storage returns an appropriate response and type (Content-Type: video/mp4) whereas for the same request Cloud CDN returns a text/plain response with a Age, Etag, Cache-Control and Date.

Any idea what would cause cloud CDN to return that response?


Solution

  • The reason you get a 304 response is because the request sent by AVPlayer includes an if-none-match header, and one of the caches has a response with a matching etag.

    The request from AVPlayer is specifically asking for a 304 response if any cache happens to have a response with the given etag. (See RFC9110/13.1.2.) Generally, a client (such as AVPlayer) only sends an if-none-match request because it has a cached copy--it's just checking whether there's a newer one. The 304 response should cause AVPlayer to use the response it had previously cached.

    You say that this is AVPlayer's first request. If AVPlayer doesn't already have a cached response, then it shouldn't send if-none-match (or if-modified-since) headers. If it does have a cached response, it should play it. Either way, this looks like a bug in the Flutter app, AVPlayer, or somewhere in between. I suggest that you figure out why the if-none-match header is present.