Play (mp4) videos with mediaelement.js in fancyBox

One of the questions I have seen often is : how to stream (self-hosted) videos in a web page using fancyBox?

A more elaborated question is : how to stream videos consistently through most modern web browsers and devices in a modal box like fancybBox?
 

UPDATE [Feb 15, 2015] : The fullscreen issues reported with v2.16.1 have been fixed in the latest v2.16.3.

WARNING [Nov 07, 2014] : I have been notified about some issues of this method with the latest MEJS v2.16.1, like video in fullscreen mode only shows an empty (black or white) window, video doesn't load in flash fallback mode in Opera, etc. While those issues are being addressed, I would recommend you to use MEJS v2.15.1 if you want to use this method to display (MP4) videos in fancybox with MEJS.

 

play videos in fancyBox with mediaelement.js
The context

The advantage of using a "light box" (like fancyBox) to stream videos is they will be loaded until they are required only, avoiding the risk of slowing down the page load with a big chunk of unnecessary video data if they were displayed in-line.

Something that we have to bear in mind is that fancyBox doesn't include a build-in way to play videos so the first thing we need to get is a video player.

There are many options in the market, some Open Source and some commercial like it can be the case of jwplayer.

In this tutorial we will use mediaelement.js (MEJS), a free and open source HTML5 video player built by John Dyer.

For the purpose of this tutorial, we are going to use mp4 (h.264) files only because this is (arguably) the most popular video format as today. If you want to stream a video format other than mp4, make sure you check the "Browser and Device support" at the mediaelement.js website.
 

Loading the scripts

Make sure you include the basic js and css files :

<link rel="stylesheet" type="text/css" href="path/jquery.fancybox.min.css" media="screen"/>
<link rel="stylesheet" type="text/css" href="path/mediaelementplayer.css" media="screen" />

<script src="path/jquery.js"></script>
<script src="path/jquery.fancybox.js"></script>
<script src="path/mediaelement-and-player.min.js"></script>
The basic html

The only thing need we need to start is the list of the videos we want to play :

<a href="path/video01.mp4"></a>
<a href="path/video02.mp4"></a>
<a href="path/video03.mp4"></a>

We may want to show each video with its own particular properties like size, title/caption, thumbnail, etc. We can pass that information to fancyBox (and MEJS) using (HTML5) data attributes like :

<a href="path/video01.mp4" class="fancy_video" data-width="320" data-height="240" data-caption="First video" data-poster="path/v_thumb01.jpg">Video 01</a>

Notice that the data-poster attribute will point to the image that is shown inside the video container.

HINT: If you want to include your videos in a fancyBox gallery, you could add the data-fancybox-group attribute too.

 

MEJS how-to

To play videos with MEJS, we need to use the HTML5 <video> tag like :

<video width="320" height="240" poster="path/v_thumb01.jpg" controls="controls" preload="none"></video>

Notice that we don't need to add the video tag in our regular html page since we are going to construct that specific html structure inside our fancyBox script.

If you are planing to use multiple codecs for various browsers and also want to include a Flash fallback for browsers that don't support the video tag, please check "Option B" at the mediaelement.js documentation for the proper html structure.

Hint : If you are using mp4 files only as in this tutorial, then you don't need to set a fallback html code, not even for IE7+ since MEJS will automatically add it if needed

 

The Fancybox script

As we mentioned above, we may be passing some properties to each video (through data attributes) that we will need to set within the MEJS initialization script. However, since we may eventually omit some specific data information in part of our links, it's also important to validate whether those data values were passed or not and apply default values instead.

var $video_player, _videoHref, _videoPoster, _videoWidth, _videoHeight, _dataCaption, _player, _isPlaying = false, _verIE = getInternetExplorerVersion();
jQuery(document).ready(function ($) {
    jQuery(".fancy_video").fancybox({
        // set type of content (remember, we are building the HTML5 <video> tag as content)
        type       : "html",
        // other API options
        beforeLoad : function () {
            // build the HTML5 video structure for fancyBox content with specific parameters
            _videoHref   = this.href;
            // validates if data values were passed otherwise set defaults
            _videoPoster = typeof this.element.data("poster")  !== "undefined" ? this.element.data("poster")  :  "";
            _videoWidth  = typeof this.element.data("width")   !== "undefined" ? this.element.data("width")   : 360;
            _videoHeight = typeof this.element.data("height")  !== "undefined" ? this.element.data("height")  : 360;
            _dataCaption = typeof this.element.data("caption") !== "undefined" ? this.element.data("caption") :  "";
            // construct fancyBox title (optional)
            this.title = _dataCaption ? _dataCaption : (this.title ? this.title : "");
            // set fancyBox content and pass parameters
            this.content = "<video id='video_player' src='" + _videoHref + "'  poster='" + _videoPoster + "' width='" + _videoWidth + "' height='" + _videoHeight + "'  controls='controls' preload='none' ></video>";
            // set fancyBox dimensions
            this.width = _videoWidth;
            this.height = _videoHeight;
        },
        afterShow : function () {
            // initialize MEJS player
            var $video_player = new MediaElementPlayer('#video_player', {
                    defaultVideoWidth : this.width,
                    defaultVideoHeight : this.height,
                    success : function (mediaElement, domObject) {
                        mediaElement.play(); // autoplay video (optional)
                    } // success
                });
        }
    });
}); // ready

Notice that we retrieve the data attributes information inside the beforeLoad callback and create the video element in the DOM with those specific attributes. Then we initializes MEJS inside the afterShow callback, once the video element exists in the DOM and inside of the fancyBox container.

Hint : If you set mediaElement.play() within the success setting, the video will start playing just right after the player has been successfully loaded. You may not set this method to save visitor's bandwidth

  

Known issues and workarounds

"null" is null or not an object : IE < 9

This error occurs when inserting and removing MEJS dynamically, as is the case with fancyBox. It's triggered under any of the following scenarios :

  • fancyBox is closed while the video is playing
  • navigating to prev/next element of a fancyBox gallery while the video is playing

Since IE < 9 doesn't support the (HTML5) video tag, MEJS uses a flash player fallback (flashmediaelement.swf.)

It is thought that some Flash objects are not properly removed after dynamically removing the MEJS container. You can learn more about the issue here.

The suggested workaround was to use the .remove() method (included since MEJS v2.14.0) to safely remove Flash objects in IE. So if we initialized MEJS like

$video_player = new MediaElementPlayer('#video_player', { ... });

Then we could remove the player like

$video_player.remove();

However, I found that this workaround doesn't work "out-of-the-box" in IE < 9. A second workaround was overwriting the underlying media element instance of the success setting that we could use for IE (although this new instance will also work with any other browser) :

success : function (mediaElement, domObject) {
    // overwrite the "mediaElement" instance to use with IE < 9
    _player = mediaElement; // then refer to this to remove Flash objects in IE
    // autoplay video (optional)
    _player.play(); // <== we can also refer to the new instance for the play method
}
HINT : Notice we had to initialize the variable _player at the top of our script before we could use it throughout our callbacks

 
Now, calling the method _player.remove() from within the beforeClose fancyBox's callback should fix the first of the scenarios mentioned above.

beforeClose : function () {
    _player.remove();
}

Nonetheless, there are a couple of important warnings to consider :

WARNING : navigating to the next/prev element of a fancyBox gallery (if any) while the video is playing, will not close but transition fancyBox, therefore the method _player.remove() won't be called and the js error will be triggered

 
To prevent that, we also need to call the _player.remove() method from within the beforeLoad callback, to remove the flash objects before any next/prev element, with a new MEJS initialization is loaded.
 

WARNING : Since the _player instance is not created until the video starts playing, calling _player.remove() will trigger another js error if the video hasn't started yet or it hasn't played at least once

 
To prevent that, we need to add an event listener that monitors if the video, is either "playing" or it has been played at least once. Then we can enable the flag _isPlaying (initialized at the top of our script) after the event has been triggered :

success : function (mediaElement, domObject) {
    _player = mediaElement; // override the "mediaElement" instance to be used outside the success setting
    _player.play(); // autoplay video (optional)
    _player.addEventListener('playing', function () {
        _isPlaying = true;
    }, false);
} // success

Validating the _isPlaying flag within the beforeLoad (and beforeClose) callback(s), will help us to decide whether we should call the _player.remove() method or not :

beforeLoad : function () {
    // if video is playing and we navigate to next/prev element of a fancyBox gallery
    // safely remove Flash objects in IE
    if ( _isPlaying ) {
        // video is playing
        _player.remove(); // remove player instance for IE
        _isPlaying = false; // reinitialize flag
    };
    ... etc.
} // beforeLoad

Notice that we intended to use _player.remove() to fix an issue with IE, however, at this point _isPlaying will return true if the video is playing and it will call _player.remove() regardless the browser we are using.

Although calling _player.remove() won't have an impact in most browsers, it will trigger a js error (surprisingly) in IE9 and above (IE11 so far).

HINT : We could safely use $video_player.remove() for most browsers including IE9+ but not IE < 9 where we should use _player.remove()

 
To sort this potential issue out, we may need an additional step to detect the IE version. We will use the function suggested in the article Detecting IE more effectively at Microsft Developer Network website :

function getInternetExplorerVersion() {
    // Returns the version of Internet Explorer or -1 (other browser)
    var rv = -1; // Return value assumes failure.
    if (navigator.appName == 'Microsoft Internet Explorer') {
        var ua = navigator.userAgent;
        var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
        if (re.exec(ua) != null)
            rv = parseFloat(RegExp.$1);
    };
    return rv;
};
HINT : notice we already initialized the variable
_verIE = getInternetExplorerVersion() at the top of our script

 
Now we could validate the IE's version along the _isPlaying flag to decide whether to use _player.remove() or $video_player.remove()

beforeLoad : function () {
    // if video is playing and we navigate to next/prev element of a fancyBox gallery
    // safely remove Flash objects in IE
    if (_isPlaying && (_verIE > -1)) {
        // video is playing AND we are using IE
        _verIE < 9.0 ? _player.remove() : $video_player.remove(); // remove player instance for IE
        _isPlaying = false; // reinitialize flag
    };
    ... etc.
} // beforeLoad
HINT : Don't forget to apply the same validation within beforeClose callback

 

"fullscreen" button does nothing : IE, Opera

While viewing a video on IE or Opera, clicking the fullscreen button doesn't toggle the browser to fullscreen mode.

According to MEJS author "you do need to click on the Go Fullscreen text that appears above the button instead". This is (supposedly) due to IE (and some other browsers?) limitations.

MEJS fullscreen text

The issue is documented here (refer to John Dyer's post.)

IMPORTANT : The "Go Fullscreen" text will appear (on mouse hover) until the video is actually playing or it has already played at least once. If you haven't set an autoplay option, the text won't be visible first load.

 

The video hangs while trying to auto play after opening fancyBox : webkit browsers

It seems that webkit browsers fire some methods before the player is completely ready. This issue will be triggered if we set the (optional) _player.play() method in the success setting. This behavior is particularly true when MEJS is dynamically added to the DOM.

To workaround the issue, we could add an event listener to detect when the video canplay like :

success : function (mediaElement, domObject) {
    _player = mediaElement; // override the "mediaElement" instance to be used outside the success setting
    _player.addEventListener('canplay', function () {
        _canPlay = true;
    }, false);
    ... etc.
} // success
WARNING : HTML5 video events don't bubble and they won't fire on dynamically added elements

 
Since we are adding/removing the video tag dynamically into the DOM, we can't rely on canplay event listener because it won't fire but until the video is actually playing.

Notice the canplay event would fire for static (hard-coded) video tag(s) in our html page though.

The workaround that seems to do the trick is (re-)loading the player after it has been inserted, using the .load() method within the success setting like :

success : function (mediaElement, domObject) {
    _player = mediaElement; // override the "mediaElement" instance to be used outside the success setting
    _player.load(); // fixes webkit firing any method before player is ready
    _player.play(); // autoplay video (optional)
    _player.addEventListener('playing', function () {
        _isPlaying = true;
    }, false);
} // success

 

The working code and demo

You can see a fancyBox gallery of mp4 videos in the demo page

DEMO
 
The full working code as in the demo page :

// Detecting IE more effectively : http://msdn.microsoft.com/en-us/library/ms537509.aspx
function getInternetExplorerVersion() {
    // Returns the version of Internet Explorer or -1 (other browser)
    var rv = -1; // Return value assumes failure.
    if (navigator.appName == 'Microsoft Internet Explorer') {
        var ua = navigator.userAgent;
        var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
        if (re.exec(ua) != null)
            rv = parseFloat(RegExp.$1);
    };
    return rv;
};
// set some general variables
var $video_player, _videoHref, _videoPoster, _videoWidth, _videoHeight, _dataCaption, _player, _isPlaying = false, _verIE = getInternetExplorerVersion();
jQuery(document).ready(function ($) {  
    jQuery(".fancy_video")
    .prepend("<span class=\"playbutton\"/>") //cosmetic : append a play button image
    .fancybox({
        // set type of content (remember, we are building the HTML5 <video> tag as content)
        type       : "html",
        // other API options
        scrolling  : "no",
        padding    : 0,
        nextEffect : "fade",
        prevEffect : "fade",
        nextSpeed  : 0,
        prevSpeed  : 0,
        fitToView  : false,
        autoSize   : false,
        modal      : true, // hide default close and navigation buttons
        helpers    : {
            title  : {
                type : "over"
            },
            buttons: {} // use buttons helpers so navigation button won't overlap video controls
        },
        beforeLoad : function () {
            // if video is playing and we navigate to next/prev element of a fancyBox gallery
            // safely remove Flash objects in IE
            if (_isPlaying && (_verIE > -1)) {
                // video is playing AND we are using IE
                _verIE < 9.0 ? _player.remove() : $video_player.remove(); // remove player instance for IE
                _isPlaying = false; // reinitialize flag
            };
            // build the HTML5 video structure for fancyBox content with specific parameters
            _videoHref   = this.href;
            // validates if data values were passed otherwise set defaults
            _videoPoster = typeof this.element.data("poster")  !== "undefined" ? this.element.data("poster")  :  "";
            _videoWidth  = typeof this.element.data("width")   !== "undefined" ? this.element.data("width")   : 360;
            _videoHeight = typeof this.element.data("height")  !== "undefined" ? this.element.data("height")  : 360;
            _dataCaption = typeof this.element.data("caption") !== "undefined" ? this.element.data("caption") :  "";
            // construct fancyBox title (optional)
            this.title = _dataCaption ? _dataCaption : (this.title ? this.title : "");
            // set fancyBox content and pass parameters
            this.content = "<video id='video_player' src='" + _videoHref + "'  poster='" + _videoPoster + "' width='" + _videoWidth + "' height='" + _videoHeight + "'  controls='controls' preload='none' ></video>";
            // set fancyBox dimensions
            this.width = _videoWidth;
            this.height = _videoHeight;
        },
        afterShow : function () {
            // initialize MEJS player
            var $video_player = new MediaElementPlayer('#video_player', {
                    defaultVideoWidth : this.width,
                    defaultVideoHeight : this.height,
                    success : function (mediaElement, domObject) {
                        _player = mediaElement; // override the "mediaElement" instance to be used outside the success setting
                        _player.load(); // fixes webkit firing any method before player is ready
                        _player.play(); // autoplay video (optional)
                        _player.addEventListener('playing', function () {
                            _isPlaying = true;
                        }, false);
                    } // success
                });
        },
        beforeClose : function () {
            // if video is playing and we close fancyBox
            // safely remove Flash objects in IE
            if (_isPlaying && (_verIE > -1)) {
                // video is playing AND we are using IE
                _verIE < 9.0 ? _player.remove() : $video_player.remove(); // remove player instance for IE
                _isPlaying = false; // reinitialize flag
            };
        }
    });
}); // ready

This code has been tested with most modern web browsers, including IE7+ and iOS Twitter's build-in browser.

Download the code

For reference purposes, the complete demo file is available at
GitHub

Final notes

You can enhance your layout or adapt the fancyBox API options to your needs. Notice in our demo, we used the fancyBox buttons helper to avoid the gallery navigation arrows overlapping the video player controls. Also notice we disable transition effects because they didn't seem to render very well with videos, which take longer to display than other content.

You may want to play with your own options until your fancyBox renders the way you want it.

Disclaimer

All trademarks, videos and images remain property of their respective holders, and are used for demo purposes only.