From 008770c132c399570ebd6d4ef1f891e06db3958c Mon Sep 17 00:00:00 2001 From: Ian Date: Sat, 18 Oct 2014 10:50:46 +0100 Subject: [PATCH] WebTV: Add Home/End, PgUp/PgDn, Left/Right plus overall code cosmetics --- src/webui/static/tv.js | 829 ++++++++++++++++++++++------------------- 1 file changed, 441 insertions(+), 388 deletions(-) diff --git a/src/webui/static/tv.js b/src/webui/static/tv.js index a9c46b95..44d7282c 100644 --- a/src/webui/static/tv.js +++ b/src/webui/static/tv.js @@ -2,6 +2,14 @@ Ext.namespace('tv'); Ext.namespace('tv.ui'); +/* Define key code mappings for navigation keys */ + +if (VK_END === undefined) + var VK_END = 0x23; + +if (VK_HOME === undefined) + var VK_HOME = 0x24; + if (VK_LEFT === undefined) var VK_LEFT = 0x25; @@ -48,249 +56,257 @@ if (VK_PAGE_DOWN === undefined) tv.ui.VideoPlayer = Ext.extend(Ext.Panel, (function() { var profiles = { - pass: { - profile: 'pass', - mimetype: 'video/MP2T' - }, - webm: { - profile: 'webtv-vp8-vorbis-webm', - playlist: false, - mimetype: 'video/webm; codecs="vp8.0, vorbis"' - }, - hls: { - profile: 'webtv-h264-aac-mpegts', - playlist: true, - mimetype: 'application/x-mpegURL; codecs="avc1.42E01E, mp4a.40.2"' - }, - apple: { - profile: 'webtv-h264-aac-mpegts', - playlist: true, - mimetype: 'application/vnd.apple.mpegURL; codecs="avc1.42E01E, mp4a.40.2"' - }, - ts: { - profile: 'webtv-h264-aac-mpegts', - playlist: false, - mimetype: 'video/MP2T; codecs="avc1.42E01E, mp4a.40.2"' - }, - mkv: { - profile: 'webtv-h264-aac-matroska', - playlist: false, - mimetype: 'video/x-matroska; codecs="avc1.42E01E, mp4a.40.2"' - }, + pass: { + profile: 'pass', + mimetype: 'video/MP2T' + }, + webm: { + profile: 'webtv-vp8-vorbis-webm', + playlist: false, + mimetype: 'video/webm; codecs="vp8.0, vorbis"' + }, + hls: { + profile: 'webtv-h264-aac-mpegts', + playlist: true, + mimetype: 'application/x-mpegURL; codecs="avc1.42E01E, mp4a.40.2"' + }, + apple: { + profile: 'webtv-h264-aac-mpegts', + playlist: true, + mimetype: 'application/vnd.apple.mpegURL; codecs="avc1.42E01E, mp4a.40.2"' + }, + ts: { + profile: 'webtv-h264-aac-mpegts', + playlist: false, + mimetype: 'video/MP2T; codecs="avc1.42E01E, mp4a.40.2"' + }, + mkv: { + profile: 'webtv-h264-aac-matroska', + playlist: false, + mimetype: 'video/x-matroska; codecs="avc1.42E01E, mp4a.40.2"' + }, }; return { - constructor: function(config) { - this.params = {}; - tv.ui.VideoPlayer.superclass.constructor.call(this, config); + constructor: function(config) { + this.params = {}; + tv.ui.VideoPlayer.superclass.constructor.call(this, config); - Ext.applyIf(this.params, { - profile : '', // stream profile - playlist : false // don't use m3u8 playlist - }); - }, + Ext.applyIf(this.params, { + profile : '', // stream profile + playlist : false // don't use m3u8 playlist + }); + }, - initComponent: function() { - Ext.apply(this, { - baseCls : 'tv-video-player', - html : '', - bufferLength: 3000, //ms + initComponent: function() { + Ext.apply(this, { + baseCls : 'tv-video-player', + html : '', + bufferLength: 3000, //ms - listeners: { - beforedestroy: { - fn: function(dv, items) { - this.video = null; - } - }, - bodyresize: { - fn: function(panel, width, height) { - this.video.setSize(width, height); - } - }, - render: { - fn: function() { - this.message = this.body.createChild({ - tag : 'div', - cls : 'tv-video-message', - html: '' - }); - this.message.setVisibilityMode(Ext.Element.DISPLAY); - this.message.hide(); + listeners: { + beforedestroy: { + fn: function(dv, items) { + this.video = null; + } + }, + bodyresize: { + fn: function(panel, width, height) { + this.video.setSize(width, height); + } + }, + render: { + fn: function() { + this.message = this.body.createChild({ + tag : 'div', + cls : 'tv-video-message', + html: '' + }); + this.message.setVisibilityMode(Ext.Element.DISPLAY); + this.message.hide(); - this.video = this.body.createChild({ - tag : 'video', - html : "Your browser doesn't support html5 video" - }); + this.video = this.body.createChild({ + tag : 'video', + html : "Your browser doesn't support html5 video" + }); - this.source = this.video.createChild({tag: 'source'}); - this.source.dom.addEventListener('error', this.error.bind(this)); + this.source = this.video.createChild({tag: 'source'}); + this.source.dom.addEventListener('error', this.error.bind(this)); - this.stop(); + this.stop(); - var self = this; - this.video.dom.addEventListener('error', this.error.bind(this)); - this.video.dom.addEventListener('loadeddata', function() { - setTimeout(function() { - self.play(); - }, self.bufferLength); - }); - } - } - } - }); - tv.ui.VideoPlayer.superclass.initComponent.apply(this, arguments); - }, + var self = this; + this.video.dom.addEventListener('error', this.error.bind(this)); + this.video.dom.addEventListener('loadeddata', function() { + setTimeout(function() { + self.play(); + }, + self.bufferLength); + }); + } + } + } + }); + tv.ui.VideoPlayer.superclass.initComponent.apply(this, arguments); + }, - _getUrl: function(uuid, params) { - var url = ''; + _getUrl: function(uuid, params) { + var url = ''; - if(params.playlist) - url += 'playlist/channel/' - else - url += 'stream/channel/' - - url += uuid; - if (params.profile) - url += "?profile=" + params.profile; - - return url; - }, + if(params.playlist) + url += 'playlist/channel/' + else + url += 'stream/channel/' + + url += uuid; - _getProfile: function() { - var el = this.video.dom; + if (params.profile) + url += "?profile=" + params.profile; + + return url; + }, - // chrome can handle h264+aac within mkv, given that h264 codecs are available - if(Ext.isChrome && - el.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') == 'probably') - return profiles['mkv']; + _getProfile: function() { + var el = this.video.dom; - for (var key in profiles) - if(el.canPlayType(profiles[key].mimetype) == 'probably') - return profiles[key]; - - for (var key in profiles) - if(el.canPlayType(profiles[key].mimetype) == 'maybe') - return profiles[key]; - - return {}; - }, + // chrome can handle h264+aac within mkv, given that h264 codecs are available + if(Ext.isChrome && + el.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') == 'probably') + return profiles['mkv']; - error: function(e) { - var url = this.source.dom.src; - if(url && url != document.location.href) { - this.body.removeClass('tv-video-loading'); - this.body.removeClass('tv-video-idle'); - this.body.addClass('tv-video-error'); - this.video.hide(); + for (var key in profiles) + if(el.canPlayType(profiles[key].mimetype) == 'probably') + return profiles[key]; + + for (var key in profiles) + if(el.canPlayType(profiles[key].mimetype) == 'maybe') + return profiles[key]; + + return {}; + }, - this.message.update('An unknown error occurred.'); + error: function(e) { + var url = this.source.dom.src; - var err = e.target.error; - if(err) { - switch (err.code) { - case err.MEDIA_ERR_ABORTED: - this.message.update('You aborted the video playback.'); - break; - - case err.MEDIA_ERR_NETWORK: - this.message.update('A network error caused the video ' + - 'download to fail part-way.'); - break; - case err.MEDIA_ERR_DECODE: - this.message.update('The video playback was aborted due to ' + - 'a corruption problem or because the video ' + - 'used features your browser did not support.'); - break; - case err.MEDIA_ERR_SRC_NOT_SUPPORTED: - this.message.update('The video could not be loaded, either because ' + - 'the server or network failed or because the ' + - 'format is not supported.'); - break; - } - } - this.message.show(); - } - }, + if(url && url != document.location.href) { + this.body.removeClass('tv-video-loading'); + this.body.removeClass('tv-video-idle'); + this.body.addClass('tv-video-error'); + this.video.hide(); - stop: function() { - this.message.hide(); - this.body.removeClass('tv-video-loading'); - this.body.removeClass('tv-video-error'); - this.body.addClass('tv-video-idle'); - this.source.dom.src = ''; - this.video.dom.load(); - }, + this.message.update('An unknown error occurred.'); - pause: function() { - this.video.dom.pause(); - }, + var err = e.target.error; - setVolume: function(vol) { - this.video.dom.volume = vol / 100.0; - }, + if(err) { + switch (err.code) { - getVolume: function() { - return Math.round(100 * this.video.dom.volume); - }, + case err.MEDIA_ERR_ABORTED: + this.message.update('You aborted the video playback.'); + break; - setDisplaySize: function(width, height) { - this.video.setSize(width, height); - }, + case err.MEDIA_ERR_NETWORK: + this.message.update('A network error caused the video ' + + 'download to fail part-way.'); + break; + + case err.MEDIA_ERR_DECODE: + this.message.update('The video playback was aborted due to ' + + 'a corruption problem or because the video ' + + 'used features your browser did not support.'); + break; + + case err.MEDIA_ERR_SRC_NOT_SUPPORTED: + this.message.update('The video could not be loaded, either because ' + + 'the server or network failed or because the ' + + 'format is not supported.'); + break; + } + } + this.message.show(); + } + }, - setProfile: function(pro) { - this.params.profile = pro; - }, + stop: function() { + this.message.hide(); + this.body.removeClass('tv-video-loading'); + this.body.removeClass('tv-video-error'); + this.body.addClass('tv-video-idle'); + this.source.dom.src = ''; + this.video.dom.load(); + }, - isIdle: function() { - return this.body.hasClass('tv-video-idle'); - }, + pause: function() { + this.video.dom.pause(); + }, - fullscreen: function() { - var dom = this.video.dom; + setVolume: function(vol) { + this.video.dom.volume = vol / 100.0; + }, - if(typeof dom.requestFullScreen !== 'undefined') - dom.requestFullScreen(); + getVolume: function() { + return Math.round(100 * this.video.dom.volume); + }, - else if(typeof dom.mozRequestFullScreen !== 'undefined') - dom.mozRequestFullScreen(); + setDisplaySize: function(width, height) { + this.video.setSize(width, height); + }, - else if(typeof dom.webkitRequestFullScreen !== 'undefined') - dom.webkitEnterFullscreen(); - }, + setProfile: function(pro) { + this.params.profile = pro; + }, - play: function() { - this.message.hide(); - this.body.removeClass('tv-video-loading'); - this.body.removeClass('tv-video-idle'); - this.body.removeClass('tv-video-error'); + isIdle: function() { + return this.body.hasClass('tv-video-idle'); + }, - this.video.show(); - this.video.dom.play(); - }, + fullscreen: function() { + var dom = this.video.dom; - zapTo: function(uuid, config) { - var config = config || {} - var params = {} + if(typeof dom.requestFullScreen !== 'undefined') + dom.requestFullScreen(); - if (!this.params.profile) - Ext.apply(params, this._getProfile(), this.params); - else - Ext.apply(params, this.params); - Ext.apply(params, config); + else if(typeof dom.mozRequestFullScreen !== 'undefined') + dom.mozRequestFullScreen(); - this.video.hide(); - this.stop(); + else if(typeof dom.webkitRequestFullScreen !== 'undefined') + dom.webkitEnterFullscreen(); + }, - this.message.update('Loading...'); - this.message.show(); + play: function() { + this.message.hide(); + this.body.removeClass('tv-video-loading'); + this.body.removeClass('tv-video-idle'); + this.body.removeClass('tv-video-error'); - this.body.removeClass('tv-video-idle'); - this.body.removeClass('tv-video-error'); - this.body.addClass('tv-video-loading'); + this.video.show(); + this.video.dom.play(); + }, - this.source.dom.src = this._getUrl(uuid, params); - this.video.dom.load(); - } + zapTo: function(uuid, config) { + var config = config || {} + var params = {} + + if (!this.params.profile) + Ext.apply(params, this._getProfile(), this.params); + else + Ext.apply(params, this.params); + + Ext.apply(params, config); + + this.video.hide(); + this.stop(); + + this.message.update('Loading...'); + this.message.show(); + + this.body.removeClass('tv-video-idle'); + this.body.removeClass('tv-video-error'); + this.body.addClass('tv-video-loading'); + + this.source.dom.src = this._getUrl(uuid, params); + this.video.dom.load(); + } }; }())); @@ -298,241 +314,278 @@ tv.ui.VideoPlayer = Ext.extend(Ext.Panel, (function() { tv.ui.ChannelList = Ext.extend(Ext.DataView, { initComponent: function() { - Ext.apply(this, { - cls: 'tv-list', - overClass: 'tv-list-item-over', - selectedClass: 'tv-list-item-selected', - itemSelector:'div.tv-list-item', - singleSelect: true, - tpl: new Ext.XTemplate( - '', - '
', - '{name}', - '
', - '
'), + Ext.apply(this, { + cls: 'tv-list', + overClass: 'tv-list-item-over', + selectedClass: 'tv-list-item-selected', + itemSelector:'div.tv-list-item', + singleSelect: true, + tpl: new Ext.XTemplate( + '', + '
', + '{name}', + '
', + '
'), - listeners: { - selectionchange: { - fn: function(dv, items) { - if(items.length == 0) - return; + listeners: { + selectionchange: { + fn: function(dv, items) { + if(items.length == 0) + return; - var node = this.getNode(items[0]); - node = Ext.get(node); - node.scrollIntoView(this.el); - } - }, - dblclick: { - fn: function() { - this.fireEvent('naventer'); - } - } - } - }); + var node = this.getNode(items[0]); + node = Ext.get(node); + node.scrollIntoView(this.el); + } + }, + dblclick: { + fn: function() { + this.fireEvent('naventer'); + } + } + } + }); - this.addEvents( - 'navup', - 'navdown', - 'navleft', - 'navright', - 'navback', - 'naventer' + this.addEvents( + 'navup', + 'navdown', + 'navleft', + 'navright', + 'navback', + 'naventer', + 'pageup', + 'pagedown' ); - tv.ui.ChannelList.superclass.initComponent.apply(this, arguments); + tv.ui.ChannelList.superclass.initComponent.apply(this, arguments); }, visibleItems: function() { - var nodes = this.getNodes(0, 0); - if(nodes.length == 0) - return 0; + var nodes = this.getNodes(0, 0); + if(nodes.length == 0) + return 0; - var height = this.getTemplateTarget().getHeight(); - var itemHeight = Ext.get(nodes[0]).getHeight() - return Math.floor(height / itemHeight); + var height = this.getTemplateTarget().getHeight(); + var itemHeight = Ext.get(nodes[0]).getHeight() + return Math.floor(height / itemHeight); }, onRender : function(ct, position) { tv.ui.ChannelList.superclass.onRender.call(this, ct, position); - Ext.dd.ScrollManager.register(this.el); + Ext.dd.ScrollManager.register(this.el); - this.getTemplateTarget().set({tabindex: Ext.id(undefined, '0')}); - this.getTemplateTarget().on('keydown', function(e) { - switch(e.getKey()) { + this.getTemplateTarget().set({tabindex: Ext.id(undefined, '0')}); + this.getTemplateTarget().on('keydown', function(e) { - case VK_LEFT: - this.fireEvent('navleft'); - break; +//IH + console.log('keypress:', e.getKey()); +// + switch(e.getKey()) { - case VK_RIGHT: - this.fireEvent('navright'); - break; + case VK_LEFT: + this.fireEvent('navleft'); + break; - case VK_UP: - this.fireEvent('navup', 1); - break; + case VK_RIGHT: + this.fireEvent('navright'); + break; - case VK_DOWN: - this.fireEvent('navdown', 1); - break; + case VK_UP: + this.fireEvent('navup', 1); + break; - case VK_PAGE_UP: - var cnt = this.visibleItems(); - this.fireEvent('navup', cnt); - break; + case VK_DOWN: + this.fireEvent('navdown', 1); + break; - case VK_PAGE_DOWN: - var cnt = this.visibleItems(); - this.fireEvent('navdown', cnt); - break; + case VK_PAGE_UP: + var cnt = this.visibleItems(); + this.fireEvent('pageup', cnt); + break; - case VK_SPACE: - case VK_ENTER: - this.fireEvent('naventer'); - break; + case VK_PAGE_DOWN: + var cnt = this.visibleItems(); + this.fireEvent('pagedown', cnt); + break; - case VK_BACKSPACE: - case VK_ESCAPE: - case VK_BACK: - this.fireEvent('navback'); - break; + case VK_HOME: + this.fireEvent('pagefirst'); + break; - default: - return false; - } + case VK_END: + this.fireEvent('pagelast'); + break; - e.stopEvent(); - return true; + case VK_SPACE: + case VK_ENTER: + this.fireEvent('naventer'); + break; - }.bind(this)); + case VK_BACKSPACE: + case VK_ESCAPE: + case VK_BACK: + this.fireEvent('navback'); + break; + + default: + return false; + } + + e.stopEvent(); + return true; + + }.bind(this)); } }); tv.app = function() { return { - init: function() { + init: function() { - var channelStore = new Ext.data.JsonStore({ - autoLoad: {params:{start: 0, limit: 8}}, // limit initial page size to 8 - root : 'entries', - fields : ['icon_public_url', 'number', 'name', 'uuid'], - id : 'uuid', - sortInfo : { - field : 'number', // WIBI: Ideally, sort the whole channel list at source - direction : "ASC" - }, - url : "api/channel/grid" - }); + var channelStore = new Ext.data.JsonStore({ + autoLoad: {params:{start: 0, limit: 8}}, // limit initial page size to 8 + root : 'entries', + fields : ['icon_public_url', 'number', 'name', 'uuid'], + id : 'uuid', + sortInfo : { + field : 'number', // WIBI: Ideally, sort the whole channel list at source + direction : "ASC" + }, + url : "api/channel/grid" + }); - var videoPlayer = new tv.ui.VideoPlayer({ - params: { }, - renderTo: Ext.getBody() - }); - videoPlayer.setDisplaySize('100%', '00%'); + var videoPlayer = new tv.ui.VideoPlayer({ + params: { }, + renderTo: Ext.getBody() + }); + + videoPlayer.setDisplaySize('100%', '00%'); var chList = new tv.ui.ChannelList({ - autoScroll: true, - store: channelStore - }); + autoScroll: true, + store: channelStore + }); // Play button that calls the "I've pressed Enter!" event when clicked var playButton = new Ext.Button({ - text: 'Play Selected Channel', - handler: function() { - chList.fireEvent('naventer'); - } - }); + text: 'Play Selected Channel', + handler: function() { + chList.fireEvent('naventer'); + } + }); // Paging bar so you can move through the list of channels - var pageBar = new Ext.PagingToolbar({ - store: channelStore, - pageSize: 8 // replicates initial page size - }); + var pageBar = new Ext.PagingToolbar({ + store: channelStore, + pageSize: 8 // replicates initial page size + }); - var chListPanel1 = new Ext.Panel({ - items: [ pageBar, playButton ], - cls: 'tv-channel-list-header' - }); + var chListPanel1 = new Ext.Panel({ + items: [ pageBar, playButton ], + cls: 'tv-channel-list-header' + }); - var chListPanel2 = new Ext.Panel({ - items: [ chList ], - cls: 'tv-channel-list-content' - }); + var chListPanel2 = new Ext.Panel({ + items: [ chList ], + cls: 'tv-channel-list-content' + }); - var chListPanel = new Ext.Panel({ - title:'Channels', - items: [ chListPanel1, chListPanel2 ], - cls: 'tv-channel-list', - renderTo: Ext.getBody() - }); + var chListPanel = new Ext.Panel({ + title:'Channels', + items: [ chListPanel1, chListPanel2 ], + cls: 'tv-channel-list', + renderTo: Ext.getBody() + }); - window.onresize = function() { - var h = chListPanel.el.getHeight(); - h -= chListPanel.header.getHeight(); - h -= 250; - - chList.setHeight(h); - }; + window.onresize = function() { + var h = chListPanel.el.getHeight(); + h -= chListPanel.header.getHeight(); + h -= 250; + chList.setHeight(h); + }; - chListPanel.on('show', function() { - window.onresize(); - }); + chListPanel.on('show', function() { + window.onresize(); + }); - chList.on('navback', function() { - chListPanel.hide(); - chList.blur(); - }); + chList.on('navback', function() { + chListPanel.hide(); + chList.blur(); + }); - chList.on('naventer', function() { - var indices = this.getSelectedIndexes(); - if(indices.length == 0) - return; - - var item = this.store.getAt(indices[0]); - videoPlayer.zapTo(item.id); - chListPanel.hide(); - chList.blur(); - }); + chList.on('naventer', function() { + var indices = this.getSelectedIndexes(); + if(indices.length == 0) + return; - chList.on('navup', function(cnt) { - var indices = chList.getSelectedIndexes(); - if(indices.length == 0) - this.select(this.store.getTotalCount() - 1); - else if(indices[0] - cnt >= 0) - this.select(indices[0] - cnt); - else - this.select(0); - }); + var item = this.store.getAt(indices[0]); + videoPlayer.zapTo(item.id); + chListPanel.hide(); + chList.blur(); + }); - chList.on('navdown', function(cnt) { - var indices = chList.getSelectedIndexes(); - if(indices.length == 0) - this.select(0); - else if(indices[0] + cnt < this.store.getTotalCount()) - this.select(indices[0] + cnt); - else - this.select(this.store.getTotalCount() - 1); - }); + chList.on('navup', function(cnt) { + var indices = chList.getSelectedIndexes(); - chList.on('navleft', function() { +//IH + console.log(indices); + console.log(indices.length); + console.log(this.store.getTotalCount()); +// - }); + if(indices.length == 0) + this.select(this.store.getTotalCount() - 1); + else if(indices[0] - cnt >= 0) + this.select(indices[0] - cnt); + else + this.select(0); + }); - chList.on('navright', function() { + chList.on('navdown', function(cnt) { + var indices = chList.getSelectedIndexes(); + if(indices.length == 0) + this.select(0); + else if(indices[0] + cnt < this.store.getTotalCount()) + this.select(indices[0] + cnt); + else + this.select(this.store.getTotalCount() - 1); + }); - }); + chList.on('navleft', function() { + pageBar.movePrevious(); + }); - Ext.getDoc().on('keydown', function(e) { - switch(e.getKey()) { - case VK_ENTER: - chListPanel.show(); - chList.focus(); - } - }); - - chListPanel.show(); - chList.focus(); - } + chList.on('navright', function() { + pageBar.moveNext(); + }); + + chList.on('pageup', function() { + pageBar.movePrevious(); + }); + + chList.on('pagedown', function() { + pageBar.moveNext(); + }); + + chList.on('pagefirst', function() { + pageBar.moveFirst(); + }); + + chList.on('pagelast', function() { + pageBar.moveLast(); + }); + + Ext.getDoc().on('keydown', function(e) { + switch(e.getKey()) { + case VK_ENTER: + chListPanel.show(); + chList.focus(); + } + }); + + chListPanel.show(); + chList.focus(); + } }; }(); // end of app