前言
在上一篇文章中已經(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))
說說這里遇到的坑吧
在第一種方式中,先是獲取圖片的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])
由于第一種方式發(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;切換到第一種方式
正常兩個(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)條太不和諧了施掏。