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?
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.
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.
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.
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.
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 }
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 :
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.
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).
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; };
_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
"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.
The issue is documented here (refer to John Dyer's post.)
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
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.