Переглянути джерело

feat(remote playback): first implementation of player controls

David Loiret 9 роки тому
батько
коміт
2d31a06d9c

Різницю між файлами не показано, бо вона завелика
+ 74 - 40
Apple-TV/web/index.html


+ 330 - 0
Resources/web/playerControl.css

@@ -0,0 +1,330 @@
+.player-control {
+    position: relative;
+    margin: auto;
+    width: 70%;
+    background: #2a2a2a;
+    box-sizing: border-box;
+    border-radius: 5px;
+    height: 80px;
+    -moz-box-sizing: border-box;
+    font-family: Arial, sans-serif;
+    padding: 0;
+    bottom: 20px;
+    z-index: 2;
+    opacity: 1;
+    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
+    -webkit-transition: opacity 0.3s ease-in;
+    transition: opacity 0.3s ease-in;
+    -moz-user-select: none;
+    -webkit-user-select: none;
+    user-select: none;
+    cursor: default;
+}
+
+.player-control .progress {
+    width: 76%;
+    height: 8px;
+    border-radius: 5px;
+    background: #676767;
+    box-shadow: inset 0 -5px 10px rgba(0, 0, 0, 0.1);
+    float: left;
+    cursor: pointer;
+    margin: 40px 0 0 0;
+    padding: 0;
+    position: relative;
+}
+
+.player-control .progress-bar {
+    background: #fff;
+    box-shadow: inset -30px 0px 69px -20px #89f6f5;
+    border-radius: 5px;
+    height: 100%;
+    position: relative;
+    z-index: 999;
+    width: 0;
+}
+
+.player-control .button-holder {
+    position: relative;
+    left: 10px;
+}
+
+.player-control .progress-button {
+    top: -3px;
+    background: #fff;
+    box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
+    border-radius: 30px;
+    width: 6px;
+    height: 15px;
+    position: absolute;
+    left: -1px;
+    text-decoration: overline;
+}
+
+.player-control [class^="buffered"] {
+    background: rgba(255, 255, 255, 0.1);
+    position: absolute;
+    top: 0;
+    left: 30px;
+    height: 100%;
+    border-radius: 5px;
+    z-index: 1;
+}
+
+.player-control .play-pause {
+    display: inline-block;
+    font-size: 2.3em;
+    float: left;
+    text-shadow: 0 0 0 #fff;
+    color: rgba(255, 255, 255, 0.8);
+    width: 10%;
+    padding: 32px 0 0 3%;
+    cursor: pointer;
+    font-variant: small-caps;
+}
+
+.player-control .play, .player-control .pause-button {
+    -webkit-transition: all 0.2s ease-out;
+}
+
+.player-control .play .pause-button, .player-control .pause .play-button {
+    display: none;
+}
+
+.player-control .pause-button {
+    padding: 0 2px;
+    box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    height: 34px;
+}
+
+.player-control .pause-button span {
+    background: #fff;
+    width: 8px;
+    height: 24px;
+    float: left;
+    display: block;
+}
+
+.player-control .pause-button span:first-of-type {
+    margin: 0 4px 0 0;
+}
+
+.player-control .time {
+    color: #fff;
+    font-weight: bold;
+    font-size: 1.2em;
+    position: absolute;
+    right: 0;
+    top: 16px;
+}
+
+.player-control .stime, .ttime {
+    color: #444;
+}
+
+.player-control .play:hover {
+    text-shadow: 0 0 5px #fff;
+}
+
+.player-control .play:active, .pause-button:active span {
+    text-shadow: 0 0 7px #fff;
+}
+
+.player-control .pause-button:hover span {
+    box-shadow: 0 0 5px #fff;
+}
+
+.player-control .pause-button:active span {
+    box-shadow: 0 0 7px #fff;
+}
+
+.player-control .volume {
+    position: relative;
+    float: left;
+    width: 5%;
+    margin: 0 0 0 4%;
+    height: 100%;
+}
+
+.player-control .volume-icon {
+    padding: 1.5%;
+    height: 100%;
+    cursor: pointer;
+    box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    -webkit-transition: all 0.15s linear;
+}
+
+.player-control .volume-holder {
+    height: 100px;
+    width: 100%;
+    background: black;
+    position: absolute;
+    display: none;
+    background: #4f4f4f;
+    left: 0;
+    border-radius: 5px 5px 0 0;
+    top: -100px;
+}
+
+.player-control .volume-bar-holder {
+    background: #333;
+    width: 8px;
+    box-shadow: inset 0px 0px 5px rgba(0, 0, 0, 0.3);
+    margin: 15px auto;
+    height: 80px;
+    border-radius: 5px;
+    position: relative;
+    cursor: pointer;
+}
+
+.player-control .volume-button {
+    background: #fff;
+    box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
+    border-radius: 30px;
+    width: 15px;
+    height: 8px;
+    margin-left: -3px;
+    margin-top: -4px; /* height / 2 center the mouse */
+}
+
+.player-control .volume-button-holder {
+    position: relative;
+    top: 0;
+}
+
+.player-control .volume-bar {
+    background: #fff;
+    box-shadow: inset -30px 0px 69px -20px #89f6f5;
+    border-radius: 5px;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    bottom: 0;
+}
+
+.player-control .volume-icon span {
+    width: 20%;
+    height: 13%;
+    background-color: #fff;
+    display: block;
+    position: relative;
+    z-index: 1;
+    font-weight: bold;
+    top: 50%;
+    color: #fff;
+    left: 22%;
+}
+
+.player-control .volume-icon span:before,
+.player-control .volume-icon span:after {
+    content: '';
+    position: absolute;
+}
+
+.player-control .volume-icon span:before {
+    width: 0;
+    height: 0px;
+    border: 1em solid transparent;
+    border-left: none;
+    border-right-color: #fff;
+    z-index: 2;
+    top: -6px;
+    left: 10%;
+    margin-top: -31%;
+}
+
+.player-control .volume-icon span:after {
+    width: 2%;
+    height: 2%;
+    border: 1px solid #fff;
+    left: 200%;
+    border-width: 0px 0px 0 0;
+    top: 5px;
+    border-radius: 0 50px 0 0;
+    -webkit-transform: rotate(45deg);
+    -moz-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    -o-transform: rotate(45deg);
+    transform: rotate(45deg);
+    font-variant: small-caps;
+}
+
+.player-control .v-change-10 span:after {
+    border-width: 10px 10px 0 0;
+    top: 0;
+}
+
+.player-control .v-change-9 span:after {
+    border-width: 9px 9px 0 0;
+    top: 1px;
+}
+
+.player-control .v-change-8 span:after {
+    border-width: 8px 8px 0 0;
+    top: 1px;
+}
+
+.player-control .v-change-7 span:after {
+    border-width: 7px 7px 0 0;
+    top: 2px;
+}
+
+.player-control .v-change-6 span:after {
+    border-width: 6px 6px 0 0;
+    top: 2px;
+}
+
+.player-control .v-change-5 span:after {
+    border-width: 5px 5px 0 0;
+    top: 3px;
+}
+
+.player-control .v-change-4 span:after {
+    border-width: 4px 4px 0 0;
+    top: 3px;
+}
+
+.player-control .v-change-3 span:after {
+    border-width: 3px 3px 0 0;
+    top: 4px;
+}
+
+.player-control .v-change-2 span:after {
+    border-width: 2px 2px 0 0;
+    top: 4px;
+}
+
+.player-control .v-change-1 span:after {
+    border-width: 1px 1px 0 0;
+    top: 5px;
+}
+
+.player-control .v-change-0 span:after {
+    border-width: 0px 0px 0 0;
+    top: 5px;
+}
+
+.player-control .v-change-0 span:after {
+    content: '+';
+    -webkit-transform: rotate(45deg);
+    font-size: 20px;
+    top: -7px;
+    left: 28px;
+}
+
+.player-control .title {
+    position: absolute;
+    line-height: 40px;
+    text-align: center;
+    width: 100%;
+}
+
+.player-control .volume:hover {
+    background-color: #4f4f4f;
+}
+
+.player-control .volume:hover .volume-holder {
+    display: block !important;
+}

+ 569 - 0
Resources/web/playerControl.js

@@ -0,0 +1,569 @@
+$(function() {
+
+    /**
+     * Ws is a wrapper of the WebSocket API
+     * @class Ws
+     */
+    var Ws = function(options, managerOptions) {
+        options = options || {};
+        managerOptions = managerOptions || {};
+
+        if (!options.url)
+            throw "Cannot open a socket without a url";
+
+        //@TODO: add try catch & retry
+        this.onMessageCallbacks = [];
+        this.socket = new WebSocket(options.url);
+        this.url = options.url;
+        this.maxTry = 4;
+        this.recoTry = 0;
+    };
+
+    /**
+     * @method init
+     */
+    Ws.prototype.init = function() {
+        var self = this;
+        this.socket.onopen = function() {
+            self._onOpen(this);
+        };
+
+        this.socket.onmessage = function(e) {
+            self.onMessage(e);
+        };
+
+        this.socket.onclose = function() {
+            self._onClose();
+        };
+
+        this.socket.onerror = function(error) {
+            self._onError(error);
+        };
+    };
+
+    /**
+     * @method _onOpen
+     * @param {Object} e
+     * @private
+     */
+    Ws.prototype._onOpen = function(e) {
+        console.log(e);
+        this.recoTry = 0;
+    };
+
+    /**
+     * On message call message callbacks
+     * @method onMessage
+     * @param {Object} e
+     */
+    Ws.prototype.onMessage = function(e) {
+        //@TODO: is json?
+        var message = $.parseJSON(e.data);
+        for (var i = 0, length = this.onMessageCallbacks.length; i < length; i++) {
+            var cb = this.onMessageCallbacks[i];
+            if (typeof cb === 'function') {
+                cb(message);
+            }
+        }
+    };
+
+    /**
+     * @method _onError
+     * @param {Object} e
+     * @private
+     */
+    Ws.prototype._onError = function(e) {
+        console.log(e);
+    };
+
+    /**
+     * @method _onClose
+     * @param {Object} e
+     * @private
+     */
+    Ws.prototype._onClose = function(e) {
+        console.log(e);
+        //try to reco?
+        if (this.maxTry > this.recoTry) {
+            return;
+        }
+        this.socket = new WebSocket(this.url);
+        this.init();
+        this.recoTry++;
+    };
+
+    /**
+     * Send stringify & send message to the socket
+     * @method sendMessage
+     * @param message
+     */
+    Ws.prototype.sendMessage = function(message) {
+        message = JSON.stringify(message);
+        this.socket.send(message);
+    };
+
+    /**
+     * @class PlayerControl
+     */
+    var PlayerControl = function(options) {
+        options = options || {};
+        if (!options.socket instanceof Ws)
+            throw "You need to provide a socket instance";
+
+        if (!options.element || !options.element.length)
+            throw "Element not found";
+
+        this._playing = false;
+        this._vClicked = false;
+        this._progDragged = false;
+        this._progClicked = false;
+        this.element = options.element;
+        this.socket = options.socket;
+        this.duration = 7200;
+        this.currentTime = 0;
+        this.volume = 0.5;
+        this._previousVolume = 1;
+        this.timeInterval = null;
+        this.enableInterval = false;
+    };
+
+    /**
+     * @method init
+     */
+    PlayerControl.prototype.init = function() {
+        //Add event listener
+        var self = this;
+        //Update time
+        self.updateTime();
+        var buttonHeight = self.element.find('.volume-button').height();
+        var y = buttonHeight * this.volume * 10;
+        self.updateVolume(y);
+        var progWidth = this.element.find('.progress').width();
+        var x = (progWidth / self.duration) * self.currentTime;
+        self.updateProgress(x);
+
+        $(window).bind('mouseup', function(e) {
+            self._progClicked = false;
+            self._progDragged = false;
+            self._vClicked = false;
+            self._dragVolume = false;
+        });
+
+        // Shortcuts
+        $(window).bind('keydown', function(e) {
+            switch (e.which) {
+                case 32:
+                    e.preventDefault();
+                    self.playPause();
+                    break;
+                case 37: // left
+                    var time = self.currentTime - (self.duration / 100);
+                    self.seekTo(true, time);
+                    break;
+
+                case 39: // right
+                    var time = self.currentTime + (self.duration / 100);
+                    self.seekTo(true, time);
+                    break;
+
+                case 38: // up
+                    e.preventDefault();
+                    var buttonHeight = self.element.find('.volume-button').height();
+                    var y = buttonHeight * (self.volume + 0.1) * 10;
+                    self.updateVolume(y);
+                    break;
+
+                case 40: // down
+                    e.preventDefault();
+                    var buttonHeight = self.element.find('.volume-button').height();
+                    var y = buttonHeight * (self.volume - 0.1) * 10;
+                    self.updateVolume(y);
+                    break;
+
+                default:
+                    return; // exit this handler for other keys
+            }
+
+        });
+
+        this.element.find('.play-pause').bind('click', function() {
+            self.playPause();
+        });
+
+        this.element.find('.progress').bind('mousedown', function(e) {
+            self.mousedownProgress(e);
+        });
+
+        this.element.bind('mousemove', function(e) {
+            if (self._progClicked) {
+                self.dragProgress(e);
+            }
+            if (self._vClicked) {
+                self.dragVolume(e);
+            }
+        });
+
+        this.element.find('.volume-bar-holder').bind('mousedown', function(e) {
+            self._vClicked = true;
+            var y = self.element.find('.volume-bar-holder').height() - (e.pageY - self.element.find('.volume-bar-holder').offset().top);
+            self.updateVolume(y);
+        });
+
+        this.element.find('.volume-icon').bind('mousedown', function() {
+            if (self.volume) {
+                self._previousVolume = self.volume;
+                self.volume = 0;
+            } else {
+                self.volume = self._previousVolume;
+            }
+            var buttonHeight = self.element.find('.volume-button').height();
+            var y = buttonHeight * self.volume * 10;
+            self.updateVolume(y);
+        });
+
+    };
+
+    /**
+     * @method updateVolume
+     * @param {number} y
+     */
+    PlayerControl.prototype.updateVolume = function(y) {
+        var buttonHeight = this.element.find('.volume-button').height();
+        var barHolderHeight = this.element.find('.volume-bar-holder').height();
+        if (y > barHolderHeight) {
+            y = barHolderHeight;
+        }
+        this.element.find('.volume-bar').css({
+            height: y + 'px'
+        });
+        //between 1 and 0
+        this.volume = this.element.find('.volume-bar').height() / barHolderHeight;
+        this.animateVolume();
+    };
+
+    /**
+     * @method playPause
+     */
+    PlayerControl.prototype.playPause = function() {
+        if (this.isEnded()) {
+            return this.pause();
+        }
+
+        if (this._playing) {
+            this.pause(true);
+        }
+        else {
+            this.play(true);
+        }
+    };
+
+    /**
+     * @method animateVolume
+     */
+    PlayerControl.prototype.animateVolume = function() {
+        var volumeIcon = this.element.find('.volume-icon');
+        volumeIcon.removeClass().addClass('volume-icon v-change-' + parseInt(this.volume * 10));
+    };
+
+    /**
+     * @method getMouseProgressPosition
+     * @param {Object} e - DOM event
+     * @returns {number}
+     */
+    PlayerControl.prototype.getMouseProgressPosition = function(e) {
+        return e.pageX - this.element.find('.progress').offset().left;
+    };
+
+    /**
+     * @method mousedownProgress
+     * @param {Object} e - DOM event
+     */
+    PlayerControl.prototype.mousedownProgress = function(e) {
+        var progWidth = this.element.find('.progress').width();
+        this._progClicked = true;
+        var x = this.getMouseProgressPosition(e);
+
+        this.currentTime = Math.round((x / progWidth) * this.duration);
+        this.seekTo(true, this.currentTime);
+    };
+
+    /**
+     * @method dragProgress
+     * @param {Object} e - DOM event
+     */
+    PlayerControl.prototype.dragProgress = function(e) {
+        this._progDragged = true;
+        var progMove = 0;
+        var x = this.getMouseProgressPosition(e);
+        var progWidth = this.element.find('.progress').width();
+        if (this._playing && (this.currentTime < this.duration)) {
+            this.play();
+        }
+        if (x <= 0) {
+            progMove = 0;
+            this.currentTime = 0;
+        }
+        else if (x > progWidth) {
+            this.currentTime = this.duration;
+            progMove = progWidth;
+        }
+        else {
+            progMove = x;
+            this.currentTime = Math.round((x / progWidth) * this.duration);
+        }
+        this.seekTo(true, this.currentTime);
+    };
+
+    /**
+     * @method dragVolume
+     * @param {Object} e - DOM event
+     */
+    PlayerControl.prototype.dragVolume = function(e) {
+        this._dragVolume = true;
+        var volHeight = this.element.find('.volume-bar-holder').height();
+        var y = volHeight - (e.pageY - this.element.find('.volume-bar-holder').offset().top);
+        var volMove = 0;
+        if (y <= 0) {
+            volMove = 0;
+        } else if (y > this.element.find('.volume-bar-holder').height() ||
+            (y / volHeight) === 1) {
+            volMove = volHeight;
+        } else {
+            volMove = y;
+        }
+        this.updateVolume(volMove);
+    };
+
+    /**
+     * @method updateTime
+     * @param {number} currentTime
+     */
+    PlayerControl.prototype.updateTime = function(currentTime) {
+        var progWidth = this.element.find('.progress').width();
+        currentTime = currentTime ||
+        Math.round(($('.progress-bar').width() / progWidth) * this.duration);
+        currentTime /= 1000;
+        var duration = this.duration / 1000;
+        var seconds = 0,
+            minutes = Math.floor(currentTime / 60),
+            tminutes = Math.round(duration / 60),
+            tseconds = Math.round((duration) - (tminutes * 60));
+
+
+        seconds = Math.round(currentTime) - (60 * minutes);
+
+        if (seconds > 59) {
+            seconds = Math.round(currentTime) - (60 * minutes);
+            if (seconds === 60) {
+                minutes = Math.round(currentTime / 60);
+                seconds = 0;
+            }
+        }
+
+        // Set a zero before the number if less than 10.
+        if (seconds < 10) {
+            seconds = '0' + seconds;
+        }
+        if (tseconds < 10) {
+            tseconds = '0' + tseconds;
+        }
+
+        this.element.find('.ctime').html(minutes + ':' + seconds);
+        this.element.find('.ttime').html(tminutes + ':' + tseconds);
+    };
+
+    /**
+     * @method updateProgress
+     * @param {number} x
+     */
+    PlayerControl.prototype.updateProgress = function(x) {
+        var buttonWidth = this.element.find('.progress-button').width();
+
+        this.element.find('.progress-bar').css({
+            width: x
+        });
+
+        this.element.find('.progress-button').css({
+            left: x - (2 * buttonWidth) - (buttonWidth / 2) + 'px'
+        });
+    };
+
+    /**
+     * @method isEnded
+     * @returns {boolean}
+     */
+    PlayerControl.prototype.isEnded = function() {
+        return this.currentTime >= this.duration;
+    };
+
+    /**
+     * @method play
+     * @param {boolean} send
+     */
+    PlayerControl.prototype.play = function(send) {
+        var self = this;
+        var progWidth = this.element.find('.progress').width();
+        var delay = 1000;
+        if (this.timeInterval) {
+            clearInterval(this.timeInterval);
+        }
+        //@TODO: use requestFrame instead
+
+        this._playing = true;
+        this.element
+            .find('.play-pause')
+            .addClass('pause')
+            .removeClass('play');
+
+        if (send) {
+            this.socket.sendMessage({
+                type: 'pause'
+            });
+        }
+
+        if (!this.enableInterval) {
+            return;
+        }
+        this.timeInterval = setInterval(function() {
+            if (self.isEnded()) {
+                self.currentTime = self.duration;
+                self.pause();
+                return clearInterval(this.timeInterval);
+            }
+            self.currentTime += 1;
+            self.updateTime(self.currentTime);
+            var x = (progWidth / self.duration) * self.currentTime;
+            self.updateProgress(x);
+        }, delay);
+    };
+
+    /**
+     * @method pause
+     * @param {boolean} send
+     */
+    PlayerControl.prototype.pause = function(send) {
+        if (this.timeInterval) {
+            clearInterval(this.timeInterval);
+        }
+
+        this._playing = false;
+        this.element
+            .find('.play-pause')
+            .addClass('play')
+            .removeClass('pause');
+
+        if (send) {
+            this.socket.sendMessage({
+                type: 'pause',
+                currentTime: this.currentTime,
+                media: {
+                    id: this.id
+                }
+            });
+        }
+    };
+
+    /**
+     * @method seekTo
+     * @param {boolean} send
+     * @param {number} time
+     */
+    PlayerControl.prototype.seekTo = function(send, time) {
+        //@TODO: make a debounce when dragging & key
+        var progWidth = this.element.find('.progress').width();
+
+        if (time < 0) {
+            time = 0;
+        }
+        if (time > this.duration) {
+            time = this.duration;
+        }
+
+        this.currentTime = time;
+
+        if (this._playing) {
+            this.play();
+        }
+
+        var x = (progWidth / this.duration) * this.currentTime;
+        this.updateProgress(x);
+        this.updateTime(this.currentTime);
+        if (send) {
+            this.socket.sendMessage({
+                type: 'seekTo',
+                currentTime: time
+            });
+        }
+    };
+
+    /**
+     * @method goTo
+     * @param {boolean} send
+     */
+    PlayerControl.prototype.goTo = function(send) {
+        this.socket.sendMessage({
+            type: 'goTo'
+        });
+    };
+
+    /**
+     * @method playing
+     * @param {Object} message
+     */
+    PlayerControl.prototype.playing = function(message) {
+        this.currentTime = message.currentTime;
+        this.duration = message.media.duration;
+        this.title = message.media.title;
+        this.id = message.media.id;
+        var titleElement = this.element.find('.title');
+        titleElement.text(this.title);
+        this.play();
+    }
+
+    /**
+     * Instanciation of the Ws class
+     */
+    //@TODO: This URL need to be updated at runtime
+    var URL = 'ws://192.168.0.14:8888';
+
+    var socket = new Ws({
+        url: URL
+    });
+    socket.init();
+
+    var playerControl = new PlayerControl({
+        socket: socket,
+        element: $('.player-control')
+    });
+    playerControl.init();
+
+    /**
+     * Map which method is allowed by event type
+     */
+    var TYPE_MAP = {
+        play: function(message) {
+            playerControl.play(message);
+        },
+        pause: function(message) {
+            playerControl.pause(message);
+        },
+        playing: function(message) {
+            playerControl.playing(message);
+        },
+        seekTo: function(message) {
+            playerControl.seekTo(null, message.currentTime);
+        }
+    };
+
+    /**
+     * Manage incoming messages
+     */
+    socket.onMessageCallbacks.push(function(message) {
+        var key = 'type';
+        var type = message[key];
+        if (!type || typeof TYPE_MAP[type] !== 'function')
+            return;
+        TYPE_MAP[type](message);
+    });
+
+});

+ 16 - 0
VLC for iOS.xcodeproj/project.pbxproj

@@ -25,6 +25,8 @@
 		41CD695C1A29D72600E60BCE /* VLCBoxController.m in Sources */ = {isa = PBXBuildFile; fileRef = 41CD69591A29D72600E60BCE /* VLCBoxController.m */; };
 		41CD695D1A29D72600E60BCE /* VLCBoxTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 41CD695B1A29D72600E60BCE /* VLCBoxTableViewController.m */; };
 		690D0AB5E34F29B46839C92E /* libPods-VLC-TV.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C12FD7AD79D78B13860606CE /* libPods-VLC-TV.a */; };
+		6B4E33D11BF2A39400A35255 /* playerControl.css in Resources */ = {isa = PBXBuildFile; fileRef = 6B4E33CF1BF2A39400A35255 /* playerControl.css */; };
+		6B4E33D21BF2A39400A35255 /* playerControl.js in Resources */ = {isa = PBXBuildFile; fileRef = 6B4E33D01BF2A39400A35255 /* playerControl.js */; };
 		7AC8629D1765DC560011611A /* style.css in Resources */ = {isa = PBXBuildFile; fileRef = 7AC8629B1765DC560011611A /* style.css */; };
 		7AC862A61765E9510011611A /* jquery-1.10.1.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 7AC8629E1765E90C0011611A /* jquery-1.10.1.min.js */; };
 		7AC862A71765E9510011611A /* jquery.fileupload.js in Resources */ = {isa = PBXBuildFile; fileRef = 7AC8629F1765E90C0011611A /* jquery.fileupload.js */; };
@@ -507,6 +509,8 @@
 		521108CBC3CAA0810AF3CBA8 /* Pods-vlc-ios.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-vlc-ios.debug.xcconfig"; path = "Pods/Target Support Files/Pods-vlc-ios/Pods-vlc-ios.debug.xcconfig"; sourceTree = "<group>"; };
 		6104D285EA8F1AEDA6136FCF /* Pods-VLC-TV.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VLC-TV.release.xcconfig"; path = "Pods/Target Support Files/Pods-VLC-TV/Pods-VLC-TV.release.xcconfig"; sourceTree = "<group>"; };
 		6AB5E0853B398B35369938EC /* Pods-vlc-ios.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-vlc-ios.release.xcconfig"; path = "Pods/Target Support Files/Pods-vlc-ios/Pods-vlc-ios.release.xcconfig"; sourceTree = "<group>"; };
+		6B4E33CF1BF2A39400A35255 /* playerControl.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = playerControl.css; path = Resources/web/playerControl.css; sourceTree = SOURCE_ROOT; };
+		6B4E33D01BF2A39400A35255 /* playerControl.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = playerControl.js; path = Resources/web/playerControl.js; sourceTree = SOURCE_ROOT; };
 		7AC8629B1765DC560011611A /* style.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = style.css; sourceTree = "<group>"; };
 		7AC8629E1765E90C0011611A /* jquery-1.10.1.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "jquery-1.10.1.min.js"; sourceTree = "<group>"; };
 		7AC8629F1765E90C0011611A /* jquery.fileupload.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = jquery.fileupload.js; sourceTree = "<group>"; };
@@ -1145,6 +1149,15 @@
 			name = Pods;
 			sourceTree = "<group>";
 		};
+		6B4E33CC1BF2A35A00A35255 /* web */ = {
+			isa = PBXGroup;
+			children = (
+				6B4E33CF1BF2A39400A35255 /* playerControl.css */,
+				6B4E33D01BF2A39400A35255 /* playerControl.js */,
+			);
+			name = web;
+			sourceTree = "<group>";
+		};
 		7D1052EA1A4DCC1700295F08 /* OneDrive */ = {
 			isa = PBXGroup;
 			children = (
@@ -1766,6 +1779,7 @@
 		7DEC8BE61BD688AD006E1093 /* Resources */ = {
 			isa = PBXGroup;
 			children = (
+				6B4E33CC1BF2A35A00A35255 /* web */,
 				7DEDD38B1BE936130053802C /* About Contents.html */,
 				7D2D7FD21BD2F109002D6B6D /* VLC-TV.entitlements */,
 				7D13294E1BA1F10100BE647E /* Assets.xcassets */,
@@ -2419,12 +2433,14 @@
 				7D405ED61BEA1F56006ED886 /* jquery-1.10.1.min.js in Resources */,
 				7DEC8BE91BD68BC9006E1093 /* Settings.bundle in Resources */,
 				7D405ED51BEA1F56006ED886 /* Raleway.woff in Resources */,
+				6B4E33D21BF2A39400A35255 /* playerControl.js in Resources */,
 				7D405ED81BEA1F56006ED886 /* jquery.iframe-transport.js in Resources */,
 				DD3EAC131BE26166003668DA /* VLCRemoteBrowsingTVCell.xib in Resources */,
 				7D3E528C1BD7B5E100309D15 /* VLCCloudServicesTVViewController.xib in Resources */,
 				7D405EDA1BEA1F56006ED886 /* style.css in Resources */,
 				7D7EF3DB1BD56B5900CD4CEE /* VLCOpenNetworkStreamTVViewController.xib in Resources */,
 				7DDE41901BE9225A0065C53A /* VLCAboutViewController.xib in Resources */,
+				6B4E33D11BF2A39400A35255 /* playerControl.css in Resources */,
 				7D405EE21BEA1FAD006ED886 /* index.html in Resources */,
 				7D51B3B11BF0EEF4005AF4D5 /* VLCPlaybackInfoMediaInfoTVViewController.xib in Resources */,
 				7DEDD38D1BE936F30053802C /* SourceCodePro-Regular.ttf in Resources */,