Video.js: video will play in all browsers EXCEPT safari

Created on 31 Oct 2016  路  16Comments  路  Source: videojs/video.js

Description

My videos were working perfectly in all major browsers until this fall when they stopped working in Safari only. They still work fine in Chrome, Firefox, and Internet Explorer.

Here is a reduced test case. Any help would be appreciated, I am not sure what changed.

Steps to reproduce

Explain in detail the exact steps necessary to reproduce the issue.

  1. try to play the video in Firefox, Chrome, or Internet Explorer
  2. now try to play the video in Safari

Results

Expected

I expect the video to play in Safari just like it does in other browsers.

Actual

I get a message that says "The media could not be loaded, either because the server or network failed or because the format is not supported."

Error output

In the Safari console I see this error:
(CODE:4 MEDIA_ERR_SRC_NOT_SUPPORTED)"
"The media could not be loaded, either because the server or network failed or because the format is not supported."
MediaError {code: 4, message: "The media could not be loaded, either because the 鈥k failed or because the format is not supported.", status: null, MEDIA_ERR_CUSTOM: 0, MEDIA_ERR_ABORTED: 1, ...

Additional Information

versions

videojs

v5.11.9

browsers

only Safari (I am using 10.0.1)

OSes

only Mac OS as far as I know

plugins

no

Most helpful comment

I end up in the same issue and I'm not managing the server. So I found a really dirty workaround: downloading the whole stream manually and push it to the player:

var xhr = new XMLHttpRequest();
xhr.open('GET', streamUrl, true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
  if (this.status == 200) {
    var myBlob = this.response;
    var vid = (window.webkitURL || window.URL).createObjectURL(myBlob);
    var video = document.getElementById("video");
    video.src = vid;
   }
  }

xhr.send();

As I said, it's really dirty and should not be used unless you really need to because you will have to wait for the whole video to be loaded in the browser. My streams last 5min or less in low resolution so it's not that big a deal, but it can be for larger video!

All 16 comments

Thanks for the reduced test case.

There are a couple of issues here:

  • The server returns an incorrect content-type header, Content-Type: application/mp4. This should be video/mp4. Some browsers are more fussy than others about incorrect types.
  • The server is not honouring range requests. Because of this the video will also fail if you attempt to seek on Chrome. Adding a Accept-Ranges: none response header should stop the browser asking for a range, but accepting and responding to range requests will give a better experience. [Edit: Safari appears to absolutely require range support]

The browser is handling the request here, so what's changed must be either a change in Safari itself (e.g. in Safari 10) or a change in how the server is delivering the video files.

Maybe we should build a source/stream validator... things like this come up so often.

Thanks mister-ben for identifying those issues. I have corrected the mime type and added 'Accept-Ranges: none' but that has not helped. If you guys have any other ideas I would love to hear them!

I too believe that it is a change in Safari, I just have no idea how to fix this. Could it be related to the fact that I am using HTTPS?

So Apple's doc for video in Safari on iPhone calls out that byte range support is required. I don't see an equivalent doc for Mac OS Safari, but it's likely the requirement is the same. I can play that video from another server that does support byte ranges, so it looks like adding that support to the server is the way to go here.

https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html

It seems that byte range handling is a requirement for iOS for a long time already, perhaps they also made it a requirement for Mac OS X Safari.

If I put the file on my local server, I can use it in Safari (and my server does support ranged requests)

Thanks you guys! I will trouble-shoot my server / file-serving code. The file is currently being streamed via PHP off a Linux server.

enabling byte range handling in my php stream code solved my problems

@claerosystems How did you do it? Possible to share a piece of code?

I鈥檓 loading a video stored on dropbox into a videojs player on my digitalocean ubuntu server with php and having the same problem playing videos on iphone/ipad/ios. Any ideas if it鈥檚 server, dropbox, browser or videojs?

Sure seabass, here is the code I used, it is a total hack as I have never gone back to clean it up, but it works (you can see the code block I pulled from somewhere on the net and just pasted it in):

` $file_path = $target_object->get_filename_with_path($column_name);

                if ( ! file_exists($file_path)) {
                    throw new CL4_Exception_File('The file that was attempted to be sent to the browser does not exist: :file:', array(':file:' => $file_path), CL4_Exception_File::FILE_DOES_NOT_EXIST);
                }

                header('Content-type: video/mp4');
                header('Content-Disposition: inline; filename="' . $filename . '"');

                $fp = @fopen($file_path, 'rb');

                $size   = filesize($file_path); // File size
                $length = $size;           // Content length
                $start  = 0;               // Start byte
                $end    = $size - 1;       // End byte
                // Now that we've gotten so far without errors we send the accept range header
                /* At the moment we only support single ranges.
                 * Multiple ranges requires some more work to ensure it works correctly
                 * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
                 *
                 * Multirange support annouces itself with:
                 * header('Accept-Ranges: bytes');
                 *
                 * Multirange content must be sent with multipart/byteranges mediatype,
                 * (mediatype = mimetype)
                 * as well as a boundry header to indicate the various chunks of data.
                 */
                header("Accept-Ranges: 0-$length");
                // header('Accept-Ranges: bytes');
                // multipart/byteranges
                // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
                if (isset($_SERVER['HTTP_RANGE'])){
                    $c_start = $start;
                    $c_end   = $end;

                    // Extract the range string
                    list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
                    // Make sure the client hasn't sent us a multibyte range
                    if (strpos($range, ',') !== false){
                        // (?) Shoud this be issued here, or should the first
                        // range be used? Or should the header be ignored and
                        // we output the whole content?
                        header('HTTP/1.1 416 Requested Range Not Satisfiable');
                        header("Content-Range: bytes $start-$end/$size");
                        // (?) Echo some info to the client?
                        exit;
                    } // fim do if
                    // If the range starts with an '-' we start from the beginning
                    // If not, we forward the file pointer
                    // And make sure to get the end byte if spesified
                    if ($range{0} == '-'){
                        // The n-number of the last bytes is requested
                        $c_start = $size - substr($range, 1);
                    } else {
                        $range  = explode('-', $range);
                        $c_start = $range[0];
                        $c_end   = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
                    } // fim do if
                    /* Check the range and make sure it's treated according to the specs.
                     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
                     */
                    // End bytes can not be larger than $end.
                    $c_end = ($c_end > $end) ? $end : $c_end;
                    // Validate the requested range and return an error if it's not correct.
                    if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size){
                        header('HTTP/1.1 416 Requested Range Not Satisfiable');
                        header("Content-Range: bytes $start-$end/$size");
                        // (?) Echo some info to the client?
                        exit;
                    } // fim do if

                    $start  = $c_start;
                    $end    = $c_end;
                    $length = $end - $start + 1; // Calculate new content length
                    fseek($fp, $start);
                    header('HTTP/1.1 206 Partial Content');
                } // fim do if

                // Notify the client the byte range we'll be outputting
                header("Content-Range: bytes $start-$end/$size");
                header("Content-Length: $length");

                // Start buffered download
                $buffer = 1024 * 8;
                while(!feof($fp) && ($p = ftell($fp)) <= $end){
                    if ($p + $buffer > $end){
                        // In case we're only outputtin a chunk, make sure we don't
                        // read past the length
                        $buffer = $end - $p + 1;
                    } // fim do if

                    set_time_limit(0); // Reset time limit for big files
                    echo fread($fp, $buffer);
                    flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
                } // fim do while

                fclose($fp);


                exit;

`

@cnakamoto Thanks! :) I will try this. Will this work on files hosted on a differet server (dropbox in my case) and does it require to download the whole file before it can play it or just fetch the headers and it鈥檚 good to go?

@cnakamoto Thanks again. The script runs with any problems, but it appears that I can play locally hosted video files on my iphone even without the script. It seems like external files, in my case the dropbox link does not play, and I can't get the script to take the dropbox url. Any ideas?

@seabasss I only used this to stream local files on my web server. I don't know if you could do this directly from the dropbox servers , but you could always download the file temporarily and stream it and then just delete when done.

@cnakamoto Thank you! That sounds like an option, if it's not too heavy to load, and not the whole file needs to be downloaded before it's possible to play. I'll see if I can read about how to do that.

Running this little guy on the Dropbox links seem to fix it for me! :D

str_replace('www.dropbox.com/s/', 'dl.dropboxusercontent.com/s/', $url);

I end up in the same issue and I'm not managing the server. So I found a really dirty workaround: downloading the whole stream manually and push it to the player:

var xhr = new XMLHttpRequest();
xhr.open('GET', streamUrl, true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
  if (this.status == 200) {
    var myBlob = this.response;
    var vid = (window.webkitURL || window.URL).createObjectURL(myBlob);
    var video = document.getElementById("video");
    video.src = vid;
   }
  }

xhr.send();

As I said, it's really dirty and should not be used unless you really need to because you will have to wait for the whole video to be loaded in the browser. My streams last 5min or less in low resolution so it's not that big a deal, but it can be for larger video!

I end up in the same issue and I'm not managing the server. So I found a really dirty workaround: downloading the whole stream manually and push it to the player:

var xhr = new XMLHttpRequest();
xhr.open('GET', streamUrl, true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
  if (this.status == 200) {
    var myBlob = this.response;
    var vid = (window.webkitURL || window.URL).createObjectURL(myBlob);
    var video = document.getElementById("video");
    video.src = vid;
   }
  }

xhr.send();

As I said, it's really dirty and should not be used unless you really need to because you will have to wait for the whole video to be loaded in the browser. My streams last 5min or less in low resolution so it's not that big a deal, but it can be for larger video!

You've saved my life! Thanks a lot!!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zhulduz picture zhulduz  路  3Comments

borm picture borm  路  3Comments

victorpfm picture victorpfm  路  4Comments

TheKassaK picture TheKassaK  路  3Comments

jeonghwaYoo picture jeonghwaYoo  路  3Comments