Html <video> tag works intermittently

I have a Phoenix controller that does an Http GET to CouchDb to retrieve a video file, and forward to the Svelte JS client (an html video tag).

The client is an SPA at a single Phoenix route. The following controller is for the API:

def get_video(conn, params) do
    vid_id = params["video_id"]
    response = CouchDb.get_video(vid_id)
    data = response.body
    content_type = "video";
    hdr = Enum.find(response.headers, fn x -> elem(x, 0) == "content-type" end)
    if (hdr) do
      content_type = elem(hdr, 1)
    end

   conn
    |> put_layout(false)
    |> put_root_layout(false)
    |> put_resp_content_type(content_type, nil)
    |> send_resp(200, data)
  end

The response.headers from the CouchDb.get_video call look like this:

[
  {"accept-ranges", "bytes"},
  {"cache-control", "must-revalidate"},
  {"content-length", "21180417"},
  {"content-md5", "YyPKVd3NFiQ6wm3sTndwdw=="},
  {"content-security-policy", "sandbox"},
  {"content-type", "video"},
  {"date", "Thu, 12 Sep 2024 00:16:38 GMT"},
  {"etag", "\"YyPKVd3NFiQ6wm3sTndwdw==\""},
  {"server", "CouchDB/3.3.3 (Erlang OTP/24)"},
  {"x-couch-request-id", "21401eae1f"},
  {"x-couchdb-body-time", "0"}
]

The problem is that the video contents work randomly; e.g., the first render might show a playable video, and then navigate to a different page, a similar video fails, and back to the first and it fails, too. The network console shows that no GET request is issued when it fails.

Any idea what might be wrong with the Phoenix controller code? Thanks for any help.

A similar NodeJS API works fine with the same client:

apiRouter.get("/videos/:db_name/:video_id", async (req : Request, res : Response) => {
    let video_id = req.params["video_id"];
    try {
        const result = await db.getVideoByMediaId(fetch, video_id);
        let {ok, data, content_type, content_length} = result;
        if (!ok || !data) {
            console.log("err not found")
            return res.status(StatusCodes.NOT_FOUND).json({error : `Video not found!`})
        }
        res.set('Content-Length', content_length.toString());
        res.set('Content-Type', `${content_type}`);
        return res.send(data) 
        // sets content_length and mime type
    } catch (error) {
        return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({error})
    }
})

Hmm, there might be a scoping issue at play where content_type never get re-assigned to the content type from the response headers.

    content_type = "video";
    hdr = Enum.find(response.headers, fn x -> elem(x, 0) == "content-type" end)
    if (hdr) do
      content_type = elem(hdr, 1)
    end
    IO.inspect(content_type) # would return "video" even when `hdr` is truthy e.g. `{"content-type", "hdr"}`

The quick fix would be something like content_type = if hdr, do: elem(hdr, 1).

That said, I’d suggest pattern matching paired with List.keyfind/4. It accepts as its last parameter an optional default value to return when there’s no matching tuple in the list.

{"content-type", content_type} = 
    List.keyfind(response.headers, "content-type", 0, {"content-type", "video"})

Thanks @codeanpeace , that’s a good suggestion but I can see that the content_type header is arriving with the response.

I’m worried that there is something that I don’t understand about how <video> tags generate browser requests – that I need to be doing something more on the server side. For instance, Phoenix is automatically adding the content-length header – does it automatically handle breaking the fetch up into multiple requests? Something doesn’t feel right, because I read that the <video> tags can generate partial range requests, or to send the response in chunks – which I assume is done under the hood, but if not I’m doing something all wrong…?

For instance, this describes a problem when having to write a server to respond to byte range requests. Do I have to write code to do that, or does Phoenix do it under the hood and I can treat it like one big request? I guess that’s the heart of my question.

Adding Accept-Range: bytes to the response headers seems to have fixed it:

  conn
    |> put_layout(false)
    |> put_root_layout(false)
    |> put_resp_content_type(content_type, nil)
    |> put_resp_header("Accept-Ranges", "bytes")
    |> send_resp(200, data)

No idea why the node/express server works without this, but it seems to be needed here, in Phoenix.

(Accept-range tells the browser that it can make subsequent, partial requests for bytes.)