Search code examples
video-streamingandroid-mediaplayercloudflare-workers

Sending an R2 Video Stream through CloudFlare Workers leads to a crash in Android MediaPlayer and Safari


Here is the important piece of TS code that trys so find a video-file locate in the R2-Storage from CloudFlare.

const { id, ref, hash } = req.param();
if (!await isHashValid(hash))
  return c.text("Unauthorized", 401);

const file: R2ObjectBody = await AR_POSTCARDS.get(`${id}/${ref}`);
if (file === null) return new Response("Object Not Found", { status: 404 });
const headers = new Headers();
file.writeHttpMetadata(headers);
headers.set("etag", file.httpEtag);
headers.set("Content-Type", "video/mp4");

const { readable, writable } = new TransformStream();
file.body?.pipeTo(writable);
return new Response(readable, {
   headers
});

The goal is, that an Android-Mediaplayer can access the worker-url which runs the above code and gets back the video source.

  mediaPlayer = new MediaPlayer();
  mediaPlayer.setDataSource(url);

Unfortunately this doesn't work out of the box. The Mediaplayer accepts the url, but throws an error event:
response code = 200
W/MediaHTTPConnection: readAt 3273303 / 32768 => java.net.ProtocolException
I/http: on error: 1 -2147483648

I can't find much information what's going on, so i'll appreciate all help.

Im wondering if there's a part which im missing, for example a valid cors header etc.. Current situation is,

  • that the given worker-url runs correct in a chrome browser.
  • Safari doesn't play the video and give me an error (Failed to load resource: ...)
  • The following curl command also downloads the desired video file correct, which tells me that downloading the file kinda works.
curl http://127.0.0.1:8787/r2/video/{id}/video.mp4/{hash}> test.mp4

Solution

  • In case anyone has the same problem:

    The problem is, you need to have a partial content response (status code 206).

    If you google it, there a some headers you have to set.

    In order to fix this issue, you need to extract the range from the request and respond with a 206:

      const { id, ref } = c.req.param();
    
      // get start and end from range header
      const range = c.req.header("Range");
      const startEnd : number[] = range?.replace('bytes=', '')?.split('-')?.map(val => parseInt(val));
      const [start, end] = startEnd ?? [0, 1];
    
      try {
        const options: R2GetOptions = {
          range: {
            offset: start,
            length: end-start,
          }
        };
        const file: R2ObjectBody = await R2_BUCKET.get(`${id}/${ref}`, options);
        if (file === null) return new Response("Object Not Found", { status: 404 });
    
        const headers = new Headers();
        file.writeHttpMetadata(headers);
        headers.set("etag", file.httpEtag);
        headers.set("Content-Type", "video/mp4");
        headers.set("Accept-Ranges", "bytes");
        headers.set("Content-Range", `bytes ${start ?? 0}-${end ?? 1}/${file.size}`);
        headers.set("Content-Length", `${end - start}`);
        
    
        return new Response(file.body, {
          headers,
          status: 206,
        });
    

    Another usefull link: AVPlayer with 206