Kendo UI has a nice array of widgets to offer. Not as vast as Telerik’s archiac RadControls suite, but it’s getting closer with every release. Every once in awhile, I wish Kendo had a missing widget available, instead of scouring the web for a carousel or a tag cloud. Fortunately, Kendo UI has a stellar framework where you can create your own widgets. In this post, I would like to introduce a new Kendo UI Media Player widget that is web, mobile, and MVVM compatible.
Yet Another Media Player?
Ok, so “yet another” media player you may ask. There are a couple of good ones out there, such as MediaElementand jPlayer, and even powerful video players like FlowPlayer, JW Player, SublimeVideo, and Video.js. However, a few things were lacking: no MVVM support, little or no playlist capability, and flexibility with integrating your own button controls. Making it a first-class citizen of Kendo UI is a nice added bonus.
To give you a peek at the end game, below is what we will end up:
It does video too. Are you excited yet? So let’s get started…
HTML5 Media Support
With HTML5, built-in media support via the <audio> and <video> elements were introduced, offering a rich API and the ability to easily embed media into HTML documents. All we are going to do is wrap the HTML5 API’s with a Kendo widget and expose some extended functionality. Forget falling back to Flash since this would unnecessarily complicate things and we really need Flash to rest in peace.
Media Events and Methods
Let’s determine what kind of events and methods to expose. For the native HTML5 <audio> and <video>elements, there are API’s we will simply expose through our widget:
HTML Media Element events:
- ended
- error
- loadeddata
- loadedmetadata
- loadstart
- pause
- play
- playing
- progress
- ratechange
- seeked
- seeking
- timeupdate
- volumechange
HTML Media Element methods:
- play
- pause
- stop
- playbackRate
- readyState
- seeking
- currentTime
- volume
- muted
On top of the native API’s, we will be providing our own sugar:
Custom events:
- change
- dataBinding
- dataBound
- playlistEnded
Custom methods:
- mediaSrc
- getMediaByFile
- getLoadedMedia
- isLastMedia
- add
- previous
- next
- toggleControls
- toggleLoop
- toggleLoopAll
- toggleContinuous
Notice implied by the events and methods, we will have a full fledged dataSource behind our Kendo Media Player widget.
Kendo UI Media Player Skeleton
It is time to lay down the skeleton that will be used for our widget. We will be extending kendo.ui.Widget to provide initialization and integration with Kendo UI dataSource and MVVM.
kendo.ui.plugin(kendo.ui.Widget.extend({ /** * Constructor * @param element * @param options */ init: function (element, options) { //BASE CALL TO WIDGET INITIALIZATION kendo.ui.Widget.fn.init.call(this, element, options); //INITIALIZE PARTS this.initStyles(); this.initElements(); this.initEvents(); this.initDataSource(); }, /** * Widget options for initialization */ options: { name: 'MediaPlayer', type: 'audio', preload: 'auto', autoBind: true, autoPlay: false, enableControls: true, enableLoop: false, enableLoopAll: true, enableContinuous: true, enablePlaylist: true, enableStyles: true, playSelector: null, pauseSelector: null, stopSelector: null, previousSelector: null, nextSelector: null, controlsSelector: null, loopSelector: null, loopAllSelector: null, continuousSelector: null, template: '', playlistTemplate: '' }, /** * Public API events used by widgets or MVVM */ events: [ //Called before mutating DOM DATABINDING, //Called after mutating DOM DATABOUND, //The metadata has loaded or changed, indicating a change in duration of the media. This is sent, for example, when the media has loaded enough that the duration is known. DURATIONCHANGE, //Sent when playback completes. ENDED, //Sent when an error occurs. The element's error attribute contains more information. ERROR, //The first frame of the media has finished loading. LOADEDDATA, //The media's metadata has finished loading; all attributes now contain as much useful information as they're going to. LOADEDMETADATA, //Sent when loading of the media begins. LOADSTART, //Sent when playback is paused. PAUSE, //Sent when playback of the media starts after having been paused; that is, when playback is resumed after a prior pause event. PLAY, //Sent when the media begins to play (either for the first time, after having been paused, or after ending and then restarting). PLAYING, //Sent periodically to inform interested parties of progress downloading the media. Information about the current amount of the media that has been downloaded is available in the media element's buffered attribute. PROGRESS, //Sent when the playback speed changes. RATECHANGE, //Sent when a seek operation completes. SEEKED, //Sent when a seek operation begins. SEEKING, //The time indicated by the element's currentTime attribute has changed. TIMEUPDATE, //Sent when the audio volume changes (both when the volume is set and when the muted attribute is changed). VOLUMECHANGE, //Sent when playlist completes. PLAYLISTENDED ], /** * Register CSS style rules */ initStyles: function () { }, /** * Create templates for rendering to DOM */ initElements: function () { }, /** * Bind events */ initEvents: function () { }, /** * Creates the data source */ initDataSource: function() { }, /** * Change data source dynamically via MVVM * @param dataSource */ setDataSource: function(dataSource) { }, /** * DOM elements that represent the output for MVVM */ items: function() { }, /** * Re-renders the widget with all associated data */ refresh: function() { }, /** * Set media source for HTML element * @param value */ mediaSrc: function (value) { }, /** * Gets media by file from data source * @param value */ getMediaByFile: function (value) { }, /** * Get currently loaded media * @returns {*} */ getLoadedMedia: function () { }, /** * Is the loaded media the last in the playlist * @returns {boolean} */ isLastMedia: function () { }, /** * Add media to data source * @param value */ add: function (value) { }, /** * Plays media */ play: function (value) { }, /** * Pauses media */ pause: function () { }, /** * Stops media */ stop: function () { }, /** * The current rate at which the media is being played back. * @param value * @returns {*} */ playbackRate: function (value) { }, /** * The readiness state of the media. */ readyState: function () { }, /** * Indicates whether the media is in the process of seeking to a new position. */ seeking: function () { }, /** * Seek to specified seconds * or returns the number of seconds the browser has played */ currentTime: function (value) { }, /** * Increase or decrease volume of player * @param value * @returns volume */ volume: function (value) { }, /** * Gets or sets muting the player * @param value * @returns {*} */ muted: function (value) { }, /** * Go to the previous media */ previous: function () { }, /** * Go to the next media */ next: function () { }, /** * Enables or disables controls * @param value */ toggleControls: function (value) { }, /** * Enables or disables loop functionality * @param value */ toggleLoop: function (value) { }, /** * Enables or disables loop all functionality * @param value */ toggleLoopAll: function (value) { }, /** * Enables or disables continuous functionality * @param value */ toggleContinuous: function (value) { } }));
This is basically our API for our Kendo UI Media Player widget. Notice the options to configure it, along with the available events and methods it will use.
To enable MVVM support for binding properties and events to observable objects, we will also have to add custom binders at the end. Below is the skeleton for that. Notice I first have to create a “mediaplayer” namespace under the widget binders. That way, I’m not listening to other widgets for these binder, nor am I overwriting any other widget’s binders either. Thanks to my teammate, Jeff Valore, for this valuable, undocumented piece of knowledge.
kendo.data.binders.widget.mediaplayer = {}; kendo.data.binders.widget.mediaplayer.controls = kendo.data.Binder.extend }); kendo.data.binders.widget.mediaplayer.loop = kendo.data.Binder.extend({ }); kendo.data.binders.widget.mediaplayer.loopAll = kendo.data.Binder.extend({ }); kendo.data.binders.widget.mediaplayer.continuous = kendo.data.Binder.extend }); kendo.data.binders.widget.mediaplayer.playlistended = kendo.data.Binder.extend({ }); //BASE BINDER FOR MVVM MEDIA EVENTS var MediaBinder = kendo.data.Binder.extend({ }); //BIND MVVM MEDIA EVENTS kendo.data.binders.widget.mediaplayer.durationchange = MediaBinder.extend({ eventName: DURATIONCHANGE }); kendo.data.binders.widget.mediaplayer.ended = MediaBinder.extend({ eventName: ENDED }); kendo.data.binders.widget.mediaplayer.error = MediaBinder.extend({ eventName: ERROR }); kendo.data.binders.widget.mediaplayer.loadeddata = MediaBinder.extend({ eventName: LOADEDDATA }); kendo.data.binders.widget.mediaplayer.loadedmetadata = MediaBinder.extend({ eventName: LOADEDMETADATA }); kendo.data.binders.widget.mediaplayer.loadstart = MediaBinder.extend({ eventName: LOADSTART }); kendo.data.binders.widget.mediaplayer.pause = MediaBinder.extend({ eventName: PAUSE }); kendo.data.binders.widget.mediaplayer.play = MediaBinder.extend({ eventName: PLAY }); kendo.data.binders.widget.mediaplayer.playing = MediaBinder.extend({ eventName: PLAYING }); kendo.data.binders.widget.mediaplayer.progress = MediaBinder.extend({ eventName: PROGRESS }); kendo.data.binders.widget.mediaplayer.ratechange = MediaBinder.extend({ eventName: RATECHANGE }); kendo.data.binders.widget.mediaplayer.seeked = MediaBinder.extend({ eventName: SEEKED }); kendo.data.binders.widget.mediaplayer.seeking = MediaBinder.extend({ eventName: SEEKING }); kendo.data.binders.widget.mediaplayer.timeupdate = MediaBinder.extend({ eventName: TIMEUPDATE }); kendo.data.binders.widget.mediaplayer.volumechange = MediaBinder.extend({ eventName: VOLUMECHANGE });
Kendo UI Media Player Flesh
We are now ready add flesh to our widget. I will cover it step-by-step, then offer the complete widget at the end. First, let’s add a custom CSS class to our widget in case someone’s to reference or style it later. We do this in the init:
init: function (element, options) { //BASE CALL TO WIDGET INITIALIZATION kendo.ui.Widget.fn.init.call(this, element, options); //ADD CSS TO WIDGET FOR STYLING this.element.addClass('k-mediaplayer'); //INITIALIZE PARTS this.initStyles(); this.initElements(); this.initEvents(); this.initDataSource(); },
Kendo UI Media Player DOM
For the initialization of the widget parts, we will add our own styles and mutate the DOM. I will add styles programmatically so I don’t have to have a dependency on an outside stylesheet file. It is only a few CSS rules, so it’s not so bad.
initStyles: function () { //ADD CSS RULES DYNAMICALLY var addCssRule = function(styles) { var style = document.createElement('style'); style.type = 'text/css'; if (style.styleSheet) style.styleSheet.cssText = styles; //IE else style.innerHTML = styles; //OTHERS document.getElementsByTagName('head')[0].appendChild(style); }; if (this.options.enableStyles) { //ADD CSS RULES FOR WIDGET addCssRule('.k-mediaplayer { width: 99%; padding: 10px; }'); addCssRule('.km-root .k-mediaplayer { width: 100%; }'); addCssRule('.k-mediaplayer audio, .k-mediaplayer video { width: 100%; }'); addCssRule('.k-mediaplayer .playlist { padding: 0; margin: 0; border-top: 1px dotted #ccc; }'); addCssRule('.k-mediaplayer .playlist li { cursor: pointer; list-style: none; padding: 3px; padding: 10px; border: 1px dotted #ccc; border-top: none; }'); addCssRule('.k-mediaplayer .playlist li:hover, .k-mediaplayer .playlist li.selected { background-color: #ddd; }'); } }
For the DOM element manipulation, I will create the HTML element using our private method called _createMediaTag, which will be based on whether the user passed in ‘audio‘ or ‘video‘ for the media type. Then it will create the HTML media tag accordingly. We will also render the playlist if enabled using a Kendo template for easy binding to an array of objects. Also, caching the HTML elements into the widget properties would be convenient and preformant for later use.
initElements: function () { var templateHtml = ''; //BUILD AND CACHE OUTPUT FOR RENDERING MEDIA CONTROL this.element.html(this._createMediaTag()); this.mediaElement = this.element.find(this.options.type)[0]; //BUILD PLAYLIST TEMPLATE IF APPLICABLE if (this.options.enablePlaylist) { //BUILD OUTPUT FOR RENDERING PLAYLIST templateHtml += this.options.playlistTemplate || ('<ul class="playlist"># for (var i = 0; i < data.length; i++) { #' + '<li data-file="#= data[i].file #">#= data[i].title #</li>' + '# } #</ul>'); } //CACHE CONTENT PLACEHOLDERS FOR LATER USE this.element.append('<div class="content-wrapper"></div>'); this.contentElement = this.element.find('.content-wrapper'); //COMPILE TEMPLATE FOR LATER USE this.template = kendo.template(templateHtml); }
In our widget, we must have a refresh method that will be called when the DOM needs to be updated. This update will be triggered by a consuming developer or automatically by MVVM when data changes. Below you will notice that we are explicitly triggering the dataBinding event before the rendering and after it. That will call any associated events subscribed to it.
For the actual rendering itself, we bind our dataSource to the template we created in the initElement method. This will hold our playlist, then we add it to the content placeholder that we also created in the initElementmethod. In case the media type was changed, we have to replace the HTML element in the DOM also.
/** * Re-renders the widget with all associated data */ refresh: function() { //TRIGGER DATA BINDING BEFORE RENDER this.trigger(DATABINDING); //INITIALIZE VARIABLES var view = this.dataSource.view(), html = view.length ? this.template(view) : ''; //RENDER DATA TO DOM PLACEHOLDER this.contentElement.html(html); //REPLACE MEDIA DOM ELEMENT IF APPLICABLE if (this.options.type != this.mediaElement.tagName.toLowerCase()) { var temp = $(this._createMediaTag()); $(this.mediaElement).replaceWith(temp); this.mediaElement = temp[0]; } //POPULATE FIRST MEDIA IF NONE LOADED OR DOES NOT EXIST if (view.length && (!this.mediaSrc() || !this.getMediaByFile(this.mediaSrc()))) { this.mediaSrc(view[0].file); this.refreshDisplay(); } //TRIGGER DATA BINDING AFTER RENDER COMPLETE this.trigger(DATABOUND); }, /** * Updates the interface based on new or updated media */ refreshDisplay: function () { var me = this; var playlistItems = this.contentElement.find('.playlist li'); //RESET DISPLAY playlistItems.removeClass('selected'); //SELECT ACTIVE MEDIA FROM PLAYLIST IF APPLICABLE if (this.mediaSrc()) { playlistItems.each(function () { var $this = $(this); //MATCH LOADED MEDIA TO PLAYLIST ITEM if ($this.attr('data-file') == me.mediaSrc()) { //ACTIVATE ELEMENT $this.addClass('selected'); return false; } }); } }
When the rendering is complete, we call our own method above, refreshDisplay, to handle any selected or active elements. These are just indicators to the user, such as which playlist file is currently being played.
Kendo UI Media Player Events
Next comes the handling of events. We will bind custom events to our widget and native events directly to the HTML media element.
/** * Bind events */ initEvents: function () { var me = this; var $document = $(document.body); //DETERMINE CLICK EVENT TO USE //http://stackoverflow.com/questions/10165141/jquery-on-and-delegate-doesnt-work-on-ipad var clickEventName = (kendo.support.mobileOS && kendo.support.mobileOS.browser == 'mobilesafari') ? 'touchstart' : 'click'; //BIND MEDIA EVENTS (FOR NON-MVVM) this._bindMedia(DURATIONCHANGE, this.options.durationChange); this._bindMedia(ENDED, this.options.ended); this._bindMedia(ERROR, this.options.error); this._bindMedia(LOADEDDATA, this.options.loadedData); this._bindMedia(LOADEDMETADATA, this.options.loadedMetadata); this._bindMedia(LOADSTART, this.options.loadStart); this._bindMedia(PAUSE, this.options.pause); this._bindMedia(PLAY, this.options.play); this._bindMedia(PLAYING, this.options.playing); this._bindMedia(PROGRESS, this.options.progress); this._bindMedia(RATECHANGE, this.options.rateChange); this._bindMedia(SEEKED, this.options.seeked); this._bindMedia(SEEKING, this.options.seeking); this._bindMedia(TIMEUPDATE, this.options.timeUpdate); this._bindMedia(VOLUMECHANGE, this.options.volumeChange); //HANDLE PLAYLIST IF APPLICABLE this._bindMedia(ENDED, function () { //HANDLE LAST ITEM IF APPLICABLE if (me.isLastMedia()) { //EXECUTE CALLBACK FOR ENDED PLAYLIST me.trigger(PLAYLISTENDED); //LOOP TO BEGINNING IF APPLICABLE if (me.options.enableLoopAll) { me.next(); } else { me.stop(); } } else { //PLAY NEXT ITEM IN PLAYLIST IF APPLICABLE if (me.options.enableContinuous) { me.next(); } else { me.stop(); } } }); //HANDLE PLAYLIST ENDED EVENT IF APPLICABLE (FOR NON-MVVM) if (this.options.playlistEnded) { this.bind(PLAYLISTENDED, this.options.playlistEnded); } //SCRUB MEDIA PROPERTIES IF NEEDED this.bind(DATABINDING, function () { var data = this.dataSource.data(); //ITERATE THROUGH MEDIA for (var i = 0; i < data.length; i++) { //ASSIGN TITLE IF APPLICABLE if (!data[i].title) { data[i].title = me._convertFileToTitle(data[i].file); } } }); //HANDLE PLAYLIST EVENTS if (this.options.enablePlaylist) { //ALLOW NAVIGATION OF PLAYLIST ITEMS this.contentElement.on('click', '.playlist li', function () { //LOAD SELECTED ITEM FROM PLAYLIST me.play($(this).attr('data-file')); }); } //SUBSCRIBE EVENTS TO CUSTOM SELECTORS IF APPLICABLE if (this.options.playSelector) { $document.on(clickEventName, this.options.playSelector, function (e) { e.preventDefault(); me.play(); }); } if (this.options.pauseSelector) { $document.on(clickEventName, this.options.pauseSelector, function (e) { e.preventDefault(); me.pause(); }); } if (this.options.stopSelector) { $document.on(clickEventName, this.options.stopSelector, function (e) { e.preventDefault(); me.stop(); }); } if (this.options.previousSelector) { $document.on(clickEventName, this.options.previousSelector, function (e) { e.preventDefault(); me.previous(); }); } if (this.options.nextSelector) { $document.on(clickEventName, this.options.nextSelector, function (e) { e.preventDefault(); me.next(); }); } }, /** * Binds event to media player * @param name * @param callback */ _bindMedia: function (name, callback) { if (name && callback) { //HANDLE BINDING FOR MEDIA PLAYER AFTER RENDER this.bind(DATABOUND, function () { if (this.mediaElement) { //BIND EVENT TO MEDIA PLAYER AFTER IT RENDERS this.mediaElement.addEventListener(name, callback); } }); } }
I am determining the click event name based on whether it is iOS or not. Apparently, there is a restriction on mobile Safari that won’t let you bubble up delegate events all the way to the document body, unless it’s a touchstart event. I am using this later in the method to bind selectors that were configured by the consuming developer. That way, you have full control over what elements on your page control the media player.
Also in the events initialization, I am binding all the native events directly to the actual HTML media element. I wrapped it in a private function called _bindMedia so it was more convenience, but eventually it simply does this.mediaElement.addEventListener(name, callback).
I added my own custom event to the media ended event to handle continuous play and triggering my custom widget event called playlistEnded. I am doing this by triggering the event, which I bound later directly to my widget using this.bind(PLAYLISTENDED, this.options.playlistEnded).
I am also scrubbing my data before rendering using the dataBinding event. If the playlist is enabled, I am wiring up some click events to navigate through it.
Kendo UI Media Player DataSource
The DataSource is what makes our widget very powerful. We can feed with an array or web service. And the DataSource itself has a vast API that is automatically exposed through our widget. Below is the initialization of it:
/** * Creates the data source */ initDataSource: function() { //IF DATA SOURCE IS DEFINED AND THE REFRESH HANDLER IS WIRED UP, //UNBIND BECAUSE DATA SOURCE MUST BE REBUILT if (this.dataSource && this._refreshHandler) { //UNBIND SO BINDING CAN BE WIRED UP AFTER DATA SOURCE CREATION this.dataSource.unbind(CHANGE, this._refreshHandler); } else { //CREATE CONNECTION BETWEEN INTERNAL _refreshHandler AND PUBLIC REFRESH FUNCTION this._refreshHandler = $.proxy(this.refresh, this); } //CREATE DATA SOURCE FROM ARRAY OR CONFIG OBJECT this.dataSource = kendo.data.DataSource.create(this.options.dataSource); //NOW BIND DATA SOURCE TO REFRESH OF WIDGET this.dataSource.bind(CHANGE, this._refreshHandler); //FETCH DATA FIRST TIME IF APPLICABLE if (this.options.autoBind) { this.dataSource.fetch(); } }, /** * Change data source dynamically via MVVM * @param dataSource */ setDataSource: function(dataSource) { //SET THE INTERNAL DATA SOURCE EQUAL TO THE ONE PASSED IN BY MVVM this.options.dataSource = dataSource; //REBUILD THE DATA SOURCE IF NECESSARY OR JUST REASSIGN this.initDataSource(); }
There is a _refreshHandler method that is inherited from the Kendo widget base class. This is used by MVVM to automatically trigger it once it needs to refresh the widget on some observable change. We need to be aware of that event. If it has been bound before for some reason, we first unbind it so it doesn’t get bound multiple times to the DataSource.
We then wire up the _refreshHandler to our own refresh method by using $.proxy. It’s just another way to alias methods together.
Now comes the part where we take the consuming developer’s data source that was fed during the configuration of the media player and create a DataSource out of it. By using kendo.data.DataSource.create, we are giving the option to configure the widget using a DataSource object or a plain array.
Finally, we are ready to bind the change event to the _refreshHandler when it changes. Basically, it will re-render it when the data has changed. Our widget is also aware of the autoBind property, which it will fetch if set (fetch only calls the remote data once even if fetch is called multiple times).
For MVVM, we overrode the setDataSource method so it can set and re-initialize the DataSource when needed.
Kendo UI Media Player Methods
Although, I will not cover logic in all the methods, I will provide you with an example of how I am wrapping the HTML media element API:
/** * Pauses media */ pause: function () { this.mediaElement.pause(); }
The mediaElement property is one of the elements I cached in the initElements method. Throughout my widget, I can refer to this reference instead of constantly trying to search the DOM for it. This mediaElement is simply the <audio> or <video> element. The “pause()” method is a native HTML5 method on the DOM element, it’s not even jQuery! I am just calling it in the underlying logic of my widget methods.
Kendo UI Media Player Binders
To allow consuming developers to bind properties and events in the “data-bind” attribute in the HTML, I have to wire up some binders, otherwise I will get an error from Kendo saying: The xyz binding is not supported by the MediaPlayer widget. Although there is some confusion around it sometimes, it’s not so bad once you see an actual working example:
kendo.data.binders.widget.mediaplayer.continuous = kendo.data.Binder.extend({ refresh: function () { var value = this.bindings.continuous.get(); var widget = this.element; widget.toggleContinuous(value); } }); kendo.data.binders.widget.mediaplayer.playlistended = kendo.data.Binder.extend({ init: function (widget, bindings, options) { var me = this; kendo.data.Binder.fn.init.call(this, widget, bindings, options); //HANDLE BINDING FOR MEDIA PLAYER widget.bind(PLAYLISTENDED, function () { //EXECUTE CALLBACK OPTION me.bindings.playlistended.get(); }); }, refresh: function () {} }); //BASE BINDER FOR MVVM MEDIA EVENTS var MediaBinder = kendo.data.Binder.extend({ eventName: null, init: function (widget, bindings, options) { var me = this; kendo.data.Binder.fn.init.call(this, widget, bindings, options); //HANDLE BINDING FOR MEDIA PLAYER widget._bindMedia(this.eventName, function () { me.bindings[me.eventName].get(); }); }, refresh: function () {} }); //BIND MVVM MEDIA EVENTS kendo.data.binders.widget.mediaplayer.durationchange = MediaBinder.extend({ eventName: DURATIONCHANGE });
For the continuous binder, I am only overriding the refresh event. That’s because all I need to do is first check if the widget is a media player, then call one of the underlying methods. I pass in the value that is bound from the observable object, which is coming from this.bindings.continuous.get(). This will get triggered every time that property changes.
The playlistended event is handled a bit differently since it needs to be bound to the widget and not simply a widget method call. We do this in the initialization of the binder simply by calling widget.bind. Now any time widget.trigger is called against the same event, this function will get called along with any other functions subscribed to the event. We actually trigger the playlistended event in the in the initEvents method based on some criteria.
The last set of binders are the binding of events to the native HTML media element. I extended the kendo.data.Binder class to be used for this set of bindings. So for durationchange, ended, error, playing, pause, etc, it will call the _bindMedia method from our widget, which if you remember, eventually calls this.mediaElement.addEventListener.
Putting It All Together
With the above explanations, let’s see the completed code to get a full overview of it:
/** * Widget for playing audio */ var CHANGE = 'change', DATABINDING = 'dataBinding', DATABOUND = 'dataBound', DURATIONCHANGE = 'durationchange', ENDED = 'ended', ERROR = 'error', LOADEDDATA = 'loadeddata', LOADEDMETADATA = 'loadedmetadata', LOADSTART = 'loadstart', PAUSE = 'pause', PLAY = 'play', PLAYING = 'playing', PROGRESS = 'progress', RATECHANGE = 'ratechange', SEEKED = 'seeked', SEEKING = 'seeking', TIMEUPDATE = 'timeupdate', VOLUMECHANGE = 'volumechange', PLAYLISTENDED = 'playlistEnded'; kendo.ui.plugin(kendo.ui.Widget.extend({ mediaElement: null, contentElement: null, dataSource: null, /** * Constructor * @param element * @param options */ init: function (element, options) { //BASE CALL TO WIDGET INITIALIZATION kendo.ui.Widget.fn.init.call(this, element, options); //ADD CSS TO WIDGET FOR STYLING this.element.addClass('k-mediaplayer'); //INITIALIZE PARTS this.initStyles(); this.initElements(); this.initEvents(); this.initDataSource(); }, /** * Widget options for initialization */ options: { name: 'MediaPlayer', type: 'audio', preload: 'auto', autoBind: true, autoPlay: false, enableControls: true, enableLoop: false, enableLoopAll: true, enableContinuous: true, enablePlaylist: true, enableStyles: true, playSelector: null, pauseSelector: null, stopSelector: null, previousSelector: null, nextSelector: null, controlsSelector: null, loopSelector: null, loopAllSelector: null, continuousSelector: null, template: '', playlistTemplate: '' }, /** * Public API events used by widgets or MVVM */ events: [ //Called before mutating DOM DATABINDING, //Called after mutating DOM DATABOUND, //The metadata has loaded or changed, indicating a change in duration of the media. This is sent, for example, when the media has loaded enough that the duration is known. DURATIONCHANGE, //Sent when playback completes. ENDED, //Sent when an error occurs. The element's error attribute contains more information. ERROR, //The first frame of the media has finished loading. LOADEDDATA, //The media's metadata has finished loading; all attributes now contain as much useful information as they're going to. LOADEDMETADATA, //Sent when loading of the media begins. LOADSTART, //Sent when playback is paused. PAUSE, //Sent when playback of the media starts after having been paused; that is, when playback is resumed after a prior pause event. PLAY, //Sent when the media begins to play (either for the first time, after having been paused, or after ending and then restarting). PLAYING, //Sent periodically to inform interested parties of progress downloading the media. Information about the current amount of the media that has been downloaded is available in the media element's buffered attribute. PROGRESS, //Sent when the playback speed changes. RATECHANGE, //Sent when a seek operation completes. SEEKED, //Sent when a seek operation begins. SEEKING, //The time indicated by the element's currentTime attribute has changed. TIMEUPDATE, //Sent when the audio volume changes (both when the volume is set and when the muted attribute is changed). VOLUMECHANGE, //Sent when playlist completes. PLAYLISTENDED ], /** * Register CSS style rules */ initStyles: function () { //ADD CSS RULES DYNAMICALLY var addCssRule = function(styles) { var style = document.createElement('style'); style.type = 'text/css'; if (style.styleSheet) style.styleSheet.cssText = styles; //IE else style.innerHTML = styles; //OTHERS document.getElementsByTagName('head')[0].appendChild(style); }; if (this.options.enableStyles) { //ADD CSS RULES FOR WIDGET addCssRule('.k-mediaplayer { width: 99%; padding: 10px; }'); addCssRule('.km-root .k-mediaplayer { width: 100%; }'); addCssRule('.k-mediaplayer audio, .k-mediaplayer video { width: 100%; }'); addCssRule('.k-mediaplayer .playlist { padding: 0; margin: 0; border-top: 1px dotted #ccc; }'); addCssRule('.k-mediaplayer .playlist li { cursor: pointer; list-style: none; padding: 3px; padding: 10px; border: 1px dotted #ccc; border-top: none; }'); addCssRule('.k-mediaplayer .playlist li:hover, .k-mediaplayer .playlist li.selected { background-color: #ddd; }'); } }, /** * Create templates for rendering to DOM */ initElements: function () { var templateHtml = ''; //BUILD AND CACHE OUTPUT FOR RENDERING MEDIA CONTROL this.element.html(this._createMediaTag()); this.mediaElement = this.element.find(this.options.type)[0]; //BUILD PLAYLIST TEMPLATE IF APPLICABLE if (this.options.enablePlaylist) { //BUILD OUTPUT FOR RENDERING PLAYLIST templateHtml += this.options.playlistTemplate || ('<ul class="playlist"># for (var i = 0; i < data.length; i++) { #' + '<li data-file="#= data[i].file #">#= data[i].title #</li>' + '# } #</ul>'); } //CACHE CONTENT PLACEHOLDERS FOR LATER USE this.element.append('<div class="content-wrapper"></div>'); this.contentElement = this.element.find('.content-wrapper'); //COMPILE TEMPLATE FOR LATER USE this.template = kendo.template(templateHtml); }, /** * Bind events */ initEvents: function () { var me = this; var $document = $(document.body); //DETERMINE CLICK EVENT TO USE //http://stackoverflow.com/questions/10165141/jquery-on-and-delegate-doesnt-work-on-ipad var clickEventName = (kendo.support.mobileOS && kendo.support.mobileOS.browser == 'mobilesafari') ? 'touchstart' : 'click'; //BIND MEDIA EVENTS (FOR NON-MVVM) this._bindMedia(DURATIONCHANGE, this.options.durationChange); this._bindMedia(ENDED, this.options.ended); this._bindMedia(ERROR, this.options.error); this._bindMedia(LOADEDDATA, this.options.loadedData); this._bindMedia(LOADEDMETADATA, this.options.loadedMetadata); this._bindMedia(LOADSTART, this.options.loadStart); this._bindMedia(PAUSE, this.options.pause); this._bindMedia(PLAY, this.options.play); this._bindMedia(PLAYING, this.options.playing); this._bindMedia(PROGRESS, this.options.progress); this._bindMedia(RATECHANGE, this.options.rateChange); this._bindMedia(SEEKED, this.options.seeked); this._bindMedia(SEEKING, this.options.seeking); this._bindMedia(TIMEUPDATE, this.options.timeUpdate); this._bindMedia(VOLUMECHANGE, this.options.volumeChange); //HANDLE PLAYLIST IF APPLICABLE this._bindMedia(ENDED, function () { //HANDLE LAST ITEM IF APPLICABLE if (me.isLastMedia()) { //EXECUTE CALLBACK FOR ENDED PLAYLIST me.trigger(PLAYLISTENDED); //LOOP TO BEGINNING IF APPLICABLE if (me.options.enableLoopAll) { me.next(); } else { me.stop(); } } else { //PLAY NEXT ITEM IN PLAYLIST IF APPLICABLE if (me.options.enableContinuous) { me.next(); } else { me.stop(); } } }); //HANDLE PLAYLIST ENDED EVENT IF APPLICABLE (FOR NON-MVVM) if (this.options.playlistEnded) { this.bind(PLAYLISTENDED, this.options.playlistEnded); } //SCRUB MEDIA PROPERTIES IF NEEDED this.bind(DATABINDING, function () { var data = this.dataSource.data(); //ITERATE THROUGH MEDIA for (var i = 0; i < data.length; i++) { //ASSIGN TITLE IF APPLICABLE if (!data[i].title) { data[i].title = me._convertFileToTitle(data[i].file); } } }); //HANDLE PLAYLIST EVENTS if (this.options.enablePlaylist) { //ALLOW NAVIGATION OF PLAYLIST ITEMS this.contentElement.on('click', '.playlist li', function () { //LOAD SELECTED ITEM FROM PLAYLIST me.play($(this).attr('data-file')); }); } //SUBSCRIBE EVENTS TO CUSTOM SELECTORS IF APPLICABLE if (this.options.playSelector) { $document.on(clickEventName, this.options.playSelector, function (e) { e.preventDefault(); me.play(); }); } if (this.options.pauseSelector) { $document.on(clickEventName, this.options.pauseSelector, function (e) { e.preventDefault(); me.pause(); }); } if (this.options.stopSelector) { $document.on(clickEventName, this.options.stopSelector, function (e) { e.preventDefault(); me.stop(); }); } if (this.options.previousSelector) { $document.on(clickEventName, this.options.previousSelector, function (e) { e.preventDefault(); me.previous(); }); } if (this.options.nextSelector) { $document.on(clickEventName, this.options.nextSelector, function (e) { e.preventDefault(); me.next(); }); } }, /** * Creates the data source */ initDataSource: function() { //IF DATA SOURCE IS DEFINED AND THE REFRESH HANDLER IS WIRED UP, //UNBIND BECAUSE DATA SOURCE MUST BE REBUILT if (this.dataSource && this._refreshHandler) { //UNBIND SO BINDING CAN BE WIRED UP AFTER DATA SOURCE CREATION this.dataSource.unbind(CHANGE, this._refreshHandler); } else { //CREATE CONNECTION BETWEEN INTERNAL _refreshHandler AND PUBLIC REFRESH FUNCTION this._refreshHandler = $.proxy(this.refresh, this); } //CREATE DATA SOURCE FROM ARRAY OR CONFIG OBJECT this.dataSource = kendo.data.DataSource.create(this.options.dataSource); //NOW BIND DATA SOURCE TO REFRESH OF WIDGET this.dataSource.bind(CHANGE, this._refreshHandler); //FETCH DATA FIRST TIME IF APPLICABLE if (this.options.autoBind) { this.dataSource.fetch(); } }, /** * Change data source dynamically via MVVM * @param dataSource */ setDataSource: function(dataSource) { //SET THE INTERNAL DATA SOURCE EQUAL TO THE ONE PASSED IN BY MVVM this.options.dataSource = dataSource; //REBUILD THE DATA SOURCE IF NECESSARY OR JUST REASSIGN this.initDataSource(); }, /** * DOM elements that represent the output for MVVM */ items: function() { return this.element.find('.playlist li'); }, /** * Re-renders the widget with all associated data */ refresh: function() { //TRIGGER DATA BINDING BEFORE RENDER this.trigger(DATABINDING); //INITIALIZE VARIABLES var view = this.dataSource.view(), html = view.length ? this.template(view) : ''; //RENDER DATA TO DOM PLACEHOLDER this.contentElement.html(html); //REPLACE MEDIA DOM ELEMENT IF APPLICABLE if (this.options.type != this.mediaElement.tagName.toLowerCase()) { var temp = $(this._createMediaTag()); $(this.mediaElement).replaceWith(temp); this.mediaElement = temp[0]; } //POPULATE FIRST MEDIA IF NONE LOADED OR DOES NOT EXIST if (view.length && (!this.mediaSrc() || !this.getMediaByFile(this.mediaSrc()))) { this.mediaSrc(view[0].file); this.refreshDisplay(); } //TRIGGER DATA BINDING AFTER RENDER COMPLETE this.trigger(DATABOUND); }, /** * Updates the interface based on new or updated media */ refreshDisplay: function () { var me = this; var playlistItems = this.contentElement.find('.playlist li'); //RESET DISPLAY playlistItems.removeClass('selected'); //SELECT ACTIVE MEDIA FROM PLAYLIST IF APPLICABLE if (this.mediaSrc()) { playlistItems.each(function () { var $this = $(this); //MATCH LOADED MEDIA TO PLAYLIST ITEM if ($this.attr('data-file') == me.mediaSrc()) { //ACTIVATE ELEMENT $this.addClass('selected'); return false; } }); } }, /** * Set media source for HTML element * @param value */ mediaSrc: function (value) { if (value || value === '') { this.mediaElement.src = value; if (value) this.mediaElement.load(); } return $(this.mediaElement).attr('src'); }, /** * Gets media by file from data source * @param value */ getMediaByFile: function (value) { //VALIDATE INPUT if (!value) return; var data = this.dataSource.data(); for (var i = 0; i < data.length; i++) { //FIND MATCHING MEDIA FRoM DATA SOURCE if (value == data[i].file) { return data[i]; break; } } }, /** * Get currently loaded media * @returns {*} */ getLoadedMedia: function () { return this.getMediaByFile(this.mediaSrc()); }, /** * Is the loaded media the last in the playlist * @returns {boolean} */ isLastMedia: function () { //VALIDATE if (!this.mediaSrc() || !this.dataSource.total()) return false; //DETERMINE IF LOADED MEDIA IS THE LAST TO PLAY return this.getLoadedMedia().file == this.dataSource.at(this.dataSource.total() - 1).file; }, /** * Add media to data source * @param value */ add: function (value) { //VALIDATE INPUT if (!value) return; //CONVERT FILE TO OBJECT IF APPLICABLE if (typeof value == 'string') { value = { file: value }; } if (value.file) { //ASSIGN TITLE IF APPLICABLE if (!value.title) { value.title = this._convertFileToTitle(value.file); } //ADD TO DATA SOURCE AND RETURN VALUE this.dataSource.add(value); return value; } }, /** * Plays media */ play: function (value) { //HANDLE SUPPLIED MEDIA IF APPLICABLE if (value) { //RETRIEVE DATA FROM SOURCE IF APPLICABLE if (typeof value == 'string') { //ADD OR GET EXISTING MEDIA OBJECT value = this.getMediaByFile(value) || this.add(value); } else if (!this.getMediaByFile(value.file)) { //ADD TO DATA SOURCE IF APPLICABLE value = this.add(value); } //LOAD MEDIA TO PLAYER this.mediaSrc(value.file); } //POPULATE FIRST MEDIA IF APPLICABLE if (!this.mediaSrc() && this.dataSource.total()) { this.mediaSrc(this.dataSource.at(0).file); } //PLAY MEDIA this.mediaElement.play(); //UPDATE INTERFACE this.refreshDisplay(); }, /** * Pauses media */ pause: function () { this.mediaElement.pause(); }, /** * Stops media */ stop: function () { this.pause(); this.mediaSrc(''); //UPDATE INTERFACE this.refreshDisplay(); }, /** * The current rate at which the media is being played back. * @param value * @returns {*} */ playbackRate: function (value) { if ($.isNumeric(value)) { this.mediaElement.playbackRate = value; } return this.mediaElement.playbackRate; }, /** * The readiness state of the media. */ readyState: function () { return this.mediaElement.readyState; }, /** * Indicates whether the media is in the process of seeking to a new position. */ seeking: function () { return this.mediaElement.seeking; }, /** * Seek to specified seconds * or returns the number of seconds the browser has played */ currentTime: function (value) { if ($.isNumeric(value)) { this.mediaElement.currentTime = value; } return this.mediaElement.currentTime; }, /** * Increase or decrease volume of player * @param value * @returns volume */ volume: function (value) { if ($.isNumeric(value)) { this.mediaElement.volume = value; } return this.mediaElement.volume; }, /** * Gets or sets muting the player * @param value * @returns {*} */ muted: function (value) { if (value === true || value === false) { this.mediaElement.muted = value; } return this.mediaElement.muted; }, /** * Go to the previous media */ previous: function () { var data = this.dataSource.data(); for (var i = 0; i < data.length; i++) { if (this.mediaSrc() == data[i].file) { //LAST FILE IN PLAYLIST if (i == 0) { this.stop(); break; } //UPDATE MEDIA PLAYER this.play(data[i - 1].file); break; } } }, /** * Go to the next media */ next: function () { var data = this.dataSource.data(); for (var i = 0; i < data.length; i++) { if (this.mediaSrc() == data[i].file) { //LAST FILE IN PLAYLIST if (i == data.length - 1) { //DETERMINE NEXT IF AT THE END if (this.options.enableLoopAll) this.play(data[0].file); else this.stop(); break; } //UPDATE MEDIA PLAYER this.play(data[i + 1].file); break; } } }, /** * Enables or disables controls * @param value */ toggleControls: function (value) { this.options.enableControls = this._toggleMediaAttribute('controls', value); }, /** * Enables or disables loop functionality * @param value */ toggleLoop: function (value) { this.options.enableLoop = this._toggleMediaAttribute('loop', value); }, /** * Enables or disables loop all functionality * @param value */ toggleLoopAll: function (value) { this.options.enableLoopAll = value || (value !== false && !this.options.enableLoopAll); }, /** * Enables or disables continuous functionality * @param value */ toggleContinuous: function (value) { this.options.enableContinuous = value || (value !== false && !this.options.enableContinuous); }, /** * Build media tag for HTML DOM * @returns {string|string} * @private */ _createMediaTag: function () { //BUILD MEDIA HTML TAG return this.options.template || ('<' + this.options.type + ' src=""' + ' preload="' + this.options.preload + '"' + (this.options.enableControls ? ' controls' : '') + (this.options.enableLoop ? ' loop' : '') + (this.options.autoPlay ? ' autoplay' : '') + '><p>Your browser does not support the ' + this.options.type + ' element.</p>' + '</' + this.options.type + '>'); }, /** * Converts file path to title * @param value * @returns {XML|string|void} */ _convertFileToTitle: function (value) { return value ? value.split('/').pop().replace(/\.[^/.]+$/, '') : ''; }, /** * Toggles attribute on media element * @param attr * @param value */ _toggleMediaAttribute: function (attr, value) { var enable = value || (value !== false && !this.mediaElement.hasAttribute(attr)); if (enable) this.mediaElement.setAttribute(attr, ''); else this.mediaElement.removeAttribute(attr); return enable; }, /** * Binds event to media player * @param name * @param callback */ _bindMedia: function (name, callback) { if (name && callback) { //HANDLE BINDING FOR MEDIA PLAYER AFTER RENDER this.bind(DATABOUND, function () { if (this.mediaElement) { //BIND EVENT TO MEDIA PLAYER AFTER IT RENDERS this.mediaElement.addEventListener(name, callback); } }); } } })); //CREATE BINDER NAMESPACE kendo.data.binders.widget.mediaplayer = {}; kendo.data.binders.widget.mediaplayer.controls = kendo.data.Binder.extend({ refresh: function () { var value = this.bindings.controls.get(); var widget = this.element; widget.toggleControls(value); } }); kendo.data.binders.widget.mediaplayer.loop = kendo.data.Binder.extend({ refresh: function () { var value = this.bindings.loop.get(); var widget = this.element; widget.toggleLoop(value); } }); kendo.data.binders.widget.mediaplayer.loopAll = kendo.data.Binder.extend({ refresh: function () { var value = this.bindings.loopAll.get(); var widget = this.element; widget.toggleLoopAll(value); } }); kendo.data.binders.widget.mediaplayer.continuous = kendo.data.Binder.extend({ refresh: function () { var value = this.bindings.continuous.get(); var widget = this.element; widget.toggleContinuous(value); } }); kendo.data.binders.widget.mediaplayer.playlistended = kendo.data.Binder.extend({ init: function (widget, bindings, options) { var me = this; kendo.data.Binder.fn.init.call(this, widget, bindings, options); //HANDLE BINDING FOR MEDIA PLAYER widget.bind(PLAYLISTENDED, function () { //EXECUTE CALLBACK OPTION me.bindings.playlistended.get(); }); }, refresh: function () {} }); //BASE BINDER FOR MVVM MEDIA EVENTS var MediaBinder = kendo.data.Binder.extend({ eventName: null, init: function (widget, bindings, options) { var me = this; kendo.data.Binder.fn.init.call(this, widget, bindings, options); //HANDLE BINDING FOR MEDIA PLAYER widget._bindMedia(this.eventName, function () { me.bindings[me.eventName].get(); }); }, refresh: function () {} }); //BIND MVVM MEDIA EVENTS kendo.data.binders.widget.mediaplayer.durationchange = MediaBinder.extend({ eventName: DURATIONCHANGE }); kendo.data.binders.widget.mediaplayer.ended = MediaBinder.extend({ eventName: ENDED }); kendo.data.binders.widget.mediaplayer.error = MediaBinder.extend({ eventName: ERROR }); kendo.data.binders.widget.mediaplayer.loadeddata = MediaBinder.extend({ eventName: LOADEDDATA }); kendo.data.binders.widget.mediaplayer.loadedmetadata = MediaBinder.extend({ eventName: LOADEDMETADATA }); kendo.data.binders.widget.mediaplayer.loadstart = MediaBinder.extend({ eventName: LOADSTART }); kendo.data.binders.widget.mediaplayer.pause = MediaBinder.extend({ eventName: PAUSE }); kendo.data.binders.widget.mediaplayer.play = MediaBinder.extend({ eventName: PLAY }); kendo.data.binders.widget.mediaplayer.playing = MediaBinder.extend({ eventName: PLAYING }); kendo.data.binders.widget.mediaplayer.progress = MediaBinder.extend({ eventName: PROGRESS }); kendo.data.binders.widget.mediaplayer.ratechange = MediaBinder.extend({ eventName: RATECHANGE }); kendo.data.binders.widget.mediaplayer.seeked = MediaBinder.extend({ eventName: SEEKED }); kendo.data.binders.widget.mediaplayer.seeking = MediaBinder.extend({ eventName: SEEKING }); kendo.data.binders.widget.mediaplayer.timeupdate = MediaBinder.extend({ eventName: TIMEUPDATE }); kendo.data.binders.widget.mediaplayer.volumechange = MediaBinder.extend({ eventName: VOLUMECHANGE });
The screenshot for mobile was already supplied. It should make more sense now how I was able to wire up the tab strip to the player. In the initEvents method, it is looking for any available selectors supplied and calling the widget methods to manipulate the player. Here is what the HTML declaration looks like:
<section data-role="layout" data-id="default"> <header data-role="header"> <div data-role="navbar"> <a data-role="button" data-icon="sounds" data-align="left" data-click="kendo.mobile.application.options.modelScope.viewModel.onAudioLoadedClick"></a> <span data-role="view-title"></span> <a data-role="button" data-icon="organize" data-align="right" data-click="kendo.mobile.application.options.modelScope.viewModel.onVideoLoadedClick"></a> </div> </header> <footer data-role="footer"> <div data-role="tabstrip"> <a data-icon="rewind" class="previous">Previous</a> <a data-icon="pause" class="pause">Pause</a> <a data-icon="play" class="play">Play</a> <a data-icon="stop" class="stop">Stop</a> <a data-icon="fastforward" class="next">Next</a> </div> </footer> </section> <div data-role="view" data-layout="default" data-title="Kendo Media Player" data-model="viewModel" data-bind="events: { init: onViewInit, show: onViewShow }"> <div data-role="mediaplayer" data-play-selector=".km-footer .play" data-pause-selector=".km-footer .pause" data-stop-selector=".km-footer .stop" data-previous-selector=".km-footer .previous" data-next-selector=".km-footer .next" data-bind="source: mediaSource, ended: onMediaEnded, playlistended: onMediaPlaylistEnded, controls: enableControls, loop: enableLoop, loopAll: enableLoopAll, continuous: enableContinuous"> </div> <ul data-role="listview" class="audio-options" data-style="inset" data-type="group"> <li> Configuration <ul> <li>Controls <input type="checkbox" class="controls" data-role="switch" data-bind="checked: enableControls"></li> <li>Loop <input type="checkbox" class="loop" data-role="switch" data-bind="checked: enableLoop"></li> <li>Loop All <input type="checkbox" class="loop-all" data-role="switch" data-bind="checked: enableLoopAll"></li> <li>Continuous <input type="checkbox" class="continuous" data-role="switch" data-bind="checked: enableContinuous"></li> </ul> </li> </ul> </div> <script> //INITIALIZE APP require([ 'kendo.mediaplayer' ], function() { var viewModel = kendo.observable({ enableControls: true, enableLoop: false, enableLoopAll: false, enableContinuous: true, mediaSource: [], videoSource: [ { title: 'Wildlife', file: 'https://archive.org/download/Windows7WildlifeSampleVideo/Wildlife_512kb.mp4' }, { title: 'Clematis', file: 'https://archive.org/download/CEP304/CEP304_512kb.mp4' } ], audioSource: [ { title: 'Jungle River', file: 'https://ia600506.us.archive.org/17/items/Sounds_of_Nature_Collection/02_Jungle_River.mp3' }, { title: 'Tropical Rain Forest', file: 'https://ia700506.us.archive.org/17/items/Sounds_of_Nature_Collection/04_A_Tropical_Rain_Forest.mp3' }, { title: 'Thunder and River', file: 'https://ia600506.us.archive.org/17/items/Sounds_of_Nature_Collection/20_Thunder_And_Rain.mp3' }, { file: 'https://ia600506.us.archive.org/17/items/Sounds_of_Nature_Collection/41_Ocean_Waves.mp3' }, { title: 'Frog Chorus', file: 'https://ia700506.us.archive.org/17/items/Sounds_of_Nature_Collection/53_Frog_Chorus.mp3' } ], onViewInit: function (e) { //DEFAULT TO AUDIO SOURCE this.set('mediaSource', this.get('audioSource')); }, onViewShow: function (e) { //CLEAR MEDIA CONTROLS ON FIRST LOAD this.clearMediaControls(); }, onMediaEnded: function() { console.log('Media ended!'); }, onMediaPlaylistEnded: function() { console.log('Playlist ended!'); }, onAudioLoadedClick: function () { var context = kendo.mobile.application.options.modelScope.viewModel; //CONVERT TO AUDIO SOURCE $('[data-role="mediaplayer"]').getKendoMediaPlayer().options.type = 'audio'; context.set('mediaSource', context.get('audioSource')); context.clearMediaControls(); }, onVideoLoadedClick: function () { var context = kendo.mobile.application.options.modelScope.viewModel; //CONVERT TO VIDEO SOURCE $('[data-role="mediaplayer"]').getKendoMediaPlayer().options.type = 'video'; context.set('mediaSource', context.get('videoSource')); context.clearMediaControls(); }, clearMediaControls: function () { kendo.widgetInstance($('.km-footer [data-role="tabstrip"]')).clear(); } }); //INITIALIZE MOBILE APP new kendo.mobile.Application(document.body, { modelScope: { viewModel: viewModel } }); }); </script>
And here is the screenshot again:
Conclusion
As you can see, Kendo UI is a very powerful framework for creating rich and complex widgets. The ability to integrate with MVVM and DataSource takes you to a whole new level of web development. It creates a clear separation of concerns and provides automatic capability.
This source code is posted to our GitHub repo. Also, you can try out this demo live by clicking here for the mobileand here for the desktop versions. I plan on adding this to Kendo UI’s source code at GitHub and create a pull request for it. Please provide any feedback or issues.
Happy Coding!!
Leave a Reply