Greasy Fork is available in English.

bilibili 直播 HTML5 播放器

B 站的直播的 HTML5 播放器

// ==UserScript==
// @name         bilibili 直播 HTML5 播放器
// @namespace    https://www.kindjeff.com/
// @version      2017.2.24
// @description  B 站的直播的 HTML5 播放器
// @author       kindJeff
// @match        http://live.bilibili.com/*
// @match        https://live.bilibili.com/*
// @require      https://cdn.bootcss.com/hls.js/0.6.21/hls.min.js
// @require      https://cdn.rawgit.com/weizhenye/Danmaku/b2ae44f7dcd28b27c31e5db5b512b9474d0396c7/dist/danmaku.min.js
// @run-at       document-end
// ==/UserScript==

var room_id;

setTimeout(function(){
    //var xhr = new XMLHttpRequest();
    //xhr.onreadystatechange=function(){
    //    if (xhr.readyState==4 && xhr.status==200){
    //        eval(xhr.responseText);
            var link = $('#player_object').children('[name="flashvars"]').val();
            room_id = link.match(/cid=.*?&/)[0].slice(4,-1);
            get_url_and_replace_player(room_id);
            init_danmaku();
            set_danmu_control();
            click_list();
    //    }
    //};
    //xhr.open('GET', 'https://raw.githubusercontent.com/weizhenye/Danmaku/master/dist/danmaku.min.js');
    //xhr.send();
}, 2000);


function get_url_and_replace_player(room_id){
    var api_url = 'https://api.live.bilibili.com/api/playurl?platform=h5&cid=' + room_id;
    $.ajax({
        url: api_url,
        type: "GET",
        dataType: 'json',
        success: function(data){
            replace_player(data.data);

            if(window.df_danmu_ws!==undefined){
                window.i_close_it_myself = true;
                window.df_danmu_ws.close();
                window.df_danmu_ws = undefined;
            }
            var df_domain = 'broadcastlv.chat.bilibili.com';
            var df_portobj = {'ws':7170, 'wss':7172};
            window.df_danmu_ws = new DanmuSocket(parseInt(room_id), df_domain, df_portobj);
            window.df_danmu_ws.setListener(danmuListener);
        }
    });
}

function replace_player(m3u8_url){
    var w = $('#js-player-decorator').width();
    var h = $('#js-player-decorator').height();

    remove_player();

    var player = document.createElement('video');
    player.id = 'h5_player';
    player.style.width = '100%';
    player.style.height = '100%';
    player.style.position = 'absolute';
    player.setAttribute('controls', 'controls');
    document.getElementById('js-player-decorator').appendChild(player);

    if(Hls.isSupported()) {
        var video = document.getElementById('h5_player');
        var hls = new Hls();
        hls.loadSource(m3u8_url);
        hls.attachMedia(video);
        hls.on(Hls.Events.MANIFEST_PARSED,function() {
          video.play();
        });
    }
}

function remove_player(){
    var flash_player = document.getElementById('player_object');
    if(flash_player!==null)
        flash_player.remove();

    var html5_player = document.getElementById('h5_player');
    if(html5_player!==null)
        html5_player.remove();
}

function click_list(){
    if(window.location.pathname==='/'){
        $($('[role="list"]')[0]).children().on('click', function(){
            var room_id = $(this).attr('data-cid');
            get_url_and_replace_player(room_id);
        });
    }
}


/* danmaku */
const rawHeaderLen = 16;
const packetOffset = 0;
const headerOffset = 4;
const verOffset = 6;
const opOffset = 8;
const seqOffset = 12;
var pako = window.pako;
var textDecoder = getDecoder(true);
var textEncoder = getEncoder();
var heartbeatInterval;

function getDecoder (isUseful) {
    if(window['TextDecoder'] && isUseful) {
        return new window['TextDecoder']();
    } else {
        return {
            decode: (buf) => {
                return decodeURIComponent(window.escape(String.fromCharCode.apply(null, new Uint8Array(buf))));
            }
        }
    }
}

function getEncoder () {
    if(window['TextEncoder']) {
        return new window['TextEncoder']();
    } else {
        return {
            encode: (str) => {
                let buf = new ArrayBuffer(str.length);
                let bufView = new Uint8Array(buf);
                for (let i = 0, strlen = str.length; i < strlen; i++) {
                    bufView[i] = str.charCodeAt(i);
                }
                return bufView;
            }
        }
    }
}

function mergeArrayBuffer(ab1, ab2) {
    var u81 = new Uint8Array(ab1),
        u82 = new Uint8Array(ab2),
        res = new Uint8Array(ab1.byteLength + ab2.byteLength);
    res.set(u81, 0);
    res.set(u82, ab1.byteLength);
    return res.buffer;
}

class DanmuSocket {

    constructor (roomid,domain,portobj) {
        const ws = window.location.protocol.indexOf('https') > -1 ? 'wss' : 'ws';
        const port = portobj[ws];
        this.connection = new WebSocket(ws + "://"+ domain +":"+ port +"/sub");
        this.connection.binaryType = 'arraybuffer';
        this.connection.onopen = this.firstConnection.bind(this);
        this.connection.onmessage = onMessage.bind(this);
        this.connection.onclose = onClose.bind(this);
        this.connection.onerror = onError.bind(this);
        this.roomid = roomid
    }

    firstConnection () {
        console.log("Danmu WebSocket Server Connected.");
        console.log("Handshaking...");
        var token = JSON.stringify({
            'uid': 0,
            'roomid': this.roomid
        });
        var headerBuf = new ArrayBuffer(rawHeaderLen);
        var headerView = new DataView(headerBuf, 0);
        var bodyBuf = textEncoder.encode(token);
        headerView.setInt32(packetOffset, rawHeaderLen + bodyBuf.byteLength);
        headerView.setInt16(headerOffset, rawHeaderLen);
        headerView.setInt16(verOffset, 1);
        headerView.setInt32(opOffset, 7);
        headerView.setInt32(seqOffset, 1);
        this.connection.send(mergeArrayBuffer(headerBuf, bodyBuf));
    }

    heartBeat () {
        var headerBuf = new ArrayBuffer(rawHeaderLen);
        var headerView = new DataView(headerBuf, 0);
        headerView.setInt32(packetOffset, rawHeaderLen);
        headerView.setInt16(headerOffset, rawHeaderLen);
        headerView.setInt16(verOffset, 1);
        headerView.setInt32(opOffset, 2);
        headerView.setInt32(seqOffset, 1);
        this.connection.send(headerBuf);
    }

    closeHeartBeat () {
        clearInterval(this.heartBeating);
    }

    send (data) {
        this.connection.send(data);
    }

    close () {
        this.connection.close();
    }

    setListener (listener) {
        this._listener = listener;
    }

}

function onMessage (evt) {
    var data = evt.data;
    var dataView = new DataView(data, 0);
    var packetLen = dataView.getInt32(packetOffset);
    var headerLen = dataView.getInt16(headerOffset);
    var ver = dataView.getInt16(verOffset);
    var op = dataView.getInt32(opOffset);
    var seq = dataView.getInt32(seqOffset);

    switch(op) {
        case 8:
            this.heartBeat();
            heartbeatInterval = setInterval(this.heartBeat.bind(this), 30 * 1000);
        break;
        case 3:
            // console.log("online: " + dataView.getInt32(16));
            if (this._listener) this._listener('online', dataView.getInt32(16));
        break;
        case 5:
            var packetView = dataView;
            var msg = data;
            var msgBody;
            for (var offset=0; offset<msg.byteLength; offset+=packetLen) {
                packetLen = packetView.getInt32(offset);
                headerLen = packetView.getInt16(offset+headerOffset);
                msgBody = textDecoder.decode(msg.slice(offset+headerLen, offset+packetLen));
                if (!msgBody) {
                    textDecoder = getDecoder(false);
                    msgBody = textDecoder.decode(msg.slice(offset+headerLen, offset+packetLen));
                }
                if (this._listener) this._listener('msg', msgBody);
            }
        break;
    }
}

function onClose () {
    if (heartbeatInterval) clearInterval(heartbeatInterval);
    if(! i_close_it_myself){
        var delay = Math.floor(Math.random() * (6 - 3) + 3);
        setTimeout(this.firstConnection.bind(this), delay * 1000);
        console.log(delay);
    }
    i_close_it_myself = false;
}

function onError () {
    console.log("Client Error.");
}


/*******************/
function change_online(online) {
    $('span.v-bottom').text(online + ' 人');
}

function emit_danmu(data) {
    if(data.cmd==='DANMU_MSG'){
        var msg = data.info[1];
        window.df_danmaku.emit({
            text: msg,
            canvasStyle: {
                font: data.info[0][2]+'px sans-serif',
                textAlign: 'start',
                textBaseline: 'bottom',
                direction: 'inherit',
                fillStyle: '#fff',
                strokeStyle: '#000',
                lineWidth: 1.2,
                shadowBlur: 0,
                shadowColor: '#000',
                shadowOffsetX: 0,
                shadowOffsetY: 0,
                filter: 'none',
                globalAlpha: 1.0
            }
        })
    }else if(data.cmd==='WELCOME'){

    }else if(data.cmd==='SEND_GIFT'){

    }
}

function append_danmu(data) {
    if(data.cmd==='DANMU_MSG'){
        var u_name = data.info[2][1];
        var uid = data.info[2][0];
        var lv = data.info[4][0];
        var rank = data.info[4][3]; if(typeof(rank)=='string'&&rank.indexOf('>')!==-1) {rank.replace('>', '&gt;')}
        var msg = data.info[1];
        // console.log(u_name,uid,lv,rank,msg);
        var comment_div = '<div class="msg-item-ctnr"><div class="chat-msg " data-uname="'+u_name+'" data-uid="'+uid+'"><div class="user-level-icon lv-'+lv+'"> UL '+lv+' <div class="user-level-info"><p>用户等级:'+lv+'</p><p><a href="http://live.bilibili.com/rank" target="_blank">排名:'+rank+'</a></p></div></div><span class="user-name color">'+u_name+' : </span><span class="msg-content">'+msg+'</span></div></div>';
        $(comment_div).appendTo('#chat-msg-list');
        if($('#chat-msg-list').children().length>100)
            $('#chat-msg-list').children(':first').remove();
        $("#chat-msg-list").scrollTop($("#chat-msg-list")[0].scrollHeight);
    }
}

function danmuListener(content_type, content){
    if(content_type==='online'){
        if(window.dom_changed===undefined){
            $('#h5_player').prev().appendTo('#js-player-decorator');
            window.dom_changed = true;
        }
        change_online(content);
    }else if(content_type==='msg'){
        var content_obj = JSON.parse(content);
        emit_danmu(content_obj);
        append_danmu(content_obj);
    }
}

function init_danmaku() {
    window.df_danmaku = new Danmaku();
    df_danmaku.init({
        container: $('#js-player-decorator')[0],
        video: $("#h5_player")[0],
        engine:'canvas'
    });
    $('canvas')[0].style.position = 'absolute';

    // send danmu
    function send_danmu(){
        var msg = $("#df-danmu-textbox").val();
        var xhr = new XMLHttpRequest();
        // xhr.setRequestHeader('X-Cookie', document.cookie);
        xhr.open('POST', 'http://live.bilibili.com/msg/send');
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.send($.param({
            color: 16777215,
            fontsize: 25,
            mode: 1,
            msg: msg,
            rnd: Math.floor(Date.now() / 1000),
            roomid: room_id
        }));
    }
    $("#danmu-textbox").off('keypress');
    $("#danmu-textbox").off('keyup');
    $("#danmu-textbox").off('keydown');
    $("#danmu-send-btn").off('click');

    $("#danmu-textbox")[0].id = 'df-danmu-textbox';
    $("#danmu-send-btn")[0].id = 'df-danmu-send-btn';
    $("#df-danmu-textbox").on('keyup', function (e) {
        if(e.keyCode == 13){
            send_danmu();
            $("#df-danmu-textbox").val('');
            e.preventDefault();
            return false;
        }
        return true;
    });
    $("#df-danmu-send-btn").on('click', function (e) {
        e.preventDefault();
        send_danmu();
        $("#df-danmu-textbox").val('');
    });
}

function set_danmu_control(){
    if(location.pathname==='/'){
        return;
    }

    var control_btn = $("<button>关闭弹幕</button>");
    control_btn.css('border-radius', '5px');
    control_btn.css('font-size', '12px');
    control_btn.height('21px');
    $('.room-info.tag-ctnr.v-top').children().remove();
    control_btn.appendTo('.room-info.tag-ctnr.v-top');
    control_btn.on('click', function () {
        if(control_btn.text()=='打开弹幕'){
            control_btn.text('关闭弹幕');
            $('canvas')[0].style.display = 'block';
        }else{
            $('canvas')[0].style.display = 'none';
            control_btn.text('打开弹幕');
        }
    });
}