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> <java.version>17</java.version>
<javacv.version>1.5.8</javacv.version> <javacv.version>1.5.8</javacv.version>
<ffmpeg.version>5.1.2</ffmpeg.version>
<lombok.version>1.18.24</lombok.version> <lombok.version>1.18.24</lombok.version>
<kurento.version>6.18.0</kurento.version> <kurento.version>6.18.0</kurento.version>
<springdoc.version>2.0.0-RC1</springdoc.version> <springdoc.version>2.0.0-RC1</springdoc.version>
@@ -189,49 +188,6 @@
<artifactId>kurento-client</artifactId> <artifactId>kurento-client</artifactId>
<version>${kurento.version}</version> <version>${kurento.version}</version>
</dependency> </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> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>

View File

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

View File

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

View File

@@ -37,20 +37,6 @@
<groupId>com.acgist</groupId> <groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-kurento</artifactId> <artifactId>taoyao-webrtc-kurento</artifactId>
</dependency> </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> </dependencies>
</project> </project>

View File

@@ -21,6 +21,16 @@ public class Meeting {
*/ */
@Schema(title = "会议标识", description = "会议标识") @Schema(title = "会议标识", description = "会议标识")
private String id; 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) { public Meeting create(String sn) {
final Meeting meeting = new Meeting(); final Meeting meeting = new Meeting();
meeting.setId(this.idService.buildIdToString()); // meeting.setId(this.idService.buildIdToString());
meeting.setId("1");
meeting.setSns(new CopyOnWriteArrayList<>()); meeting.setSns(new CopyOnWriteArrayList<>());
meeting.setCreator(sn); meeting.setCreator(sn);
meeting.addSn(sn); meeting.addSn(sn);

View File

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

View File

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

View File

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

View File

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

View File

@@ -373,10 +373,6 @@
取消订阅终端媒体流(终端取消拉流) 取消订阅终端媒体流(终端取消拉流)
### 候选信令5004
IceCandidate
### 暂停信令5004 ### 暂停信令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 @Override
public boolean timeout(long timeout) { public boolean timeout(long timeout) {
return !this.authorized && System.currentTimeMillis() - this.time > timeout; return System.currentTimeMillis() - this.time > timeout;
} }
@Override @Override

View File

@@ -36,7 +36,7 @@ public class ClientSessionManager {
@Scheduled(cron = "${taoyao.scheduled.session:0 * * * * ?}") @Scheduled(cron = "${taoyao.scheduled.session:0 * * * * ?}")
public void scheduled() { public void scheduled() {
this.closeTimeoutSession(); this.closeTimeout();
} }
/** /**
@@ -65,7 +65,9 @@ public class ClientSessionManager {
* @param message 消息 * @param message 消息
*/ */
public void unicast(String to, Message 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()); message.getHeader().setSn(v.sn());
v.push(message); v.push(message);
}); });
@@ -77,7 +79,7 @@ public class ClientSessionManager {
* @param message 消息 * @param message 消息
*/ */
public void broadcast(Message message) { public void broadcast(Message message) {
this.sessions.forEach(v -> { this.sessions().forEach(v -> {
message.getHeader().setSn(v.sn()); message.getHeader().setSn(v.sn());
v.push(message); v.push(message);
}); });
@@ -90,7 +92,9 @@ public class ClientSessionManager {
* @param message 消息 * @param message 消息
*/ */
public void broadcast(String from, Message 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()); message.getHeader().setSn(v.sn());
v.push(message); v.push(message);
}); });
@@ -102,7 +106,7 @@ public class ClientSessionManager {
* @return 终端会话 * @return 终端会话
*/ */
public ClientSession session(String sn) { public ClientSession session(String sn) {
return this.sessions.stream() return this.sessions().stream()
.filter(v -> StringUtils.equals(sn, v.sn())) .filter(v -> StringUtils.equals(sn, v.sn()))
.findFirst() .findFirst()
.orElse(null); .orElse(null);
@@ -122,14 +126,18 @@ public class ClientSessionManager {
* @return 所有终端会话 * @return 所有终端会话
*/ */
public List<ClientSession> sessions() { public List<ClientSession> sessions() {
return this.sessions; return this.sessions.stream()
.filter(ClientSession::authorized)
.toList();
} }
/** /**
* @return 所有终端状态 * @return 所有终端状态
*/ */
public List<ClientSessionStatus> status() { 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("定时关闭超时会话"); log.debug("定时关闭超时会话");
this.sessions.stream() this.sessions.stream()
.filter(v -> !v.authorized())
.filter(v -> v.timeout(this.taoyaoProperties.getTimeout())) .filter(v -> v.timeout(this.taoyaoProperties.getTimeout()))
.forEach(v -> { .forEach(v -> {
log.debug("关闭超时会话:{}", 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; package com.acgist.taoyao.signal.event.media;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.acgist.taoyao.boot.model.Message; import com.acgist.taoyao.boot.model.Message;
@@ -26,10 +25,10 @@ public class MediaCandidateEvent extends ApplicationEventAdapter {
} }
/** /**
* @return 终端列表 * @return 接收终端标识
*/ */
public List<String> getSns() { public String getTo() {
return this.get("sns"); 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() { public String getTo() {
return this.get("to"); return this.get("to");

View File

@@ -25,7 +25,7 @@ public class MediaSubscribeEvent extends ApplicationEventAdapter {
} }
/** /**
* @return 终端标识(订阅的谁) * @return 接收终端标识
*/ */
public String getTo() { public String getTo() {
return this.get("to"); 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 @Protocol
public class MediaCandidateProtocol extends ProtocolMapAdapter { public class MediaCandidateProtocol extends ProtocolMapAdapter {
public static final Integer PID = 5004; public static final Integer PID = 5999;
public MediaCandidateProtocol() { public MediaCandidateProtocol() {
super(PID, "候选信令"); 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.Bean;
import org.springframework.context.annotation.Configuration; 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.MediaCandidateListener;
import com.acgist.taoyao.webrtc.mesh.listener.MediaOfferListener;
import com.acgist.taoyao.webrtc.mesh.listener.MediaPublishListener; import com.acgist.taoyao.webrtc.mesh.listener.MediaPublishListener;
import com.acgist.taoyao.webrtc.mesh.listener.MediaSubscribeListener; import com.acgist.taoyao.webrtc.mesh.listener.MediaSubscribeListener;
@@ -30,6 +32,18 @@ public class MeshAutoConfiguration {
return new MediaSubscribeListener(); return new MediaSubscribeListener();
} }
@Bean
@ConditionalOnMissingBean
public MediaOfferListener mediaOfferListener() {
return new MediaOfferListener();
}
@Bean
@ConditionalOnMissingBean
public MediaAnswerListener mediaAnswerListener() {
return new MediaAnswerListener();
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public MediaCandidateListener mediaCandidateListener() { 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; package com.acgist.taoyao.webrtc.mesh.listener;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import com.acgist.taoyao.boot.model.Message; import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.signal.event.media.MediaCandidateEvent; import com.acgist.taoyao.signal.event.media.MediaCandidateEvent;
import com.acgist.taoyao.signal.listener.MediaListenerAdapter; import com.acgist.taoyao.signal.listener.MediaListenerAdapter;
import lombok.extern.slf4j.Slf4j;
/** /**
* 候选监听 * 候选监听
* *
* @author acgist * @author acgist
*/ */
@Slf4j
public class MediaCandidateListener extends MediaListenerAdapter<MediaCandidateEvent> { public class MediaCandidateListener extends MediaListenerAdapter<MediaCandidateEvent> {
@Override @Override
public void onApplicationEvent(MediaCandidateEvent event) { public void onApplicationEvent(MediaCandidateEvent event) {
final String sn = event.getSn(); final String sn = event.getSn();
final List<String> sns = event.getSns(); final String to = event.getTo();
if(CollectionUtils.isEmpty(sns)) { if(sn.equals(to)) {
log.debug("忽略候选消息(相同终端):{}-{}", sn, to);
return; return;
} }
final Message message = event.getMessage(); final Message message = event.getMessage();
final Map<String, Object> mergeBody = event.mergeBody(); final Map<String, Object> mergeBody = event.mergeBody();
mergeBody.put("from", sn); 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.event.media.MediaPublishEvent;
import com.acgist.taoyao.signal.listener.MediaListenerAdapter; import com.acgist.taoyao.signal.listener.MediaListenerAdapter;
import lombok.extern.slf4j.Slf4j;
/** /**
* 发布监听 * 发布监听
* *
* @author acgist * @author acgist
*/ */
@Slf4j
public class MediaPublishListener extends MediaListenerAdapter<MediaPublishEvent> { public class MediaPublishListener extends MediaListenerAdapter<MediaPublishEvent> {
@Override @Override
public void onApplicationEvent(MediaPublishEvent event) { public void onApplicationEvent(MediaPublishEvent event) {
final String sn = event.getSn(); final String sn = event.getSn();
final String to = event.getTo(); final String to = event.getTo();
if(sn.equals(to)) {
log.debug("忽略发布消息(相同终端):{}-{}", sn, to);
return;
}
final Message message = event.getMessage(); final Message message = event.getMessage();
final Map<String, Object> mergeBody = event.mergeBody(); final Map<String, Object> mergeBody = event.mergeBody();
mergeBody.put("from", sn); 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.event.media.MediaSubscribeEvent;
import com.acgist.taoyao.signal.listener.MediaListenerAdapter; import com.acgist.taoyao.signal.listener.MediaListenerAdapter;
import lombok.extern.slf4j.Slf4j;
/** /**
* 订阅监听 * 订阅监听
* *
* @author acgist * @author acgist
*/ */
@Slf4j
public class MediaSubscribeListener extends MediaListenerAdapter<MediaSubscribeEvent> { public class MediaSubscribeListener extends MediaListenerAdapter<MediaSubscribeEvent> {
@Override @Override
public void onApplicationEvent(MediaSubscribeEvent event) { public void onApplicationEvent(MediaSubscribeEvent event) {
final String sn = event.getSn(); final String sn = event.getSn();
final String to = event.getTo(); final String to = event.getTo();
if(sn.equals(to)) {
log.debug("忽略订阅消息(相同终端):{}-{}", sn, to);
return;
}
final Message message = event.getMessage(); final Message message = event.getMessage();
final Map<String, Object> mergeBody = event.mergeBody(); final Map<String, Object> mergeBody = event.mergeBody();
mergeBody.put("from", sn); mergeBody.put("from", sn);