77

I have a video as a background to a web page, and I am trying to get it to loop. Here is the code:

<video autoplay='true' loop='true' muted='true'>
  <source src='/admin/wallpapers/linked/4ebc66e899727777b400003c' type='video/mp4'></source>
</video>

Even though I have told the video to loop, it does not. I also tried to get it to loop with the onended attribute (as per this Mozilla support thread, I also tried that bit of jQuery). Nothing has worked so far. Is it an issue with Chrome, or my code?

Edit:

I checked the Network events and HEAD of a working copy (http://fhsclock-labs.heroku.com/no-violence) versus the application I'm trying to get working. The difference is the working copy is serving up the video from a static asset on Heroku (via Varnish, apparently), whilst mine is serving from GridFS (MongoDB).

The Network tab of Chrome's Inspector show that in my application, the video is requested three times. One time the Status is "pending", the second is "canceled", and the final one is 200 OK. The working copy only shows two requests, one's Status is pending and the other is 206 Partial Content. However, after the video plays once, that request changes to "Cancelled" and it makes another request for that video. In my application, that does not happen.

As for Type, in my application, two are "undefined" and the other "video/mp4" (which it is supposed to be). In the working app, all of the requests are "video/mp4".

In addition, I'm getting Resource interpreted as Other but transferred with MIME type undefined. warnings in the Console.

I'm not really quite sure where to begin on this. It's my belief that the issue is server-side, as serving the file as static assets works fine. It could be that the server isn't sending the correct content type. It could be an issue with GridFS. I do not know.

At any rate, the source is here. Any insight that you can offer is appreciated.

Ethan Turkeltaub
  • 2,931
  • 8
  • 30
  • 45

9 Answers9

140

Ah, I just stumbled into this exact problem.

As it turns out, looping (or any sort of seeking, for that matter) in <video> elements on Chrome only works if the video file was served up by a server that understands partial content requests. i.e. the server needs to honor requests that contain a "Range" header with a 206 "Partial Content" response. This is even the case if the video is small enough to be fully buffered by chrome, and no more server-round trips are made: if your server didn't honor chrome's Range request the first time, the video will not be loopable or seekable.

So yes, an issue with GridFS, although arguably Chrome should be more forgiving.

Chris
  • 26,744
  • 48
  • 193
  • 345
afri
  • 1,501
  • 1
  • 10
  • 6
  • 3
    Just had the same problem. I'm using nginx so I had to purge nginx, install from source with http://nginx.org/en/docs/http/ngx_http_mp4_module.html and restart everything. – chriscauley Sep 20 '12 at 21:04
  • The same happens with php built-in server (in my dev environment). – Marcel Burkhard Sep 22 '15 at 07:36
  • 3
    Same problem with Django 1.7.10 runserver. – Lauri Elias Nov 27 '15 at 13:57
  • I had the same problem due to a badly configured corporate firewall - a range request gets stripped and you get the entire file back. Safari refuses to play it at all, Chrome plays it once. Nothing can be done about that apart from maybe switching to HLS and some fallback JS player that supports that. I could troubleshoot with `curl -v -r0-1 ...` – w00t Mar 19 '16 at 19:23
  • @Marcel Burkhard Hey, can you show me the PHP code that you used to solve this problem? I have the same problem and very new to PHP. – Ng2-Fun Jan 09 '17 at 13:59
  • As of PHP 5.4 there is a built-in server, but that simple web-server does not understand partial content requests. You probably only have this problem locally, as you will be running your application with a "real" webserver like nginx/apache when you deploy your application. There is no php code to solve this as PHP is not even involved when serving static content that way. – Marcel Burkhard Jan 17 '17 at 21:18
  • Sorry this is so late but rather than ask a new question, hoped to get a response from here. Are you saying all one needs to do is change the response.status to 206 for chrome to allow seek in the video control playback? I server the video as full stream to the chrome audio player. Please advise how best to get this to work. – vbNewbie Feb 13 '18 at 09:21
23

Simplest workaround:

$('video').on('ended', function () {
  this.load();
  this.play();
});

The 'ended' event fires when the video reaches the end, video.load() resets the video to the beginning, and video.play() starts it playing immediately once loaded.

This works well with Amazon S3 where you don't have as much control over server responses, and also gets around Firefox issues related to video.currentTime not being settable if a video is missing its length metadata.

Similar javascript without jQuery:

document.getElementsByTagName('video')[0].onended = function () {
  this.load();
  this.play();
};
d2vid
  • 2,014
  • 1
  • 22
  • 25
6

Just in case none of the answers above help you, make sure you don't have your inspector running with the Disable cache option checked. Since Chrome grabs the video from cache, it will basically work once. Just debugged this for 20 minutes before realizing this was the cause. For reference and so I know I am not the only one someone else's chromium bug report.

srussking
  • 346
  • 4
  • 10
6

Looks like its been an issue in the past, there are at least two closed bugs on it, but both state that it was fixed:

http://code.google.com/p/chromium/issues/detail?id=39683

http://code.google.com/p/chromium/issues/detail?id=18846

Since Chrome and Safari both use webkit based browsers you might be able to use some of these work arounds: http://blog.millermedeiros.com/2011/03/html5-video-issues-on-the-ipad-and-how-to-solve-them/

function restartVideo(){
vid.currentTime = 0.1; //setting to zero breaks iOS 3.2, the value won't update, values smaller than 0.1 was causing bug as well.
vid.play();
}

//loop video
vid.addEventListener('ended', restartVideo, false);
John
  • 6,503
  • 3
  • 37
  • 58
  • @Ramesh could you guys take a look at this question as well? http://stackoverflow.com/questions/31634678/where-to-find-the-response-video-data Has to do with video data – committedandroider Jul 28 '15 at 05:15
4

For anyone coming on this page 9 years later and if all the above answers didn't work: I had this issue too and I thought the source of the issue was either my browsers or with the server.

I've later noticed that the other websites on internet which use looping videos they don't have issue with looping videos. To troubleshoot I have downloaded a random video from one of the sites and I visited and uploaded on my own server to delightedly find out it was working, so it seemed that the source of the issue was the video I was using.

Then I fixed my video with an online video converter website (don't want to publicize any in particular but the first ones from a quick google research do work) and alas, this solved the issue.

I'm not sure what the real reason of the issue was. I do assume there was a conversion or compression error of the original video that was handed me from my client.

MacK
  • 2,132
  • 21
  • 29
Hataman
  • 51
  • 3
  • Happened to me too: apparently converting an ogv to m4v with VLC generated a broken video file. Using an online ogv -> mp4 converted fixed it. – Guy Incognito Sep 24 '22 at 11:04
  • Amazing! Thanks! For me ```ffmpeg -i video.mp4 new.mp4``` then ```mv new.mp4 video.mp4``` did the trick – Mouradif Dec 11 '22 at 17:34
3

My situation:

I have the exact same problem, however changing the header of the response message alone didnt do. No loop, replay or seek. Also a pure stop doesnt work, but that could be my configuration.

Answer:

According to some sites (couldnt find them anymore) its also possible to trigger the load() method right after the video ends, and before the next one is supposed to start. That should reload the source causing a once again working video/audio element.

@john

Please note that your answers/links are normal bugs, and not focused on this problem. Using a server/webserver is what causes this problem. Whereas the bugs these links describe are of a different kind. Thats also why the answer isnt working.

I hope it helps, i am still looking for a solution.

WhoKnows
  • 100
  • 1
  • 11
  • What I actually ended up doing was serving it from a different source: before I was using MongoDB's GridFS. I believe the problem stemmed from serving the file incorrectly. I'm using Amazon S3 now without an issue. – Ethan Turkeltaub Dec 05 '12 at 13:13
  • Ohh.. we solved it too, but i cant really explain it. It seems like range requests wasnt working for the service, so we added these lines to our method that downloads the files. response.Headers.Add("Accept-Ranges", "bytes"); response.Headers.Add("Content-Range", rangeValue.Replace("=", " ") + (resource.Value.Length - 1).ToString() + "/" + resource.Value.Length.ToString()); response.StatusCode = HttpStatusCode.PartialContent; This enabled the range requests, and manipulates the header to let the server etc. know its a range request. code 206 instead of 200. – WhoKnows Dec 06 '12 at 10:13
  • response.Headers.Add("Accept-Ranges", "bytes"); response.Headers.Add("Content-Range", rangeValue.Replace("=", " ") + (resource.Value.Length - 1).ToString() + "/" + resource.Value.Length.ToString()); response.StatusCode = HttpStatusCode.PartialContent; – WhoKnows Dec 06 '12 at 10:13
  • 1
    Im sorry if this is a bit unclear, but maybe it will help someone someday :) Thanks for pointing me in the right direction! – WhoKnows Dec 06 '12 at 10:14
  • Thanks! I'm using video.js and calling load on end got the video looping in production when previously it only would locally. I found that I can check if the currentTime === duration on end to see if the video isn't looping back to start and in that case call load instead of play. – funrob Aug 26 '13 at 20:37
2

I know this doesn't pertain exactly to the question asked, but if someone comes across this when having a similar issue, make sure that you have your sources in order properly.

I was loading an mp4 and a webm file and noticed that the video was not looping in Chrome. It was because the webm file was the first source listed so Chrome was loading the webm file and not the mp4.

Hope that helps someone else that comes across this issue.

<video autoplay loop>
    <source src="/path-to-vid/video.mp4" type="video/mp4">
    <source src="/path-to-vid/video.webm" type="video/webm">
</video>
ferne97
  • 1,063
  • 1
  • 10
  • 20
1

it is super lame but dropbox use the right status code. So upload to dropbox and replace the www by dl.

Thus using a dropbox url the video play fine.

gabrielstuff
  • 1,286
  • 1
  • 13
  • 16
1

I had same issue and inevitably solved problem by streaming the content.

e.g this is the code with PHP laravel blade html code which is requesting to streaming route:

<video>
    <source src="{{route('getVideoStream',$videoId)}}" type="video/mp4"/>
</video>

in the Controller I will stream video and return it as laravel stream function:

   public function getVideoStream($videoId){

        $path = $pathOfVideo;

        $headers = [
            'Content-Type' => 'video/mp2t',
            'Content-Length' => File::size($path),
            'Content-Disposition' => 'attachment; filename="start.mp4"'
        ];

        $stream = new VideoStream($path);

        return response()->stream(function () use ($stream) {
            $stream->start();
        });
    }

and VideoStream Class is the streaming class I found from a GitHub gist:

class VideoStream
{
    private $path = "";
    private $stream = "";
    private $buffer = 102400;
    private $start = -1;
    private $end = -1;
    private $size = 0;

    function __construct($filePath)
    {
        $this->path = $filePath;
    }

    /**
     * Open stream
     */
    private function open()
    {
        if (!($this->stream = fopen($this->path, 'rb'))) {
            die('Could not open stream for reading');
        }

    }

    /**
     * Set proper header to serve the video content
     */
    private function setHeader()
    {
        ob_get_clean();
        header("Content-Type: video/mp4");
        header("Cache-Control: max-age=2592000, public");
        header("Expires: " . gmdate('D, d M Y H:i:s', time() + 2592000) . ' GMT');
        header("Last-Modified: " . gmdate('D, d M Y H:i:s', @filemtime($this->path)) . ' GMT');
        $this->start = 0;
        $this->size = filesize($this->path);
        $this->end = $this->size - 1;
        header("Accept-Ranges: 0-" . $this->end);

        if (isset($_SERVER['HTTP_RANGE'])) {

            $c_start = $this->start;
            $c_end = $this->end;

            list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
            if (strpos($range, ',') !== false) {
                header('HTTP/1.1 416 Requested Range Not Satisfiable');
                header("Content-Range: bytes $this->start-$this->end/$this->size");
                exit;
            }
            if ($range == '-') {
                $c_start = $this->size - substr($range, 1);
            } else {
                $range = explode('-', $range);
                $c_start = $range[0];

                $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
            }
            $c_end = ($c_end > $this->end) ? $this->end : $c_end;
            if ($c_start > $c_end || $c_start > $this->size - 1 || $c_end >= $this->size) {
                header('HTTP/1.1 416 Requested Range Not Satisfiable');
                header("Content-Range: bytes $this->start-$this->end/$this->size");
                exit;
            }
            $this->start = $c_start;
            $this->end = $c_end;
            $length = $this->end - $this->start + 1;
            fseek($this->stream, $this->start);
            header('HTTP/1.1 206 Partial Content');
            header("Content-Length: " . $length);
            header("Content-Range: bytes $this->start-$this->end/" . $this->size);
        } else {
            header("Content-Length: " . $this->size);
        }

    }

    /**
     * close curretly opened stream
     */
    private function end()
    {
        fclose($this->stream);
        exit;
    }

    /**
     * perform the streaming of calculated range
     */
    private function stream()
    {
        $i = $this->start;
        set_time_limit(0);
        while (!feof($this->stream) && $i <= $this->end) {
            $bytesToRead = $this->buffer;
            if (($i + $bytesToRead) > $this->end) {
                $bytesToRead = $this->end - $i + 1;
            }
            $data = fread($this->stream, $bytesToRead);
            echo $data;
            flush();
            $i += $bytesToRead;
        }
    }

    /**
     * Start streaming video content
     */
    function start()
    {
        $this->open();
        $this->setHeader();
        $this->stream();
        $this->end();
    }
}