From 1dc57cfdbf9af59a0548c79d2e485dae9e7ca379 Mon Sep 17 00:00:00 2001 From: brianjjones Date: Thu, 6 Mar 2014 15:26:00 -0800 Subject: [PATCH 1/1] Initial commit of the Multimediaplayer app Change-Id: I61526aec80bd0fb5d77cec6e138140d024e41268 --- Makefile | 20 + components/infoPanel/infoPanel.js | 52 +++ components/spectrumAnalyzer/spectrumAnalyzer.js | 253 ++++++++++ components/timeProgressBar/timeProgressBar.js | 119 +++++ config.xml | 22 + css/music_library.css | 87 ++++ css/style.css | 359 ++++++++++++++ icon.png | Bin 0 -> 2216 bytes images/audio-placeholder.jpg | Bin 0 -> 8159 bytes images/container-placeholder.jpg | Bin 0 -> 5608 bytes images/default-placeholder.jpg | Bin 0 -> 51331 bytes images/video-placeholder.jpg | Bin 0 -> 3501 bytes index.html | 206 +++++++++ js/carousel.js | 221 +++++++++ js/localcontent.js | 590 ++++++++++++++++++++++++ js/main.js | 112 +++++ js/mediacontent.js | 225 +++++++++ js/multimedialibrary.js | 484 +++++++++++++++++++ js/remotecontent.js | 369 +++++++++++++++ js/utils.js | 118 +++++ packaging/html5-ui-multimediaplayer.changes | 4 + packaging/html5-ui-multimediaplayer.spec | 36 ++ 22 files changed, 3277 insertions(+) create mode 100644 Makefile create mode 100644 components/infoPanel/infoPanel.js create mode 100644 components/spectrumAnalyzer/spectrumAnalyzer.js create mode 100644 components/timeProgressBar/timeProgressBar.js create mode 100644 config.xml create mode 100644 css/music_library.css create mode 100644 css/style.css create mode 100644 icon.png create mode 100644 images/audio-placeholder.jpg create mode 100644 images/container-placeholder.jpg create mode 100644 images/default-placeholder.jpg create mode 100644 images/video-placeholder.jpg create mode 100644 index.html create mode 100644 js/carousel.js create mode 100644 js/localcontent.js create mode 100644 js/main.js create mode 100644 js/mediacontent.js create mode 100644 js/multimedialibrary.js create mode 100644 js/remotecontent.js create mode 100644 js/utils.js create mode 100644 packaging/html5-ui-multimediaplayer.changes create mode 100644 packaging/html5-ui-multimediaplayer.spec diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dfbd526 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +PROJECT = html5UIMultimediaplayer + +VERSION := 0.0.1 +PACKAGE = $(PROJECT)-$(VERSION) + +INSTALL_FILES = $(PROJECT).wgt +INSTALL_DIR = ${DESTDIR}/opt/usr/apps/.preinstallWidgets + +wgtPkg: + cp -r ${DESTDIR}/opt/usr/apps/_common/js/services js/ + cp -r ${DESTDIR}/opt/usr/apps/_common/css/* css/ + zip -r $(PROJECT).wgt components config.xml css icon.png images index.html js + +install: + @echo "Installing Multimediaplayer, stand by..." + mkdir -p $(INSTALL_DIR)/ + cp $(PROJECT).wgt $(INSTALL_DIR)/ + +dist: + tar czf ../$(PACKAGE).tar.bz2 . diff --git a/components/infoPanel/infoPanel.js b/components/infoPanel/infoPanel.js new file mode 100644 index 0000000..6d8c45a --- /dev/null +++ b/components/infoPanel/infoPanel.js @@ -0,0 +1,52 @@ +//audio info panel JQuery Plugin + +/** + * @module MultimediaPlayerApplication + */ +(function ($) { + "use strict"; + /** + * Class which provides methods to fill content of info panel for JQuery plugin. + * @class InfoPanelObj + * @static + */ + var InfoPanelObj = { + title: 'NOW PLAYING', + artist: 'ARTIST', + album: 'ALBUM', + name: 'SONG NAME', + /** + * Method is initializing info panel. + * @method show + * @param obj {Object} Object which contains properties title, artist, album and name. + */ + show: function (obj) { + InfoPanelObj.title = obj.title; + InfoPanelObj.artist = obj.artist; + InfoPanelObj.album = obj.album; + InfoPanelObj.name = obj.name; + this.empty(); + this.append('
' + obj.artist.toUpperCase() + '
' + + '
' + obj.album.toUpperCase() + '
' + + '
' + obj.name.toUpperCase() + '
'); + $(".nowPlaying").boxCaptionPlugin('init', obj.title.toUpperCase()); + } + }; + /** + * Class which provides acces to InfoPanelObj methods. + * @class infoPanel + * @constructor + * @param method {Object} Identificator (name) of method. + * @return Result of called method. + */ + $.fn.infoPanel = function (method) { + // Method calling logic + if (InfoPanelObj[method]) { + return InfoPanelObj[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return InfoPanelObj.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.infoPanelAPI'); + } + }; +}(jQuery)); diff --git a/components/spectrumAnalyzer/spectrumAnalyzer.js b/components/spectrumAnalyzer/spectrumAnalyzer.js new file mode 100644 index 0000000..3131bd0 --- /dev/null +++ b/components/spectrumAnalyzer/spectrumAnalyzer.js @@ -0,0 +1,253 @@ +/** + * @module MultimediaPlayerApplication + */ +(function ($) { + "use strict"; + /** + * Class which provides methods to fill content of spectrum analyzer for JQuery plugin. + * @class SpectrumAnalyzerObj + * @static + */ + var SpectrumAnalyzerObj = { + /** + * Holds value of spectrum analyzer bar. + * @property bar1 {Integer} + */ + bar1 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar2 {Integer} + */ + bar2 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar3 {Integer} + */ + bar3 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar4 {Integer} + */ + bar4 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar5 {Integer} + */ + bar5 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar6 {Integer} + */ + bar6 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar7 {Integer} + */ + bar7 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar8 {Integer} + */ + bar8 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar9 {Integer} + */ + bar9 : 0, + /** + * Holds value of spectrum analyzer bar. + * @property bar10 {Integer} + */ + bar10 : 0, + /** + * Method provides randomization for spectrum analyzer bars. + * @method spectrumAnalyzerRandomize + */ + spectrumAnalyzerRandomize : function () { + SpectrumAnalyzerObj.bar1 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar2 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar3 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar4 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar5 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar6 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar7 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar8 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar9 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.bar10 = Math.floor((Math.random() * 6) + 1); + SpectrumAnalyzerObj.showSpectrumAnalyzer(this); + }, + /** + * Method provides randomization for spectrum analyzer bars. + * @method showSpectrumAnalyzer + * @param thisObj {Object} Object which contains current object of this JQuery plugin. + */ + showSpectrumAnalyzer : function (thisObj) { + var bar1Count, bar2Count, bar3Count, bar4Count, bar5Count, bar6Count, bar7Count, bar8Count, bar9Count, bar10Count, bottom, i; + bar1Count = 2 * SpectrumAnalyzerObj.bar1; + bar2Count = 2 * SpectrumAnalyzerObj.bar2; + bar3Count = 2 * SpectrumAnalyzerObj.bar3; + bar4Count = 2 * SpectrumAnalyzerObj.bar4; + bar5Count = 2 * SpectrumAnalyzerObj.bar5; + bar6Count = 2 * SpectrumAnalyzerObj.bar6; + bar7Count = 2 * SpectrumAnalyzerObj.bar7; + bar8Count = 2 * SpectrumAnalyzerObj.bar8; + bar9Count = 2 * SpectrumAnalyzerObj.bar9; + bar10Count = 2 * SpectrumAnalyzerObj.bar10; + if (bar1Count > 12) { + bar1Count = 12; + } + if (bar2Count > 12) { + bar2Count = 12; + } + if (bar3Count > 12) { + bar3Count = 12; + } + if (bar4Count > 12) { + bar4Count = 12; + } + if (bar5Count > 12) { + bar5Count = 12; + } + if (bar6Count > 12) { + bar6Count = 12; + } + if (bar7Count > 12) { + bar7Count = 12; + } + if (bar8Count > 12) { + bar8Count = 12; + } + if (bar9Count > 12) { + bar9Count = 12; + } + if (bar10Count > 12) { + bar10Count = 12; + } + thisObj.empty(); + bottom = 0; + for (i = 0; i < bar1Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar2Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar3Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar4Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar5Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar6Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar7Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar8Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar9Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + bottom = 0; + for (i = 0; i < bar10Count; i++) { + bottom = bottom + 5; + if ((i % 2) === 0) { + thisObj.append('
'); + } else { + thisObj.append('
'); + } + } + }, + /** + * Method provides clear for spectrum analyzer bars. + * @method clearSpectrumAnalyzer + */ + clearSpectrumAnalyzer : function () { + SpectrumAnalyzerObj.bar1 = 0; + SpectrumAnalyzerObj.bar2 = 0; + SpectrumAnalyzerObj.bar3 = 0; + SpectrumAnalyzerObj.bar4 = 0; + SpectrumAnalyzerObj.bar5 = 0; + SpectrumAnalyzerObj.bar6 = 0; + SpectrumAnalyzerObj.bar7 = 0; + SpectrumAnalyzerObj.bar8 = 0; + SpectrumAnalyzerObj.bar9 = 0; + SpectrumAnalyzerObj.bar10 = 0; + SpectrumAnalyzerObj.showSpectrumAnalyzer(this); + } + }; + /** + * Class which provides acces to SpectrumAnalyzerObj methods. + * @class spectrumAnalyzer + * @constructor + * @param method {Object} Identificator (name) of method. + * @return Result of called method. + */ + $.fn.spectrumAnalyzer = function (method) { + // Method calling logic + if (SpectrumAnalyzerObj[method]) { + return SpectrumAnalyzerObj[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return SpectrumAnalyzerObj.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.spectrumAnalyzerAPI'); + } + }; +}(jQuery)); diff --git a/components/timeProgressBar/timeProgressBar.js b/components/timeProgressBar/timeProgressBar.js new file mode 100644 index 0000000..01cf3f2 --- /dev/null +++ b/components/timeProgressBar/timeProgressBar.js @@ -0,0 +1,119 @@ +//audio time progress bar JQuery Plugin +/** + * @module MultimediaPlayerApplication + */ +(function ($) { + "use strict"; + /** + * Class which provides methods to fill content of time progress bar for JQuery plugin. + * @class TimeProgressBarObj + * @static + */ + var TimeProgressBarObj = { + /** + * Holds current object of this JQuery plugin. + * @property thisObj {Object} + */ + thisObj: null, + /** + * Holds current object of position indicator. + * @property positionIndicator {Object} + */ + positionIndicator: null, + /** + * Holds current text of top right caption. + * @property rightText {String} + */ + rightText: null, + /** + * Holds current text of top left caption. + * @property leftText {String} + */ + leftText: null, + /** + * Holds current count of songs in playlist. + * @property count {Integer} + */ + count: 0, + /** + * Holds current index of song from playlist. + * @property index {Integer} + */ + index: 0, + /** + * Holds current estimation of song from playlist. + * @property estimation {String} + */ + estimation: "", + /** + * Holds current position of song from playlist. + * @property position {Integer} + */ + position: 0, + /** + * Method is initializing time progress bar. + * @method init + */ + init: function () { + this.empty(); + this.append('
' + + '0/0' + + '
' + + '
' + + '-0:00' + + '
' + + '
' + + '
' + + '
'); + + TimeProgressBarObj.positionIndicator = $('#songProgress #songSeek'); + TimeProgressBarObj.leftText = $('#songIndex'); + TimeProgressBarObj.rightText = $('#songTime'); + TimeProgressBarObj.positionIndicator.css({width: '0 %'}); + TimeProgressBarObj.thisObj = this; + + $("#songProgress").click(function (e) { + var elWidth = $(this).width(), + parentOffset = $(this).parent().offset(), + relativeXPosition = (e.pageX - parentOffset.left), //offset -> method allows you to retrieve the current position of an element 'relative' to the document + progress = 0.00; + if (elWidth > 0) { + progress = relativeXPosition / elWidth; + } + TimeProgressBarObj.thisObj.trigger('positionChanged', {position: (progress * 100)}); + }); + }, + /** + * Method is rendering position bar and position information from song. + * @method show + */ + show: function (objSong) { + $("#songIndex").empty(); + $("#songIndex").append(objSong.index + '/' + objSong.count); + $("#songTime").empty(); + $("#songTime").append(objSong.estimation); + TimeProgressBarObj.count = objSong.count; + TimeProgressBarObj.index = objSong.index; + TimeProgressBarObj.estimation = objSong.estimation; + TimeProgressBarObj.positionIndicator.css({width: objSong.position + '%'}); + TimeProgressBarObj.position = objSong.position; + } + }; + /** + * Class which provides acces to TimeProgressBarObj methods. + * @class timeProgressBar + * @constructor + * @param method {Object} Identificator (name) of method. + * @return Result of called method. + */ + $.fn.timeProgressBar = function (method) { + // Method calling logic + if (TimeProgressBarObj[method]) { + return TimeProgressBarObj[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return TimeProgressBarObj.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.infoPanelAPI'); + } + }; +}(jQuery)); diff --git a/config.xml b/config.xml new file mode 100644 index 0000000..502bf01 --- /dev/null +++ b/config.xml @@ -0,0 +1,22 @@ + + + + + + + Multimedia Player + + + + + + + + + + + diff --git a/css/music_library.css b/css/music_library.css new file mode 100644 index 0000000..66ef5fe --- /dev/null +++ b/css/music_library.css @@ -0,0 +1,87 @@ +/* ARTISTS */ +.musicLibraryContentList .musicElement { + padding: 10px 0 10px 0; + height: 70px; + box-shadow: none; + border-bottom-style: solid; + border-bottom-width: 2px; + line-height: 0; + text-align: left; + cursor: pointer; +} + +.musicLibraryContentList .musicImage { + width: 70px; + height: 70px; + display: inline-block; + margin-right: 20px; + background-position: center center; + background-repeat: no-repeat; + background-size: 100% 100%; +} + +.musicLibraryContentList .musicImage img { + width: 100%; + height: 100%; +} + +.musicLibraryContentList .musicInfoBox { + display: inline-block; + vertical-align: top; + padding-top: 11px; + background-color: transparent !important; +} + +.musicLibraryContentList .musicInfoBox.musicInfoBoxCentered { + padding-top: 20px; +} + +.musicInfoBoxShort { + max-width: 470px; +} + +.musicLibraryContentGrid { + line-height: 0; +} + +.musicLibraryContentGrid .musicElement { + width: 180px; + height: 180px; + display: inline-block; + margin-right: 7px; + margin-bottom: 10px; + line-height: 0; + vertical-align: top; + cursor: pointer; +} + +.musicLibraryContentGrid .musicImage { + width: 100%; + height: 100%; + background-position: center center; + background-repeat: no-repeat; + background-size: 100% 100%; +} + +.musicLibraryContentGrid .musicImage img { + width: 100%; + height: 100%; +} + +.musicLibraryContentGrid .musicInfoBox { + position: absolute; + top: 110px; + left: 0; + width: 180px; + height: 70px; + text-align: center; +} + +.musicLibraryContentGrid .musicElement .contentTitle { + width: 160px; + margin: 10px auto 0 auto; +} + +.textTransformUppercase { + text-transform: uppercase; +} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..40aecb8 --- /dev/null +++ b/css/style.css @@ -0,0 +1,359 @@ +.playerWrapper video { + position: absolute; + top: 230px; + left: 55px; + width: 610px; + height: 300px; + padding: 0; + margin: 0; + display: none; +} + +.carouselWrapper { + width: 765px; + height: 300px; + position: absolute; + top: 230px; + left: -15px; + padding: 0; + margin: 0; + display: block; + border-bottom-style: solid; + border-bottom-width: 1px; +} + +.carouselList { + margin: 0; + padding: 0; + list-style: none; + display: block; +} + +.carouselList li { + display: block; + float: left; +} + +.carouselItem { + margin-right: 15px; + display: block; + width: 240px; + height: 300px; + overflow: hidden; +} + +.carouselItemSelected { + box-shadow: 0 0 5px 1px #1DA2FF; +} + +.carouselImage { + width: 240px; + height: 240px; +} + +.carouselImageReflect { + -webkit-box-reflect: below 1px + -webkit-gradient(linear, left top, left bottom, from(transparent), + to(rgba(250, 250, 225, 0.4) ) ) +} + +.albumCarouselDescription { + width: 230px; + height: 95px; + bottom: 95px; + padding: 0 5px; +} + +.albumCarouselDescriptionText { + text-align: center; + top: 15px; +} + +.backgroundAudioClass { + position: absolute; + top: 0; + left: 0; + width: 720px; + height: 1280px; + -webkit-background-size: cover; + -moz-background-size: cover; + -ms-background-size: cover; + -o-background-size: cover; + background-size: cover; +} + +/* spectrum analyzer */ +.spectrumAnalyzer { + position: absolute; + top: 775px; + left: 300px; + width: 400px; + height: 110px; +} + +.barAnalyzer { + position: absolute; + width: 33px; + height: 5px; + float: left; + margin-left: 5px; +} + +.bar1Class { + left: 0; +} + +.bar2Class { + left: 38px; +} + +.bar3Class { + left: 76px; +} + +.bar4Class { + left: 114px; +} + +.bar5Class { + left: 152px; +} + +.bar6Class { + left: 190px; +} + +.bar7Class { + left: 228px; +} + +.bar8Class { + left: 266px; +} + +.bar9Class { + left: 304px; +} + +.bar10Class { + left: 342px; +} + +/* text panel artist, album, song title */ +.currentlyPlayingPreviewClass { + position: absolute; + top: 0; + left: 0; +} + +.textPanel { + position: absolute; + width: 405px; + top: 577px; + left: 55px; +} + +.blueIconText { + position: relative; + height: 25px; +} + +/* time progress bar and remaining time */ +.timeBarClass { + position: absolute; + top: 0; + left: 283px; + width: 325px; + height: 80px; + font-size: large; +} + +.infoPanelClass { + position: absolute; + left: 283px; + top: 80px; + width: 325px; +} + +.volumeControlClass { + position: absolute; + top: 928px; + left: 35px; + width: 660px; + height: 55px; + background: none; +} + +.progressPot { + position: absolute; + top: 4px; + left: 0; + width: 100%; + height: 3px; +} + +.progressBar { + position: absolute; + top: 50px; + left: 0; + width: 325px; + height: 10px; + border-style: solid; + border-width: 0 0 1px 0; +} + +.leftText { + position: absolute; + top: 10px; + left: 0; + width: 40px; + height: 15px; +} + +.rightText { + position: absolute; + top: 10px; + right: 25px; + width: 40px; + height: 15px; +} + +/*audio controls*/ +.audioControlsButtons { + position: absolute; + top: 1000px; + left: 0; + width: 610px; + padding: 0 55px; + background-repeat: no-repeat; + background-position: 50%; +} + +.previousBtn { + opacity: 1; + width: 66px; + height: 66px; + float: left; + margin: 0; + margin-right: 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.previousBtnActive { + opacity: 1; + width: 66px; + height: 66px; + float: left; + margin: 0; + margin-right: 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.prevBtnInactive { + opacity: 0.5; + width: 66px; + height: 66px; + float: left; + margin: 0; + margin-right: 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.pauseBtn { + opacity: 1; + width: 66px; + height: 66px; + float: left; + margin: 0 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.playBtn { + opacity: 1; + width: 66px; + height: 66px; + float: left; + margin: 0 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.nextBtn { + opacity: 1; + width: 66px; + height: 66px; + float: left; + margin: 0 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.nextBtnActive { + opacity: 1; + width: 66px; + height: 66px; + float: left; + margin: 0 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.nextBtnInactive { + opacity: 0.5; + width: 66px; + height: 66px; + float: left; + margin: 0 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.shuffleBtn { + width: 66px; + height: 66px; + float: left; + margin: 0 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.shuffleBtnActive { + width: 66px; + height: 66px; + float: left; + margin: 0 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.repeatBtn { + width: 66px; + height: 66px; + float: left; + margin: 0; + margin-left: 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.repeatBtnActive { + width: 66px; + height: 66px; + float: left; + margin: 0; + margin-left: 35px; + background-repeat: no-repeat; + background-position: 50%; +} + +.songNameTextPosition { + top: 12px; +} + +.artistNameTextMargin { + margin-bottom: 5px; +} \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..527249f84084dc9b5148b44778b3dcfa789ee61c GIT binary patch literal 2216 zcmbVOeLPh88lT;e(A^NFn;etI3iD=WIFs27GsCb_2)WdZnPW_2Ud$YsNwH=>ZEIO~ zWv$+8rP~YDD4~d4givj^VI=Qj%axbAXiM%H(Y=54*+1_2e9rm3J>Tc$`#itrbJDkO z^EFv#fR3=)ETiIZNl8iABsZ*7F2E5$5X9k$I3m#%k#JS0BrsR$Dp71) z(!hWeJh@N?3#Af3uaO%gO@wJ^#MAd7h-HC+9~4Uzi$o!m;gnn%j)2AE#A1D13(^WW z82YapAC*>ws$>u@7*a?Rc`i^cR|(3h}mi2~+Ic+iqvA!2tC>+z>ps*fDv!cuvtR2sij0o&uGuv8Hz zl>t5>WGn#)7fSfjB!xab@1tcva^XIR&yq{UzyiNi;YaL~nPdi;;?DH&A=346(|z#n zbf!0p#3YeDeDEanB9{OEaE3#i!Rd?Re~M*k3#mZ;^h4<*iw~Cvk{~xmj$DlZT?&Xo z8R*yydZ@B<^i-=TG<@}4Y02JD3lSsV-lEulG(SUIeYRxmQ*6|CroSXkUwxKge6`L* zU15FO_@^Jg>itKFRVRn!XL{u*CWYl_&yC3(04t|@e+M574jk;Bc~iq}S&Mmfr|0CB z;qIj2?kv{1zPo!U2`g5a85OqM$z z)t_rP8Q@TERpD~HDOEf{!lati=yL8ARR_8Lr8IkPeYQz+v5%GJVr zQ(xPK#3%u!uI*)oP0c4z)NC@@{cRagUMZ`!(`V1lpqgp;tRTN;W$Hs!2FFV?JlLJ- z_=n;6Sf6?HRKH@)XpB~L%?_J>w&LiM(eU{jzR!BS^LBQOUFx;M8DwK zUG-o7=6EXr=patRe=V*Go{7{nK5_9J*sNK-V~7|r_~JV^LA1tfN7(qL3A4fxOPl7M z5xJBMv!6z7HU7Fa`}__(R|j61_J{pPv+kscw5E@AXKv@7x>`^+c%IYe98`C?<=Vdv zbl&9mg_S-a_cV4o*L)4yijF8Q+zd$S+Q~U92vna}X`j1X+cfvw@-n!MeSn_*yLkg> zJIqV3H`qO_%pJVyD-&#MP&uZxuhl$^y3KZNU)h>d7dom=1Jl-zuCO4l`HZyR{LrMLXuk@A=$IcIT) zMSoLlx)VKHQJ3dbHQ;Nx`Evg7kyAUP@n|K^E6Lrp1s~Dcn8o@>*w7HwkH=V#@jUQHD0eZzRQdD?93k^6cwTdE0+3 zjl_iPJw$Bf)c(<6_+|rtZtQf^xMJoT=SO9m zwygVpuzhPW;ic;&=3-NIhU|1C|5VVe!h#ctZ`bBIx1T*}(}k&66J7iW+pP3|GN1p% z!IHUtW1^@s6 literal 0 HcmV?d00001 diff --git a/images/audio-placeholder.jpg b/images/audio-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6a0ff2d9b5dbffe748b274f9ba0420463a81134f GIT binary patch literal 8159 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%0DeRwJ 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 g_v-4Q+?r9h53%jYC + + + + + +Multimedia Player + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
LIBRARY
+ +
+
+
    +
    + + + + + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    + +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + diff --git a/js/carousel.js b/js/carousel.js new file mode 100644 index 0000000..915f515 --- /dev/null +++ b/js/carousel.js @@ -0,0 +1,221 @@ +/*global Utils */ + +/** + * @module MultimediaPlayerApplication + */ + +/** + * This class provides basic methods to operate with media content carousel (carouFredSel) like load and fill carousel with supplied audio content, scroll carousel to a given position, get current carousel position. + * Media content carousel represents a playlist of audio tracks and allows user to browse tracks by swiping to left (next track) or right (previous track). Each carousel's item contains thumbnail, artist name and title. + * + * @class Carousel + * @constructor + */ +var Carousel = function() { + "use strict"; + this.initializeSwipe(); +}; +/** +* This property holds audio media content array that carousel is filled with. +* @property callHistory {Array} +* @default [] +*/ +Carousel.prototype.allMediaContent = []; +/** +* This property holds carouFredSel object for internal use in carousel. +* @property swipe {Object} +* @private +*/ +Carousel.prototype.swipe = null; +/** +* This property holds callback function which is called after current element/position in carousel is changed. +* @property indexChangeCallback {Object} +* @private +*/ +Carousel.prototype.indexChangeCallback = null; +/** + * This method adds listener that will be called right after the carousel finished scrolling and current element/position is changed. + * + * @method addIndexChangeListener + * @param indexChangeCallback {function()} Callback function to be invoked when current element/position of carousel is changed. + */ +Carousel.prototype.addIndexChangeListener = function(indexChangeCallback) { + "use strict"; + + this.indexChangeCallback = indexChangeCallback; +}; + +/** + * Initializes and configures carouFredSel carousel object. + * + * @method initializeSwipe + * @private + */ + Carousel.prototype.initializeSwipe = function() { + "use strict"; + + var self = this; + if (!this.swipe) { + this.swipe = $('#carouselList').carouFredSel({ + auto : false, + circular : false, + infinite : false, + width : 765, + items : { + visible : 3 + }, + swipe : { + items : 1, + duration : 150, + onMouse : true, + onTouch : true + }, + scroll : { + items : 1, + duration : 150, + onAfter : function(data) { + if (!!self.indexChangeCallback) { + self.indexChangeCallback(self.getCurrentPosition()); + } + } + } + }); + if (!this.swipe.length) { + this.swipe = null; + } + } +}; + +/** + * Gets the position of selected carousel item. + * + * @method getCurrentPosition + */ +Carousel.prototype.getCurrentPosition = function() { + "use strict"; + var self = this; + if (!!self.swipe) { + var pos = parseInt(self.swipe.triggerHandler("currentPosition"), 10); + return pos; + } + return null; +}; + +/** + * Scrolls the carousel to given index. + * + * @method slideTo + * @param index {Integer} New position to be scrolled to. + */ +Carousel.prototype.slideTo = function(index) { + "use strict"; + if (!!this.swipe && index >= 0 && index < this.allMediaContent.length) { + this.swipe.trigger("slideTo", index); + } +}; +/** + * This method fills carousel with audio media content and scrolls immediately the carousel to the designated position. + * + * @method loadMediaContent + * @param allMediaContent {Array} Audio media content array to be filled in the carousel. + * @param index {Integer} Position to be scrolled to. + */ +Carousel.prototype.loadMediaContent = function(allMediaContent, index) { + "use strict"; + this.removeAllItems(); + this.allMediaContent = allMediaContent; + this.insertPagesToSwipe(); + if (index >= 0 && index < this.allMediaContent.length && !!this.swipe) { + this.swipe.trigger("slideTo", [ index, 0, { + duration : 0 + } ]); + } +}; + +/** + * Creates an HTML snippet representing one carousel item to be inserted into the carousel. + * + * @method createSwipeItem + * @param mediaItem {Object} Object representing audio media item's information. + * @param index {Integer} Position of item in carousel used to identify supplied audio media item's HTML DOM representation. + * @return {Object} jQuery DOM object representation of audio media item. + * @private + */ +Carousel.prototype.createSwipeItem = function(mediaItem, index) { + "use strict"; + + if (!!mediaItem) { + var carouselItem; + var thumbnail = Utils.getThumbnailPath(mediaItem); + var artist = Utils.getArtistName(mediaItem); + var album = Utils.getAlbumName(mediaItem); + var title = Utils.getMediaItemTitle(mediaItem); + + carouselItem = '
  • '; + carouselItem += ''; + carouselItem += '
    '; + carouselItem += '
    ' + artist + '
    '; + carouselItem += '
    ' + title + '
    '; + carouselItem += '
    '; + carouselItem += '
  • '; + + carouselItem = $(carouselItem); + return carouselItem; + } + + return null; +}; + +/** + * Inserts new carousel item into carousel. + * + * @method insertPagesToSwipe + * @private + */ +Carousel.prototype.insertPagesToSwipe = function() { + "use strict"; + var self = this; + var carouselItem; + var clickHandler = function() { + self.swipe.trigger("slideTo", [ $(this), -1 ]); + }; + + for ( var index = this.allMediaContent.length - 1; index >= 0; --index) { + carouselItem = this.createSwipeItem(this.allMediaContent[index], index); + if (!!carouselItem && !!this.swipe) { + this.swipe.trigger("insertItem", [ carouselItem, 0 ]); + carouselItem.click(clickHandler); + } + } + this.addCarouselEdges(); +}; + +/** + * Removes all items from the carousel. + * + * @method removeAllItems + */ +Carousel.prototype.removeAllItems = function() { + "use strict"; + var carouselItem; + + if (!!this.swipe) { + for ( var index = this.allMediaContent.length + 1; index >= 0; --index) { + this.swipe.trigger("removeItem", index); + } + } +}; + +/** + * Adds emty carousel items to the beginning and the end of the carousel + * (to make sure first and last visible items appear in the middle of screen instead of at the edges when swiped to edges of carousel). + * @method addCarouselEdges + */ +Carousel.prototype.addCarouselEdges = function() { + "use strict"; + if (!!this.swipe) { + var html = "
  • "; + this.swipe.trigger("insertItem", [ html, 0 ]); + this.swipe.trigger("insertItem", [ html, "end", true ]); + } +}; diff --git a/js/localcontent.js b/js/localcontent.js new file mode 100644 index 0000000..9bf8028 --- /dev/null +++ b/js/localcontent.js @@ -0,0 +1,590 @@ +/*global ko */ + +/** + * @module MultimediaPlayerApplication + */ + +/** + * Class representing local media content for MultiMedia Player. + * + * @class LocalContent + * @constructor + */ +var LocalContent = function() { + "use strict"; + var self = this; + this.content = tizen.content; + this.localContent = ko.observableArray([]); + this.history = ko.observableArray([]); + + this.audioType = "AUDIO"; + this.allAudioContent = ko.observableArray([]); + this.findAllAudioContent(); + + this.videoType = "VIDEO"; + this.allVideoContent = ko.observableArray([]); + this.findAllVideoContent(); + + this.alphabetFilter = ko.observable(""); + + this.localContentComputed = ko.computed(function() { + if (self.alphabetFilter() !== "") { + return ko.utils.arrayFilter(self.localContent(), function(content) { + return content.title.toString().toLowerCase().trim().indexOf(self.alphabetFilter().toString().toLowerCase().trim()) === 0; + }); + } + return self.localContent(); + }); +}; + +/** + * This method fills local content with media categories. + * + * @method fillCategories + */ +LocalContent.prototype.fillCategories = function() { + "use strict"; + var self = this; + var resultCategories = self.getCategories(); + self.localContent(resultCategories); +}; + +/** + * This method provides media categories content. + * + * @method getCategories + * @return {Array} categories array + */ +LocalContent.prototype.getCategories = function() { + "use strict"; + var self = this; + var categories = []; + categories.push({ + title : "MUSIC", + subtitle : "", + operation : "browse_category", + type : self.audioType + }); + categories.push({ + title : "VIDEOS", + subtitle : "", + operation : "browse_category", + type : self.videoType + }); + console.log(categories); + return categories; +}; + +/** + * This method fills local content array with media sub categories based on the content type. + * + * @method getCategories + */ +LocalContent.prototype.fillSubCategories = function(content) { + "use strict"; + var self = this, resultCategories = []; + switch (content.type) { + case self.audioType: + resultCategories = self.getAudioSubCategories(content.type); + break; + case self.videoType: + resultCategories = self.getVideoSubCategories(content.type); + break; + default: + console.log("Type not supported"); + break; + } + self.localContent(resultCategories); +}; + +/** + * This method provides audio categories content by type. + * + * @method getAudioSubCategories + * @param type {Object} media content type + * @return {Array} result categories array + */ +LocalContent.prototype.getAudioSubCategories = function(type) { + "use strict"; + var self = this; + var categories = [ "ARTISTS", "ALBUMS", "ALL" ]; + var resultCategories = []; + for ( var i = 0; i < categories.length; ++i) { + resultCategories.push({ + title : categories[i], + subtitle : "", + operation : "browse_" + categories[i].toLowerCase(), + type : type + }); + } + console.log(resultCategories); + return resultCategories; +}; + +/** + * This method provides video categories content by type. + * + * @method getVideoSubCategories + * @param type {Object} media content type + * @return {Array} result categories array + */ +LocalContent.prototype.getVideoSubCategories = function(type) { + "use strict"; + var self = this; + var categories = [ "ARTISTS", "ALBUMS", "ALL" ]; + var resultCategories = []; + for ( var i = 0; i < categories.length; ++i) { + resultCategories.push({ + title : categories[i], + subtitle : "", + operation : "browse_" + categories[i].toLowerCase(), + type : type + }); + } + console.log(resultCategories); + return resultCategories; +}; + +/** + * This method fills local content array with artists. + * + * @method fillArtists + * @param content {Object} media content + */ +LocalContent.prototype.fillArtists = function(content) { + "use strict"; + var self = this; + var resultArtists = self.getAllArtists(content.type); + self.localContent(resultArtists); + self.localContent.sort(self.compareByTitle); +}; + +/** + * This method provides media content based on the type. + * + * @method getMediaContentByType + * @param type {String} media content type + * @return {Object} media content + */ +LocalContent.prototype.getMediaContentByType = function(type) { + "use strict"; + var self = this, mediaContent = null; + switch (type) { + case self.audioType: + mediaContent = self.allAudioContent; + break; + case self.videoType: + mediaContent = self.allVideoContent; + break; + default: + console.log("Type not supported"); + break; + } + return mediaContent.slice(0); +}; + +/** + * This method provides all artists content by type. + * + * @method getAllArtists + * @param type {String} media content type + * @return {Array} artists array + */ +LocalContent.prototype.getAllArtists = function(type) { + "use strict"; + var self = this, resultArtists = [], mediaContent; + mediaContent = self.getMediaContentByType(type); + if (!!mediaContent && mediaContent.length) { + var artists = ko.utils.arrayMap(mediaContent, function(content) { + if (!!content.artists && content.artists.length) { + return content.artists.join(", "); + } + return "Unknown"; + }); + + if (artists.length) { + var uniqueArtists = ko.utils.arrayGetDistinctValues(artists); + ko.utils.arrayForEach(uniqueArtists, function(artist) { + var artistAlbumsAndContent = self.getArtistAlbumsAndContent(artist, type); + resultArtists.push({ + artist : artist, + type : type, + title : artist, + subtitle : artistAlbumsAndContent.albums.length + (artistAlbumsAndContent.albums.length === 1 ? " ALBUM, " : " ALBUMS, ") + + artistAlbumsAndContent.content.length + + (type === self.audioType ? (artistAlbumsAndContent.content.length === 1 ? " TRACK" : " TRACKS") : + (artistAlbumsAndContent.content.length === 1 ? " MOVIE" : " MOVIES")), + operation : "browse_artist" + }); + }); + } + } + console.log(resultArtists); + return resultArtists; +}; + +/** + * This method provides albums and media content for a given artist and type of content. + * + * @method getArtistAlbumsAndContent + * @param artist {String} artist + * @param type {String} media content type + * @return {Object} a result object + */ +LocalContent.prototype.getArtistAlbumsAndContent = function(artist, type) { + "use strict"; + var self = this; + var result = { + artist : artist, + albums : [], + content : [] + }; + var mediaContent = self.getMediaContentByType(type); + if (!!mediaContent && mediaContent.length && !!artist && artist !== "") { + var artistContent = ko.utils.arrayFilter(mediaContent, function(content) { + /*global Utils */ + var artistName = Utils.getArtistName(content); + return artistName === artist; + }); + var artistAlbums = ko.utils.arrayMap(artistContent, function(content) { + var artistAlbumName = Utils.getAlbumName(content); + return artistAlbumName; + }); + var uniqueArtistAlbums = ko.utils.arrayGetDistinctValues(artistAlbums); + if (!!uniqueArtistAlbums && uniqueArtistAlbums.length) { + result.albums = uniqueArtistAlbums; + } + if (!!artistContent && artistContent.length) { + result.content = artistContent; + } + } + console.log(result); + return result; +}; + +/** + * This method fills local content with albums for a given artist and type of content. + * + * @method fillArtistAlbums + * @param content {Object} media content info + */ +LocalContent.prototype.fillArtistAlbums = function(content) { + "use strict"; + var self = this; + var artistAlbums = self.getArtistAlbums(content.artist, content.type); + self.localContent(artistAlbums); + self.localContent.sort(self.compareByTitle); +}; + +/** + * This method provides albums for a given artist and type of content. + * + * @method getArtistAlbums + * @param artist {String} artist + * @param type {String} media content type + * @return {Array} albums array + */ +LocalContent.prototype.getArtistAlbums = function(artist, type) { + "use strict"; + var self = this, resultAlbums = []; + var artistAlbumsAndContent = self.getArtistAlbumsAndContent(artist, type); + ko.utils.arrayForEach(artistAlbumsAndContent.albums, function(album) { + var newAlbum = self.createAlbum(artist, album, type); + resultAlbums.push(newAlbum); + }); + console.log(resultAlbums); + return resultAlbums; +}; + +/** + * This method creates a new album object for a given artist, album and type of content. + * + * @method getArtistAlbums + * @param artist {String} artist + * @param album {String} album title + * @param type {String} media content type + * @return {Object} album object + */ +LocalContent.prototype.createAlbum = function(artist, album, type) { + "use strict"; + var self = this; + var newAlbum = { + artist : artist, + album : album, + title : album, + subtitle : artist, + thumbnail : self.getArtistAlbumThumbnail(artist, album, type), + operation : "browse_album", + type : type + }; + return newAlbum; +}; + +/** + * This method fills local content with album for a given artist, album and type of content. + * + * @method fillArtistAlbumContent + * @param content {Object} media content + */ +LocalContent.prototype.fillArtistAlbumContent = function(content) { + "use strict"; + var self = this; + var artistAlbumContent = self.getArtistAlbumContent(content.artist, content.album, content.type); + self.localContent(artistAlbumContent); + self.localContent.sort(self.compareByTitle); +}; + +/** + * This method provides album content for a given artist, album and type of content. + * + * @method getArtistAlbumContent + * @param artist {String} artist + * @param album {String} album title + * @param type {String} media content type + * @return {Object} content object + */ +LocalContent.prototype.getArtistAlbumContent = function(artist, album, type) { + "use strict"; + var self = this, resultContent = [], mediaContent; + mediaContent = self.getMediaContentByType(type); + if (!!mediaContent && mediaContent.length) { + resultContent = ko.utils.arrayFilter(mediaContent, function(content) { + var artistName = Utils.getArtistName(content); + var artistAlbumName = Utils.getAlbumName(content); + return artistName === artist && artistAlbumName === album; + }); + } + console.log(resultContent); + return resultContent; +}; + +/** + * This method fills local content with albums for a given type of content. + * + * @method fillAlbums + * @param content {Object} media content + */ +LocalContent.prototype.fillAlbums = function(content) { + "use strict"; + var self = this; + var allAlbums = self.getAllAlbums(content.type); + self.localContent(allAlbums); + self.localContent.sort(self.compareByTitle); +}; + +/** + * This method provides all albums for a given type of content. + * + * @method getAllAlbums + * @param type {String} media content type + * @return {Array} albums array + */ +LocalContent.prototype.getAllAlbums = function(type) { + "use strict"; + var self = this, resultAlbums = [], mediaContent; + mediaContent = self.getMediaContentByType(type); + if (!!mediaContent && mediaContent.length) { + var albums = ko.utils.arrayMap(mediaContent, function(content) { + var artistName = Utils.getArtistName(content); + var artistAlbumName = Utils.getAlbumName(content); + var album = { + artist : artistName, + album : artistAlbumName + }; + return JSON.stringify(album); + }); + if (!!albums && albums.length) { + var uniqueAlbums = ko.utils.arrayGetDistinctValues(albums); + ko.utils.arrayForEach(uniqueAlbums, function(albumJSON) { + var album = JSON.parse(albumJSON); + var newAlbum = self.createAlbum(album.artist, album.album, type); + resultAlbums.push(newAlbum); + }); + } + } + console.log(resultAlbums); + return resultAlbums; +}; + +/** + * This method fills local content with albums for a given type of content. + * + * @method fillAlbums + * @param content {Object} media content + */ +LocalContent.prototype.fillAll = function(content) { + "use strict"; + var self = this, mediaContent; + mediaContent = self.getMediaContentByType(content.type); + self.localContent(mediaContent); + self.localContent.sort(self.compareByTitle); +}; + +/** + * This method empties local content. + * + * @method clearLocalContent + */ +LocalContent.prototype.clearLocalContent = function() { + "use strict"; + var self = this; + self.localContent.removeAll(); + self.localContent([]); +}; + +/** + * This method empties history of opened local content. + * + * @method clearLocalContent + */ +LocalContent.prototype.clearHistory = function() { + "use strict"; + var self = this; + self.history.removeAll(); + self.history([]); +}; + +/** + * This method adds given local content to history of opened local content and clears local content. + * + * @method pushToHistory + * @param content {Object} media content + */ +LocalContent.prototype.pushToHistory = function(content) { + "use strict"; + var self = this; + self.clearLocalContent(); + self.history.push(content); +}; + +/** + * This method gets a thumbnail for a given artist, album and type of media content. + * + * @method getArtistAlbumThumbnail + * @param artist {String} artist + * @param album {String} album title + * @param type {String} media content type + */ +LocalContent.prototype.getArtistAlbumThumbnail = function(artist, album, type) { + "use strict"; + var self = this; + var artistAlbumContent = this.getArtistAlbumContent(artist, album, type); + var contentWithThumbnail = ko.utils.arrayFirst(artistAlbumContent, function(content) { + return !!content.thumbnailURIs && content.thumbnailURIs.length; + }); + return Utils.getThumbnailPath(contentWithThumbnail, type); +}; + +/** + * This method filters audio content out of all content. + * + * @method findAllAudioContent + */ +LocalContent.prototype.findAllAudioContent = function() { + "use strict"; + var self = this; + if (!!self.content) { + var filter = new tizen.AttributeFilter("type", "EXACTLY", self.audioType); + self.content.find(function(content) { + self.onContentArraySuccess(content, self.audioType); + }, function(error) { + self.onError(error); + }, null, filter); + } +}; + +/** + * This method filters video content out of all content. + * + * @method findAllVideoContent + */ +LocalContent.prototype.findAllVideoContent = function() { + "use strict"; + var self = this; + if (!!self.content) { + var filter = new tizen.AttributeFilter("type", "EXACTLY", self.videoType); + self.content.find(function(content) { + self.onContentArraySuccess(content, self.videoType); + }, function(error) { + self.onError(error); + }, null, filter); + } +}; + +/** + * This method is success callback for find content methods (findAllAudioContent and findAllVideoContent). + * + * @method onContentArraySuccess + * @param content {Object} media content + * @param type {String} content type + */ +LocalContent.prototype.onContentArraySuccess = function(content, type) { + "use strict"; + var self = this; + console.log(content); + + content.sort(self.compareByTitle); + + switch (type) { + case self.audioType: + self.allAudioContent(content); + break; + case self.videoType: + self.allVideoContent(content); + break; + default: + break; + } +}; + +/** + * This method compares neighbouring content items by their titles for sorting. + * + * @method compareByTitle + * @param left {Object} media content + * @param right {Object} media content + */ +LocalContent.prototype.compareByTitle = function(left, right) { + "use strict"; + var leftTitle = "Unknown"; + if (!!left.title && left.title !== "") { + leftTitle = left.title; + } + leftTitle = leftTitle.toString().trim().toLowerCase(); + var rightTitle = "Unknown"; + if (!!right.title && right.title !== "") { + rightTitle = right.title; + } + rightTitle = rightTitle.toString().trim().toLowerCase(); + return leftTitle === rightTitle ? 0 : (leftTitle < rightTitle) ? -1 : 1; +}; + +/** + * This method is error callback for find content methods (findAllAudioContent and findAllVideoContent). + * + * @method onError + * @param error {Object} returned error + */ +LocalContent.prototype.onError = function(error) { + "use strict"; + var self = this; + console.log(error); +}; + +/** + * This method gets selected local content based on the content type. + * + * @method getSelectedLocalContent + * @param type {String} media content type + * @return {Object} mediaItem object + */ +LocalContent.prototype.getSelectedLocalContent = function(type) { + "use strict"; + var self = this; + if (!!self.localContent() && self.localContent().length) { + return ko.utils.arrayFilter(self.localContent(), function(mediaItem) { + return mediaItem.type === type; + }); + } + return []; +}; \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..b41b6ad --- /dev/null +++ b/js/main.js @@ -0,0 +1,112 @@ +/*global MultimediaLibrary, Utils*/ + +/** + * Multimedia player application allows user to play back audio and video content from local sources via + * [tizen.content API](https://developer.tizen.org/dev-guide/2.2.1/org.tizen.web.device.apireference/tizen/content.html) and + * remote DLNA content via [IVI WRT MediaServer API](https://review.tizen.org/git/?p=profile/ivi/wrt-plugins-ivi.git;a=tree;f=src/MediaServer). + * Application uses HTML5 [audio](http://www.w3.org/wiki/HTML/Elements/audio) and [video](http://www.w3.org/wiki/HTML/Elements/video) elements + * to playback content from sources. + * + * Upper part of application contains a media content carousel (carouFredSel) in case audio content is selected or a rendered video (HTML5 video element). + * + * Lower part of application contains basic information about played audio/video content like thumbnail, title, artist name, album name, duration and controls like play, pause, next, previous, volume. + * Additionaly Multimedia player application can be controlled using speech recognition via {{#crossLink "Speech"}}{{/crossLink}} component. + * + * Audio/video content to be played can be selected from {{#crossLink "MultimediaLibrary"}}{{/crossLink}} component. Playback of audio and video content is controlled by {{#crossLink "AudioPlayer"}}{{/crossLink}} component. + * + * Hover and click on elements in images below to navigate to components of Multimedia Player application. + * + * + * + * top bar icons + * clock + * bottom panel + * Settings + * Multimedia library + * Multimedia carousel + * Volume control + * Control buttons + * Time progress bar + * Info panel + * Spectrum analyzer + * Thumbnail + * + * + * @module MultimediaPlayerApplication + * @main MultimediaPlayerApplication + * @class MultimediaPlayer + */ + + /** + * Adds the listener object to receive notifications when the speech recognizer returns a speech command to control multimedia player: PLAY, STOP, NEXT, PREVIOUS. + * + * @method initVoiceRecognition + */ +var initVoiceRecognition = function() { + "use strict"; + /* global Speech */ + if (typeof (Speech) !== 'undefined') { + Speech.addVoiceRecognitionListener({ + onplay : function() { + $('#multimediaPlayer').audioAPI('playPause', true); + }, + onstop : function() { + $('#multimediaPlayer').audioAPI('playPause', false); + }, + onnext : function() { + $('#multimediaPlayer').audioAPI('next'); + $('#multimediaPlayer').audioAPI('playPause', true); + }, + onprevious : function() { + $('#multimediaPlayer').audioAPI('previous'); + $('#multimediaPlayer').audioAPI('playPause', true); + } + }); + } else { + console.warn("Speech API is not available."); + } +}; + +/** + * Method which provides methods to initialize UI and create listener for + * changing themes of music player. + * + * @method init + * @constructor + */ +var bootstrap; +var init = function() { + "use strict"; + /*global Bootstrap */ + bootstrap = new Bootstrap(function(status) { + $("#libraryButton").on("click", function() { + MultimediaLibrary.show(); + }); + + $("#videoPlayer").on("click", function() { + Utils.launchFullScreen(this); + }); + + $("#topBarIcons").topBarIconsPlugin('init'); + $("#clockElement").ClockPlugin('init', 5); + $("#clockElement").ClockPlugin('startTimer'); + $('#bottomPanel').bottomPanel('init'); + + $('#multimediaPlayer').audioAPI('setControlButtonsSelector', '#controlButtons'); + $('#multimediaPlayer').audioAPI('setTimeProgressBarSelector', '#timeBar'); + $('#multimediaPlayer').audioAPI('setSpectrumAnalyzerSelector', '#spectAnalyzer'); + $('#multimediaPlayer').audioAPI('setInfoPanelSelector', '#infoPanel'); + $('#multimediaPlayer').audioAPI('setThumbnailSelector', '#thumbnail'); + $('#multimediaPlayer').audioAPI('setVolumeControlSelector', '.noVolumeSlider'); + $('#multimediaPlayer').audioAPI('init', [], "#audioPlayer", "#videoPlayer"); + + MultimediaLibrary.init(); + initVoiceRecognition(); + }); +}; + +$(document).ready(function() { + "use strict"; + // debug mode - window.setTimeout("init()", 20000); + init(); +}); diff --git a/js/mediacontent.js b/js/mediacontent.js new file mode 100644 index 0000000..c76a35c --- /dev/null +++ b/js/mediacontent.js @@ -0,0 +1,225 @@ +/****************************************************************************** + * Copyright 2012 Intel Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +/** + * @module MultimediaPlayerApplication + */ + +/** + * Class providing objects mapping the org.gnome.UPnP MediaObject2 and MediaItem2 interfaces. + * + * @class mediacontent + */ +var mediacontent = window.mediacontent = {}; + +/** + * Generic media object. + * + * @class MediaObject + * @return {Object} MediaObject objects + */ +mediacontent.MediaObject = function(proxy) { + "use strict"; + this.proxy = proxy; + if (proxy) { + this.id = proxy.Path; + this.type = proxy.Type; + this.title = proxy.DisplayName; + } + return this; +}; + +/** + * Gets the MediaObject metadata info. + * + * @method getMetaData + * @return {Object} metadata object + */ +mediacontent.MediaObject.prototype.getMetaData = function() { + "use strict"; + return this.proxy.callMethod("org.gnome.UPnP.MediaObject2", "GetMetaData", []); +}; + +/** + * Mediacontent object of type container. + * + * @class MediaContainer + * @param proxy {Object} media source object + * @return {Object} MediaContainer object + */ +mediacontent.MediaContainer = function(proxy) { + "use strict"; + mediacontent.MediaObject.call(this, proxy); + this.type = "CONTAINER"; + this.directoryURI = ""; + this.storageType = "EXTERNAL"; + return this; +}; + +mediacontent.MediaContainer.prototype = new mediacontent.MediaObject(); +mediacontent.MediaContainer.prototype.constructor = mediacontent.MediaContainer; + +/** + * Mediacontent object of type media item. Provides access to properties of media items. + * + * @class MediaItem + * @param proxy {Object} media source object + * @return {Object} MediaItem object + */ +mediacontent.MediaItem = function(proxy) { + "use strict"; + mediacontent.MediaObject.call(this, proxy); + if (proxy) { + this.mimeType = proxy.MIMEType; + if (proxy.URLs) { + this.contentURI = proxy.URLs[0]; + } else { + this.contentURI = ""; + } + this.size = proxy.Size; + this.releaseDate = proxy.Date; + this.modifiedDate = null; + this.name = this.title; + this.editableAttributes = []; + this.thumbnailURIs = []; + if (!!proxy.AlbumArtURL && proxy.AlbumArtURL !== "") { + this.thumbnailURIs.push(proxy.AlbumArtURL); + } + this.description = "Unknown"; + this.rating = 0; + } + this.type = "OTHER"; + return this; +}; + +mediacontent.MediaItem.prototype = new mediacontent.MediaObject(); +mediacontent.MediaItem.prototype.constructor = mediacontent.MediaItem; + +/** + * Mediacontent object of type video. Extends a basic media item object with video-specific attributes. + * + * @class MediaVideo + * @param proxy {Object} media source object + * @return {Object} MediaVideo object + */ +mediacontent.MediaVideo = function(proxy) { + "use strict"; + mediacontent.MediaItem.call(this, proxy); + if (proxy) { + this.duration = proxy.Duration * 1000; //Tizen's ContentVideo is in ms + this.width = proxy.Width; + this.height = proxy.Height; + if (proxy.Album) { + this.album = proxy.Album; + } else { + this.album = "Unknown"; + } + if (proxy.Artist) { + this.artists = [ proxy.Artist ]; + } else { + this.artists = [ "Unknown" ]; + } + this.geolocation = null; + } + this.type = "VIDEO"; + return this; +}; + +mediacontent.MediaVideo.prototype = new mediacontent.MediaItem(); +mediacontent.MediaVideo.prototype.constructor = mediacontent.MediaVideo; + +/** + * Mediacontent object of type audio. Extends a basic media item object with audio-specific attributes. + * + * @class MediaAudio + * @param proxy {Object} media source object + * @return {Object} MediaAudio object + */ +mediacontent.MediaAudio = function(proxy) { + "use strict"; + mediacontent.MediaItem.call(this, proxy); + if (proxy) { + this.bitrate = proxy.SampleRate; + this.duration = proxy.Duration * 1000; //Tizen's ContentAudio is in ms + if (proxy.Album) { + this.album = proxy.Album; + } else { + this.album = "Unknown"; + } + //this; + if (proxy.Artist) { + this.artists = [ proxy.Artist ]; + } else { + this.artists = [ "Unknown" ]; + } + this.genres = []; + this.composers = [ "Unknown" ]; + this.lyrics = null; + this.copyright = "Unknown"; + this.trackNumber = 0; + } + this.type = "AUDIO"; + return this; +}; + +mediacontent.MediaAudio.prototype = new mediacontent.MediaItem(); +mediacontent.MediaAudio.prototype.constructor = mediacontent.MediaAudio; + +/** + * Mediacontent object of type image. Extends a basic media item object with image-specific attributes. + * + * @class MediaImage + * @param proxy {Object} media source object + * @return {Object} MediaImage object + */ +mediacontent.MediaImage = function(proxy) { + "use strict"; + mediacontent.MediaItem.call(this, proxy); + if (proxy) { + this.width = proxy.Width; + this.height = proxy.Height; + this.orientation = "NORMAL"; + } + this.type = "IMAGE"; + return this; +}; + +mediacontent.MediaImage.prototype = new mediacontent.MediaItem(); +mediacontent.MediaImage.prototype.constructor = mediacontent.MediaImage; + +/** + * Returns appropriate media object based on the given parameter media type. + * + * @class mediaObjectForProps + * @param props {Object} media source object + * @return {Object} correct media object by props.type + */ +mediacontent.mediaObjectForProps = function(props) { + "use strict"; + if (props.Type.indexOf("container") === 0 || props.Type.indexOf("album") === 0 || props.Type.indexOf("person") === 0 || props.Type.indexOf("genre") === 0){ + return new mediacontent.MediaContainer(props); + } + if (props.Type.indexOf("video") === 0){ + return new mediacontent.MediaVideo(props); + } + if (props.Type.indexOf("audio") === 0 || props.Type.indexOf("music") === 0){ + return new mediacontent.MediaAudio(props); + } + if (props.Type.indexOf("image") === 0){ + return new mediacontent.MediaImage(props); + } + return new mediacontent.MediaItem(props); +}; diff --git a/js/multimedialibrary.js b/js/multimedialibrary.js new file mode 100644 index 0000000..4336428 --- /dev/null +++ b/js/multimedialibrary.js @@ -0,0 +1,484 @@ +/** + * @module MultimediaPlayerApplication + */ + +/** + * Class which provides methods to operate with Multimedia player library that utilizes {{#crossLink "Library"}}{{/crossLink}} component. + * Library allows user to select either LOCAL or REMOTE content. + * LOCAL content tab is devided by content type into 2 categories: MUSIC and VIDEOS. Each content type can be browsed by ARTISTS, ALBUMS or ALL (all audio tracks / all video files). Moreover ARTISTS can be browsed by ALBUMS. + * Following hierarchical list represents structure of LOCAL content: + * + * * LOCAL + * * MUSIC + * * ARTISTS + * * ALBUMS + * * ALBUMS + * * ALL + * * VIDEOS + * * ARTISTS + * * ALBUMS + * * ALBUMS + * * ALL + * + * REMOTE content tab contains alphabetically sorted list of available DLNA media servers and integrates browsing of DLNA Media Container of selected DLNA media server + * starting from root element and drill down through folders (keeps the structure as is defined by remote server). + * + * Clicking audio track in library selects all audio items from the current list, creates and sets audio player playlist, shows and fills in the media carousel and starts playing selected track. + * Clicking video file in library selects all video items from the current list, creates a sets video player playlist, shows video element and starts playing selected video. Clicking on the rendered video or back button lets the user toggle between windowed and fullscreen presentation of the video. + * + * @class MultimediaLibrary + * @static + */ +var MultimediaLibrary = { + remoteContent : null, + remoteContentReScanInterval : null, + localContent : null, + carousel : null, + speechObj : null, + /*global ko */ + mediaContentTemplate : ko.observable(""), + /** + * Holds status of music library initialization. + * + * @property initialized {Boolean} + */ + initialized : false, + /** + * Method is initializing music library. + * + * @method init + */ + init : function() { + "use strict"; + /*global RemoteContent*/ + MultimediaLibrary.remoteContent = new RemoteContent(); + MultimediaLibrary.remoteContent.setMediaSourceLostListener(function(mediaSourceId) { + if ($("#mediaContentList").length) { + if (!!MultimediaLibrary.remoteContent.selectedMediaSource() && MultimediaLibrary.remoteContent.selectedMediaSource().id === mediaSourceId) { + MultimediaLibrary.showMediaSources(); + } + } + }); + /*global LocalContent*/ + MultimediaLibrary.localContent = new LocalContent(); + /*global Carousel*/ + MultimediaLibrary.carousel = new Carousel(); + MultimediaLibrary.carousel.addIndexChangeListener(function(index) { + console.log("NEW CAROUSEL INDEX " + index); + if ($('#multimediaPlayer').audioAPI('getCurrentPlayerType') === MultimediaLibrary.localContent.audioType) { + $('#multimediaPlayer').audioAPI('play', index); + } + }); + $('#multimediaPlayer').audioAPI('addIndexChangeListener', function(index) { + console.log("NEW PLAYER INDEX " + index); + if ($('#multimediaPlayer').audioAPI('getCurrentPlayerType') === MultimediaLibrary.localContent.audioType) { + MultimediaLibrary.carousel.slideTo(index); + } + }); + + $('#musicLibrary').library("setSectionTitle", "MULTIMEDIA LIBRARY"); + $('#musicLibrary').library("init"); + + var tabMenuItems = [ { + text : "LOCAL", + selected : true + }, { + text : "REMOTE", + selected : false + } ]; + + var tabMenuModel = { + Tabs : tabMenuItems + }; + $('#library').library("setAlphabetVisible", true); + $('#musicLibrary').library("tabMenuTemplateCompile", tabMenuModel); + $('#musicLibrary').bind('eventClick_GridViewBtn', function() { + $('#musicLibrary').library('changeContentClass', "musicLibraryContentGrid"); + }); + $('#musicLibrary').bind('eventClick_ListViewBtn', function() { + $('#musicLibrary').library('changeContentClass', "musicLibraryContentList"); + }); + $('#musicLibrary').bind('eventClick_SearchViewBtn', function() { + // search code here + }); + $('#musicLibrary').bind('eventClick_menuItemBtn', function(e, data) { + MultimediaLibrary.renderTabContent(data.Index); + }); + $('#musicLibrary').bind('eventClick_closeSubpanel', function() { + }); + $("#alphabetBookmarkList").on("letterClick", function(event, letter) { + console.log(letter); + MultimediaLibrary.remoteContent.alphabetFilter(letter === "*" ? "" : letter); + MultimediaLibrary.localContent.alphabetFilter(letter === "*" ? "" : letter); + }); + MultimediaLibrary.renderTabContent($('#musicLibrary').library('getSelectetTopTabIndex')); + MultimediaLibrary.initialized = true; + }, + /** + * Shows music library panel. + * + * @method show + */ + show : function() { + "use strict"; + $('#musicLibrary').library("showPage"); + }, + /** + * Hides music library panel. + * + * @method hide + */ + hide : function() { + "use strict"; + $('#musicLibrary').library("hidePage"); + }, + /** + * Renders the Multimedia library content for given library tab. + * + * @method renderTabContent + */ + renderTabContent : function(tabIndex) { + "use strict"; + switch (tabIndex) { + case 0: + MultimediaLibrary.showLocalContent(); + break; + case 1: + MultimediaLibrary.showMediaSources(); + break; + default: + break; + } + }, + /** + * Shows local content categories [Music, Videos] in grid or list view. + * + * @method showLocalContent + */ + showLocalContent : function() { + "use strict"; + var view = ""; + switch ($('#musicLibrary').library('getSelectetLeftTabIndex')) { + /*global GRID_TAB, LIST_TAB*/ + case GRID_TAB: + view = "musicLibraryContentGrid"; + break; + case LIST_TAB: + view = "musicLibraryContentList"; + break; + default: + view = "musicLibraryContentList"; + break; + } + MultimediaLibrary.localContent.clearLocalContent(); + MultimediaLibrary.localContent.clearHistory(); + $('#musicLibrary').library('closeSubpanel'); + $('#musicLibrary').library("clearContent"); + $('#musicLibrary').library("changeContentClass", view); + MultimediaLibrary.mediaContentTemplate("localContentCategoryItemTemplate"); + var localContentElement = '
    '; + $(localContentElement).appendTo($('.' + view)); + ko.applyBindings(MultimediaLibrary.localContent); + MultimediaLibrary.localContent.fillCategories(); + }, + /** + * Shows available media sources/servers in grid or list view. + * + * @method showMediaSources + */ + showMediaSources : function() { + "use strict"; + var view = ""; + switch ($('#musicLibrary').library('getSelectetLeftTabIndex')) { + case GRID_TAB: + view = "musicLibraryContentGrid"; + break; + case LIST_TAB: + view = "musicLibraryContentList"; + break; + default: + view = "musicLibraryContentList"; + break; + } + $('#musicLibrary').library('closeSubpanel'); + $('#musicLibrary').library("clearContent"); + $('#musicLibrary').library("changeContentClass", view); + var mediaSourcesElement = '
    '; + $(mediaSourcesElement).appendTo($('.' + view)); + ko.applyBindings(MultimediaLibrary.remoteContent); + MultimediaLibrary.remoteContent.selectedMediaSource(null); + MultimediaLibrary.remoteContent.resetMediaContainers(); + MultimediaLibrary.remoteContent.resetMediaContainerItems(); + MultimediaLibrary.remoteContent.scanMediaServerNetwork(); + if (!MultimediaLibrary.remoteContentReScanInterval) { + MultimediaLibrary.remoteContentReScanInterval = setInterval(function() { + if (($("#mediaContentList").length || $("#remoteMediaServers").length) && $('#musicLibrary').library('isVisible')) { + MultimediaLibrary.remoteContent.scanMediaServerNetwork(); + } + }, 5000); + } + }, + /** + * Opens the supplied media source/server, shows the content of its root + * directory and navigation bar containing the name of selected server and + * back button to navigate back to list of available media sources. + * + * @method selectRemoteMediaSource {Object} Representation of media + * source/server to be opened. + * @param mediaSource {} + */ + selectRemoteMediaSource : function(mediaSource) { + "use strict"; + if (!!mediaSource) { + MultimediaLibrary.remoteContent.selectMediaSource(mediaSource); + var subpanelModel = { + action : function() { + MultimediaLibrary.goBackRemoteContent(); + }, + actionName : "BACK", + textTitle : "SERVER", + textSubtitle : mediaSource.friendlyName ? mediaSource.friendlyName.toUpperCase() : "-" + }; + $('#musicLibrary').library("subpanelContentTemplateCompile", subpanelModel); + } + + $('#musicLibrary').library("clearContent"); + var viewType = ""; + if ($('#musicLibrary').library('getSelectetLeftTabIndex') === GRID_TAB) { + viewType = "musicLibraryContentGrid"; + } else { + viewType = "musicLibraryContentList"; + } + $('#musicLibrary').library("changeContentClass", viewType); + var mediaContainerItemsElement = '
    '; + $(mediaContainerItemsElement).appendTo($('.' + viewType)); + ko.applyBindings(MultimediaLibrary.remoteContent); + }, + /** + * In case the supplied media object is type of container the method browses + * and shows the content of container, shows a navigation bar containing the + * title of selected container and back button to navigate back in hierarchy + * of opened containers. Otherwise it closes the library and starts playing + * video/audio or show the image. + * + * @method selectRemoteContent {} + * @param mediaItem {MediaObject} Media container or media item to be + * opened. + */ + selectRemoteContent : function(mediaItem) { + "use strict"; + if (!!mediaItem) { + MultimediaLibrary.remoteContent.selectMediaContainerItem(mediaItem); + if (mediaItem.type === "CONTAINER") { + var textTitle = "FOLDER"; + var mediaContainersLength = MultimediaLibrary.remoteContent.mediaContainers().length; + if (mediaContainersLength) { + if (mediaContainersLength > 2) { + textTitle = MultimediaLibrary.remoteContent.mediaContainers()[mediaContainersLength - 2].title.toUpperCase(); + } else { + textTitle = MultimediaLibrary.remoteContent.selectedMediaSource().friendlyName.toUpperCase(); + } + } + var subpanelModel = { + action : function() { + MultimediaLibrary.goBackRemoteContent(); + }, + actionName : "BACK", + textTitle : textTitle, + textSubtitle : mediaItem.title ? mediaItem.title.toUpperCase() : "-" + }; + $('#musicLibrary').library("subpanelContentTemplateCompile", subpanelModel); + } else { + $('#playerWrapper').audioAPI('playPause', false); + var index; + switch (mediaItem.type) { + case MultimediaLibrary.localContent.audioType: + MultimediaLibrary.showAudio(); + var audioContent = MultimediaLibrary.remoteContent.getAudioFromSelectedContainer(); + index = audioContent.indexOf(mediaItem); + MultimediaLibrary.carousel.loadMediaContent(audioContent, index); + $('#multimediaPlayer').audioAPI('playAudioContent', audioContent, index, true, mediaItem.type); + MultimediaLibrary.hide(); + break; + case MultimediaLibrary.localContent.videoType: + MultimediaLibrary.showVideo(); + var videoContent = MultimediaLibrary.remoteContent.getVideoFromSelectedContainer(); + index = videoContent.indexOf(mediaItem); + $('#multimediaPlayer').audioAPI('playAudioContent', videoContent, index, true, mediaItem.type); + MultimediaLibrary.hide(); + break; + default: + console.log("Media type not supported!"); + break; + } + } + } + }, + /** + * Navigates user back in hierarchy of opened containers/servers. + * + * @method goBackRemoteContent + */ + goBackRemoteContent : function() { + "use strict"; + if (MultimediaLibrary.remoteContent.mediaContainers().length > 1) { + MultimediaLibrary.remoteContent.mediaContainers.pop(); + var remoteContent = MultimediaLibrary.remoteContent.mediaContainers.pop(); + if (remoteContent.title.toString().toLowerCase().trim() !== "root") { + MultimediaLibrary.selectRemoteContent(remoteContent); + } else { + MultimediaLibrary.selectRemoteMediaSource(MultimediaLibrary.remoteContent.selectedMediaSource()); + } + } else { + MultimediaLibrary.showMediaSources(); + } + }, + /** + * Loads and displays selected local content based on the given content data. + * + * @method selectLocalMediaContent + * @param content {object} media content + */ + selectLocalMediaContent : function(content) { + "use strict"; + if (!!content) { + var index; + switch (content.type) { + case MultimediaLibrary.localContent.audioType: + MultimediaLibrary.showAudio(); + var audioContent = MultimediaLibrary.localContent.getSelectedLocalContent(content.type); + index = audioContent.indexOf(content); + MultimediaLibrary.carousel.loadMediaContent(audioContent, index); + $('#multimediaPlayer').audioAPI('playAudioContent', audioContent, index, true, content.type); + MultimediaLibrary.hide(); + break; + case MultimediaLibrary.localContent.videoType: + MultimediaLibrary.showVideo(); + var videoContent = MultimediaLibrary.localContent.getSelectedLocalContent(content.type); + index = videoContent.indexOf(content); + $('#multimediaPlayer').audioAPI('playAudioContent', videoContent, index, true, content.type); + MultimediaLibrary.hide(); + break; + default: + console.log("Not supported type!"); + break; + } + } + }, + /** + * Displays audio player, hides video player. + * + * @method showAudio + */ + showAudio : function() { + "use strict"; + $("#videoPlayer").css({ + display : "none" + }); + $("#audioPlayer").css({ + display : "block" + }); + $("#carouselWrapper").css({ + display : "block" + }); + }, + /** + * Displays video player, hides audio player. + * + * @method showVideo + */ + showVideo : function() { + "use strict"; + $("#audioPlayer").css({ + display : "none" + }); + $("#carouselWrapper").css({ + display : "none" + }); + $("#videoPlayer").css({ + display : "block" + }); + }, + + /** + * Sets the local content for MultiMedia library based on the given content. + * + * @method selectLocalContent + * @param content {Object} media content + */ + selectLocalContent : function(content) { + "use strict"; + console.log(content); + var self = this; + + var subpanelModel; + + if (MultimediaLibrary.localContent.history().length) { + var title = MultimediaLibrary.localContent.history()[MultimediaLibrary.localContent.history().length - 1].title; + subpanelModel = { + action : function() { + MultimediaLibrary.goBackLocalContent(); + }, + actionName : "BACK", + textTitle : title.toUpperCase(), + textSubtitle : content.title ? content.title.toUpperCase() : "-" + }; + } else { + subpanelModel = { + action : function() { + MultimediaLibrary.goBackLocalContent(); + }, + actionName : "BACK", + textTitle : "LOCAL", + textSubtitle : content.title ? content.title.toUpperCase() : "-" + }; + } + $('#musicLibrary').library("subpanelContentTemplateCompile", subpanelModel); + + MultimediaLibrary.localContent.pushToHistory(content); + + switch (content.operation) { + case "browse_category": + MultimediaLibrary.mediaContentTemplate("localContentSubCategoryItemTemplate"); + MultimediaLibrary.localContent.fillSubCategories(content); + break; + case "browse_artists": + MultimediaLibrary.mediaContentTemplate("localContentArtistItemTemplate"); + MultimediaLibrary.localContent.fillArtists(content); + break; + case "browse_artist": + MultimediaLibrary.mediaContentTemplate("localContentAlbumItemTemplate"); + MultimediaLibrary.localContent.fillArtistAlbums(content); + break; + case "browse_album": + MultimediaLibrary.mediaContentTemplate("localContentAudioVideoItemTemplate"); + MultimediaLibrary.localContent.fillArtistAlbumContent(content); + break; + case "browse_albums": + MultimediaLibrary.mediaContentTemplate("localContentAlbumItemTemplate"); + MultimediaLibrary.localContent.fillAlbums(content); + break; + case "browse_all": + MultimediaLibrary.mediaContentTemplate("localContentAudioVideoItemTemplate"); + MultimediaLibrary.localContent.fillAll(content); + break; + default: + break; + } + }, + /** + * Navigates user back in history of opened local content. + * + * @method goBackLocalContent + */ + goBackLocalContent : function() { + "use strict"; + if (MultimediaLibrary.localContent.history().length > 1) { + MultimediaLibrary.localContent.history.pop(); + var localContent = MultimediaLibrary.localContent.history.pop(); + MultimediaLibrary.selectLocalContent(localContent); + } else { + MultimediaLibrary.showLocalContent(); + } + } +}; \ No newline at end of file diff --git a/js/remotecontent.js b/js/remotecontent.js new file mode 100644 index 0000000..86bbb84 --- /dev/null +++ b/js/remotecontent.js @@ -0,0 +1,369 @@ +/* global ko */ + +/** + * @module MultimediaPlayerApplication + */ + +/** + * Class representing remote media content for MultiMedia Player. + * + * @class RemoteContent + * @constructor + */ +var RemoteContent = function() { + "use strict"; + var self = this; + this.mediaServer = tizen.mediaserver; + this.mediaSources = ko.observableArray([]); + this.selectedMediaSource = ko.observable(null); + + this.mediaContainers = ko.observableArray([]); + this.selectedMediaContainer = ko.observable(null); + + this.mediaContainerItems = ko.observableArray([]); + this.selectedMediaContainerItem = ko.observable(null); + + this.currentBrowseOperation = ""; + this.alphabetFilter = ko.observable(""); + this.onMediaSourceLost = null; + + this.mediaSourcesComputed = ko.computed(function() { + if (self.alphabetFilter() !== "") { + return ko.utils.arrayFilter(self.mediaSources(), function(mediaSource) { + return mediaSource.friendlyName.toString().toLowerCase().trim().indexOf(self.alphabetFilter().toString().toLowerCase().trim()) === 0; + }); + } + return self.mediaSources(); + }); + this.mediaContainersComputed = ko.computed(function() { + if (self.alphabetFilter() !== "") { + return ko.utils.arrayFilter(self.mediaContainers(), function(mediaContainer) { + return mediaContainer.title.toString().toLowerCase().trim().indexOf(self.alphabetFilter().toString().toLowerCase().trim()) === 0; + }); + } + return self.mediaContainers(); + }); + this.mediaContainerItemsComputed = ko.computed(function() { + if (self.alphabetFilter() !== "") { + return ko.utils.arrayFilter(self.mediaContainerItems(), function(mediaItem) { + return mediaItem.title.toString().toLowerCase().trim().indexOf(self.alphabetFilter().toString().toLowerCase().trim()) === 0; + }); + } + return self.mediaContainerItems(); + }); +}; + +/** + * Scans network for available DLNA server and adds it to media sources. + * + * @method scanMediaServerNetwork + */ +RemoteContent.prototype.scanMediaServerNetwork = function() { + "use strict"; + var self = this; + if (!!self.mediaServer) { + self.clearDisappearedMediaSources(); + self.mediaServer.scanNetwork(function(source) { + self.addMediaSource(source); + }, function(err) { + console.log("An error has occured while scanning network: " + err.message); + console.log(err); + }); + } +}; + +/** + * Adds given media source to the list. + * + * @method addMediaSource + * @param source {Object} media source + */ +RemoteContent.prototype.addMediaSource = function(source) { + "use strict"; + var self = this; + console.log(source); + if (!!source) { + if (!source.friendlyName) { + return; + } + source.timestamp = new Date().getTime(); + var sourceExists = false; + for ( var i = 0; i < self.mediaSources().length; ++i) { + var src = self.mediaSources()[i]; + if (src.id === source.id) { + self.mediaSources()[i] = source; + sourceExists = true; + break; + } + } + if (!sourceExists) { + self.mediaSources.push(source); + } + + self.mediaSources.sort(function(left, right) { + var leftFriendlyName = "Unknown"; + if (!!left.friendlyName && left.friendlyName !== "") { + leftFriendlyName = left.friendlyName; + } + leftFriendlyName = leftFriendlyName.toString().trim().toLowerCase(); + var rightFriendlyName = "Unknown"; + if (!!right.friendlyName && right.friendlyName !== "") { + rightFriendlyName = right.friendlyName; + } + rightFriendlyName = rightFriendlyName.toString().trim().toLowerCase(); + return leftFriendlyName === rightFriendlyName ? 0 : (leftFriendlyName < rightFriendlyName) ? -1 : 1; + }); + } +}; + +/** + * Sets given media source as selected and adds new media container based on the media source and sets it as selected. + * + * @method selectMediaSource + * @param mediaSource {Object} media source + */ +RemoteContent.prototype.selectMediaSource = function(mediaSource) { + "use strict"; + var self = this; + console.log(mediaSource); + self.selectedMediaSource(null); + if (!!mediaSource) { + self.selectedMediaSource(mediaSource); + self.resetMediaContainers(); + self.resetMediaContainerItems(); + var mediaSourceContainerProps = { + DisplayName : mediaSource.root.title, + Path : mediaSource.root.id, + Type : mediaSource.root.type + }; + /*global mediacontent*/ + var mediaContainer = new mediacontent.MediaContainer(mediaSourceContainerProps); + self.mediaContainers.push(mediaContainer); + self.selectMediaContainer(mediaContainer); + } +}; + +/** + * Sets given media container as selected. + * + * @method selectMediaContainer + * @param mediaSourceContainer {Object} media source container + */ +RemoteContent.prototype.selectMediaContainer = function(mediaSourceContainer) { + "use strict"; + var self = this; + console.log(mediaSourceContainer); + if (!!mediaSourceContainer) { + self.resetMediaContainerItems(); + + for ( var i = self.mediaContainers().length - 1; i >= 0; --i) { + if (self.mediaContainers()[i] !== mediaSourceContainer) { + self.mediaContainers.pop(); + } else { + break; + } + } + console.log(self.mediaContainers()); + self.selectedMediaContainer(mediaSourceContainer); + self.browseMediaSourceContainer(self.selectedMediaSource(), mediaSourceContainer); + } +}; + +/** + * Sets given media container item as selected. + * + * @method selectMediaContainerItem + * @param mediaContainerItem {Object} media source container + */ +RemoteContent.prototype.selectMediaContainerItem = function(mediaContainerItem) { + "use strict"; + var self = this; + console.log(mediaContainerItem); + if (!!mediaContainerItem) { + self.selectedMediaContainerItem(mediaContainerItem); + if (mediaContainerItem.type === "CONTAINER") { + self.mediaContainers.push(mediaContainerItem); + self.selectMediaContainer(mediaContainerItem); + } + } +}; + +/** + * Sets selected media source to null and empties media sources. + * + * @method resetMediaSource + */ +RemoteContent.prototype.resetMediaSource = function() { + "use strict"; + var self = this; + self.selectedMediaSource(null); + self.mediaSources.removeAll(); + self.mediaSources([]); +}; + +/** + * Removes expired media sources and invokes onMediaSourceLost listener. + * + * @method clearDisappearedMediaSources + */ +RemoteContent.prototype.clearDisappearedMediaSources = function() { + "use strict"; + var self = this; + if (self.mediaSources().length) { + for ( var i = self.mediaSources().length - 1; i >= 0; --i) { + if (new Date().getTime() - self.mediaSources()[i].timestamp > 10000) { + var mediaSourceId = self.mediaSources()[i].id; + self.mediaSources.remove(self.mediaSources()[i]); + if (!!self.onMediaSourceLost) { + self.onMediaSourceLost(mediaSourceId); + } + } + } + } +}; + +/** + * Sets the listener to receive notifications when media source is lost. + * + * @method setMediaSourceLostListener + * @param onMediaSourceLost {Function(mediaSourceId)} Event listener to be set. + */ +RemoteContent.prototype.setMediaSourceLostListener = function(onMediaSourceLost) { + "use strict"; + var self = this; + if (!!onMediaSourceLost) { + self.onMediaSourceLost = onMediaSourceLost; + } +}; + +/** + * Sets selected media container to null and empties media containers. + * + * @method resetMediaContainers + */ +RemoteContent.prototype.resetMediaContainers = function() { + "use strict"; + var self = this; + self.selectedMediaContainer(null); + self.mediaContainers.removeAll(); + self.mediaContainers([]); +}; + +/** + * Sets selected media container item to null and empties media container items. + * + * @method resetMediaContainerItems + */ +RemoteContent.prototype.resetMediaContainerItems = function() { + "use strict"; + var self = this; + self.selectedMediaContainerItem(null); + self.mediaContainerItems.removeAll(); + self.mediaContainerItems([]); +}; + +/** + * Gets media source by its id. + * + * @method getMediaSourceById + * @param id (String) media source id + */ +RemoteContent.prototype.getMediaSourceById = function(id) { + "use strict"; + var self = this; + var mediaSource = ko.utils.arrayFirst(self.mediaSources(), function(ms) { + return ms.id === id; + }); + return mediaSource; +}; + +/** + * Gets media container by its id. + * + * @method getMediaContainerById + * @param id {String} media container id + */ +RemoteContent.prototype.getMediaContainerById = function(id) { + "use strict"; + var self = this; + var mediaContainer = ko.utils.arrayFirst(self.mediaContainers(), function(mc) { + return mc.id === id; + }); + return mediaContainer; +}; + +/** + * Browses given media source container. + * + * @method browseMediaSourceContainer + * @param source {Object} media source + * @param container {Object} media container + */ +RemoteContent.prototype.browseMediaSourceContainer = function(source, container) { + "use strict"; + var self = this; + var browseCount = 100; + var browseOffset = 0; + var localOp = "Browse_" + source.id + "_" + container.id; + + function browseErrorCB(str) { + console.log("Error browsing " + container.id + " : " + str); + } + + function browseContainerCB(jsonArray) { + console.log(jsonArray); + if (self.currentBrowseOperation !== localOp) { + return; + } + for ( var i = 0; i < jsonArray.length; ++i) { + self.mediaContainerItems.push(mediacontent.mediaObjectForProps(jsonArray[i])); + } + + if (jsonArray.length === browseCount) { + browseOffset += browseCount; + source.browse(container.id, "+DisplayName", browseCount, browseOffset, browseContainerCB, browseErrorCB); + } else { + self.currentBrowseOperation = ""; + } + } + + if (self.currentBrowseOperation === localOp) { + return; + } + + self.currentBrowseOperation = localOp; + + source.browse(container.id, "+DisplayName", browseCount, browseOffset, browseContainerCB, browseErrorCB); +}; + +/** + * Gets audio media items from selected container. + * + * @method getAudioFromSelectedContainer + */ +RemoteContent.prototype.getAudioFromSelectedContainer = function() { + "use strict"; + var self = this; + if (!!self.mediaContainerItemsComputed() && self.mediaContainerItemsComputed().length) { + return ko.utils.arrayFilter(self.mediaContainerItemsComputed(), function(mediaItem) { + return mediaItem.type === "AUDIO"; + }); + } + return []; +}; + +/** + * Gets video media items from selected container. + * + * @method getVideoFromSelectedContainer + */ +RemoteContent.prototype.getVideoFromSelectedContainer = function() { + "use strict"; + var self = this; + if (!!self.mediaContainerItemsComputed() && self.mediaContainerItemsComputed().length) { + return ko.utils.arrayFilter(self.mediaContainerItemsComputed(), function(mediaItem) { + return mediaItem.type === "VIDEO"; + }); + } + return []; +}; \ No newline at end of file diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..33d0500 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,118 @@ +/** + * @module MultimediaPlayerApplication + */ + +/** + * Utility class with helper methods for Multimedia player. + * + * @class Utils + */ +var Utils = { + /** + * Provides default thumbnails for given media types. + * + * @method getDefaultThumbnailByType + * @param type {String} media type + */ + getDefaultThumbnailByType : function(type) { + "use strict"; + var thumbnail = ""; + switch (type) { + case "AUDIO": + thumbnail = "/images/audio-placeholder.jpg"; + break; + case "VIDEO": + thumbnail = "/images/video-placeholder.jpg"; + break; + case "CONTAINER": + thumbnail = "/images/container-placeholder.jpg"; + break; + default: + thumbnail = "/images/default-placeholder.jpg"; + break; + } + return thumbnail; + }, + /** + * Gets thumbnail path out of media item object if exists otherwise gets default placeholder by type. + * + * @method getThumbnailPath + * @param mediaItem {Object} media item object + * @param type {String} media type + * @return {String} thumbnail path + */ + getThumbnailPath : function(mediaItem, type) { + "use strict"; + if (!!mediaItem) { + if (!!mediaItem.thumbnailURIs && mediaItem.thumbnailURIs.length) { + return mediaItem.thumbnailURIs[0]; + } + if (!!mediaItem.type) { + return this.getDefaultThumbnailByType(mediaItem.type); + } + } + return this.getDefaultThumbnailByType(type || ""); + }, + /** + * Gets artist's name out of media item object. + * + * @method getArtistName + * @param mediaItem {Object} media item + * @return {String} artist name + */ + getArtistName : function(mediaItem) { + "use strict"; + if (!!mediaItem && !!mediaItem.artists && mediaItem.artists.length) { + return mediaItem.artists.join(", "); + } + return "Unknown"; + }, + /** + * Gets album name out of media item object. + * + * @method getAlbumName + * @param mediaItem {Object} media item + * @return {String} album name + */ + getAlbumName : function(mediaItem) { + "use strict"; + if (!!mediaItem && !!mediaItem.album && mediaItem.album !== "") { + return mediaItem.album; + } + return "Unknown"; + }, + /** + * Gets media item title out of media item object. + * + * @method getMediaItemTitle + * @param mediaItem {Object} media item + * @return {String} media title + */ + getMediaItemTitle : function(mediaItem) { + "use strict"; + if (!!mediaItem && !!mediaItem.title && mediaItem.title !== "") { + return mediaItem.title; + } + if (!!mediaItem && !!mediaItem.name && mediaItem.name !== "") { + return mediaItem.name; + } + return "Unknown"; + }, + /** + * Calls fullscreen request for given html element. + * + * @method launchFullScreen + * @param element {Object} + */ + launchFullScreen : function(element) { + "use strict"; + console.log("Launching full screen"); + if (element.requestFullScreen) { + element.requestFullScreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullScreen) { + element.webkitRequestFullScreen(); + } + } +}; \ No newline at end of file diff --git a/packaging/html5-ui-multimediaplayer.changes b/packaging/html5-ui-multimediaplayer.changes new file mode 100644 index 0000000..7b4dd89 --- /dev/null +++ b/packaging/html5-ui-multimediaplayer.changes @@ -0,0 +1,4 @@ +* Thu Mar 06 2014 brianjjones b3d35c5 +- Initial commit of the Multimediaplayer app + + diff --git a/packaging/html5-ui-multimediaplayer.spec b/packaging/html5-ui-multimediaplayer.spec new file mode 100644 index 0000000..dd7ef99 --- /dev/null +++ b/packaging/html5-ui-multimediaplayer.spec @@ -0,0 +1,36 @@ +Name: html5_UI_Multimediaplayer +Summary: A proof of concept pure html5 UI +Version: 0.0.1 +Release: 1 +Group: Applications/System +License: Apache 2.0 +URL: http://www.tizen.org +Source0: %{name}-%{version}.tar.bz2 +BuildRequires: zip +BuildRequires: html5_UI_Common +Requires: wrt-installer +Requires: wrt-plugins-ivi + +%description +A proof of concept pure html5 UI + +%prep +%setup -q -n %{name}-%{version} + +%build + +make wgtPkg + +%install +rm -rf %{buildroot} +%make_install + +%post + wrt-installer -i /opt/usr/apps/.preinstallWidgets/html5UIMultimediaplayer.wgt; + +%postun + wrt-installer -un html5POC07.Multimediaplayer + +%files +%defattr(-,root,root,-) +/opt/usr/apps/.preinstallWidgets/html5UIMultimediaplayer.wgt -- 2.7.4