[+] 终端

This commit is contained in:
acgist
2022-11-13 00:58:07 +08:00
parent 605e0fbbe7
commit 4ce21553c3
48 changed files with 1623 additions and 192 deletions

View File

@@ -17,8 +17,8 @@
<description>服务:启动服务</description>
<properties>
<system.maven.basedir>${project.parent.basedir}</system.maven.basedir>
<system.maven.skip.assembly>false</system.maven.skip.assembly>
<taoyao.maven.basedir>${project.parent.basedir}</taoyao.maven.basedir>
<taoyao.maven.skip.assembly>false</taoyao.maven.skip.assembly>
</properties>
<dependencies>
@@ -34,18 +34,6 @@
<groupId>com.acgist</groupId>
<artifactId>taoyao-meeting</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-sfu</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-mcu</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-mesh</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>

View File

@@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.acgist.taoyao.boot.config.MediaProperties;
import com.acgist.taoyao.boot.config.WebrtcProperties;
import io.swagger.v3.oas.annotations.Operation;
@@ -20,9 +21,17 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@RequestMapping("/config")
public class ConfigController {
@Autowired
private MediaProperties mediaProperties;
@Autowired
private WebrtcProperties webrtcProperties;
@Operation(summary = "媒体配置", description = "媒体配置")
@GetMapping("/media")
public MediaProperties media() {
return this.mediaProperties;
}
@Operation(summary = "WebRTC配置", description = "WebRTC配置")
@GetMapping("/webrtc")
public WebrtcProperties webrtc() {

View File

@@ -53,8 +53,20 @@ taoyao:
id:
sn: 0
max-index: 999999
media:
audio:
format: OPUS
samplesize: 16
samplerate: 32000
video:
format: H264
bitrate: 1200
framerate: 24
resolution: 1280*760
quality: high|standard|quick
webrtc:
type: SFU
model: SFU
framework: JITSI
stun:
- stun:stun1.l.google.com:19302
- stun:stun2.l.google.com:19302

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>终端</title>
</head>
<body>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
@charset "UTF-8";
/**文本选择*/
::selection{background:#222;color:#fff;}
/**字体大小*/
@media screen and (min-width:800px){html{font-size:16px;}}
@media screen and (min-width:1200px){html{font-size:18px;}}
@media screen and (min-width:1600px){html{font-size:20px;}}
/**默认样式*/
*{margin:0;padding:0;border:none;outline:none;box-sizing:content-box;}
html{background:#EBEBEB;}
html,body{font-family:Arial,Consolas,SimSun,"宋体";color:#222;font-weight:normal;}
body{width:100%;height:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);font-size:1rem;line-height:1.4em;}
a{color:#1155AA;text-decoration:none;}
a:link{text-decoration:none;}
a:hover{color:#4477EE;text-decoration:none;}
a:visited{text-decoration:none;}
img{border:0;}
ol,ul,li{list-style:none;}
input[type=text],textarea{box-shadow:0px 0px 3px 0px rgba(0,0,0,0.1) inset;border:1px solid rgba(0,0,0,0.1)!important;}
input[type=text]:focus,textarea:focus,input[type=text]:hover,textarea:hover{border:1px solid #1155AA!important;}
input::-webkit-calendar-picker-indicator{color:#1155AA;background:none;}
/**容器*/
.taoyao{text-align:center;}
/**直播*/
.taoyao .live > .video{width:100%;height:100%;}
.taoyao .live .handler{position:fixed;width:100%;bottom:2rem;font-size:2rem;}
/**会议*/
.taoyao .handler a{cursor:pointer;}
.taoyao > .handler{font-size:2rem;padding:1rem 0;width:100%;}
.taoyao .list{width:90vw;margin:auto;}
.taoyao .meeting{float:left;overflow:hidden;position:relative;width:calc(25% - 2rem);border:1rem solid #fff;}
.taoyao .me,.taoyao .meeting:hover{border-color:#060;}
.taoyao .meeting > .video{height:15vw;}
.taoyao .meeting > .video video{width:100%;height:100%;}
.taoyao .meeting .handler{position:absolute;bottom:0rem;text-align:center;width:100%;background:rgba(0,0,0,0.2);padding:0.2rem 0;}
.taoyao .meeting .handler a{color:#fff;}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 297 KiB

View File

@@ -3,8 +3,16 @@
<head>
<meta charset="UTF-8">
<title>桃夭</title>
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<script type="text/javascript" src="./javascript/taoyao.js"></script>
<style type="text/css">
a{width:50%;height:100%;position:fixed;text-align:center;line-height:100%;font-size:4rem;display:flex;align-items:center;justify-content:center;}
a:last-child{left:50%;}
a:hover{color:#fff;background:#060;}
</style>
</head>
<body>
<a href="./live.html">直播</a>
<a href="./meeting.html">会议</a>
</body>
</html>

View File

@@ -0,0 +1,327 @@
/**
* 桃夭WebRTC终端示例
*/
/** 音频配置 */
const defaultAudioConfig = {
// 音量0~1
volume: 0.5,
// 设备
// deviceId : '',
// 采样率8000|16000|32000|48000
sampleRate: 32000,
// 采样数16
sampleSize: 16,
// 延迟大小单位毫秒500毫秒以内较好
latency: 0.3,
// 声道数量1|2
channelCount : 1,
// 是否开启自动增益true|false
autoGainControl: false,
// 是否开启降噪功能true|false
noiseSuppression: true,
// 是否开启回音消除true|false
echoCancellation: true,
// 消除回音方式system|browser
echoCancellationType: 'system'
};
/** 视频配置 */
const defaultVideoConfig = {
// 宽度
width: 1280,
// 高度
height: 720,
// 设备
// deviceId: '',
// 帧率
frameRate: 30,
// 裁切
// resizeMode: '',
// 选摄像头user|left|right|environment
facingMode: 'environment'
}
/** 兼容 */
const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
/** 桃夭 */
function Taoyao(
webSocket,
iceServer
) {
this.webSocket = webSocket;
this.iceServer = iceServer;
this.audioStatus = true;
this.videoStatus = true;
this.audioStreamId = null;
this.videoStreamId = null;
this.audioConfig = defaultAudioConfig;
this.videoConfig = defaultVideoConfig;
/** 初始 */
this.init = function() {
if(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
navigator.mediaDevices.enumerateDevices()
.then(list => {
let audioDevice = false;
let videoDevice = false;
list.forEach(v => {
console.log('终端媒体设备', v.kind, v.label);
if(v.kind === 'audioinput') {
audioDevice = true;
} else if(v.kind === 'videoinput') {
videoDevice = true;
}
});
if(!audioDevice) {
console.log('终端没有音频输入设备');
this.audioConfig = false;
}
if(!videoDevice) {
console.log('终端没有视频输入设备');
this.videoConfig = false;
}
})
.catch(e => console.log('获取终端设备失败', e));
}
return this;
};
/** 媒体 */
this.buildUserMedia = function() {
return new Promise((resolve, reject) => {
console.log("获取终端媒体:", this.audioConfig, this.videoConfig);
if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({
audio: this.audioConfig,
video: this.videoConfig
})
.then(resolve)
.catch(reject);
} else if(navigator.getUserMedia) {
navigator.getUserMedia({
audio: this.audioConfig,
video: this.videoConfig
}, resolve, reject);
} else {
reject("获取终端媒体失败");
}
});
};
/** 本地 */
this.local = async function(localVideoId, stream) {
const localVideo = document.getElementById(localVideoId);
if ('srcObject' in localVideo) {
localVideo.srcObject = stream;
} else {
localVideo.src = URL.createObjectURL(stream);;
}
await localVideo.play();
};
/** 连接 */
this.connect = function() {
};
/** 重连 */
/** 定时 */
/** 媒体 */
/** 视频 */
/** 心跳 */
}
/*
var peer;
var socket; // WebSocket
var supportStream = false; // 是否支持使用数据流
var localVideo; // 本地视频
var localVideoStream; // 本地视频流
var remoteVideo; // 远程视频
var remoteVideoStream; // 远程视频流
var initiator = false; // 是否已经有人在等待
var started = false; // 是否开始
var channelReady = false; // 是否打开WebSocket通道
// 初始
function initialize() {
console.log("初始聊天");
// 获取视频
localVideo = document.getElementById("localVideo");
remoteVideo = document.getElementById("remoteVideo");
supportStream = "srcObject" in localVideo;
// 显示状态
if (initiator) {
setNotice("开始连接");
} else {
setNotice("加入聊天https://www.acgist.com/demo/video/?oid=FFB85D84AC56DAF88B7E22AFFA7533D3");
}
// 打开WebSocket
openChannel();
// 创建终端媒体
buildUserMedia();
}
function openChannel() {
console.log("打开WebSocket");
socket = new WebSocket("wss://www.acgist.com/video.ws/FFB85D84AC56DAF88B7E22AFFA7533D3");
socket.onopen = channelOpened;
socket.onmessage = channelMessage;
socket.onclose = channelClosed;
socket.onerror = channelError;
}
function channelOpened() {
console.log("打开WebSocket成功");
channelReady = true;
}
function channelMessage(message) {
console.log("收到消息:" + message.data);
var msg = JSON.parse(message.data);
if (msg.type === "offer") { // 处理Offer消息
if (!initiator && !started) {
connectPeer();
}
peer.setRemoteDescription(new RTCSessionDescription(msg));
peer.createAnswer().then(buildLocalDescription);
} else if (msg.type === "answer" && started) { // 处理Answer消息
peer.setRemoteDescription(new RTCSessionDescription(msg));
} else if (msg.type === "candidate" && started) {
var candidate = new RTCIceCandidate({
sdpMLineIndex : msg.label,
candidate : msg.candidate
});
peer.addIceCandidate(candidate);
} else if (msg.type === "bye" && started) {
onRemoteClose();
setNotice("对方已断开!");
} else if(msg.type === "nowaiting") {
onRemoteClose();
setNotice("对方已离开!");
}
}
function channelClosed() {
console.log("关闭WebSocket");
openChannel(); // 重新打开WebSocket
}
function channelError(event) {
console.log("WebSocket异常" + event);
}
function buildUserMedia() {
console.log("获取终端媒体");
if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({
"audio" : true,
"video" : true
})
.then(onUserMediaSuccess)
.catch(onUserMediaError);
} else {
navigator.getUserMedia({
"audio" : true,
"video" : true
}, onUserMediaSuccess, onUserMediaError);
}
}
function onUserMediaSuccess(stream) {
localVideoStream = stream;
if (supportStream) {
localVideo.srcObject = localVideoStream;
} else {
localVideo.src = URL.createObjectURL(localVideoStream);
}
if (initiator) {
connectPeer();
}
}
function onUserMediaError(error) {
alert("请打开摄像头!");
}
function connectPeer() {
if (!started && localVideoStream && channelReady) {
console.log("开始连接Peer");
started = true;
buildPeerConnection();
peer.addStream(localVideoStream);
if (initiator) {
peer.createOffer().then(buildLocalDescription);
}
}
}
function buildPeerConnection() {
//var server = {"iceServers" : [{"url" : "stun:stun.l.google.com:19302"}]};
var server = {"iceServers" : [{"url" : "stun:stun1.l.google.com:19302"}]};
peer = new PeerConnection(server);
peer.onicecandidate = peerIceCandidate;
peer.onconnecting = peerConnecting;
peer.onopen = peerOpened;
peer.onaddstream = peerAddStream;
peer.onremovestream = peerRemoveStream;
}
function peerIceCandidate(event) {
if (event.candidate) {
sendMessage({
type : "candidate",
id : event.candidate.sdpMid,
label : event.candidate.sdpMLineIndex,
candidate : event.candidate.candidate
});
} else {
console.log("不支持的candidate");
}
}
function peerConnecting(message) {
console.log("Peer连接");
}
function peerOpened(message) {
console.log("Peer打开");
}
function peerAddStream(event) {
console.log("远程视频添加");
remoteVideoStream = event.stream;
if(supportStream) {
remoteVideo.srcObject = remoteVideoStream;
} else {
remoteVideo.src = URL.createObjectURL(remoteVideoStream);
}
setNotice("连接成功");
waitForRemoteVideo();
}
function peerRemoveStream(event) {
console.log("远程视频移除");
}
function buildLocalDescription(description) {
peer.setLocalDescription(description);
sendMessage(description);
}
function sendMessage(message) {
var msgJson = JSON.stringify(message);
socket.send(msgJson);
console.log("发送信息:" + msgJson);
}
function setNotice(msg) {
document.getElementById("footer").innerHTML = msg;
}
function onRemoteClose() {
started = false;
initiator = false;
if(supportStream) {
remoteVideo.srcObject = null;
} else {
remoteVideo.src = null;
}
peer.close();
}
function waitForRemoteVideo() {
if (remoteVideo.currentTime > 0) { // 判断远程视频长度
setNotice("连接成功!");
} else {
setTimeout(waitForRemoteVideo, 100);
}
}
window.onbeforeunload = function() {
sendMessage({type : "bye"});
if(peer) {
peer.close();
}
socket.close();
}
if(!WebSocket) {
alert("你的浏览器不支持WebSocket");
} else if(!PeerConnection) {
alert("你的浏览器不支持RTCPeerConnection");
} else {
setTimeout(initialize, 100); // 加载完成调用初始化方法
}
window.onbeforeunload = function() {
socket.close();
}
*/

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>直播</title>
<link rel="stylesheet" type="text/css" href="./css/font.min.css" />
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<script type="text/javascript" src="./javascript/taoyao.js"></script>
</head>
<body>
<div class="taoyao" id="taoyao">
<div class="live">
<div class="video">
</div>
<div class="handler">
<a class="audio icon-volume-medium" title="音频状态"></a>
<a class="video icon-play2" title="视频状态"></a>
<a class="record icon-radio-checked" title="录制视频"></a>
<a class="kick icon-cancel-circle" title="退出直播"></a>
<a class="close icon-switch" title="关闭直播"></a>
</div>
</div>
</div>
<script type="text/javascript">
const live = document.getElementById('taoyao');
</script>
</body>
</html>

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>会议</title>
<link rel="stylesheet" type="text/css" href="./css/font.min.css" />
<link rel="stylesheet" type="text/css" href="./css/style.css" />
<script type="text/javascript" src="./javascript/taoyao.js"></script>
</head>
<body>
<div class="taoyao">
<div class="handler">
<a class="create icon-svg" title="创建房间"></a>
<a class="invite icon-address-book" title="邀请房间"></a>
<a class="enter icon-enter" title="进入房间"></a>
<a class="leave icon-exit" title="离开房间"></a>
<a class="close icon-switch" title="关闭房间"></a>
</div>
<div class="list" id="list">
<div class="meeting me">
<div class="video">
<video id="local"></video>
</div>
<div class="handler">
<a class="audio icon-volume-medium" title="音频状态"></a>
<a class="video icon-play2" title="视频状态"></a>
<a class="record icon-radio-checked" title="录制视频"></a>
<a class="kick icon-cancel-circle" title="踢出房间"></a>
</div>
</div>
</div>
</div>
<script type="text/javascript">
const list = document.getElementById('list');
const template = `
<div class="video">
<video></video>
</div>
<div class="handler">
<a class="audio icon-volume-medium" title="音频状态"></a>
<a class="video icon-play2" title="视频状态"></a>
<a class="record icon-radio-checked" title="录制视频"></a>
<a class="kick icon-cancel-circle" title="踢出房间"></a>
</div>
`;
for(let i = 0; i < 10; i++) {
const child = document.createElement('div');
child.className = 'meeting';
child.innerHTML = template;
list.appendChild(child);
}
const taoyao = new Taoyao();
taoyao
.init()
.buildUserMedia()
.then(stream => taoyao.local('local', stream))
.catch((e) => alert('获取终端媒体失败:' + e));
</script>
</body>
</html>

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>房间</title>
</head>
<body>
</body>
</html>