This commit is contained in:
acgist
2022-11-28 19:33:17 +08:00
parent 56dc649279
commit 7fe3babab0
28 changed files with 474 additions and 326 deletions

View File

@@ -1,92 +0,0 @@
# FFmpeg
默认使用`ffmpeg-platform`所以不用安装如果使用本地FFmpeg需要自己安装。
## FFmpeg
```
# nasm
wget https://www.nasm.us/pub/nasm/releasebuilds/2.14/nasm-2.14.tar.gz
tar zxvf nasm-2.14.tar.gz
cd nasm-2.14
./configure --prefix=/usr/local/nasm
make -j && make install
# 环境变量
vim /etc/profile
export PATH=$PATH:/usr/local/nasm/bin
source /etc/profile
# yasm
wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz
tar zxvf yasm-1.3.0.tar.gz
cd yasm-1.3.0
./configure --prefix=/usr/local/yasm
make -j && make install
# 环境变量
vim /etc/profile
export PATH=$PATH:/usr/local/yasm/bin
source /etc/profile
# x264
git clone https://code.videolan.org/videolan/x264.git
cd x264
./configure --prefix=/usr/local/x264 --libdir=/usr/local/lib --includedir=/usr/local/include --enable-shared --enable-static
make -j && make install
# 环境变量
vim /etc/profile
export PATH=$PATH:/usr/local/x264/bin
source /etc/profile
# 编码解码
# acc
https://github.com/mstorsjo/fdk-aac.git
--enable-libfdk_aac
# vpx
https://github.com/webmproject/libvpx.git
--enable-libvpx
# x265
https://bitbucket.org/multicoreware/x265
--enable-libx265
# opus
https://archive.mozilla.org/pub/opus/opus-1.2.1.tar.gz
--enable-libopus
# ffmpeg
wget http://www.ffmpeg.org/releases/ffmpeg-4.3.1.tar.xz
tar xvJf ffmpeg-4.3.1.tar.xz
cd ffmpeg-4.3.1
./configure --prefix=/usr/local/ffmpeg --enable-gpl --enable-shared --enable-libx264
# --enable-cuda --enable-cuvid --enable-nvenc --nvcc=/usr/local/cuda-11.0/bin/nvcc
make -j && make install
# 环境变量
vim /etc/profile
export PATH=$PATH:/usr/local/ffmpeg/bin
source /etc/profile
# lib
vim /etc/ld.so.conf
/usr/local/x264/lib/
/usr/local/ffmpeg/lib/
ldconfig
# 查看版本
ffmpeg -version
# 查看编解码
ffmpeg -codecs
# 格式化文件
ffmpeg -y -i source.mkv -c copy target.mp4
# 查看文件格式
ffprobe -v error -show_streams -print_format json source.mp4
```
## GPU
```
驱动
# cuda
https://developer.nvidia.com/cuda-downloads
# nv-codec-headers
https://git.videolan.org/git/ffmpeg/nv-codec-headers.git
# 验证
nvidia-smi
```

44
pom.xml
View File

@@ -24,7 +24,6 @@
<!-- 版本 -->
<java.version>17</java.version>
<javacv.version>1.5.8</javacv.version>
<ffmpeg.version>5.1.2</ffmpeg.version>
<lombok.version>1.18.24</lombok.version>
<kurento.version>6.18.0</kurento.version>
<springdoc.version>2.0.0-RC1</springdoc.version>
@@ -189,49 +188,6 @@
<artifactId>kurento-client</artifactId>
<version>${kurento.version}</version>
</dependency>
<!--
媒体FFmpeg
android-arm
android-arm64
android-x86
android-x86_64
linux-arm64
linux-armhf
linux-ppc64le
linux-x86
linux-x86_64
macosx-arm64
macosx-x86_64
windows-x86
windows-x86_64
-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>${ffmpeg.version}-${javacv.version}</version>
<exclusions>
<exclusion>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
</exclusion>
<exclusion>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}-${javacv.version}</version>
<classifier>${javacv.os.version}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>${javacv.version}</version>
<classifier>${javacv.os.version}</classifier>
</dependency>
<!-- 集合工具 -->
<dependency>
<groupId>org.apache.commons</groupId>

View File

@@ -21,10 +21,6 @@ public class MediaAudioProperties {
*/
public enum Format {
/**
* ACC
*/
ACC,
/**
* PCM
*/
@@ -41,16 +37,16 @@ public class MediaAudioProperties {
*/
@Schema(title = "格式", description = "格式")
private Format format;
/**
* 采样数
*/
@Schema(title = "采样数", description = "采样数", example = "16")
private Integer sampleSize;
/**
* 采样率
* 8000|16000|32000|48000
*/
@Schema(title = "采样率", description = "采样率", example = "8000|16000|32000|48000")
private Integer samplerate;
/**
* 采样数
*/
@Schema(title = "采样数", description = "采样数", example = "16")
private Integer samplesize;
private Integer sampleRate;
}

View File

@@ -54,7 +54,7 @@ public class MediaVideoProperties {
* 帧率(流畅)
*/
@Schema(title = "帧率", description = "帧率影响流程", example = "20|24|30|60")
private Integer framerate;
private Integer frameRate;
/**
* 分辨率(画面大小)
*/

View File

@@ -37,20 +37,6 @@
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-kurento</artifactId>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<classifier>${javacv.os.version}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<classifier>${javacv.os.version}</classifier>
</dependency>
</dependencies>
</project>

View File

@@ -21,6 +21,16 @@ public class Meeting {
*/
@Schema(title = "会议标识", description = "会议标识")
private String id;
/**
* 会议名称
*/
@Schema(title = "会议名称", description = "会议名称")
private String name;
/**
* 会议密码
*/
@Schema(title = "会议密码", description = "会议密码")
private String password;
/**
* 终端会话标识列表
*/

View File

@@ -65,7 +65,8 @@ public class MeetingManager {
*/
public Meeting create(String sn) {
final Meeting meeting = new Meeting();
meeting.setId(this.idService.buildIdToString());
// meeting.setId(this.idService.buildIdToString());
meeting.setId("1");
meeting.setSns(new CopyOnWriteArrayList<>());
meeting.setCreator(sn);
meeting.addSn(sn);

View File

@@ -32,6 +32,7 @@ public class MeetingEnterListener extends MeetingListenerAdapter<MeetingEnterEve
"id", meeting.getId(),
"sn", sn
));
// TODO返回房间列表
meeting.getSns().stream()
.filter(v -> !sn.equals(v))
.forEach(v -> this.clientSessionManager.unicast(v, message));

View File

@@ -59,12 +59,12 @@ taoyao:
media:
audio:
format: OPUS
samplesize: 16
samplerate: 32000
sample-size: 16
sample-rate: 32000
video:
format: H264
bitrate: 1200
framerate: 24
frame-rate: 24
resolution: 1280*760
quality: high|standard|quick
# WebRTC配置

View File

@@ -1,20 +1,25 @@
/** 桃夭WebRTC终端核心功能 */
/**
* 桃夭WebRTC终端核心功能
*
* 代码注意:
* 1. undefined判断使用两个等号其他情况使用三个。
*/
/** 兼容 */
const RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
const RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
const RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
/** 默认音频配置 */
const defaultAudioConfig = {
// 设备
// deviceId : '',
// 音量0~1
volume: 0.5,
// 延迟大小单位毫秒500毫秒以内较好
latency: 0.4,
// 设备
// deviceId : '',
// 采样率8000|16000|32000|48000
sampleRate: 48000,
// 采样数16
sampleSize: 16,
// 采样率8000|16000|32000|48000
sampleRate: 32000,
// 声道数量1|2
channelCount : 1,
// 是否开启自动增益true|false
@@ -28,16 +33,14 @@ const defaultAudioConfig = {
};
/** 默认视频配置 */
const defaultVideoConfig = {
// 设备
// deviceId: '',
// 宽度
width: 1280,
// 高度
height: 720,
// 设备
// deviceId: '',
// 帧率
frameRate: 24,
// 裁切
// resizeMode: '',
// 选摄像头user|left|right|environment
facingMode: 'environment'
}
@@ -57,7 +60,7 @@ const defaultRPCConfig = {
/** 信令配置 */
const signalConfig = {
/** 当前终端SN */
sn: localStorage.getItem('taoyao.sn', 'taoyao'),
sn: localStorage.getItem('taoyao.sn') || 'taoyao',
/** 当前版本 */
version: '1.0.0',
// 信令授权
@@ -76,7 +79,11 @@ const signalProtocol = {
/** 订阅 */
subscribe: 5002,
/** 候选 */
candidate: 5004
offer: 5997,
/** Answer */
answer: 5998,
/** 候选 */
candidate: 5999
},
/** 终端信令 */
client: {
@@ -303,6 +310,12 @@ const signalChannel = {
case signalProtocol.media.subscribe:
this.defaultMediaSubscribe(data);
break;
case signalProtocol.media.offer:
this.defaultMediaOffer(data);
break;
case signalProtocol.media.answer:
this.defaultMediaAnswer(data);
break;
case signalProtocol.media.candidate:
this.defaultMediaCandidate(data);
break;
@@ -329,7 +342,7 @@ const signalChannel = {
/** 终端默认回调 */
defaultClientConfig: function(data) {
this.taoyao
.configMedia(data.body.media)
.configMedia(data.body.media.audio, data.body.media.video)
.configWebrtc(data.body.webrtc);
},
defaultClientReboot: function(data) {
@@ -338,52 +351,71 @@ const signalChannel = {
},
/** 默认媒体回调 */
defaultMediaPublish: function(data) {
this.taoyao.localMediaChannel.setRemoteDescription(new RTCSessionDescription(data.body));
},
defaultMediaSubscribe: function(data) {
let self = this;
const from = data.body.from;
let remote = this.taoyao.remoteClientFilter(from);
if(!remote) {
remote = new TaoyaoClient(from);
this.taoyao.remoteClient.push(remote);
}
self.taoyao.remoteMediaChannel.setRemoteDescription(new RTCSessionDescription(data.body));
self.taoyao.remoteMediaChannel.createAnswer().then(description => {
console.debug('Local Create Answer', description);
self.taoyao.remoteMediaChannel.setLocalDescription(description);
this.taoyao.remoteClientFilter(from, true);
self.taoyao.localMediaChannel.createOffer().then(description => {
console.debug('Local Create Offer', description);
self.taoyao.localMediaChannel.setLocalDescription(description);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.publish,
signalProtocol.media.offer,
{
to: from,
sdp: description.sdp,
type: description.type
sdp: {
sdp: description.sdp,
type: description.type
}
}
));
});
},
defaultMediaOffer: function(data) {
let self = this;
const from = data.body.from;
this.taoyao.remoteClientFilter(from, true);
self.taoyao.remoteMediaChannel.setRemoteDescription(new RTCSessionDescription(data.body.sdp));
self.taoyao.remoteMediaChannel.createAnswer().then(description => {
console.debug('Remote Create Answer', description);
self.taoyao.remoteMediaChannel.setLocalDescription(description);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.answer,
{
to: from,
sdp: {
sdp: description.sdp,
type: description.type
}
}
));
});
},
defaultMediaAnswer: function(data) {
this.taoyao.localMediaChannel.setRemoteDescription(new RTCSessionDescription(data.body.sdp));
},
defaultMediaCandidate: function(data) {
if(
!data.body.candidate ||
!data.body.candidate.candidate ||
!data.body.candidate.sdpMid ||
!data.body.candidate.sdpMLineIndex
) {
console.warn('候选缺失要素', data);
if(!this.taoyao.checkCandidate(data.body.candidate)) {
console.debug('候选缺失要素', data);
return;
}
let candidate = new RTCIceCandidate(data.body.candidate);
this.taoyao.remoteMediaChannel.addIceCandidate(candidate);
console.debug('Set ICE Candidate', this.taoyao.remoteMediaChannel);
if(data.body.type === 'local') {
this.taoyao.remoteMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
} else {
this.taoyao.localMediaChannel.addIceCandidate(new RTCIceCandidate(data.body.candidate));
}
},
/** 会议默认回调 */
defaultMeetingEnter: function(data) {
this.taoyao
.mediaSubscribe(data.body.sn);
this.taoyao.mediaSubscribe(data.body.sn);
}
};
/** 终端 */
function TaoyaoClient(
sn
sn,
audioEnabled,
videoEnabled
) {
/** 终端标识 */
this.sn = sn;
@@ -391,12 +423,15 @@ function TaoyaoClient(
this.video = null;
/** 媒体信息 */
this.stream = null;
this.audioTrack = null;
this.videoTrack = null;
/** 媒体状态 */
this.audioTrack = [];
this.videoTrack = [];
/** 媒体状态:是否含有 */
this.audioStatus = false;
this.videoStatus = false;
this.recordStatus = false;
/** 媒体状态:是否播放 */
this.audioEnabled = audioEnabled == undefined ? true : audioEnabled;
this.videoEnabled = videoEnabled == undefined ? true : videoEnabled;
/** 重置 */
this.reset = function() {
}
@@ -406,80 +441,80 @@ function TaoyaoClient(
return this;
};
/** 重新加载 */
this.load = function() {
this.video.load();
this.load = async function() {
await this.video.load();
return this;
}
/** 暂停视频 */
this.pause = function() {
this.video.pause();
this.pause = async function() {
await this.video.pause();
return this;
};
/** 关闭视频 */
this.close = function() {
this.video.close();
this.close = async function() {
await this.video.close();
return this;
};
/** 设置视频对象 */
this.buildVideo = async function(videoId, stream, track) {
if(!this.video) {
/** 设置媒体 */
this.buildStream = async function(videoId, stream, track) {
if(!this.video && videoId) {
this.video = document.getElementById(videoId);
}
await this.buildStream(stream, track);
return this;
};
/** 设置媒体流 */
this.buildStream = async function(stream, track) {
if(stream) {
if(track) {
if(!this.stream) {
this.stream = stream;
this.video.srcObject = this.stream;
}
// TODO删除旧的
this.stream.addTrack(track);
if(track.kind === 'audio') {
this.audioTrack = track;
this.audioStatus = true;
} else if(track.kind === 'video') {
this.videoTrack = track;
this.videoStatus = true;
}
await this.video.load();
} else {
this.stream = stream;
this.video.srcObject = stream;
let audioTrack = stream.getAudioTracks();
let videoTrack = stream.getVideoTracks();
if(audioTrack && audioTrack.length) {
this.audioTrack = audioTrack;
this.audioStatus = true;
}
if(videoTrack && videoTrack.length) {
this.videoTrack = videoTrack;
this.videoStatus = true;
}
await this.video.load();
}
console.debug('设置媒体流', this.video, this.stream, this.audioTrack, this.videoTrack);
await this.play();
if(!this.video) {
throw new Error('视频对象无效:' + videoId);
}
if(!this.stream) {
this.stream = new MediaStream();
this.video.srcObject = this.stream;
}
if(track) {
if(track.kind === 'audio') {
this.buildAudioTrack(track);
}
if(track.kind === 'video') {
this.buildVideoTrack(track);
}
} else {
let audioTrack = stream.getAudioTracks();
let videoTrack = stream.getVideoTracks();
if(audioTrack && audioTrack.length) {
audioTrack.forEach(v => this.buildAudioTrack(v));
}
if(videoTrack && videoTrack.length) {
videoTrack.forEach(v => this.buildVideoTrack(v));
}
}
console.debug('设置媒体', this.video, this.stream, this.audioTrack, this.videoTrack);
await this.load();
await this.play();
return this;
};
/** 设置音频流 */
this.buildAudioTrack = function() {
this.buildAudioTrack = function(track) {
// 关闭旧的
// 创建新的
this.audioStatus = true;
this.audioTrack.push(track);
if(this.audioEnabled) {
this.stream.addTrack(track);
}
};
/** 设置视频流 */
this.buildVideoTrack = function() {
this.buildVideoTrack = function(track) {
// 关闭旧的
// 创建新的
this.videoStatus = true;
this.videoTrack.push(track);
if(this.videoEnabled) {
this.stream.addTrack(track);
}
};
}
/** 桃夭 */
function Taoyao(
webSocket
webSocket,
localClientAudioEnabled,
localClientVideoEnabled
) {
/** WebRTC配置 */
this.webrtc = null;
@@ -497,6 +532,9 @@ function Taoyao(
/** 本地终端 */
this.localClient = null;
this.localMediaChannel = null;
/** 本地媒体状态 */
this.localClientAudioEnabled = localClientAudioEnabled == undefined ? false : localClientAudioEnabled;
this.localClientVideoEnabled = localClientVideoEnabled == undefined ? true : localClientVideoEnabled;
/** 远程终端 */
this.remoteClient = [];
this.remoteMediaChannel = null;
@@ -505,7 +543,7 @@ function Taoyao(
/** 媒体配置 */
this.configMedia = function(audio = {}, video = {}) {
this.audioConfig = {...this.audioConfig, ...audio};
this.videoCofnig = {...this.videoCofnig, ...video};
this.videoConfig = {...this.videoConfig, ...video};
console.debug('终端媒体配置', this.audioConfig, this.videoConfig);
return this;
};
@@ -586,13 +624,17 @@ function Taoyao(
});
};
/** 远程终端过滤 */
this.remoteClientFilter = function(sn) {
this.remoteClientFilter = function(sn, autoBuild) {
let array = this.remoteClient.filter(v => v.sn === sn);
if(array.length <= 0) {
return null;
let remote = null;
if(array.length > 0) {
remote = array[0];
} else if(autoBuild) {
remote = new TaoyaoClient(sn);
this.remoteClient.push(remote);
}
return this.remoteClient.filter(v => v.sn === sn)[0];
}
return remote;
};
/** 关闭:关闭媒体 */
this.close = function() {
// TODO释放资源
@@ -605,16 +647,12 @@ function Taoyao(
this.buildMediaChannel = async function(localVideoId, stream) {
let self = this;
// 本地视频
this.localClient = new TaoyaoClient(signalConfig.sn);
await this.localClient.buildVideo(localVideoId, stream);
this.localClient = new TaoyaoClient(signalConfig.sn, this.localClientAudioEnabled, this.localClientVideoEnabled);
await this.localClient.buildStream(localVideoId, stream);
// 本地通道
this.localMediaChannel = new RTCPeerConnection(defaultRPCConfig);
if(this.localClient.audioTrack) {
this.localClient.audioTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
}
if(this.localClient.videoTrack) {
this.localClient.videoTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
}
this.localClient.audioTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
this.localClient.videoTrack.forEach(v => this.localMediaChannel.addTrack(v, this.localClient.stream));
this.localMediaChannel.ontrack = function(e) {
console.debug('Local Media Track', e);
};
@@ -633,12 +671,18 @@ function Taoyao(
}
};
this.localMediaChannel.onicecandidate = function(e) {
let sns = self.remoteClient.filter(v => v.video === null).map(v => v.sn);
console.debug('Local ICE Candidate', sns, e);
// TODO判断给谁
let to = self.remoteClient.map(v => v.sn)[0];
if(!self.checkCandidate(e.candidate)) {
console.debug('Send Local ICE Candidate Fail', e);
return;
}
console.debug('Send Local ICE Candidate', to, e);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.candidate,
{
sns: sns,
to: to,
type: 'local',
candidate: e.candidate
}
));
@@ -649,7 +693,7 @@ function Taoyao(
console.debug('Remote Media Track', e);
// TODO匹配
let remote = self.remoteClient[0];
remote.buildVideo(remote.sn, e.streams[0], e.track);
remote.buildStream(remote.sn, e.streams[0], e.track);
};
this.remoteMediaChannel.ondatachannel = function(channel) {
channel.onopen = function() {
@@ -666,12 +710,18 @@ function Taoyao(
}
};
this.remoteMediaChannel.onicecandidate = function(e) {
let sns = self.remoteClient.filter(v => v.video === null).map(v => v.sn);
console.debug('Remote ICE Candidate', sns, e);
// TODO判断给谁
let to = self.remoteClient.map(v => v.sn)[0];
if(!self.checkCandidate(e.candidate)) {
console.debug('Send Remote ICE Candidate Fail', e);
return;
}
console.debug('Send Remote ICE Candidate', to, e);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.candidate,
{
sns: sns,
to: to,
type: 'remote',
candidate: e.candidate
}
));
@@ -679,30 +729,30 @@ function Taoyao(
console.debug('打开媒体通道', this.localMediaChannel, this.remoteMediaChannel);
return this;
};
/** 校验candidate */
this.checkCandidate = function(candidate) {
if(
!candidate ||
!candidate.candidate ||
candidate.sdpMid === null ||
candidate.sdpMid === null ||
candidate.sdpMLineIndex === undefined ||
candidate.sdpMLineIndex === undefined
) {
return false;
}
return true;
};
/** 媒体信令 */
this.mediaSubscribe = function(sn, callback) {
let self = this;
let remote = self.remoteClientFilter(sn);
if(remote) {
remote.reset();
} else {
remote = new TaoyaoClient(sn);
this.remoteClient.push(remote);
}
if(self.webrtc.model === 'MESH') {
self.localMediaChannel.createOffer().then(description => {
console.debug('Local Create Offer', description);
self.localMediaChannel.setLocalDescription(description);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.subscribe,
{
to: sn,
sdp: description.sdp,
type: description.type
}
), callback);
});
}
self.remoteClientFilter(sn, true);
self.push(signalProtocol.buildProtocol(
signalProtocol.media.subscribe,
{
to: sn
}
), callback);
};
/** 会议信令 */
this.meetingCreate = function(callback) {

View File

@@ -73,10 +73,11 @@
methods: {
// 信令回调返回true表示已经处理
callback: function(data) {
let self = this;
switch(data.header.pid) {
case signalProtocol.client.heartbeat:
// 心跳
return true;
case signalProtocol.client.config:
// 如果需要下发配置生效需要在此打开媒体通道
return false;
}
return false;
},

View File

@@ -373,10 +373,6 @@
取消订阅终端媒体流(终端取消拉流)
### 候选信令5004
IceCandidate
### 暂停信令5004
终端->服务端
@@ -397,6 +393,18 @@ MCU/SFU模式有效
配置订阅媒体:码率、帧率、分辨率等等
### Offer信令5997
Offer
### Answer信令5998
Answer
### 候选信令5999
IceCandidate
## 测试
```

View File

@@ -49,7 +49,7 @@ public abstract class ClientSessionAdapter<T extends AutoCloseable> implements C
@Override
public boolean timeout(long timeout) {
return !this.authorized && System.currentTimeMillis() - this.time > timeout;
return System.currentTimeMillis() - this.time > timeout;
}
@Override

View File

@@ -36,7 +36,7 @@ public class ClientSessionManager {
@Scheduled(cron = "${taoyao.scheduled.session:0 * * * * ?}")
public void scheduled() {
this.closeTimeoutSession();
this.closeTimeout();
}
/**
@@ -65,7 +65,9 @@ public class ClientSessionManager {
* @param message 消息
*/
public void unicast(String to, Message message) {
this.sessions.stream().filter(v -> v.filterSn(to)).forEach(v -> {
this.sessions().stream()
.filter(v -> v.filterSn(to))
.forEach(v -> {
message.getHeader().setSn(v.sn());
v.push(message);
});
@@ -77,7 +79,7 @@ public class ClientSessionManager {
* @param message 消息
*/
public void broadcast(Message message) {
this.sessions.forEach(v -> {
this.sessions().forEach(v -> {
message.getHeader().setSn(v.sn());
v.push(message);
});
@@ -90,7 +92,9 @@ public class ClientSessionManager {
* @param message 消息
*/
public void broadcast(String from, Message message) {
this.sessions.stream().filter(v -> v.filterNoneSn(from)).forEach(v -> {
this.sessions().stream().
filter(v -> v.filterNoneSn(from))
.forEach(v -> {
message.getHeader().setSn(v.sn());
v.push(message);
});
@@ -102,7 +106,7 @@ public class ClientSessionManager {
* @return 终端会话
*/
public ClientSession session(String sn) {
return this.sessions.stream()
return this.sessions().stream()
.filter(v -> StringUtils.equals(sn, v.sn()))
.findFirst()
.orElse(null);
@@ -122,14 +126,18 @@ public class ClientSessionManager {
* @return 所有终端会话
*/
public List<ClientSession> sessions() {
return this.sessions;
return this.sessions.stream()
.filter(ClientSession::authorized)
.toList();
}
/**
* @return 所有终端状态
*/
public List<ClientSessionStatus> status() {
return this.sessions().stream().map(ClientSession::status).toList();
return this.sessions().stream()
.map(ClientSession::status)
.toList();
}
/**
@@ -160,9 +168,10 @@ public class ClientSessionManager {
/**
* 定时关闭超时会话
*/
private void closeTimeoutSession() {
private void closeTimeout() {
log.debug("定时关闭超时会话");
this.sessions.stream()
.filter(v -> !v.authorized())
.filter(v -> v.timeout(this.taoyaoProperties.getTimeout()))
.forEach(v -> {
log.debug("关闭超时会话:{}", v);

View File

@@ -0,0 +1,34 @@
package com.acgist.taoyao.signal.event.media;
import java.util.Map;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.client.ClientSession;
import com.acgist.taoyao.signal.event.ApplicationEventAdapter;
import lombok.Getter;
import lombok.Setter;
/**
* Answer事件
*
* @author acgist
*/
@Getter
@Setter
public class MediaAnswerEvent extends ApplicationEventAdapter {
private static final long serialVersionUID = 1L;
public MediaAnswerEvent(String sn, Map<?, ?> body, Message message, ClientSession session) {
super(sn, body, message, session);
}
/**
* @return 接收终端标识
*/
public String getTo() {
return this.get("to");
}
}

View File

@@ -1,6 +1,5 @@
package com.acgist.taoyao.signal.event.media;
import java.util.List;
import java.util.Map;
import com.acgist.taoyao.boot.model.Message;
@@ -26,10 +25,10 @@ public class MediaCandidateEvent extends ApplicationEventAdapter {
}
/**
* @return 终端列表
* @return 接收终端标识
*/
public List<String> getSns() {
return this.get("sns");
public String getTo() {
return this.get("to");
}
}

View File

@@ -0,0 +1,34 @@
package com.acgist.taoyao.signal.event.media;
import java.util.Map;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.client.ClientSession;
import com.acgist.taoyao.signal.event.ApplicationEventAdapter;
import lombok.Getter;
import lombok.Setter;
/**
* Offer事件
*
* @author acgist
*/
@Getter
@Setter
public class MediaOfferEvent extends ApplicationEventAdapter {
private static final long serialVersionUID = 1L;
public MediaOfferEvent(String sn, Map<?, ?> body, Message message, ClientSession session) {
super(sn, body, message, session);
}
/**
* @return 接收终端标识
*/
public String getTo() {
return this.get("to");
}
}

View File

@@ -25,7 +25,7 @@ public class MediaPublishEvent extends ApplicationEventAdapter {
}
/**
* @return 终端标识(发布给谁)
* @return 接收终端标识
*/
public String getTo() {
return this.get("to");

View File

@@ -25,7 +25,7 @@ public class MediaSubscribeEvent extends ApplicationEventAdapter {
}
/**
* @return 终端标识(订阅的谁)
* @return 接收终端标识
*/
public String getTo() {
return this.get("to");

View File

@@ -0,0 +1,30 @@
package com.acgist.taoyao.signal.protocol.media;
import java.util.Map;
import com.acgist.taoyao.boot.annotation.Protocol;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.client.ClientSession;
import com.acgist.taoyao.signal.event.media.MediaAnswerEvent;
import com.acgist.taoyao.signal.protocol.ProtocolMapAdapter;
/**
* Answer信令
*
* @author acgist
*/
@Protocol
public class MediaAnswerProtocol extends ProtocolMapAdapter {
public static final Integer PID = 5998;
public MediaAnswerProtocol() {
super(PID, "Answer信令");
}
@Override
public void execute(String sn, Map<?, ?> body, Message message, ClientSession session) {
this.publishEvent(new MediaAnswerEvent(sn, body, message, session));
}
}

View File

@@ -16,7 +16,7 @@ import com.acgist.taoyao.signal.protocol.ProtocolMapAdapter;
@Protocol
public class MediaCandidateProtocol extends ProtocolMapAdapter {
public static final Integer PID = 5004;
public static final Integer PID = 5999;
public MediaCandidateProtocol() {
super(PID, "候选信令");

View File

@@ -0,0 +1,30 @@
package com.acgist.taoyao.signal.protocol.media;
import java.util.Map;
import com.acgist.taoyao.boot.annotation.Protocol;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.client.ClientSession;
import com.acgist.taoyao.signal.event.media.MediaOfferEvent;
import com.acgist.taoyao.signal.protocol.ProtocolMapAdapter;
/**
* Offer信令
*
* @author acgist
*/
@Protocol
public class MediaOfferProtocol extends ProtocolMapAdapter {
public static final Integer PID = 5997;
public MediaOfferProtocol() {
super(PID, "Offer信令");
}
@Override
public void execute(String sn, Map<?, ?> body, Message message, ClientSession session) {
this.publishEvent(new MediaOfferEvent(sn, body, message, session));
}
}

View File

@@ -5,7 +5,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.acgist.taoyao.webrtc.mesh.listener.MediaAnswerListener;
import com.acgist.taoyao.webrtc.mesh.listener.MediaCandidateListener;
import com.acgist.taoyao.webrtc.mesh.listener.MediaOfferListener;
import com.acgist.taoyao.webrtc.mesh.listener.MediaPublishListener;
import com.acgist.taoyao.webrtc.mesh.listener.MediaSubscribeListener;
@@ -30,6 +32,18 @@ public class MeshAutoConfiguration {
return new MediaSubscribeListener();
}
@Bean
@ConditionalOnMissingBean
public MediaOfferListener mediaOfferListener() {
return new MediaOfferListener();
}
@Bean
@ConditionalOnMissingBean
public MediaAnswerListener mediaAnswerListener() {
return new MediaAnswerListener();
}
@Bean
@ConditionalOnMissingBean
public MediaCandidateListener mediaCandidateListener() {

View File

@@ -0,0 +1,33 @@
package com.acgist.taoyao.webrtc.mesh.listener;
import java.util.Map;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.event.media.MediaAnswerEvent;
import com.acgist.taoyao.signal.listener.MediaListenerAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* Answer监听
*
* @author acgist
*/
@Slf4j
public class MediaAnswerListener extends MediaListenerAdapter<MediaAnswerEvent> {
@Override
public void onApplicationEvent(MediaAnswerEvent event) {
final String sn = event.getSn();
final String to = event.getTo();
if(sn.equals(to)) {
log.debug("忽略Answer消息相同终端{}-{}", sn, to);
return;
}
final Message message = event.getMessage();
final Map<String, Object> mergeBody = event.mergeBody();
mergeBody.put("from", sn);
this.clientSessionManager.unicast(to, message);
}
}

View File

@@ -1,32 +1,33 @@
package com.acgist.taoyao.webrtc.mesh.listener;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.event.media.MediaCandidateEvent;
import com.acgist.taoyao.signal.listener.MediaListenerAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* 候选监听
*
* @author acgist
*/
@Slf4j
public class MediaCandidateListener extends MediaListenerAdapter<MediaCandidateEvent> {
@Override
public void onApplicationEvent(MediaCandidateEvent event) {
final String sn = event.getSn();
final List<String> sns = event.getSns();
if(CollectionUtils.isEmpty(sns)) {
final String to = event.getTo();
if(sn.equals(to)) {
log.debug("忽略候选消息(相同终端):{}-{}", sn, to);
return;
}
final Message message = event.getMessage();
final Map<String, Object> mergeBody = event.mergeBody();
mergeBody.put("from", sn);
sns.forEach(to -> this.clientSessionManager.unicast(to, message));
this.clientSessionManager.unicast(to, message);
}
}

View File

@@ -0,0 +1,33 @@
package com.acgist.taoyao.webrtc.mesh.listener;
import java.util.Map;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.event.media.MediaOfferEvent;
import com.acgist.taoyao.signal.listener.MediaListenerAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* Offer监听
*
* @author acgist
*/
@Slf4j
public class MediaOfferListener extends MediaListenerAdapter<MediaOfferEvent> {
@Override
public void onApplicationEvent(MediaOfferEvent event) {
final String sn = event.getSn();
final String to = event.getTo();
if(sn.equals(to)) {
log.debug("忽略Offer消息相同终端{}-{}", sn, to);
return;
}
final Message message = event.getMessage();
final Map<String, Object> mergeBody = event.mergeBody();
mergeBody.put("from", sn);
this.clientSessionManager.unicast(to, message);
}
}

View File

@@ -6,17 +6,24 @@ import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.event.media.MediaPublishEvent;
import com.acgist.taoyao.signal.listener.MediaListenerAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* 发布监听
*
* @author acgist
*/
@Slf4j
public class MediaPublishListener extends MediaListenerAdapter<MediaPublishEvent> {
@Override
public void onApplicationEvent(MediaPublishEvent event) {
final String sn = event.getSn();
final String to = event.getTo();
if(sn.equals(to)) {
log.debug("忽略发布消息(相同终端):{}-{}", sn, to);
return;
}
final Message message = event.getMessage();
final Map<String, Object> mergeBody = event.mergeBody();
mergeBody.put("from", sn);

View File

@@ -6,17 +6,24 @@ import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.event.media.MediaSubscribeEvent;
import com.acgist.taoyao.signal.listener.MediaListenerAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* 订阅监听
*
* @author acgist
*/
@Slf4j
public class MediaSubscribeListener extends MediaListenerAdapter<MediaSubscribeEvent> {
@Override
public void onApplicationEvent(MediaSubscribeEvent event) {
final String sn = event.getSn();
final String to = event.getTo();
if(sn.equals(to)) {
log.debug("忽略订阅消息(相同终端):{}-{}", sn, to);
return;
}
final Message message = event.getMessage();
final Map<String, Object> mergeBody = event.mergeBody();
mergeBody.put("from", sn);