How to stream Safari-compatible video?

We have a well-used app that streams certain videos. They are streamed as partial content. The backend for this system is in Clojure, using Reitit with the Ring approach. However, while they work great in Firefox and Chrome, videos fail to load in Safari. Originally we thought it was the lack of a content type (since they are referenced from UUIDs without a file extension), but fixing this did not solve the problem. Then we thought perhaps it was a webkit problem, since webkit is the engine under Safari and Safari itself is impossible to install off of mac (unless you choose an archaic version from 2017, which probably wouldn’t represent anyone anyway). So I tried Epiphany, purportedly one of the best Webkit options on Linux. Alas, it didn’t show the problem, either! So, I am left with a problem that is apparently only Safari, not even webkit, that they cannot run our videos – disabling the large number of students who are trying to use their iphones, ipads, or macbooks to view the course material videos. Has anyone run into or solved this problem? Do you know why other webkit browsers (and indeed every other vendor) have no problem, but all Mac devices on Safari-type browsers are failing?

Related might be this about range headers, but we haven’t tried implementing it yet. React Player is not Working in Safari for all the video format · Issue #1305 · cookpete/react-player · GitHub

This is 100% a wrong place to ask. You’d have a much higher chance of getting a proper answer on any Apple-specific or even a generic web dev forum.

Originally we thought it was the lack of a content type (since they are referenced from UUIDs without a file extension)

Just wanted to address this - file extensions have nothing to do with content type. In browsers, content-type is a separate header that you should set to a proper value in the response.

You should also check whether the video format is supported by Safari in the first place. Plenty of compatibility tables online for that.

Another thing worth mentioning is that on iOS both Chrome and Firefox also use WebKit.

The range header shouldn’t affect the playback itself, it should only affect the ability to seek through the video. But Safari has shown itself as an eccentric browser more than once, so who knows.

1 Like

The backend is Clojure and we are using Ring to mediate the content, with a front end using raw react (not reagent), hence our grasping at straws for the possibility it was the range header. I asked here hoping someone has navigated this issue. By default, ring will assume the content type based on the file extension, so we had suspected that problem first. Thanks for the general information about the other browsers using webkit (or is it Apple’s mutation of webkit?) on Mac as well. We had wondered if range that had been the problem as we tried to compare working services (ie YouTube) to see what differences were there; that was, at present, our last idea. Maybe the next steps are to get myself a mac so we can try it’s Safari out to see if we can isolate the actual error and test possible fixes, and to test raw mp4 without streaming as partial media to see if that, at least, works (in which case Ring might be to blame).

How you build a response is irrelevant. Ring + Clojure, SimpleHTTPServer + Python, nc, whatever - doesn’t matter.

Focus on the content of responses, not on how they’re produced. Even if Ring doesn’t provide some header or something else that Safari needs, it’s not Ring itself that’s to blame, it’s how you use it to create responses.

You have given very little detail so it’s hard to provide any actual insight. But ensuring a tight feedback loop by getting your hands on macOS probably makes the most sense. Then you’ll be able to effectively and efficiently compare working resources with non-working. And you don’t need to own a physical Apple hardware for that.

1 Like

On iOS all browsers use WebKit. On macOS however they can use their own engines so Chromium browsers are using Blink, FireFox uses Gecko etc

So, is Mac using more than the off-the-shelf Webkit? (if so, that would make a sad sense…)

Edit: I just noticed that you are distinguishing between macOS annd iOS, which is not a distinction I held previously

Thanks for that. I had just hoped that for such a large user-base as “iOS users” a basic technical hurdle like “streaming video” might be addressed with minimal fuss

Do you use HLS?

You can also try to host the videos on S3 or Google Cloud Storage. I don’t know if it will help with your specific Safari issue. But at least S3 and Google Cloud Storage handles all the range queries correctly.

However, you might consider to use something like:

It will prepare your videos automatically so that you can use adaptive streaming (DASH and HLS) which is important for mobile users with varying connection speeds. They offer a video player implementation that handles all the details for you.

Regarding range-requests, I got them working in the sunng87/jetty9-wrapper - which is using Jetty 12. I have no idea if this would solve any Safari-related problems.

In short these classes are needed:

  (:import [org.eclipse.jetty.server.handler ResourceHandler ContextHandler ContextHandlerCollection]
           [org.eclipse.jetty.util.resource Resource])

and a configuration function is needed for setting up a Jetty ResourceHandler thing (all the terms here are confusing - a Resource is a webresource, not a JVM-resource, a Handler is, of course, not a ring handler but a Jetty handler class).

(defn configurator
  "returning a configurator that can add a resource handler"
  [{:keys [resources-path
           welcome-files
           directories-listed?
           accept-ranges?
           uri-path] :as opts
    :or {accept-ranges? true
         directories-listed? false
         welcome-files ["index.html"]
         uri-path "/assets"}}]
  (fn 
    [s]
    (log/info "puts context in a contexthandlercollection")
    (let [resource-handler (doto (new ResourceHandler)
                             (.setBaseResource (org.eclipse.jetty.util.resource.Resource/newResource resources-path))
                             (.setDirectoriesListed directories-listed?)
                             (.setWelcomeFiles (into-array welcome-files))
                             (.setAcceptRanges accept-ranges?))
          file-path-context (new ContextHandler uri-path)
          _ (.setHandler file-path-context resource-handler)
          jetty-adapter-context-handler (.getHandler s)
          ch-coll (new ContextHandlerCollection (into-array [file-path-context jetty-adapter-context-handler]))]
      (.setHandler s ch-coll))))

The handler is started like this:

(jetty/run-jetty handler (-> opts (assoc :join? false
                                         :configurator (configurator {:resources-path "assets"
                                                                                         :accept-ranges true
                                                                                         :uri-path "/assets"}))))

This code excerpt is happening before the ring-adapter → ring-handler, so any authentication using ring machinery has to be solved in some other way. It would be really nice to be able to return back to the ResourceHandler from inside Ring, but I’m not sure how that could be made to work.

1 Like

Does your backend properly respond to range requests or are you just serving the whole file?

I successfully used this code to respond to range requests in my backend byte range aware file-response for ring · GitHub. I can stream at least audio to Safari using this.

I also found this that may be more memory conservative Readme — ring-range-middleware 0.1.0

I have searched ring, reitit and the web in general but can’t find anything more than a single Readme file that even mentions HLS.

I messed around with ffmpeg once and that worked ok for a very basic test. Never took it anywhere though. Didn’t really want to deal with video encoding stuff, so scrapped the entire thing.

It definitely isn’t anything reitit, ring or Clojure would even begin to do. So, ffmpeg is where you should probably start looking.

I think you’d be (significantly) better off by using an off-the-shelf video streaming server setup (for HLS or DASH) than re-implementing it in your Clojure application. You could have your application point to the relevant URLs served by that second server.

What off the shelf options?

Sorry I wasn’t clear! If you want HLS, there’s a bunch of servers that can do that (see here). If you want to do DASH, then there are many options for preparing the content (encoding, MPD, segments, etc). You can then serve those from any web server, and there are JS clients that can deal with DASH content.