<!DOCTYPE html>
|
<html>
|
<head>
|
<meta charset="UTF-8"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<title>WebRTC webcam</title>
|
<style>
|
html {
|
margin: 0;
|
padding: 0;
|
width: 100vw;
|
height: 100vh;
|
overflow: hidden;
|
font-family: '宋体';
|
}
|
body {
|
width: 100vw;
|
height: 100vh;
|
margin: 0;
|
padding: 0;
|
position: relative;
|
overflow: hidden;
|
background-color: #fff;
|
}
|
div {
|
box-sizing: border-box;
|
}
|
button {
|
padding: 8px 16px;
|
}
|
video {
|
width: 100%;
|
}
|
#operating_box {
|
position: absolute;
|
left: 200%;
|
transform: translateX(-50%);
|
top: 0;
|
padding: 20px;
|
border: 1px solid #000;
|
background-color: #eee;
|
border-radius: 10px;
|
width: 300px;
|
height: 300px;
|
z-index: 10;
|
}
|
|
#media {
|
position: absolute;
|
width: 100vw;
|
height: 100vh;
|
top: 0;
|
left: 0;
|
z-index: 3;
|
}
|
#media .center {
|
width: 100%;
|
height: 100%;
|
position: relative;
|
}
|
#media .center .background {
|
position: absolute;
|
left: 50%;
|
transform: translateX(-50%);
|
width:100vw;
|
height: 100vh;
|
z-index: 1;
|
}
|
#media .center .center_title {
|
position: absolute;
|
left: 0;
|
top: 30px;
|
padding: 0 22px;
|
width: 100%;
|
height: 128px;
|
z-index: 9;
|
display: flex;
|
}
|
#media .center .center_title .center_title_left {
|
width: 50%;
|
height: 128px;
|
line-height: 128px;
|
font-size: 50px;
|
font-weight: 700;
|
display: flex;
|
}
|
#media .center .center_title .center_title_right {
|
width: 50%;
|
height: 128px;
|
font-weight: 700;
|
padding-top: 18px;
|
padding-right: 20px;
|
text-align: right;
|
}
|
#media .center .center_title .center_title_left img {
|
width: 128px;
|
height: 128px;
|
}
|
|
#media .center .center_operating {
|
position: absolute;
|
width: 88px;
|
top: 50%;
|
right: 20px;
|
z-index: 9;
|
transform: translateY(-50%);
|
}
|
#media .center .center_operating .btn_item {
|
width: 88px;
|
height: 120px;
|
margin-bottom: 20px;
|
}
|
#media .center .center_operating .btn_item:hover {
|
cursor: pointer;
|
}
|
#media .center .center_operating .btn_item .btn_item_img {
|
width: 88px;
|
height: 88px;
|
background: #BCC7BF;
|
padding: 16px;
|
border-radius: 44px;
|
}
|
#media .center .center_operating .btn_item .btn_item_text {
|
width: 100%;
|
height: 32px;
|
line-height: 32px;
|
font-size: 20px;
|
text-align: center;
|
color: #000;
|
font-weight: 600;
|
}
|
#media .center .center_operating .btn_item .btn_item_img img {
|
width: 100%;
|
height: 100%;
|
}
|
|
#media .center .center_toload {
|
position: absolute;
|
left: 0;
|
bottom: 20%;
|
width: 100%;
|
height: 198px;
|
text-align: center;
|
font-size: 50px;
|
font-weight: 600;
|
z-index: 9;
|
}
|
#media .center .center_toload img {
|
width: 128px;
|
height: 128px;
|
margin-bottom: 20px;
|
}
|
.rotation {
|
animation: rotate 5s linear infinite;
|
}
|
@keyframes rotate {
|
from {
|
transform: rotate(0deg);
|
}
|
to {
|
transform: rotate(360deg);
|
}
|
}
|
#media .center .video_box {
|
position: absolute;
|
left: 0;
|
top: 0;
|
width: 100%;
|
height: 100%;
|
z-index: 8;
|
}
|
|
#media .center .center_list {
|
position: absolute;
|
top: 20%;
|
left: 50px;
|
width: 53%;
|
height: 60%;
|
z-index: 10;
|
overflow-y: auto;
|
overflow-x: hidden;
|
}
|
#media .center .center_list::-webkit-scrollbar{
|
width: 0px !important;
|
}
|
#media .center .center_list::-webkit-scrollbar-thumb {
|
background: #e5e6eb;
|
}
|
#media .center .center_list::-webkit-scrollbar-thumb:hover {
|
background: #c9cdd4;
|
}
|
#media .center .center_list .center_list_item {
|
width: 100%;
|
min-height: 88px;
|
padding-left: 108px;
|
padding-top: 11px;
|
padding-bottom: 11px;
|
position: relative;
|
margin-bottom: 40px;
|
}
|
#media .center .center_list .center_list_item .center_list_img {
|
position: absolute;
|
left: 0;
|
top: 0;
|
width: 88px;
|
height: 88px;
|
border-radius: 44px;
|
padding: 16px;
|
background: #034A50;
|
}
|
#media .center .center_list .center_list_item .center_list_img img {
|
width: 100%;
|
height: 100%;
|
}
|
#media .center .center_list .center_list_item .center_list_text {
|
width: auto;
|
display: inline-block;
|
min-height: 66px;
|
background: rgba(209, 227, 255, 0.5);
|
border-radius: 20px;
|
font-size: 25px;
|
line-height: 33px;
|
font-weight: 500;
|
padding: 18px;
|
}
|
</style>
|
</head>
|
<body>
|
<div id="operating_box">
|
<!-- <button id="PullFlowStart" onclick="PullFlowStart()">Start</button> -->
|
<button id="PullFlowStop" onclick="PullFlowStop">Stop</button>
|
<form class="form-inline" id="echo-form">
|
<div class="form-group">
|
<textarea cols="2" rows="3" style="width:260px;height:50px;" class="form-control" id="message">test</textarea>
|
</div>
|
<button type="submit" class="btn btn-default">Send</button>
|
</form>
|
</div>
|
|
<div id="media">
|
<div class="center">
|
<img class="background" src="./image/bg.png" alt="">
|
<div class="center_title">
|
<div class="center_title_left">
|
<img src="./image/logo.png" alt="">
|
<div>妙手AI Studio</div>
|
</div>
|
<div class="center_title_right" id="Date_box"></div>
|
</div>
|
<div class="center_operating" style="display: none;">
|
<div class="btn_item" id="start_recording">
|
<div class="btn_item_img"><img src="./image/ly.png" alt=""></div>
|
<div class="btn_item_text">提问</div>
|
</div>
|
|
<div class="btn_item" id="end_recording">
|
<div class="btn_item_img"><img class="rotation" src="./image/jz.png" alt=""></div>
|
<div class="btn_item_text">提问</div>
|
</div>
|
|
<div class="btn_item" id="clear_interrupt">
|
<div class="btn_item_img"><img src="./image/zt.png" alt=""></div>
|
<div class="btn_item_text">打断</div>
|
</div>
|
<div class="btn_item" id="clear_record">
|
<div class="btn_item_img"><img src="./image/qp.png" alt=""></div>
|
<div class="btn_item_text">清屏</div>
|
</div>
|
</div>
|
<div class="center_list" id="message-list"></div>
|
<div class="center_toload" id="center_toload">
|
<img src="./image/jz.png" class="rotation" alt="">
|
<div>互动问答数字人系统</div>
|
</div>
|
<div class="video_box">
|
<audio id="audio" autoplay></audio>
|
<video id="video" style="width:100vw;height: 100vh;" autoplay playsinline></video>
|
</div>
|
</div>
|
</div>
|
<script type="text/javascript" src="http://cdn.sockjs.org/sockjs-0.3.4.js"></script>
|
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.1.min.js"></script>
|
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.9.5/dist/ffmpeg.min.js"></script>
|
|
<script>
|
// 获取时间
|
let now = new Date();
|
const Date_box = document.getElementById('Date_box');
|
let getDate = function() {
|
now = new Date();
|
|
Date_box.innerHTML = `
|
<div style="font-size: 30px;">
|
${now.getFullYear()}年${(now.getMonth() + 1).toString().padStart(2, '0')}月${now.getDate().toString().padStart(2, '0')}日
|
</div>
|
<div style="font-size: 50px;">
|
${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}
|
</div>`;
|
}
|
getDate();
|
let timer = setInterval(getDate, 3000);
|
window.addEventListener('beforeunload', function(event) {
|
clearInterval(timer);
|
});
|
|
|
// 录制音频
|
let start_recording = document.getElementById('start_recording');
|
let end_recording = document.getElementById('end_recording');
|
end_recording.style.display = 'none';
|
|
let mediaRecorder;
|
let audioChunks = []; // 存储录音数据的数组
|
|
|
let volumeIndex = 0
|
|
let audioContext;
|
let microphone;
|
let analyser;
|
let javascriptNode;
|
let audioStream;
|
let rafID;
|
// 获取麦克风权限并开始录音
|
async function startRecording() {
|
volumeIndex = 0
|
|
try {
|
start_recording.style.display = 'none';
|
end_recording.style.display = 'block';
|
|
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
|
|
|
// 监听录音音量 小于阈值大约5秒后自动停止录音
|
audioStream = stream;
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
analyser = audioContext.createAnalyser();
|
microphone = audioContext.createMediaStreamSource(stream);
|
javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
|
|
analyser.smoothingTimeConstant = 0.3;
|
analyser.fftSize = 1024;
|
|
microphone.connect(analyser);
|
analyser.connect(javascriptNode);
|
javascriptNode.connect(audioContext.destination);
|
// microphone.connect(audioContext.destination); // 直接连接麦克风到扬声器
|
|
javascriptNode.onaudioprocess = () => {
|
let array = new Uint8Array(analyser.frequencyBinCount);
|
analyser.getByteTimeDomainData(array);
|
let values = 0;
|
for (let i = 0; i < array.length; i++) {
|
let value = (array[i] - 128) / 128;
|
values += value * value;
|
}
|
let average = Math.sqrt(values / array.length);
|
|
if (average * 1000 < 10) {
|
volumeIndex++
|
} else {
|
// volumeIndex--
|
}
|
if (volumeIndex > 40) {
|
stopRecording()
|
}
|
// console.log('实时音量 ==>',average * 1000);
|
};
|
|
updateVolume();
|
// ------------------------------------------------------
|
|
mediaRecorder = new MediaRecorder(stream);
|
mediaRecorder.ondataavailable = event => {
|
audioChunks.push(event.data); // 收集录音数据
|
};
|
mediaRecorder.start(); // 开始录音
|
let div = document.createElement('div');
|
div.className = 'center_list_item listen';
|
div.style.display = 'none'
|
div.innerHTML = `<div class="center_list_img"><img src="./image/yh.png" alt=""></div><div class="center_list_text">聆听中...</div>`;
|
document.getElementById('message-list').appendChild(div);
|
document.getElementsByClassName('listen')[document.getElementsByClassName('listen').length - 1].style.display = 'block';
|
setTimeout(() => {
|
scrollToBottom();
|
},400)
|
} catch (e) {
|
console.error('获取麦克风权限失败:', e);
|
}
|
}
|
// 停止录音并获取音频文件
|
function stopRecording() {
|
start_recording.style.display = 'block';
|
end_recording.style.display = 'none';
|
stop();
|
// InsertMessage('AI Studio','请稍等,正在为您查询',false)
|
volumeIndex = 0
|
mediaRecorder.stop(); // 结束录音
|
// 当录音完成时,获取音频文件
|
mediaRecorder.onstop = async e => {
|
let blob = new Blob(audioChunks, {type: 'audio/webm'});
|
AutomaticSpeechRecognition(blob);
|
|
// const reader = new FileReader();
|
// reader.addEventListener('load', function() {
|
// if (reader.result) {
|
// console.log('获取音频文件 base64 ==>',reader.result);
|
// }
|
// });
|
// reader.readAsDataURL(blob)
|
audioChunks = [];
|
};
|
}
|
function updateVolume() {
|
rafID = requestAnimationFrame(updateVolume);
|
}
|
function stop() {
|
if (microphone) {
|
microphone.disconnect();
|
}
|
if (analyser) {
|
analyser.disconnect();
|
}
|
if (javascriptNode) {
|
javascriptNode.disconnect();
|
}
|
if (audioContext) {
|
audioContext.close();
|
}
|
if (audioStream) {
|
audioStream.getTracks().forEach(track => track.stop());
|
}
|
if (rafID) {
|
cancelAnimationFrame(rafID);
|
}
|
}
|
|
// 按钮点击事件绑定
|
start_recording.addEventListener('click', startRecording);
|
end_recording.addEventListener('click', stopRecording);
|
|
// 语音识别
|
function AutomaticSpeechRecognition(file) {
|
// console.log(file);
|
|
if (!file) {
|
return false
|
}
|
// 创建一个FormData对象
|
const formData = new FormData();
|
|
// 添加表单数据
|
formData.append('files', file);
|
formData.append('keys', 'string');
|
formData.append('lang','auto');
|
|
// 发送POST请求
|
fetch('http://192.168.1.109:50000/api/v1/asr', {
|
method: 'POST',
|
headers: {
|
// 'Content-Type': 'multipart/form-data',
|
// // 'Connection': 'keep-alive',
|
// 'Accept': 'application/json'
|
},
|
body: formData
|
})
|
.then(response => response.json())
|
.then(data => {
|
if (data.result.length > 0 && data.result[0].clean_text) {
|
document.getElementsByClassName('listen')[document.getElementsByClassName('listen').length - 1].style.display = 'none'
|
InsertMessage('user',data.result[0].clean_text)
|
COZE_CN_Quiz(data.result[0].clean_text);
|
}
|
})
|
.catch(error => console.error(error));
|
}
|
|
// 向coze对应的bot发送接口请求
|
|
let message = '';
|
|
function COZE_CN_Quiz (v) {
|
let div = document.createElement('div');
|
div.className = 'center_list_item'
|
div.innerHTML = `<div class="center_list_img"><img src="./image/logo.png" alt=""></div><div class="center_list_text Studio">...</div>`;
|
list.appendChild(div);
|
message = '';
|
// 发送 POST 请求
|
fetch('https://api.coze.cn/v3/chat', {
|
method: 'POST', // 或者 'GET'
|
headers: {
|
'Authorization': 'Bearer pat_1rlKQFpFtutedMcs04NEWwGHyH8WprgUiAt5PwuWfH1gRBYTzwnTjtdpT83LQ4kg',
|
'Content-Type': 'application/json',
|
'Connection': 'keep-alive',
|
'Accept': '*/*'
|
},
|
body: JSON.stringify({
|
bot_id: "7436945270199746560",
|
user_id: "1",
|
stream: true,
|
auto_save_history:true,
|
additional_messages:[
|
{
|
role:"user",
|
content: v,
|
content_type:"text"
|
}
|
]
|
}), // 转换数据为 JSON 格式
|
cache: 'no-cache', //
|
credentials: 'same-origin', //
|
redirect: 'follow', //
|
referrerPolicy: 'no-referrer', //
|
}).then(response => {
|
if (response.body) {
|
let reader = response.body.getReader();
|
reader.read().then(function processStream({ done, value }) {
|
if (done) {
|
ReceivedText(false)
|
return;
|
}
|
let str = new TextDecoder('utf-8').decode(value);
|
if (str.indexOf('event:conversation.message.delta') > -1) {
|
try {
|
let str_ = str.replace(/event:conversation.message.delta/g, '');
|
|
let arr = []
|
str_.split('\n').forEach(item => {
|
if (item) {
|
let s = item.split('data:')[1]
|
if (s) {
|
arr.push(JSON.parse(s))
|
}
|
}
|
})
|
|
arr.forEach(item => {
|
if (item.type === 'answer') {
|
ReceivedText(item)
|
}
|
})
|
} catch (error) {
|
console.log('文本编译失败! ==>',error);
|
}
|
}
|
return reader.read().then(processStream); // 递归读取
|
});
|
}
|
})
|
.catch(e => {
|
alert('问题解析失败!')
|
console.error(e)
|
});
|
}
|
let messageLength = '';
|
let messageState = false
|
function ReceivedText (v) {
|
if (v === false) {
|
InsertMessage('AI Studio',messageLength,false);
|
messageLength = '';
|
messageState = false
|
} else {
|
message += v.content.replace(/\s/g, "")
|
if (hasPunctuation(v.content)) {
|
messageLength += v.content.replace(/\s/g, "");
|
InsertMessage('AI Studio',messageLength,false)
|
messageLength = ''
|
setTimeout(() => {
|
messageState = true;
|
document.getElementsByClassName('Studio')[document.getElementsByClassName('Studio').length - 1].innerText = message;
|
},2000)
|
} else {
|
messageLength += v.content.replace(/\s/g, "")
|
if (messageLength.length >= 20 && hasPunctuation(v.content)) {
|
InsertMessage('AI Studio',messageLength,false)
|
messageLength = ''
|
}
|
if (messageState) {
|
document.getElementsByClassName('Studio')[document.getElementsByClassName('Studio').length - 1].innerText = message;
|
}
|
}
|
|
}
|
}
|
window.COZE_CN_Quiz = COZE_CN_Quiz;
|
function hasPunctuation(str) {
|
return /[,。,:;!?]/.test(str);
|
}
|
|
|
let InsertMessageList = [];
|
const list = document.getElementById('message-list');
|
function InsertMessage (type,v,state) {
|
if (type === 'user') {
|
InsertMessageList.push({
|
type: 'user',
|
text: v
|
})
|
let div = document.createElement('div');
|
div.className = 'center_list_item'
|
div.innerHTML = `<div class="center_list_img"><img src="./image/yh.png" alt=""></div><div class="center_list_text">${v}</div>`;
|
list.appendChild(div);
|
|
} else {
|
if (!v) {
|
return false
|
}
|
// 发送消息
|
if (state) {
|
// 打断
|
fetch('/human', {
|
body: JSON.stringify({
|
text: v,
|
type: 'echo',
|
interrupt: true,
|
sessionid: sessionid,
|
}),
|
headers: {
|
'Content-Type': 'application/json'
|
},
|
method: 'POST'
|
});
|
} else {
|
fetch('/human', {
|
body: JSON.stringify({
|
text: v,
|
type: 'echo',
|
interrupt: false,
|
sessionid: sessionid,
|
}),
|
headers: {
|
'Content-Type': 'application/json'
|
},
|
method: 'POST'
|
});
|
}
|
};
|
scrollToBottom()
|
};
|
// 清屏
|
let clearRecord = document.getElementById('clear_record');
|
let messageList = document.getElementById('message-list');
|
clearRecord.addEventListener('click', function () {
|
messageList.innerHTML = ''
|
});
|
// 列表滚动条
|
function scrollToBottom() {
|
messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight;
|
};
|
// 打断
|
let clearInterrupt = document.getElementById('clear_interrupt');
|
clearInterrupt.addEventListener('click', function () {
|
InsertMessage('AI Studio','播报停止',true);
|
});
|
</script>
|
|
<script>
|
// 视频拉流
|
var pc = null;
|
let sessionid = 0
|
function negotiate() {
|
pc.addTransceiver('video', { direction: 'recvonly' });
|
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
return pc.createOffer().then((offer) => {
|
return pc.setLocalDescription(offer);
|
}).then(() => {
|
// wait for ICE gathering to complete
|
return new Promise((resolve) => {
|
if (pc.iceGatheringState === 'complete') {
|
resolve();
|
} else {
|
const checkState = () => {
|
if (pc.iceGatheringState === 'complete') {
|
pc.removeEventListener('icegatheringstatechange', checkState);
|
resolve();
|
}
|
};
|
pc.addEventListener('icegatheringstatechange', checkState);
|
}
|
});
|
}).then(() => {
|
var offer = pc.localDescription;
|
return fetch('/offer', {
|
body: JSON.stringify({
|
sdp: offer.sdp,
|
type: offer.type,
|
}),
|
headers: {
|
'Content-Type': 'application/json'
|
},
|
method: 'POST'
|
});
|
}).then((response) => {
|
return response.json();
|
}).then((answer) => {
|
sessionid = answer.sessionid
|
return pc.setRemoteDescription(answer);
|
}).catch((e) => {
|
// alert(e);
|
console.log('视频拉流失败 ==>',e);
|
setTimeout(() => {
|
PullFlowStart()
|
},3000)
|
});
|
}
|
const videos = document.getElementById('video');
|
const audios = document.getElementById('audio');
|
|
// 开始视频拉流
|
function PullFlowStart(ifUseSTUNserver) {
|
var config = {
|
sdpSemantics: 'unified-plan'
|
};
|
if (ifUseSTUNserver) {
|
// 是否开启 UseSTUNserver
|
config.iceServers = [{ urls: ['stun:stun.l.google.com:19302'] }];
|
}
|
pc = new RTCPeerConnection(config);
|
// connect audio / video
|
pc.addEventListener('track', (evt) => {
|
if (evt.track.kind == 'video') {
|
videos.srcObject = evt.streams[0];
|
} else {
|
audios.srcObject = evt.streams[0];
|
}
|
});
|
document.getElementById('center_toload').style.display = 'none';
|
document.getElementsByClassName('center_operating')[0].style.display = 'block';
|
negotiate();
|
|
}
|
|
// 停止视频拉流
|
function PullFlowStop() {
|
document.getElementById('center_toload').style.display = 'block';
|
setTimeout(() => {
|
if (pc && pc.close) {
|
pc.close();
|
}
|
}, 500);
|
}
|
setTimeout(() => {
|
PullFlowStart()
|
},3000)
|
</script>
|
<script type="text/javascript" charset="utf-8">
|
console.log(
|
`谷歌浏览器实现允许本地跨域访问:
|
创建一个新的文件夹:在本地磁盘上创建一个新的文件夹,例如命名为MyChromeDevUserData(文件夹名可以自定义)。
|
修改浏览器快捷方式:
|
找到谷歌浏览器的快捷方式,右键点击属性。
|
在“快捷方式”选项卡下,找到“目标”字段。
|
在目标字段的末尾添加以下参数以禁用同源策略并指定用户数据目录:空格加 --disable-web-security --user-data-dir=路径\MyChromeDevUserData。
|
其中路径应替换为你在第1步中创建的文件夹的实际路径。
|
启动浏览器:保存快捷方式的修改后,重新启动浏览器。你应该会看到一个提示,表明你正在使用不受支持的命令行标记--disable-web-security,这表示浏览器已经配置为允许跨域访问。
|
通过上述步骤,你可以在本地开发环境中绕过浏览器的同源策略限制,从而允许跨域访问`
|
);
|
|
</script>
|
</body>
|
</html>
|