代碼主要實現了集成騰訊雲Web IM , 就是一個網頁聊天,可以發送文字,語音,視頻通話,發送錄音。
但是騰訊雲的IM不支持錄音發送,所以自己加上了這個功能。下面請看效果圖和代碼。有相關的需求可以聯繫作者。
後端PHP源碼下載地址:
效果圖:
。。。
html代碼:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="./js/vconsole.min.js"></script>
<link rel="stylesheet" href="./css/bootstrap-material-design.min.css">
<link rel="stylesheet" href="./css/common.css">
<link rel="stylesheet" href="./css/toastify.min.css">
<script>
// var vConsole = new VConsole();
</script>
<title>TRTC Web SDK Samples - 基礎音視頻通話</title>
<!-- 引入 音視頻通話 TRTC WEB SDK 腳本 -->
<meta name="keywords" content="醫療健康問答">
<meta name="description" content="醫療健康問答">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no">
<link rel="stylesheet" href="css/iconfont.css?v=231583415054">
<link rel="stylesheet" href="css/animate.css?v=231583415054">
<link rel="stylesheet" href="css/style.css?v=231583415054">
<link rel="stylesheet" href="css/swiper.min.css?v=231583415054">
<link rel="stylesheet" href="css/xhj.css?v=231583415054">
<link rel="stylesheet" href="index.css">
<script type="text/javascript" src="js/jquery.1.11.js?v=231583415054"></script>
<style id="__WXWORK_INNER_SCROLLBAR_CSS">::-webkit-scrollbar { width: 12px !important; height: 12px !important; }::-webkit-scrollbar-track:vertical { }::-webkit-scrollbar-thumb:vertical { background-color: rgba(136, 141, 152, 0.5) !important; border-radius: 10px !important; background-clip: content-box !important; border:2px solid transparent !important; } ::-webkit-scrollbar-track:horizontal { }::-webkit-scrollbar-thumb:horizontal { background-color: rgba(136, 141, 152, 0.5) !important; border-radius: 10px !important; background-clip: content-box !important; border:2px solid transparent !important; } ::-webkit-resizer { display: none !important; }</style>
<script type="text/javascript" src="js/swiper.min.js?v=231583415054"></script>
<script type="text/javascript" src="js/xhj.js?v=231583415054"></script>
<script type="text/javascript">
var tpl_src = "/Public/web";
</script>
<link rel="stylesheet" href="css/iconfont.css?v=231583415054">
<link rel="stylesheet" href="js/remodal.css">
<script src="js/remodal.js"></script>
<link rel="stylesheet" href="css/ask.css?v=231583415054">
<style type="text/css">
.send_block_item_voiceBg{
font-size: 15px;
border: 1px solid #009688;
height: 4rem;
line-height: 4rem;
text-align: center;
margin: 1.575rem;
width: 12.5rem;
border-radius: 10px;
background-color: #00A4FF;
color: white;
}
.send_block {
opacity: 1;
}
.send_block_item {
display: flex;
flex-direction: column;
}
.send_block_item img {
width: 2.5rem;
}
.cctbt {
margin-top: -30px;
}
</style>
</head>
<body style="overflow: hidden; background:#eee">
<div id="page" class="page">
<div v-if="openImgShow" @click="openImg('hide')" class="send_block_bg"></div>
<div v-if="openImgShow">
<img class="openImgUrl" :src="openimgsrc"></div>
<div v-if="audio_start_status" class="send_block_bg"></div>
<div v-if="audio_send_status" class="jietingzhong">
<div class="jietingTxt">上傳中...</div>
</div>
<div v-if="audio_start_status">
<img class="openImgUrl" src="./images/video.png"></div>
<div v-if="jietingzhong" class="jietingzhong">
<div class="send_block_bg">
</div>
<img class="jietingImg" src="./images/jieting.png">
<div class="jietingTxt">等待對方接聽 ......</div>
</div>
<div v-if="upStatus" class="jietingzhong">
<div class="send_block_bg">
</div>
<img class="jietingImg" src="./images/jieting.png">
<div class="jietingTxt">上傳中 ......</div>
</div>
<div class="video-grid" id="video_grid">
<div id="local_stream" class="video-placeholder">
<div id="local_video_info" class="video-info"></div>
</div>
</div>
<div v-if="cameras&&video_ing" class="video_btn">
<div class="video_btn_item" @click="qiehuan">切換攝像頭</div>
<div class="video_btn_item" @click="guaduan">掛斷</div>
</div>
<div class="header ct"> <a @click="outVideo" class="return"></a>
<h1>回覆提問者 always 問題{{txtMsg}}<span></span></h1>
</div>
<div class="chatbox">
<div class="prl20">
<ul style="overflow:auto;" class="chat-list" ref="chatContent">
<div v-for="(item,idx) in messageList">
<!-- 右邊 -->
<li class="r" v-if="item.from==im_id">
<p class="ct-time" v-if="item.time">{{item.my_time}}</p>
<div class="ct-item">
<div class="ct-head"><img :src="u_hrd_img"></div>
<!-- 文本 -->
<div class="ct-text" @click="clickTxt(item.payload.text)" v-if='item._elements[0].type=="TIMTextElem"'>{{item.payload.text}}
<p class="video_txt" v-if='item.payload.text=="發起視頻聊天邀請"'></p>
</div>
<!-- 圖片 -->
<div v-if='item._elements[0].type=="TIMImageElem"' class="ct-text im1">
<img @click="openImg" :src="item._elements[0].content.imageInfoArray[0].imageUrl" />
</div>
<!-- 文件 fileName fileUrl-->
<div v-if='item._elements[0].type=="TIMFileElem"'>
<div class="ct-text" @click="openFile">文件:{{item.payload.fileName}}</div>
<div v-if="fileShow" @click="openFile" class="send_block_bg"></div>
<iframe v-show="fileShow" class="filename" :src="item.payload.fileUrl" frameborder='1'></iframe>
</div>
</div>
</li>
<!-- 左邊 -->
<li v-else>
<p class="ct-time">{{item.my_time}}</p>
<div class="ct-item">
<div class="ct-head"><img :src="t_u_hrd_img"></div>
<!-- 文本 -->
<div v-if='item._elements[0].type=="TIMTextElem"' @click="clickTxt(item.payload.text)" class="ct-text">
{{item.payload.text}}
<p class="video_txt" v-if='item.payload.text=="發起視頻聊天邀請"&&idx==messageList.length-1'><span @click="sendVideo(2)">接受
</span><span @click="refuseVideo()" style="margin-left: 25px;"> 拒絕</span></p>
</div>
<!-- 圖片 -->
<div v-if='item._elements[0].type=="TIMImageElem"' class="ct-text im1">
<img @click="openImg" :src="item._elements[0].content.imageInfoArray[0].imageUrl">
</div>
<!-- 文件 fileName fileUrl-->
<div v-if='item._elements[0].type=="TIMFileElem"'>
<div class="ct-text" @click="openFile">文件:{{item.payload.fileName}}</div>
<div v-if="fileShow" @click="openFile" class="send_block_bg"></div>
<div v-if="fileShow">
<iframe class="filename" :src="item.payload.fileUrl" frameborder='1'></iframe></div>
</div>
</div>
</li>
</div>
</ul><!-- 輸入框 -->
<div style="height:52px;"></div>
</div>
</div>
<div v-if="send_block_show" @click="send_block_show=false,show_audio=false" class="send_block_bg">
</div>
<div v-if="send_block_show" class="send_block">
<div v-if="show_audio" @click="audio_start()" class=" send_block_item_voiceBg">點擊開始錄音</div>
<div v-if="show_audio" @click="audio_end()" class=" send_block_item_voiceBg">結束錄音併發送</div>
<div v-if="!show_audio" class="send_block_item" @click="sendImg">
<div> <img src="./images/51db2326125cf77a37f502f51794980.png"> </div>
<div class="cctbt">發送圖片</div>
</div>
<div v-if="!show_audio" class="send_block_item" @click="show_audio_click()">
<div>
<img src="images/video.png"></div>
<div class="cctbt">發送錄音</div>
</div>
<div v-if="!show_audio" class="send_block_item" @click="sendVideo(2)">
<div>
<img src="images/6e6ef015ae1c8c7903c968f4cf88856.png"></div>
<div class="cctbt">視頻聊天</div>
</div>
</div>
<div class="chat-input fxied" v-show="!send_block_show">
<div class="chat-input-top">
<div class="ct-top"> <span class="ctadd" @click="send_block_show_click"><img src="images/add.png" alt=""></span>
<!-- <div class="textarea kj_re_title" contenteditable="true"></div> -->
<!-- <textarea class="textarea kj_re_title" v-model="txtMsg" ></textarea> -->
<!-- <div class="textarea" contenteditable="true" v-html="txtMsg" @input="changeText"></div> -->
<div class="textarea" id="textarea" contenteditable="true" v-html="txtMsg" @blur="changeText"></div>
<button class="ctbtn btn_submit_re_title" v-on:click="sendTxt">發送</button>
</div>
</div>
</div>
<input type="file" accept="image/*" id="input_img" multiple style="display: none" @change="ImgFileChange">
<input type="file" id="input_file" style="display: none" @change="fileChange">
</div>
<script src="./js/jquery-3.2.1.min.js"></script>
<script src="./js/popper.js"></script>
<script src="./js/toastify.js"></script>
<script src="./js/bootstrap-material-design.min.js"></script>
<script>
$(document).ready(function() {
$('body').bootstrapMaterialDesign();
});
</script>
<!-- 引入 TRTC WEB SDK 腳本 -->
<script src="./js/trtc.js"></script>
<!-- Demo 相關腳本 -->
<script src="./js/lib-generate-test-usersig.min.js"></script>
<script src="./js/debug/GenerateTestUserSig.js"></script>
<script src="./js/utils.js"></script>
<script src="./js/rtc-client.js"></script>
<script src="./js/tim-js.js"></script>
<script src="./js/cos-js-sdk-v5.min.js"></script>
<script src="./index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<link rel="shortcut icon" type="image/png" href="https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/assets/icon.png">
<script src="https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/recorder.mp3.min.js"></script>
<script>
var rec = Recorder();
var app = new Vue({
el: '#page',
data: {
show_audio: false,
audio_src_autoplay:"",
audio_src:"",
t_u_hrd_img: GetRequest().t_u_hrd_img,
u_hrd_img: GetRequest().u_hrd_img,
jietingzhong: false,
messageList: [],
upStatus: false,
video_idx: 0,
cameras: null,
im_id: userID,
video_ing: false,
fileShow: false,
send_block_show: false,
openImgShow: '',
openimgsrc: '',
txtMsg: "",
audio_send_status:false,
audio_start_status:false,
audio_start_time: 0,
audio_ent_time: 0
},
watch: {
messageList() {
console.log("messageList change");
this.$nextTick(() => {
var h = $(".chat-list").innerHeight();
$(".chatbox").scrollTop(h);
})
}
},
methods: {
changeText(e){
console.log('------',e.target.innerHTML)
// this.txtMsg =e.target.innerHTML;
this.txtMsg=e.target.innerText
},
show_audio_click() {
this.show_audio = true;
},
clickTxt(txt) {
console.log('txt', txt)
if (txt.indexOf("錄音-") != -1) {
this.audio_play(txt);
}
},
audio_start() {
console.log('開始錄音')
this.audio_start_status=true;
this.audio_start_time = new Date().getTime();
rec.open(function() {
rec.start();
})
},
audio_play(txt) {
let aa = txt.split("錄音-")[1]
let url = "https://jayjing.wang/audio/upload/" + aa + ".mp3";
console.log('播放錄音',url)
let audio = new Audio()
audio.src = url;
audio.play();
},
audio_end(status) {
this.audio_start_status=false;
console.log('停止錄音', status)
this.show_audio = false;
this.audio_ent_time = new Date().getTime();
if (this.audio_ent_time - this.audio_start_time < 1000) {
Toast.notify('說話不能小於1秒 - ')
return
}
rec.stop(function(blob, duration) {
Toast.notify('上傳中···')
this.audio_send_status = true;
/***方式二:使用FormData用multipart/form-data表單上傳文件***/
var form = new FormData();
let nowTimeId = new Date().getTime()
form.append("file", blob, nowTimeId + ".mp3"); //和普通form表單並無二致,後端接收到upfile參數的文件,文件名爲recorder.mp3
form.append("id", nowTimeId);
//...其他表單參數
$.ajax({
url: 'https://jayjing.wang/audio/up.php',
type: "POST",
contentType: false //讓xhr自動處理Content-Type header,multipart/form-data需要生成隨機的boundary
,
processData: false //不要處理data,讓xhr自動處理
,
data: form,
success: function(v) {
this.audio_send_status = false;
console.log("上傳成功", v);
that.sendAudio(nowTimeId);
},
error: function(s) {
this.audio_send_status = false;
console.error("上傳失敗", s);
}
});
//-----↑↑↑以上纔是主要代碼↑↑↑-------
}, function(msg) {
console.log("錄音失敗:" + msg);
});
},
sendAudio(nowTimeId) {
console.log('發送錄音')
let txt = '錄音-' + nowTimeId;
this.sendTxt(txt);
},
qiehuan() {
if (this.cameras) {
// 切換攝像頭
let cameraId = this.cameras[this.video_idx].deviceId;
rtc.localStream_.switchDevice('video', cameraId).then(() => {
console.log('switch camera success', cameraId);
if (this.video_idx < this.cameras.length - 1) {
this.video_idx = this.video_idx + 1
} else {
this.video_idx = 0
}
});
}
},
guaduan() {
rtc.leave();
this.video_ing = false;
this.jietingzhong = false
rtc = null;
},
outVideo() {
location.reload();
},
refuseVideo() {
let txt = '未接聽';
this.sendTxt(txt);
},
sendVideo(e) {
console.log('11111111', e)
this.txtMsg = "";
if (rtc) return;
rtc = new RtcClient({
userId: userID,
roomId: roomID,
sdkAppId,
userSig: userSig
});
rtc.join().then(() => {
if (rtc.isJoined_) {
let txt = '發起視頻聊天邀請';
this.sendTxt(txt);
Toast.notify('等待接聽 - ')
this.jietingzhong = true;
this.video_ing = true;
// 遠端用戶進房
rtc.client_.on('peer-join', evt => {
this.jietingzhong = false;
})
// 遠端用戶退房
rtc.client_.on('peer-leave', evt => {
console.log('========================================')
this.video_ing = false;
this.send_block_show = false;
rtc.leave();
rtc = null
location.reload()
})
} else {
this.video_ing = false;
}
this.send_block_show = false;
TRTC.getCameras().then(devices => {
this.cameras = devices;
});
});
console.log('rtc------------------------------', rtc);
},
send_block_show_click() {
this.send_block_show = true;
},
// 發送消息
sendMsg(message) {
this.txtMsg = "";
this.send_block_show = false;
let promise = tim.sendMessage(message);
promise.then(function(msg) {
// 發送成功
console.log("發送成功", msg);
that.setMessageList(msg.data.message)
}).catch(function(imError) {
// 發送失敗
console.warn('sendMessage error:', imError);
});
},
openImg(e) {
console.log(e)
if (e == "hide") {
this.openImgShow = false;
} else {
this.openImgShow = true;
this.openimgsrc = e.target.currentSrc;
}
},
// 打開文件
openFile() {
console.log('打開文件')
this.fileShow = !this.fileShow;
// $('#see_file').click();
},
// 發送文件
sendFile() {
console.log('端發送圖片消息')
$('#input_file').click();
},
fileChange(e) {
this.upStatus = true;
// Web 端發送文件消息示例1 - 傳入 DOM 節點
// 1. 創建文件消息實例,接口返回的實例可以上屏
let message = tim.createFileMessage({
to: to_user,
conversationType: TIM.TYPES.CONV_C2C,
// 消息優先級,用於羣聊(v2.4.2起支持)。如果某個羣的消息超過了頻率限制,後臺會優先下發高優先級的消息,詳細請參考:https://cloud.tencent.com/document/product/269/3663#.E6.B6.88.E6.81.AF.E4.BC.98.E5.85.88.E7.BA.A7.E4.B8.8E.E9.A2.91.E7.8E.87.E6.8E.A7.E5.88.B6)
// 支持的枚舉值:TIM.TYPES.MSG_PRIORITY_HIGH, TIM.TYPES.MSG_PRIORITY_NORMAL(默認), TIM.TYPES.MSG_PRIORITY_LOW, TIM.TYPES.MSG_PRIORITY_LOWEST
// priority: TIM.TYPES.MSG_PRIORITY_NORMAL,
payload: {
file: document.getElementById('input_file'),
},
onProgress: (event) => {
if (event == 1) {
this.upStatus = false
}
console.log('file uploading:', event)
}
});
this.sendMsg(message)
},
// 發送圖片
ImgFileChange(e) {
this.upStatus = true;
console.log('eeeee', e, document.getElementById('input_img'))
// Web 端發送圖片消息
// 1. 創建消息實例,接口返回的實例可以上屏
let message = tim.createImageMessage({
to: to_user,
conversationType: TIM.TYPES.CONV_C2C,
payload: {
file: document.getElementById('input_img'),
},
onProgress: (event) => {
if (event == 1) {
this.upStatus = false
}
console.log('------------file uploading:', event)
}
});
this.sendMsg(message)
},
sendImg() {
console.log('端發送圖片消息')
$('#input_img').click();
},
// 獲取消息列表
getList() {
// 打開某個會話時,第一次拉取消息列表
tim.getMessageList({
conversationID: conversationID,
count: 15
}).then(function(imResponse) {
console.log('----------第一次拉取消息列表', imResponse)
let list = imResponse.data.messageList;
list.forEach((item, idx) => {
if (idx != 0) {
// console.log('item.time-list[idx-1].time', item.time - list[idx - 1].time)
if (item.time - list[idx - 1].time > 500) {
item.my_time = timestampToTime(item.time);
} else {
item.my_time = ""
}
}
if (idx == 0) {
item.my_time = timestampToTime(item.time);
}
})
that.messageList = imResponse.data.messageList; // 消息列表。
// 下拉查看更多消息
// let promise = tim.getMessageList({ conversationID: conversationID, nextReqMessageID, count: 15 });
// promise.then(function (imResponse) {
// const messageList = imResponse.data.messageList; // 消息列表。
// const nextReqMessageID = imResponse.data.nextReqMessageID; // 用於續拉,分頁續拉時需傳入該字段。
// const isCompleted = imResponse.data.isCompleted; // 表示是否已經拉完所有消息。
// });
});
},
setMessageList(obj) {
let idx = this.messageList.length;
obj.idx = idx;
that.messageList.push(obj)
console.log('添加聊天列表數據', that.messageList)
},
sendTxt(txt = "") {
console.log('+++++++++',$("#textarea").val())
console.log('this.txtMsg:',this.txtMsg)
let message = tim.createTextMessage({
to: to_user,
conversationType: TIM.TYPES.CONV_C2C,
payload: {
text: this.txtMsg ? this.txtMsg : txt
}
});
this.sendMsg(message)
},
im_on() {
// 監聽事件,例如:
tim.on(TIM.EVENT.SDK_READY, function(event) {
console.log('-------收到離線消息和會話列表同步完畢通知')
that.getList()
});
tim.on(TIM.EVENT.MESSAGE_RECEIVED, (event) => {
console.log('-------收到推送的單聊、羣聊、羣提示、羣系統通知的新消息', event)
if (event.data[0].payload.text == "未接聽") {
rtc.leave();
this.video_ing = false;
this.jietingzhong = false
}
that.setMessageList(event.data[0])
});
tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, function(event) {
console.log('-------收到會話列表更新通知', event)
});
},
},
mounted() {
that = this;
let host = window.location.protocol + "//" + window.location.host;
console.log('------------------', host)
this.im_on();
}
})
</script>
</body>
</html>
JS代碼:
var that;
var sdkAppId = 1400325026;
var options = {
SDKAppID: sdkAppId // 接入時需要將0替換爲您的即時通信 IM 應用的 SDKAppID
};
let rtc = null;
var to_user = "user_1959"
// var to_user = "user_24384277"
// var userID = "user_25"
// var userSig = "eJyrVgrxCdYrSy1SslIy0jNQ0gHzM1NS80oy0zLBwqXFqUXxRqZQqeKU7MSCgswUJStDEwMDYyNTAyMziExJZm4qUNTUwtjCwMjIFCqaWlGQWQQUNzMwsTAwgJqRmQ4016ugICsoQDsoOcM0NSqroNIn2Dkn19AyKMMzudDfwLQg2zcxPF8-ysXX09FWqRYAKGQxhQ__"
var userID = "user_3n"
var userSig ="eJwtzFELgjAYheH-suuwr*lsCF2uEIsRSVA3EbnGVzjmXBlG-72hXp7nwPsl5fYQvZUjGaERkNmwsVLG4x0HfrXKXWIzXW31vFqLFckWCUBMGdB0fDzWKijjwQESNqr6WHTBU0g4wNRAHbq84U1ZHHPXz8W6r0W*lC63Rac7quX*ITdn9Cdxk8buVuT3Bz4RMjE_";
// var userID = "user_86943595"
// var userSig = "eJwtzM0KgkAUBeB3mW2htxnvOAotWvRHEkWptYrQUS6SmZok0bsn6vJ853C*7OydjEaXzGXcADbtM8U6rymhnt*VLm9KOpZAB8dBFWf3oqCYuTMLQHAELoempofuFJVQQiDgoPpTUNm5BEsBjB*Udu-HZbuixheL4BLxDUXb9uXBWibhzs78aBJcD2ZuYkp7*zlnvz8LnzI5"
var roomID = "889974741"
var conversationID = "C2C" + to_user;
var tim = TIM.create(options);
var pageImResponse;
tim.setLogLevel(0); // 普通級別,日誌量較多,接入時建議使用
tim.registerPlugin({
'cos-js-sdk': COS
});
let promise = tim.login({
userID,
userSig
}).then(function(imResponse) {
pageImResponse = imResponse;
console.log('登錄成功', imResponse.data); // 登錄成功
}).catch(function(imError) {
console.warn('login error:', imError); // 登錄失敗的相關信息
});
// let trc_Data = {
// mode: 'videoCall',
// sdkAppId,
// userId: userID,
// userSig
// }
// console.log('----------------------trc_Data ', trc_Data);
//時間戳轉日期格式
function timestampToTime(timestamp) {
var date = new Date(timestamp * 1000); //時間戳爲10位需*1000,時間戳爲13位的話不需乘1000
Y = date.getFullYear() + '-';
M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
D = date.getDate() + ' ';
h = date.getHours() + ':';
m = date.getMinutes() + ':';
s = date.getSeconds();
return Y + M + D + h + m + s;
}
//獲取url中參數
function GetRequest()
{
var url = location.search; //獲取url中"?"符後的字串
var theRequest = new Object();
if (url.indexOf("?") != -1)
{
var str = url.substr(1);
strs = str.split("&");
for (var i = 0; i < strs.length; i++)
{
theRequest[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1]);
}
}
return theRequest;
}
引用的 audio.js 代碼:
var HZRecorder = function (stream, config) {
config = config || {};
config.sampleBits = config.sampleBits || 8; //採樣數位 8, 16
config.sampleRate = config.sampleRate || (44100 / 6); //採樣率(1/6 44100)
//創建一個音頻環境對象
audioContext = window.AudioContext || window.webkitAudioContext;
var context = new audioContext();
//將聲音輸入這個對像
var audioInput = context.createMediaStreamSource(stream);
//設置音量節點
var volume = context.createGain();
audioInput.connect(volume);
//創建緩存,用來緩存聲音
var bufferSize = 4096;
// 創建聲音的緩存節點,createScriptProcessor方法的
// 第二個和第三個參數指的是輸入和輸出都是雙聲道。
var recorder = context.createScriptProcessor(bufferSize, 2, 2);
var audioData = {
size: 0 //錄音文件長度
, buffer: [] //錄音緩存
, inputSampleRate: context.sampleRate //輸入採樣率
, inputSampleBits: 16 //輸入採樣數位 8, 16
, outputSampleRate: config.sampleRate //輸出採樣率
, oututSampleBits: config.sampleBits //輸出採樣數位 8, 16
, input: function (data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
}
, compress: function () { //合併壓縮
//合併
var data = new Float32Array(this.size);
var offset = 0;
for (var i = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
//壓縮
var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
var length = data.length / compression;
var result = new Float32Array(length);
var index = 0, j = 0;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
return result;
}
, encodeWAV: function () {
var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
var bytes = this.compress();
var dataLength = bytes.length * (sampleBits / 8);
var buffer = new ArrayBuffer(44 + dataLength);
var data = new DataView(buffer);
var channelCount = 1;//單聲道
var offset = 0;
var writeString = function (str) {
for (var i = 0; i < str.length; i++) {
data.setUint8(offset + i, str.charCodeAt(i));
}
};
// 資源交換文件標識符
writeString('RIFF'); offset += 4;
// 下個地址開始到文件尾總字節數,即文件大小-8
data.setUint32(offset, 36 + dataLength, true); offset += 4;
// WAV文件標誌
writeString('WAVE'); offset += 4;
// 波形格式標誌
writeString('fmt '); offset += 4;
// 過濾字節,一般爲 0x10 = 16
data.setUint32(offset, 16, true); offset += 4;
// 格式類別 (PCM形式採樣數據)
data.setUint16(offset, 1, true); offset += 2;
// 通道數
data.setUint16(offset, channelCount, true); offset += 2;
// 採樣率,每秒樣本數,表示每個通道的播放速度
data.setUint32(offset, sampleRate, true); offset += 4;
// 波形數據傳輸率 (每秒平均字節數) 單聲道×每秒數據位數×每樣本數據位/8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
// 快數據調整數 採樣一次佔用字節數 單聲道×每樣本的數據位數/8
data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
// 每樣本數據位數
data.setUint16(offset, sampleBits, true); offset += 2;
// 數據標識符
writeString('data'); offset += 4;
// 採樣數據總數,即數據總大小-44
data.setUint32(offset, dataLength, true); offset += 4;
// 寫入採樣數據
if (sampleBits === 8) {
for (var i = 0; i < bytes.length; i++, offset++) {
var s = Math.max(-1, Math.min(1, bytes[i]));
var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
val = parseInt(255 / (65535 / (val + 32768)));
data.setInt8(offset, val, true);
}
} else {
for (var i = 0; i < bytes.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, bytes[i]));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
return new Blob([data], { type: 'audio/wav' });
}
};
//開始錄音
this.start = function () {
audioInput.connect(recorder);
recorder.connect(context.destination);
};
//停止
this.stop = function () {
recorder.disconnect();
};
// 結束
this.end = function() {
context.close();
};
// 繼續
this.again = function() {
recorder.connect(context.destination);
};
//獲取音頻文件
this.getBlob = function () {
this.stop();
return audioData.encodeWAV();
};
//回放
this.play = function (audio) {
audio.src = window.URL.createObjectURL(this.getBlob());
};
//上傳
this.upload = function (url, callback) {
var fd = new FormData();
fd.append('audioData', this.getBlob());
var xhr = new XMLHttpRequest();
if (callback) {
xhr.upload.addEventListener('progress', function (e) {
callback('uploading', e);
}, false);
xhr.addEventListener('load', function (e) {
callback('ok', e);
}, false);
xhr.addEventListener('error', function (e) {
callback('error', e);
}, false);
xhr.addEventListener('abort', function (e) {
callback('cancel', e);
}, false);
}
xhr.open('POST', url);
xhr.send(fd);
};
//音頻採集
recorder.onaudioprocess = function (e) {
audioData.input(e.inputBuffer.getChannelData(0));
//record(e.inputBuffer.getChannelData(0));
};
};
//拋出異常
HZRecorder.throwError = function (message) {
throw new function () { this.toString = function () { return message; };};
};
//是否支持錄音
HZRecorder.canRecording = (navigator.getUserMedia != null);
//獲取錄音機
HZRecorder.get = function (callback, config) {
if (callback) {
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(function(stream) {
let rec = new HZRecorder(stream, config);
callback(rec);
})
.catch(function(error) {
HZRecorder.throwError('無法錄音,請檢查設備狀態');
});
}
};
window.HZRecorder = HZRecorder;