基于WebSocket的在線聊天室(二)

效果預(yù)覽

前言


在上一篇文章中已經(jīng)對(duì)websocket的做了一定的介紹珍特,并給出了一個(gè)文本聊天室的例子,本文將繼續(xù)對(duì)其進(jìn)行功能擴(kuò)展硝枉,加上語(yǔ)音和視頻的功能(感覺瞬間高大上了有木有 *_*)

相關(guān)技術(shù)


在做功能之前也是找了不少資料的抱既,發(fā)現(xiàn)網(wǎng)上對(duì)于視頻通話的實(shí)現(xiàn)基本都是采用了WebRTC這么個(gè)東東

WebRTC僻澎,名稱源自網(wǎng)頁(yè)實(shí)時(shí)通信(Web Real-Time Communication)的縮寫涩盾,是一個(gè)支持網(wǎng)頁(yè)瀏覽器進(jìn)行實(shí)時(shí)語(yǔ)音對(duì)話或視頻對(duì)話的技術(shù),是谷歌2010年以6820萬(wàn)美元收購(gòu)Global IP Solutions公司而獲得的一項(xiàng)技術(shù)捕捂。2011年5月開放了工程的源代碼瑟枫,在行業(yè)內(nèi)得到了廣泛的支持和應(yīng)用,成為下一代視頻通話的標(biāo)準(zhǔn)指攒。

看別人的例子用起來(lái)貌似挺簡(jiǎn)單的樣子慷妙,但因?yàn)槭枪雀璧臇|西,還要連STUN服務(wù)器什么的允悦,就放棄了深入研究膝擂,有興趣的自行百度吧。本文中是根據(jù)websocket和html5的一些特性進(jìn)行開發(fā)的。

主要步驟


1. 調(diào)用攝像頭
2. 畫面捕捉
3. 圖片傳輸
4. 圖片接收和繪制

注:上面步驟以視頻傳輸為例架馋,音頻部分類似狞山,但也有區(qū)別,后面再講

調(diào)用攝像頭


jAlert = function(msg, title, callback) {
    alert(msg);
    callback && callback();
};
//媒體請(qǐng)求成功后調(diào)用的回調(diào)函數(shù)
sucCallBack = function(stream) {
    //doSomething
};
//媒體請(qǐng)求失敗后調(diào)用的回調(diào)函數(shù)
errCallBack = function(error) {
    if (error.PERMISSION_DENIED) {
        jAlert('您拒絕了瀏覽器請(qǐng)求媒體的權(quán)限', '提示');
    } else if (error.NOT_SUPPORTED_ERROR) {
        jAlert('對(duì)不起叉寂,您的瀏覽器不支持?jǐn)z像頭/麥克風(fēng)的API萍启,請(qǐng)使用其他瀏覽器', '提示');
    } else if (error.MANDATORY_UNSATISFIED_ERROR) {
        jAlert('指定的媒體類型未接收到媒體流', '提示');
    } else {
        jAlert('相關(guān)硬件正在被其他程序使用中', '提示');
    }
};
//媒體請(qǐng)求的參數(shù),video:true表示調(diào)用攝像頭屏鳍,audio:true表示調(diào)用麥克風(fēng)
option = {video:true}
//兼容各個(gè)瀏覽器
navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);

var userAgent = navigator.userAgent,
    msgTitle = '提示',
    notSupport = '對(duì)不起勘纯,您的瀏覽器不支持?jǐn)z像頭/麥克風(fēng)的API,請(qǐng)使用其他瀏覽器';
try {
    if (navigator.getUserMedia) {
        if (userAgent.indexOf('MQQBrowser') > -1) {
            errCallBack({
                NOT_SUPPORTED_ERROR: 1
            });
            return false;
        }
        navigator.getUserMedia(option, sucCallBack, errCallBack);
    } else {
        /*
        if (userAgent.indexOf("Safari") > -1 && userAgent.indexOf("Oupeng") == -1 && userAgent.indexOf("360 Aphone") == -1) {
            //由于沒有Safari瀏覽器不能對(duì)已有方法進(jìn)行測(cè)試孕蝉,有需要可以自行度之
        } //判斷是否Safari瀏覽器
        */
        errCallBack({
            NOT_SUPPORTED_ERROR: 1
        });
        return false;
    }
} catch (err) {
    errCallBack({
        NOT_SUPPORTED_ERROR: 1
    });
    return false;
}

兼容性測(cè)試:

IE系列:攝像頭/麥克風(fēng)都不支持

Edge:攝像頭/麥克風(fēng)都支持屡律,不過有個(gè)小BUG腌逢,請(qǐng)求攝像頭的時(shí)候會(huì)提示“是否允許xxx使用你的麥克風(fēng)”降淮,影響不大。

Chrome系列(包括支持極速模式的國(guó)產(chǎn)山寨瀏覽器):攝像頭基本都支持搏讶,語(yǔ)音功能不一定都支持(和chromium的版本以及電腦的聲卡驅(qū)動(dòng)有關(guān)系)佳鳖,今天更新了qq瀏覽器,貌似最新的chromium(47.0.2526.80)不能直接訪問媒體API了

getUserMedia() no longer works on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gl/rStTGz for more details.

解決辦法:

用HTTPS或者在chrome運(yùn)行參數(shù)里加--able-web-security

Firefox(45.0.2):攝像頭/麥克風(fēng)都支持

  • 硬件設(shè)備同時(shí)僅能被一個(gè)瀏覽器訪問媒惕,不過同一個(gè)瀏覽器可以打開多個(gè)標(biāo)簽頁(yè)來(lái)多次調(diào)用
  • 文章最后會(huì)給出的項(xiàng)目里面有幾個(gè)頁(yè)面是用來(lái)測(cè)試媒體 API 的系吩,自己去尋找吧

畫面捕捉


從上面一步我們已經(jīng)取得了媒體流(sucCallBack中的stream),這一步就要對(duì)它進(jìn)行處理妒蔚。

首先穿挨,將其作為video控件的視頻源

sucCallBack = function(stream) { 
    var video = document.getElementById("myVideo");
    video.src = window.URL.createObjectURL(stream);
    video.play();
}

接著,將video中的畫面繪制到canvas上

var video = document.getElementById("myVideo");
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
video.addEventListener("play", drawCanvas);
function drawCanvas() {
    if (video.paused || video.ended) {
        return;
     }
    context.drawImage(video, 0, 0, 640, 360);//將video當(dāng)前畫面繪制到畫布上
    setTimeout(drawCanvas, 30);
}

圖片傳輸


從canvas獲得圖片

var canvas = document.getElementById("canvas");
var img = new Image();
//形如"data:image/png;base64,iVBORw0KG..."逗號(hào)前內(nèi)容為文件類型肴盏,格式科盛,編碼類型,逗號(hào)之后為base64編碼內(nèi)容
var url = canvas.toDataURL("image/png");
img.src = url;
document.body.appendChild(img);

通過websocket對(duì)圖片進(jìn)行傳輸菜皂,java代碼和上文中基本差不多贞绵,就多了一個(gè)@OnMessage注釋的方法,

@OnMessage(maxMessageSize = 10000000)
public void OnMessage(ByteBuffer message) {
    //TODO
}

注意maxMessageSize參數(shù),由于一張圖片比較大,如果該值設(shè)置過小的話恍飘,服務(wù)端無(wú)法接收而導(dǎo)致連接直接斷開

定時(shí)發(fā)送數(shù)據(jù)

function sendFrame(){
    //TODO
    setTimeout(sendFrame, 300)
}
//或
setTimeout(function() {
    requestAnimationFrame(sendFrame)
}, 300)

requestAnimationFrame的區(qū)別在于如果此頁(yè)面不是瀏覽器當(dāng)前窗口的當(dāng)前標(biāo)簽(即此標(biāo)簽頁(yè)被掛起)榨崩,那么其中的回調(diào)函數(shù)(sendFrame)會(huì)被掛起,直到此標(biāo)簽頁(yè)被激活后再執(zhí)行章母,而只使用setTimeout在標(biāo)簽頁(yè)被掛起的時(shí)候還會(huì)繼續(xù)執(zhí)行母蛛。所以requestAnimationFrame可以用來(lái)實(shí)現(xiàn) 掛起頁(yè)面達(dá)到暫停視頻通訊 的效果

客戶端有兩種發(fā)送方式,一種是發(fā)送Blob對(duì)象

function getWebSocket(host) {
    var socket;
    if ('WebSocket' in window) {
        socket = new WebSocket(host)
    } else if ('MozWebSocket' in window) {
        socket = new MozWebSocket(host)
    }
    return socket
};
// 將base64編碼的二進(jìn)制數(shù)據(jù)轉(zhuǎn)為Blob對(duì)象
function dataURLtoBlob(dataurl) {
    var arr = dataurl.split(','),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], {
        type: mime
    });
};
function sendFrame(){
    // socket = getWebSocket ("ws://" + window.location.host + "/websocket/chat")
    socket.send(dataURLtoBlob(canvas.toDataURL("image/png")));//使用已建立的socket對(duì)象發(fā)送數(shù)據(jù)
    setTimeout(sendFrame, 300);
}

另一種是先用對(duì)象包裝乳怎,再轉(zhuǎn)為字符串發(fā)送

msg = {
    type: 3,
    msg: canvas.toDataURL("image/png").substring("data:image/png;base64,".length),// 截掉內(nèi)容頭
};
// 對(duì)數(shù)據(jù)進(jìn)行base64解碼彩郊,減少要發(fā)送的數(shù)據(jù)量
msg.msg = window.atob(msg.msg);
// 序列化成json串
socket.send(JSON.stringify(msg))

說說這里遇到的坑吧

  1. 在第一種方式中,先是獲取圖片的Blob文件,但為了讓其他用戶知道是誰(shuí)發(fā)送的焦辅,需要發(fā)送發(fā)送者的username(或者id)博杖,本來(lái)試過先用string發(fā)送username,再緊跟著發(fā)送一個(gè)Blob,雖然并未出錯(cuò)但是感覺數(shù)據(jù)多了會(huì)錯(cuò)亂,而且接收的時(shí)候處理起來(lái)比較復(fù)雜,于是就想著把username并入Blob。不過貌似沒有string和Blob直接合并的方法筷登,于是我先把字符串生成一個(gè)Blob("text/plain"),然后把兩個(gè)Blob合并起來(lái)
    new Blob([textBlob, imageBlob])

  2. 由于第一種方式發(fā)送的時(shí)候會(huì)生成一個(gè)Blob對(duì)象剃根,再加上是通過setTimeout這種定時(shí)遞歸得到方式發(fā)送的,內(nèi)存占用(在chrome任務(wù)管理器中查看)蹭蹭蹭暴漲前方,沒多久就占用幾百兆內(nèi)存狈醉,雖然在幾處地方手動(dòng)置為null來(lái)釋放引用有點(diǎn)改善,但效果也不是很明顯惠险,個(gè)人感覺是頻率太高(100ms間隔即1秒10幀左右的圖片)導(dǎo)致GC來(lái)不及釋放苗傅。相較之下,第二種方式雖然內(nèi)存也會(huì)漲班巩,不過基本會(huì)穩(wěn)定在50M左右渣慕。這個(gè)問題以后再細(xì)究,如果哪位看官有想法歡迎評(píng)論指教抱慌。
    在項(xiàng)目代碼中設(shè)置videoClient.sendType = 1;切換到第一種方式

  3. 正常兩個(gè)人(窗口)視頻通訊的時(shí)候逊桦,第一種方式毫無(wú)壓力,不過再增加用戶之后就爆了抑进,每個(gè)窗口都在不斷的斷開重連强经,從接收到的數(shù)據(jù)看應(yīng)該是數(shù)據(jù)包間的內(nèi)容錯(cuò)亂了。因?yàn)槲乙粋€(gè)Blob是用戶名和圖片數(shù)據(jù)組合在一起的寺渗,形如zhangsaniVBORw0KG.....匿情,其中'zhangsan'為用戶名,后面部分為圖片數(shù)據(jù),數(shù)據(jù)錯(cuò)亂了之后就變成了#%#sanVBORw0KG.....之類的信殊,這樣正常解析就會(huì)出錯(cuò)導(dǎo)致連接斷開炬称。所以,人多了就只能采用第二種方式發(fā)送了鸡号。

圖片接收和繪制


對(duì)應(yīng)上一步的兩種方式转砖,接收?qǐng)D片也有兩種方式

在onmessage方法中先這么處理

if (typeof(message.data) == "string") {
    //TODO
} else if (message.data instanceof Blob) {
    //TODO
}

第一種接收方式(Blob)

function renderFrame(_host, blob) {
    readBlobAsDataURL(_host, function(host) {
        _host = null;
        host = DataURLtoString(host);
        var canvas = document.getElementById("canvas"),
            url = URL.createObjectURL(blob),
            img = new Image();
        img.onload = function() {
            window.URL.revokeObjectURL(url);
            var context = canvas.getContext("2d");
            context.drawImage(img, 0, 0, canvas.width, canvas.height);
            img = null;
            blob = null;
        };
        img.src = url;
    })
};
// 通過FileReader來(lái)讀取Blob中封裝的字符串內(nèi)容
function readBlobAsDataURL(blob, callback) {
    var a = new FileReader();
    a.onload = function(e) {
        callback(e.target.result);
    };
    a.readAsDataURL(blob);
}
// 進(jìn)行base64編碼解碼
function DataURLtoString(dataurl) {
    return window.atob(dataurl.substring("data:text/plain;base64,".length))
}
var offset = 14;//我采用14位長(zhǎng)度的時(shí)間戳字符串作為username
renderFrame(new Blob([msg.data.slice(0, offset)], {
    type: "text/plain"
}), new Blob([msg.data.slice(offset)], {
    type: "image/png"
}))

第二種方式

renderFrame2 = function(host, data) {
    var canvas = document.getElementById("canvas"),
        img = new Image();
    img.onload = function() {
        window.URL.revokeObjectURL(url);
        var context = canvas.getContext("2d");
        context.drawImage(img, 0, 0, canvas.width, canvas.height);
        img = null;
    };
    // 對(duì)數(shù)據(jù)重新進(jìn)行base64編碼后作為圖片的源
    img.src = "data:image/png;base64," + window.btoa(data)
};
// 將socket.onmessage方法中接收到的數(shù)據(jù)轉(zhuǎn)為msg對(duì)象
var msg = JSON.parse(message.data);
renderFrame2(msg.host, msg.msg)

至此視頻部分的內(nèi)容都結(jié)束了,接下來(lái)講下音頻部分的差異

調(diào)用麥克風(fēng)


同攝像頭調(diào)用的第一步鲸伴,設(shè)置 option = {audio:true} 得到麥克風(fēng)的流府蔗,但是不能向攝像頭一樣直接給video控件的src賦值就好了,還需要下面一系列的操作汞窗。

1. 創(chuàng)建“錄音機(jī)對(duì)象”

audioContext = window.AudioContext || window.webkitAudioContext;
context = new audioContext();
config = {
    inputSampleRate: context.sampleRate,//輸入采樣率,取決于平臺(tái)
    inputSampleBits: 16,//輸入采樣數(shù)位 8, 16
    outputSampleRate: 44100 / 6,//輸出采樣率
    oututSampleBits: 8,//輸出采樣數(shù)位 8, 16
    channelCount: 2,//聲道數(shù)
    cycle: 500,//更新周期,單位ms
    volume: _config.volume || 1 //音量
};
var bufferSize = 4096;//緩存大小
//創(chuàng)建“錄音機(jī)對(duì)象”
recorder = context.createScriptProcessor(bufferSize, config.channelCount, config.channelCount); // 第二個(gè)和第三個(gè)參數(shù)指的是輸入和輸出的聲道數(shù)
buffer = [];//音頻數(shù)據(jù)緩沖區(qū)
bufferLength = 0;//音頻數(shù)據(jù)緩沖區(qū)長(zhǎng)度

2. 將音頻輸入和“錄音機(jī)對(duì)象”關(guān)聯(lián)

//通過音頻流創(chuàng)建輸入音頻對(duì)象
audioInput = context.createMediaStreamSource(stream);//stream即getUserMedia成功后得到的流
//設(shè)置錄音機(jī)錄音處理事件姓赤,每次緩存(上一步中)滿了執(zhí)行回調(diào)函數(shù),
recorder.onaudioprocess = function(e) {
    var inputbuffer = e.inputBuffer,
        channelCount = inputbuffer.numberOfChannels,
        length = inputbuffer.length;
    channel = new Float32Array(channelCount * length);
    for (var i = 0; i < length; i++) {
        for (var j = 0; j < channelCount; j++) {
            channel[i * channelCount + j] = inputbuffer.getChannelData(j)[i];
        }
    }
    buffer.push(channel);//緩存數(shù)據(jù)存入音頻緩沖區(qū)
    bufferLength += channel.length;
};
// 創(chuàng)建 '音量對(duì)象'仲吏,作為 '音頻輸入' 和 '錄音機(jī)對(duì)象' 連接的橋梁
volume = context.createGain();
audioInput.connect(volume);
volume.connect(recorder);
// 當(dāng)然也可以不通過 '音量對(duì)象' 直連 '錄音機(jī)'不铆,但不能同時(shí)使用兩種方式
// audioInput.connect(this.recorder);
recorder.connect(context.destination);//context.destination為音頻輸出
//連接完就開始執(zhí)行onaudioprocess方法
//recorder.disconnect()可以停止“錄音”
updateSource(callback);

3. 獲取“音頻流”

麥克風(fēng)的輸入都已經(jīng)連接至“錄音機(jī)對(duì)象”蝌焚,“錄音機(jī)對(duì)象”再將數(shù)據(jù)不斷存入緩沖區(qū),所以要得到穩(wěn)定的“音頻流”(不是真正意義上的流)可以以一定時(shí)間間隔(1秒)提取以此緩沖區(qū)的數(shù)據(jù)轉(zhuǎn)為一秒時(shí)長(zhǎng)的音頻文件誓斥,將之通過audio控件播放即可只洒。注意,這樣處理會(huì)導(dǎo)致聲音有1秒的延遲劳坑,可以減少這個(gè)時(shí)間間隔接近“實(shí)時(shí)”的效果毕谴。

第一部分:數(shù)據(jù)壓縮合并

function compress() { //合并壓縮
    //合并
    var buffer = this.buffer,
        bufferLength = this.bufferLength;
    this.buffer = []; //將緩沖區(qū)清空
    this.bufferLength = 0;
    var data = new Float32Array(bufferLength);
    for (var i = 0, offset = 0; i < buffer.length; i++) {
        data.set(buffer[i], offset);
        offset += buffer[i].length;
    }
    //根據(jù)采樣頻率進(jìn)行壓縮
    var config = this.config,
        compression = parseInt(config.inputSampleRate / config.outputSampleRate),
        //計(jì)算壓縮率
        length = parseInt(data.length / compression),
        result = new Float32Array(length);
    index = 0;
    while (index < length) {
        result[index] = data[index++ * compression];
    }
    return result;//合并壓縮后的數(shù)據(jù)
}

第二部分:將上一步的數(shù)據(jù)編碼成WAV格式的文件

function encodeWAV(bytes) {
    var config = this.config,
        sampleRate = Math.min(config.inputSampleRate, config.outputSampleRate),
        sampleBits = Math.min(config.inputSampleBits, config.oututSampleBits),
        dataLength = bytes.length * (sampleBits / 8),
        buffer = new ArrayBuffer(44 + dataLength),
        view = new DataView(buffer),
        channelCount = config.channelCount,
        offset = 0,
        volume = config.volume;

    writeUTFBytes = function(str) {
        for (var i = 0; i < str.length; i++) {
            view.setUint8(offset + i, str.charCodeAt(i));
        }
    };
    // 資源交換文件標(biāo)識(shí)符 
    writeUTFBytes('RIFF');
    offset += 4;
    // 下個(gè)地址開始到文件尾總字節(jié)數(shù),即文件大小-8 
    view.setUint32(offset, 44 + dataLength, true);
    offset += 4;
    // WAV文件標(biāo)志
    writeUTFBytes('WAVE');
    offset += 4;
    // 波形格式標(biāo)志 
    writeUTFBytes('fmt ');
    offset += 4;
    // 過濾字節(jié),一般為 0x10 = 16 
    view.setUint32(offset, 16, true);
    offset += 4;
    // 格式類別 (PCM形式采樣數(shù)據(jù)) 
    view.setUint16(offset, 1, true);
    offset += 2;
    // 通道數(shù) 
    view.setUint16(offset, channelCount, true);
    offset += 2;
    // 采樣率,每秒樣本數(shù),表示每個(gè)通道的播放速度 
    view.setUint32(offset, sampleRate, true);
    offset += 4;
    // 波形數(shù)據(jù)傳輸率 (每秒平均字節(jié)數(shù)) 單聲道×每秒數(shù)據(jù)位數(shù)×每樣本數(shù)據(jù)位/8 
    view.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true);
    offset += 4;
    // 快數(shù)據(jù)調(diào)整數(shù) 采樣一次占用字節(jié)數(shù) 單聲道×每樣本的數(shù)據(jù)位數(shù)/8 
    view.setUint16(offset, channelCount * (sampleBits / 8), true);
    offset += 2;
    // 每樣本數(shù)據(jù)位數(shù) 
    view.setUint16(offset, sampleBits, true);
    offset += 2;
    // 數(shù)據(jù)標(biāo)識(shí)符 
    writeUTFBytes('data');
    offset += 4;
    // 采樣數(shù)據(jù)總數(shù),即數(shù)據(jù)總大小-44 
    view.setUint32(offset, dataLength, true);
    offset += 4;
    // 寫入采樣數(shù)據(jù) 
    if (sampleBits === 8) {
        for (var i = 0; i < bytes.length; i++, offset++) {
            var val = bytes[i] * (0x7FFF * volume);
            val = parseInt(255 / (65535 / (val + 32768)));
            view.setInt8(offset, val, true);
        }
    } else if (sampleBits === 16) {
        for (var i = 0; i < bytes.length; i++, offset += 2) {
            var val = bytes[i] * (0x7FFF * volume);
            view.setInt16(offset, val, true);
        }
    }
    return new Blob([view], {
        type: 'audio/wav'
    });
}

第三部:將上步得到的音頻文件作為audio控件的聲音源

function updateSource(callback) {
    var blob = encodeWAV(this.compress());
    var audio= document.getElementById("audio");
    //對(duì)上一秒播放的音頻源進(jìn)行釋放
    var url = audio.src;
    url && window.URL.revokeObjectURL(url);
    if (blob.size > 44) { //size為44的時(shí)候,數(shù)據(jù)部分為空
        audio.src = window.URL.createObjectURL(blob);
    }
    setTimeout(function() {
        updateSource(callback);
    }, config.cycle);
}

麥克風(fēng)的捕捉比起攝像頭真的是麻煩很多距芬,不能直接獲取音頻流涝开,要進(jìn)行數(shù)據(jù)壓縮(采樣頻率,聲道數(shù)框仔,樣本位數(shù))舀武,還要轉(zhuǎn)為wav編碼(其他音頻格式的編碼還沒去查過)。不過得到音頻的Blob文件之后就可以和“視頻傳輸"一樣通過Blob或string兩種數(shù)據(jù)類型收發(fā)离斩。

總結(jié)


  • 對(duì)于上面提到的“內(nèi)存泄漏”的情況還有待去研究银舱。
  • 音頻傳輸?shù)脑挘緛?lái)音頻文件就小捐腿,再加上壓縮處理纵朋,傳輸?shù)臄?shù)據(jù)很小,但是圖片傳輸就不一樣了茄袖,我的項(xiàng)目中傳輸?shù)膱D片用的100*100規(guī)格,我測(cè)試了下需要500k/s左右的上傳和下載速度嘁锯,在本機(jī)或者局域網(wǎng)中宪祥,再大點(diǎn)的尺寸或者用戶再多點(diǎn)都毫無(wú)壓力,但是部署到我的阿里云老爺機(jī)(1G內(nèi)存,單核CPU)上家乘,網(wǎng)速(上傳速度最快300k/s)和后臺(tái)處理性能兩方面原因?qū)е铝搜舆t有一兩分鐘(>_<|||)蝗羊,所以后續(xù)還要繼續(xù)研究圖片壓縮的問題。
  • 終于吃力地把語(yǔ)音視頻功能實(shí)現(xiàn)了仁锯,感覺還是得找機(jī)會(huì)研究下WebRTC耀找,享受下別人造好的輪子。

參考文章:


彩蛋


整個(gè)項(xiàng)目的代碼已經(jīng)上傳至github

很多朋友對(duì)于項(xiàng)目構(gòu)建比較疑惑业崖,這里列一下導(dǎo)入步驟:

  • Eclipse
1. 從別的項(xiàng)目下復(fù)制一個(gè).project文件, 修改其中的projectDescription-name(最好刪除一些無(wú)關(guān)的內(nèi)容)
2. Eclipse->import->General->Existing Projects into Workspace 導(dǎo)入項(xiàng)目
3. 右鍵項(xiàng)目Properties, Project Facets勾選 Dynamic Web Module 和 Java, 點(diǎn)擊確定
4. 右鍵項(xiàng)目Properties->Deployment Assembly,刪除WebContent這一行野芒,添加WebRoot目錄為根目錄
5. 進(jìn)入Java Build Path->Source, Default output floder 修改為 WebRoot/WEB-INF/classes
6. Java Build Path->Libary->Add Libary->Server Runtime, 選擇Tomcat
7. 右鍵tomcat的server,Add and Remove, Add這個(gè)項(xiàng)目
8. 雙擊tomcat的server進(jìn)入tomcat的配置頁(yè),點(diǎn)擊Modules標(biāo)簽頁(yè)->Edit, 將Path設(shè)為 /, [Ctrl + S]保存配置
9. 啟動(dòng)server,瀏覽器訪問127.0.0.1:8080
10. 部署項(xiàng)目的另一種方式:tomcat的配置頁(yè)->Add External Web Module,選擇WebRoot目錄,對(duì)于普通項(xiàng)目更推薦這種方式双炕。
  • IntelliJ IDEA
1. File->Open直接打開項(xiàng)目目錄
2. 根據(jù)右下角的自動(dòng)檢測(cè)狞悲,配置web.xml
3. Project Struture->Sources,選擇src目錄右鍵指定為Sources目錄
4. Project Struture->Modules->Use module compile output path, 編譯路徑修改為 當(dāng)前項(xiàng)目的WebRoot\WEB-INF\classes
5. Project Struture->Dependencies->Add->Libary->Tomcat(添加tomcat庫(kù)), Add->Libary->JARs or directories, 引入項(xiàng)目lib下的fastjson包
6. Project Struture->Artifacts->Add->Web Application:Exploded->From Modules->OK(不要使用Facets下提示的自動(dòng)創(chuàng)建Artifact)
7. 添加tomcat,修改tomcat配置(Classes添加tomcat\lib\websocket-api.jar, 當(dāng)然這個(gè)包可以按第5步單獨(dú)引入,不過要把Scope改為Provided),Deployment->Add->Artifact
8. 啟動(dòng)tomcat,瀏覽器訪問127.0.0.1:8080
9. 部署項(xiàng)目的另一種方式:修改tomcat配置,Deployment->Add->External Source,選擇WebRoot目錄,對(duì)于普通項(xiàng)目更推薦這種方式妇斤。

注意:tomcat的lib不能打包到WEB-INF/lib目錄下

項(xiàng)目中的幾個(gè)頁(yè)面:
  • main.html:聊天室入口頁(yè)面
  • cameraTest.html:測(cè)試攝像頭功能
  • microphoneTest*.html:測(cè)試麥克風(fēng)功能

本文中的代碼都是從項(xiàng)目中抽取出來(lái)的摇锋,并不完整丹拯,詳細(xì)用法還是看項(xiàng)目代碼吧。另外荸恕,攝像頭和麥克風(fēng)調(diào)用我分別封裝成Camera類和MicroPhone類乖酬,以后簡(jiǎn)單調(diào)用即可,調(diào)用方式參見測(cè)試頁(yè)面融求。

小小的吐槽


簡(jiǎn)書的風(fēng)格看著很舒服剑刑,寫文章排版也很省事,就是滾動(dòng)條樣式?jīng)]設(shè)置双肤,這么丑的滾動(dòng)條太不和諧了施掏。


轉(zhuǎn)載請(qǐng)注明出處:http://www.reibang.com/p/03a74d489f34

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市茅糜,隨后出現(xiàn)的幾起案子七芭,更是在濱河造成了極大的恐慌,老刑警劉巖蔑赘,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狸驳,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡缩赛,警方通過查閱死者的電腦和手機(jī)耙箍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)酥馍,“玉大人辩昆,你說我怎么就攤上這事≈继唬” “怎么了汁针?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)砚尽。 經(jīng)常有香客問我施无,道長(zhǎng),這世上最難降的妖魔是什么必孤? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任猾骡,我火速辦了婚禮,結(jié)果婚禮上敷搪,老公的妹妹穿的比我還像新娘兴想。我一直安慰自己,他們只是感情好购啄,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布襟企。 她就那樣靜靜地躺著,像睡著了一般狮含。 火紅的嫁衣襯著肌膚如雪顽悼。 梳的紋絲不亂的頭發(fā)上曼振,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音蔚龙,去河邊找鬼冰评。 笑死,一個(gè)胖子當(dāng)著我的面吹牛木羹,可吹牛的內(nèi)容都是我干的甲雅。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼坑填,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼抛人!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起脐瑰,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤妖枚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后苍在,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绝页,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年寂恬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了续誉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡初肉,死狀恐怖酷鸦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情朴译,我是刑警寧澤井佑,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站眠寿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏焦蘑。R本人自食惡果不足惜盯拱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望例嘱。 院中可真熱鬧狡逢,春花似錦、人聲如沸拼卵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)腋腮。三九已至雀彼,卻和暖如春壤蚜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背徊哑。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工袜刷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人莺丑。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓著蟹,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親梢莽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子萧豆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容