Youtube Storyboard

mouse-over video thumbnail to preview video by cycling through the storyboard

目前為 2014-07-08 提交的版本,檢視 最新版本

// ==UserScript==
// @name           Youtube Storyboard
// @namespace      hbb_works
// @description    mouse-over video thumbnail to preview video by cycling through the storyboard
// @version        1.2r1407080550
// @include        http://*youtube.com*
// @include        https://*youtube.com*
// @grant          none
// ==/UserScript==


const INTERVAL = 200; // default interval in milliseconds
var div;
var loader;

function getPos(element) {
    var ref = document.body.getBoundingClientRect();
    var rel = element.getBoundingClientRect();
    return {
        left: rel.left - ref.left,
        top: rel.top - ref.top
    };
}

(function init() {
    document.addEventListener('mouseover', onMouseOver, false);
    loader = document.createElement('img');
    div = document.createElement('div');
    div.id = 'storyboardPlayer';
    div.style.display = 'none';
    div.style.position = 'absolute';
    div.style.zIndex = 99;
    document.getElementById('page') .appendChild(div)
})();

function onMouseOver(event) {
    var target = event.target;
    var src = target.src;
    if (target.nodeName == 'IMG' && src && (/default\.jpg$/.exec(src) || /0\.jpg$/.exec(src)))
    {
        var a = new Animator(target);
        var onMouseOut = function () {
            if (div.style.display == 'none') {
                a.stop();
            }
            target.removeEventListener('mouseout', onMouseOut);
        };
        target.addEventListener('mouseout', onMouseOut);
        a.getStoryboardSpecs(function () {
            a.start();
        });
    }
}

var Animator = function (target) {
    this.target = target;
    this.frame = 0;
    this.sheet = 0;
    this.interval = INTERVAL;
    this.src = target.src
    this.isMouseOver = true;
    // get video id
    this.v = (/vi\/(.*?)\//.exec(this.src) || []) [1];
    if (this.v) {
        this.width = target.width;
        this.height = target.height;
    }
    var p = getPos(target);
    div.style.top = p.top + 'px';
    div.style.left = p.left + 'px';
    div.style.width = this.width + 'px';
    div.style.height = this.height + 'px';
    
    // adjustments for small thumbnails in list of related videos
    if (target.parentElement.classList.contains('yt-uix-simple-thumb-wrap')) {
        var spanHeight = target.parentElement.getBoundingClientRect() .height;
        div.style.top = p.top + ((target.height - spanHeight) / 2) + 'px';
        div.style.height = spanHeight + 'px';
        this.height = spanHeight;
    }
};
Animator.prototype.parseSpec = function (storyboard_spec) {
    var lines = storyboard_spec.split('|');
    var q;
    if (!this.quality) {
        this.quality = - 1;
        do {
            this.quality++;
            q = lines[this.quality + 1].split('#');
        } while (parseInt(q[0], 10) < this.width && this.quality + 2 < lines.length);
    }
    var q = lines[this.quality + 1].split('#');
    var s = {
        url: lines[0].replace('$L', this.quality) .replace('$N', q[6]),
        width: parseInt(q[0], 10),
        height: parseInt(q[1], 10),
        count: parseInt(q[2], 10),
        cols: parseInt(q[3], 10),
        rows: parseInt(q[4], 10),
        sigh: q[7]
    };
    s.sheetSize = s.cols * s.rows;
    s.sheetCount = ((s.count / s.sheetSize) | 0) + 1; // bitwise OR to loose decimals
    s.countOnLastSheet = ((s.count - 1) % s.sheetSize) + 1;
    return this.spec = s;
};
Animator.prototype.loadImage = function (callback) {
    clearInterval(this.token);
    var onLoad = (function () {
        div.style.backgroundImage = 'url(' + loader.src + ')';
        loader.removeEventListener('load', onLoad);
        callback.apply(this);
    }).bind(this);
    loader.addEventListener('load', onLoad);
    loader.src = this.spec.url.replace('$M', this.sheet) + '?sigh=' + this.spec.sigh;
};
Animator.prototype.getStoryboardSpecs = function (callback, fromWatch) {
    this.xhr = new XMLHttpRequest();
    this.xhr.onreadystatechange = (function () {
        if (this.isMouseOver && this.xhr.readyState == 4 && this.xhr.status == 200) {
            if (fromWatch)
                var spec = (/"storyboard_spec":\s*(".*?")/.exec(this.xhr.responseText) || []) [1];
            else
                var spec = (/&storyboard_spec=(.*?)&/.exec(this.xhr.responseText) || []) [1];
            if (spec) {
                if (fromWatch)
                    spec = eval(spec); // remove backslashes
                this.parseSpec(decodeURIComponent(spec));
                callback.apply(this);
            } 
            else if (!fromWatch) {
                this.getStoryboardSpecs(callback, true);
            }
        }
    }).bind(this);
    if (fromWatch)
        this.xhr.open('GET', '/watch?v=' + this.v, true);
    else
        this.xhr.open('GET', '/get_video_info?video_id=' + this.v, true);
    this.xhr.send();
};
Animator.prototype.start = function () {
    this.frame = 0;
    this.sheet = 0;
    var lastTick;
    var next = function () {
        if (this.isMouseOver) {
            if (this.spec.fit == 'height') {
                var w = this.spec.width * this.height / this.spec.height;
                var offset = (this.width - w) / 2;
                div.style.backgroundPosition = -((this.frame % this.spec.cols) * w - offset) + 'px ' +
                                               -(((this.frame / this.spec.cols) | 0) * this.height) + 'px'; // bitwise OR with 0 to loose decimals
            } 
            else {
                var h = this.spec.height * this.width / this.spec.width;
                var offset = (this.height - h) / 2;
                div.style.backgroundPosition = -((this.frame % this.spec.cols) * this.width) + 'px ' +
                                               -(((this.frame / this.spec.cols) | 0) * h - offset) + 'px';
            }
            this.frame++;
            if ((this.frame == this.spec.sheetSize) || (this.sheet == this.spec.sheetCount - 1 && this.frame == this.spec.countOnLastSheet))
            {
                clearInterval(this.token);
                this.frame = 0;
                this.sheet = (this.sheet + 1) % this.spec.sheetCount;
                this.loadImage(function () {
                    this.token = setInterval(next, this.interval);
                });
            }
        } 
        else {
            this.stop();
        }
    }.bind(this);
    
    this.loadImage(function () {
        if (this.isMouseOver) {
            div.style.display = null;
            var onMouseOut = (function () {
                this.stop();
                div.removeEventListener('mouseout', onMouseOut);
            }).bind(this);
            div.addEventListener('mouseout', onMouseOut);
            if ((this.width / this.height) <= (this.spec.width / this.spec.height)) {
                this.spec.fit = 'height';
                div.style.backgroundSize = ((this.spec.width * this.height / this.spec.height) * this.spec.cols) + 'px ' + 
                                           (this.height * this.spec.rows) + 'px';
            } 
            else {
                this.spec.fit = 'width';
                div.style.backgroundSize = (this.width * this.spec.cols) + 'px ' + 
                                           ((this.spec.height * this.width / this.spec.width) * this.spec.rows) + 'px';
            }
            next();
            this.token = setInterval(next, this.interval);
        }
    });
};
Animator.prototype.stop = function () {
    this.isMouseOver = false;
    clearInterval(this.token);
    div.style.display = 'none';
}