[+] 终端

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

@@ -1,14 +1,18 @@
# 桃夭
基于WebRTC实现信令服务实现Mesh、MCU和SFU三种媒体通信架构支持直播会议两种场景。
项目提供WebRTC服务信令终端已有H5示例其他终端需要自己实现。
## 授权
开源公益免费,商用需要购买授权。
## 模块
|模块|名称|描述|
|:--|:--|:--|
|taoyao|桃夭|桃之夭夭灼灼其华|
|taoyao-nat|内网穿透|STUN/TURN|
|taoyao-boot|基础|启动模块|
|taoyao-boot|启动模块|基础模块|
|taoyao-live|直播|直播、连麦|
|taoyao-test|测试|测试工具|
|taoyao-media|媒体|录制、视频美颜、AI识别、音频混音、变声、降噪|
@@ -16,12 +20,41 @@
|taoyao-server|服务|启动服务|
|taoyao-meeting|会议|会议模式、广播模式、单人对讲|
|taoyao-webrtc|WebRTC模块|WebRTC模块|
|taoyao-webrtc-jni|WebRTC JNI|WebRTC本地接口|
|taoyao-webrtc-sfu|WebRTC SFU架构|SFU架构|
|taoyao-webrtc-mcu|WebRTC MCU架构|MCU架构|
|taoyao-webrtc-mesh|WebRTC MESH架构|MESH架构|
|taoyao-webrtc-jitsi|WebRTC协议簇jitsi实现|WebRTC协议簇jitsi实现|
|taoyao-webrtc-kurento|WebRTC协议簇kurento实现|WebRTC协议簇kurento实现|
## STUN/TURN公共服务
> 终端负责推流,服务端负责处理媒体流,这些功能也可以在终端实现。主次码流没在终端实现,服务端实现可以有更多选择。
## 模块关系
```
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| taoyao-server |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| taoyao-live | taoyao-meeting |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| taoyao-signal |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| taoyao-media |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| taoyao-mcu | taoyao-sfu | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ taoyao-mesh +
| taoyao-jitsi | taoyao-kurento | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| taoyao-boot |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
## 内网穿透
请用公共STUN/TURN服务或者自行搭建coturn服务。
> 只有公网Mesh架构才需要真正的内网穿透
### STUN/TURN公共服务地址
```
stun:stun1.l.google.com:19302
@@ -33,20 +66,35 @@ stun:stun.stunprotocol.org:3478
## 信令
|功能|描述|
|:--|:--|
|注册|终端注册(同步信息)|
|关闭|终端关闭(注销)|
|心跳|终端心跳|
|进入会议|没有会议自动创建|
|离开会议|离开会议|
|关闭会议|关闭会议(所有人员离开)|
|邀请终端|会议邀请终端|
|踢出终端|会议踢出终端|
|推流|控制终端推流|
|暂停推流|控制终端暂停推流|
|订阅(分流)|控制终端暂停推流|
|暂停订阅(分流)|控制终端暂停推流|
|功能|描述|标识|响应|
|:--|:--|:--|:--|
|注册|终端注册(同步信息)|||
|关闭|终端关闭(注销)|||
|心跳|终端心跳|||
|创建会议|创建会议||返回会议ID|
|进入会议|没有会议自动创建||返回会议终端同时广播进入消息|
|离开会议|离开会议||广播离开消息|
|关闭会议|关闭会议(踢出所有人员)||广播关闭消息|
|终端列表|||返回所有终端列表|
|会议终端列表|||返回所有会议终端列表|
|直播终端列表|||返回所有直播终端列表|
|邀请终端|会议邀请终端(主动/被动)||单播邀请|
|踢出终端|会议踢出终端||单播踢出|
|开启直播||||
|关闭直播||||
|发布|控制终端推流|||
|取消发布|控制终端暂停推流|||
|订阅|订阅终端媒体流|||
|取消订阅|取消订阅终端媒体流|||
|暂停媒体流|暂停终端媒体流分流(不关媒体流通道)|||
|恢复媒体流|恢复终端媒体流分流(不关媒体流通道)|||
|开启录像||||
|关闭录像||||
|终端状态||||
|单播消息|发送指定终端|||
|广播消息|广播排除自己的所有终端|||
|全员广播消息|广播包括自己的所有终端|||
|异常|异常信息|||
## 直播
@@ -58,6 +106,8 @@ stun:stun.stunprotocol.org:3478
流媒体点对点连接,不经过服务端。
> 录制、AI识别等等功能只能在终端实现。
### MCU
终端推流到服务端,由服务端分流并且混音。
@@ -70,4 +120,4 @@ stun:stun.stunprotocol.org:3478
```
keytool -genkeypair -keyalg RSA -dname "CN=localhost, OU=acgist, O=taoyao, L=GZ, ST=GD, C=CN" -alias taoyao -validity 3650 -ext ku:c=dig,keyE -ext eku=serverAuth -ext SAN=dns:localhost,ip:127.0.0.1 -keystore taoyao.jks -keypass 123456 -storepass 123456
```
```

View File

@@ -12,14 +12,14 @@
<fileSets>
<fileSet>
<directory>${system.maven.basedir}/docs</directory>
<directory>${taoyao.maven.basedir}/docs</directory>
<outputDirectory>./</outputDirectory>
<includes>
<include>README.md</include>
</includes>
</fileSet>
<fileSet>
<directory>${system.maven.basedir}/docs/bin</directory>
<directory>${taoyao.maven.basedir}/docs/bin</directory>
<outputDirectory>bin</outputDirectory>
<fileMode>0755</fileMode>
<filtered>true</filtered>

View File

@@ -12,14 +12,14 @@
<fileSets>
<fileSet>
<directory>${system.maven.basedir}/docs</directory>
<directory>${taoyao.maven.basedir}/docs</directory>
<outputDirectory>./</outputDirectory>
<includes>
<include>README.md</include>
</includes>
</fileSet>
<fileSet>
<directory>${system.maven.basedir}/docs/bin</directory>
<directory>${taoyao.maven.basedir}/docs/bin</directory>
<outputDirectory>bin</outputDirectory>
<fileMode>0755</fileMode>
<filtered>true</filtered>

View File

@@ -12,14 +12,14 @@
<fileSets>
<fileSet>
<directory>${system.maven.basedir}/docs</directory>
<directory>${taoyao.maven.basedir}/docs</directory>
<outputDirectory>./</outputDirectory>
<includes>
<include>README.md</include>
</includes>
</fileSet>
<fileSet>
<directory>${system.maven.basedir}/docs/bin</directory>
<directory>${taoyao.maven.basedir}/docs/bin</directory>
<outputDirectory>bin</outputDirectory>
<fileMode>0755</fileMode>
<filtered>true</filtered>

View File

@@ -20,10 +20,10 @@ fi
# 启动参数
JAVA_OPTS_GC="-XX:+UseG1GC -Xlog:gc:./logs/gc.log:time,level"
JAVA_OPTS_MEM="-server ${system.maven.jvm.mem}"
JAVA_OPTS_EXT="-Dfile.encoding=${system.maven.encoding} -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true"
JAVA_OPTS_MEM="-server ${taoyao.maven.jvm.mem}"
JAVA_OPTS_EXT="-Dfile.encoding=${taoyao.maven.encoding} -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true"
JAVA_OPTS_APP="-Dspring.profiles.active=${profile}"
JAVA_OPTS="$JAVA_OPTS_MEM $JAVA_OPTS_EXT $JAVA_OPTS_APP ${system.maven.jvm.arg}"
JAVA_OPTS="$JAVA_OPTS_MEM $JAVA_OPTS_EXT $JAVA_OPTS_APP ${taoyao.maven.jvm.arg}"
echo "启动参数:$JAVA_OPTS"
# 启动应用

BIN
docs/taoyao-v1.0.zip Normal file

Binary file not shown.

72
pom.xml
View File

@@ -24,18 +24,19 @@
<!-- 版本 -->
<java.version>17</java.version>
<lombok.version>1.18.24</lombok.version>
<webrtc.version>1.0.32006</webrtc.version>
<kurento.version>6.18.0</kurento.version>
<springdoc.version>1.6.12</springdoc.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<collections4.version>4.4</collections4.version>
<jitsi.ice4j.version>3.0-59-g71e244d</jitsi.ice4j.version>
<jitsi.libjitsi.version>1.1-22-g5c9346c5</jitsi.libjitsi.version>
<!-- 配置 -->
<system.maven.basedir>${project.basedir}</system.maven.basedir>
<system.maven.encoding>UTF-8</system.maven.encoding>
<system.maven.skip.assembly>true</system.maven.skip.assembly>
<taoyao.maven.basedir>${project.basedir}</taoyao.maven.basedir>
<taoyao.maven.encoding>UTF-8</taoyao.maven.encoding>
<taoyao.maven.skip.assembly>true</taoyao.maven.skip.assembly>
</properties>
<modules>
<module>taoyao-nat</module>
<module>taoyao-boot</module>
<module>taoyao-live</module>
<module>taoyao-test</module>
@@ -103,11 +104,6 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-nat</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-boot</artifactId>
@@ -148,11 +144,6 @@
<artifactId>taoyao-webrtc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-jni</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-sfu</artifactId>
@@ -168,11 +159,32 @@
<artifactId>taoyao-webrtc-mesh</artifactId>
<version>${project.version}</version>
</dependency>
<!-- WebRTC -->
<dependency>
<groupId>org.webrtc</groupId>
<artifactId>google-webrtc</artifactId>
<version>${webrtc.version}</version>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-jitsi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-kurento</artifactId>
<version>${project.version}</version>
</dependency>
<!-- WebRTCjitsi -->
<dependency>
<groupId>org.jitsi</groupId>
<artifactId>ice4j</artifactId>
<version>${jitsi.ice4j.version}</version>
</dependency>
<dependency>
<groupId>org.jitsi</groupId>
<artifactId>libjitsi</artifactId>
<version>${jitsi.libjitsi.version}</version>
</dependency>
<!-- WebRTCkurento -->
<dependency>
<groupId>org.kurento</groupId>
<artifactId>kurento-client</artifactId>
<version>${kurento.version}</version>
</dependency>
<!-- 集合工具 -->
<dependency>
@@ -243,7 +255,7 @@
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${system.maven.encoding}</encoding>
<encoding>${taoyao.maven.encoding}</encoding>
</configuration>
</plugin>
<plugin>
@@ -276,7 +288,7 @@
</goals>
<configuration>
<attach>false</attach>
<skipAssembly>${system.maven.skip.assembly}</skipAssembly>
<skipAssembly>${taoyao.maven.skip.assembly}</skipAssembly>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
@@ -294,8 +306,8 @@
</activation>
<properties>
<profile>dev</profile>
<system.maven.jvm.arg></system.maven.jvm.arg>
<system.maven.jvm.mem>-Xms512M -Xmx1024M -XX:NewRatio=1 -XX:SurvivorRatio=2</system.maven.jvm.mem>
<taoyao.maven.jvm.arg></taoyao.maven.jvm.arg>
<taoyao.maven.jvm.mem>-Xms512M -Xmx1024M -XX:NewRatio=1 -XX:SurvivorRatio=2</taoyao.maven.jvm.mem>
</properties>
<build>
<plugins>
@@ -304,7 +316,7 @@
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>${system.maven.basedir}/docs/assembly/dev.xml</descriptor>
<descriptor>${taoyao.maven.basedir}/docs/assembly/dev.xml</descriptor>
</descriptors>
</configuration>
</plugin>
@@ -324,8 +336,8 @@
<id>test</id>
<properties>
<profile>test</profile>
<system.maven.jvm.arg></system.maven.jvm.arg>
<system.maven.jvm.mem>-Xms512M -Xmx1024M -XX:NewRatio=1 -XX:SurvivorRatio=2</system.maven.jvm.mem>
<taoyao.maven.jvm.arg></taoyao.maven.jvm.arg>
<taoyao.maven.jvm.mem>-Xms512M -Xmx1024M -XX:NewRatio=1 -XX:SurvivorRatio=2</taoyao.maven.jvm.mem>
</properties>
<build>
<plugins>
@@ -334,7 +346,7 @@
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>${system.maven.basedir}/docs/assembly/test.xml</descriptor>
<descriptor>${taoyao.maven.basedir}/docs/assembly/test.xml</descriptor>
</descriptors>
</configuration>
</plugin>
@@ -355,8 +367,8 @@
<id>release</id>
<properties>
<profile>release</profile>
<system.maven.jvm.arg>-Dtaoyao.password=123456</system.maven.jvm.arg>
<system.maven.jvm.mem>-Xms2048M -Xmx4096M -XX:NewRatio=1 -XX:SurvivorRatio=2</system.maven.jvm.mem>
<taoyao.maven.jvm.arg>-Dtaoyao.password=123456</taoyao.maven.jvm.arg>
<taoyao.maven.jvm.mem>-Xms2048M -Xmx4096M -XX:NewRatio=1 -XX:SurvivorRatio=2</taoyao.maven.jvm.mem>
</properties>
<build>
<plugins>
@@ -365,7 +377,7 @@
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>${system.maven.basedir}/docs/assembly/release.xml</descriptor>
<descriptor>${taoyao.maven.basedir}/docs/assembly/release.xml</descriptor>
</descriptors>
</configuration>
</plugin>

View File

@@ -75,7 +75,7 @@ import lombok.extern.slf4j.Slf4j;
@Configuration
@EnableScheduling
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableConfigurationProperties({ IdProperties.class, TaoyaoProperties.class, WebrtcProperties.class, SecurityProperties.class })
@EnableConfigurationProperties({ IdProperties.class, MediaProperties.class, TaoyaoProperties.class, WebrtcProperties.class, SecurityProperties.class })
public class BootAutoConfiguration {
@Value("${spring.application.name:taoyao}")

View File

@@ -0,0 +1,56 @@
package com.acgist.taoyao.boot.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
/**
* 音频配置
*
* @author acgist
*/
@Getter
@Setter
@Schema(title = "音频配置", description = "音频配置")
public class MediaAudioProperties {
/**
* 音频格式
*
* @author acgist
*/
public enum Format {
/**
* ACC
*/
ACC,
/**
* PCM
*/
PCM,
/**
* OPUS
*/
OPUS;
}
/**
* 格式
*/
@Schema(title = "格式", description = "格式")
private Format format;
/**
* 采样率
* 8000|16000|32000|48000
*/
@Schema(title = "采样率", description = "采样率", example = "8000|16000|32000|48000")
private Integer samplerate;
/**
* 采样数
*/
@Schema(title = "采样数", description = "采样数", example = "16")
private Integer samplesize;
}

View File

@@ -0,0 +1,29 @@
package com.acgist.taoyao.boot.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
/**
* 媒体配置
*
* @author acgist
*/
@Getter
@Setter
@Schema(title = "媒体配置", description = "媒体配置")
@ConfigurationProperties(prefix = "taoyao.media")
public class MediaProperties {
/**
* 音频配置
*/
private MediaAudioProperties audio;
/**
* 视频配置
*/
private MediaVideoProperties video;
}

View File

@@ -0,0 +1,96 @@
package com.acgist.taoyao.boot.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
/**
* 视频配置
*
* @author acgist
*/
@Getter
@Setter
@Schema(title = "视频配置", description = "视频配置")
public class MediaVideoProperties {
/**
* 视频格式
*
* @author acgist
*/
public enum Format {
/**
* VP8
*/
VP8,
/**
* VP9
*/
VP9,
/**
* H264
*/
H264,
/**
* H265
*/
H265;
}
/**
* 格式
*/
@Schema(title = "格式", description = "格式")
private Format format;
/**
* 码率(画质)
*/
@Schema(title = "码率", description = "码率影响画质", example = "600|1200|1500|1800")
private Integer bitrate;
/**
* 帧率(流畅)
*/
@Schema(title = "帧率", description = "帧率影响流程", example = "20|24|30|60")
private Integer framerate;
/**
* 分辨率(画面大小)
*/
@Schema(title = "分辨率", description = "分辨率影响画面大小", example = "1920*1080|1280*720|480*360")
private String resolution;
/**
* 宽度
*/
@Schema(title = "宽度", description = "宽度")
private Integer width;
/**
* 高度
*/
@Schema(title = "高度", description = "高度")
private Integer height;
/**
* @return 宽度
*/
public Integer getWidth() {
if(this.width == null) {
final int index = this.resolution.indexOf('*');
this.width = Integer.valueOf(this.resolution.substring(0, index).strip());
}
return this.width;
}
/**
* @return 高度
*/
public Integer getHeight() {
if(this.height == null) {
final int index = this.resolution.indexOf('*');
this.height = Integer.valueOf(this.resolution.substring(index + 1).strip());
}
return this.height;
}
}

View File

@@ -29,7 +29,7 @@ public class SecurityProperties {
*/
private String[] permit;
/**
* 用户
* 名称
*/
private String username;
/**

View File

@@ -18,11 +18,11 @@ import lombok.Setter;
public class WebrtcProperties {
/**
* 架构
* 架构
*
* @author acgist
*/
public enum Type {
public enum Model {
/**
* SFU架构
@@ -40,10 +40,33 @@ public class WebrtcProperties {
}
/**
* 类型
* 基础框架
*
* @author acgist
*/
@Schema(title = "架构类型", description = "WebRTC架构类型")
private Type type;
public enum Framework {
/**
* jitsi
*/
JITSI,
/**
* kurento
*/
KURENTO;
}
/**
* 模型
*/
@Schema(title = "架构模型", description = "WebRTC架构模型")
private Model model;
/**
* 框架
*/
@Schema(title = "基础框架", description = "WebRTC基础框架")
private Framework framework;
/**
* stun服务器
*/

View File

@@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="false" scanPeriod="60 seconds" debug="false">
<springProperty scope="context" name="system.name" source="spring.application.name" />
<springProperty scope="context" name="log.name" source="spring.application.name" />
<contextName>${system.name}</contextName>
<property name="system.path" value="logs" />
<property name="system.queue" value="2048" />
<property name="system.buffer" value="8192" />
<property name="system.history" value="30" />
<property name="system.charset" value="UTF-8" />
<property name="system.pattern" value="[${system.name}] %d{YYYY-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} %file:%line - %m%n" />
<contextName>${log.name}</contextName>
<property name="log.path" value="logs" />
<property name="log.queue" value="2048" />
<property name="log.buffer" value="8192" />
<property name="log.history" value="30" />
<property name="log.charset" value="UTF-8" />
<property name="log.pattern" value="[${log.name}] %d{YYYY-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} %file:%line - %m%n" />
<appender name="fileDebug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${system.path}/${system.name}.debug.log</file>
<file>${log.path}/${log.name}.debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<maxHistory>${system.history}</maxHistory>
<fileNamePattern>${system.path}/%d{yyyy-MM, aux}/${system.name}.debug.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>${log.history}</maxHistory>
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/${log.name}.debug.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>
<bufferSize>${system.buffer}</bufferSize>
<bufferSize>${log.buffer}</bufferSize>
<immediateFlush>false</immediateFlush>
<encoder>
<charset>${system.charset}</charset>
<pattern>${system.pattern}</pattern>
<charset>${log.charset}</charset>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
@@ -29,22 +29,22 @@
</appender>
<appender name="fileDebugAsync" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="fileDebug" />
<queueSize>${system.queue}</queueSize>
<queueSize>${log.queue}</queueSize>
<includeCallerData>true</includeCallerData>
<discardingThreshold>0</discardingThreshold>
</appender>
<appender name="fileInfo" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${system.path}/${system.name}.info.log</file>
<file>${log.path}/${log.name}.info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<maxHistory>${system.history}</maxHistory>
<fileNamePattern>${system.path}/%d{yyyy-MM, aux}/${system.name}.info.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>${log.history}</maxHistory>
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/${log.name}.info.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>
<bufferSize>${system.buffer}</bufferSize>
<bufferSize>${log.buffer}</bufferSize>
<immediateFlush>false</immediateFlush>
<encoder>
<charset>${system.charset}</charset>
<pattern>${system.pattern}</pattern>
<charset>${log.charset}</charset>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
@@ -52,22 +52,22 @@
</appender>
<appender name="fileInfoAsync" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="fileInfo" />
<queueSize>${system.queue}</queueSize>
<queueSize>${log.queue}</queueSize>
<includeCallerData>true</includeCallerData>
<discardingThreshold>0</discardingThreshold>
</appender>
<appender name="fileError" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${system.path}/${system.name}.error.log</file>
<file>${log.path}/${log.name}.error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<maxHistory>${system.history}</maxHistory>
<fileNamePattern>${system.path}/%d{yyyy-MM, aux}/${system.name}.error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>${log.history}</maxHistory>
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/${log.name}.error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>
<bufferSize>${system.buffer}</bufferSize>
<bufferSize>${log.buffer}</bufferSize>
<immediateFlush>false</immediateFlush>
<encoder>
<charset>${system.charset}</charset>
<pattern>${system.pattern}</pattern>
<charset>${log.charset}</charset>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
@@ -75,15 +75,15 @@
</appender>
<appender name="fileErrorAsync" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="fileError" />
<queueSize>${system.queue}</queueSize>
<queueSize>${log.queue}</queueSize>
<includeCallerData>true</includeCallerData>
<discardingThreshold>0</discardingThreshold>
</appender>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>${system.charset}</charset>
<pattern>${system.pattern}</pattern>
<charset>${log.charset}</charset>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>

View File

@@ -18,8 +18,8 @@
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<groupId>com.acgist</groupId>
<artifactId>taoyao-signal</artifactId>
</dependency>
</dependencies>

View File

@@ -14,8 +14,29 @@
<packaging>jar</packaging>
<name>taoyao-media</name>
<description>媒体录制、视频美颜、AI识别、音频混音、变声</description>
<description>媒体录制、视频美颜、AI识别、音频混音、变声、降噪</description>
<dependencies></dependencies>
<dependencies>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-mcu</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-sfu</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-mesh</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-jitsi</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-kurento</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -18,13 +18,8 @@
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<groupId>com.acgist</groupId>
<artifactId>taoyao-signal</artifactId>
</dependency>
</dependencies>

View File

@@ -0,0 +1,10 @@
package com.acgist.taoyao.meeting.room;
/**
* 房间
*
* @author acgist
*/
public class Room {
}

View File

@@ -1,3 +0,0 @@
# 内网穿透
请用公共STUN/TURN服务或者自行搭建coturn服务。

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.acgist</groupId>
<artifactId>taoyao</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>taoyao-nat</artifactId>
<packaging>jar</packaging>
<name>taoyao-net</name>
<description>内网穿透STUN/TURN</description>
<dependencies></dependencies>
</project>

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>

View File

@@ -19,7 +19,7 @@
<dependencies>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-boot</artifactId>
<artifactId>taoyao-media</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -3,6 +3,7 @@ package com.acgist.taoyao.signal.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import com.acgist.taoyao.signal.session.websocket.WebSocketSignal;
@@ -13,6 +14,7 @@ import com.acgist.taoyao.signal.session.websocket.WebSocketSignal;
* @author acgist
*/
@Configuration
@EnableWebSocket
public class SignalAutoConfiguration {
@Bean

View File

@@ -9,22 +9,30 @@ public interface ClientMediaHandler {
/**
* 打开
*
* @param id 终端媒体流ID
*/
void open(String id);
/**
* 暂停
*
* @param id 终端媒体流ID
*/
void pause();
void pause(String id);
/**
* 恢复
*
* @param id 终端媒体流ID
*/
void resume();
void resume(String id);
/**
* 关闭
*
* @param id 终端媒体流ID
*/
void close();
void close(String id);
}

View File

@@ -1,14 +1,81 @@
package com.acgist.taoyao.signal.media;
/**
* 终端推流
*/
public class ClientMediaPublisher {
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.acgist.taoyao.signal.media.stream.ClientMediaStream;
import lombok.extern.slf4j.Slf4j;
/**
* 终端媒体流发布者(终端推流)
*
* @author acgist
*/
@Slf4j
public class ClientMediaPublisher implements ClientMediaHandler {
/**
* 发布终端媒体流
*/
private Map<String, ClientMediaStream> streams = new ConcurrentHashMap<>();
/**
* 发布
*
* @param id 终端媒体流ID
*
* @see #open(String)
*/
public void publish(String id) {
this.open(id);
}
/**
* 取消发布
*
* @param id 终端媒体流ID
*
* @see #close(String)
*/
public void unpublish(String id) {
this.close(id);
}
@Override
public void open(String id) {
// TODO Auto-generated method stub
}
@Override
public void pause(String id) {
// TODO Auto-generated method stub
}
@Override
public void resume(String id) {
final ClientMediaStream stream = this.streams.get(id);
if(stream != null) {
try {
stream.resume();
} catch (IOException e) {
log.error("终端媒体流恢复异常:{}", id, e);
}
}
}
@Override
public void close(String id) {
final ClientMediaStream stream = this.streams.get(id);
try {
stream.close();
} catch (IOException e) {
log.error("终端媒体流关闭异常:{}", id, e);
}
}
}

View File

@@ -1,5 +1,10 @@
package com.acgist.taoyao.signal.media;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import com.acgist.taoyao.signal.media.stream.ClientMediaStream;
/**
* 终端媒体订阅者(终端拉流)
*
@@ -7,10 +12,31 @@ package com.acgist.taoyao.signal.media;
*/
public class ClientMediaSubscriber implements ClientMediaHandler {
/**
* 订阅终端媒体流
*/
private List<ClientMediaStream> streams = new CopyOnWriteArrayList<>();
/**
* 订阅
*
* @param id 终端媒体流ID
*
* @see #open(String)
*/
public void subscribe(String id) {
this.open(id);
}
/**
* 取消订阅
*
* @param id 终端媒体流ID
*
* @see #close(String)
*/
public void unsubscribe(String id) {
this.close(id);
}
@Override
@@ -20,19 +46,19 @@ public class ClientMediaSubscriber implements ClientMediaHandler {
}
@Override
public void pause() {
public void pause(String id) {
// TODO Auto-generated method stub
}
@Override
public void resume() {
public void resume(String id) {
// TODO Auto-generated method stub
}
@Override
public void close() {
public void close(String id) {
// TODO Auto-generated method stub
}

View File

@@ -1,5 +1,7 @@
package com.acgist.taoyao.signal.media.stream;
import java.io.IOException;
/**
* 终端媒体流
*
@@ -58,23 +60,31 @@ public interface ClientMediaStream {
/**
* 打开终端媒体流
*
* @throws IO异常
*/
void open();
void open() throws IOException;
/**
* 暂停终端媒体流
*
* @throws IO异常
*/
void pause();
void pause() throws IOException;
/**
* 恢复终端媒体流
*
* @throws IO异常
*/
void resume();
void resume() throws IOException;
/**
* 关闭终端媒体流
*
* @throws IO异常
*/
void close();
void close() throws IOException;
/**
* @return 终端媒体流类型

View File

@@ -1,7 +1,57 @@
# WebRTC
## 安装
## WebRTC协议栈
## 观察者
|协议|描述|
|:--|:--|
|UDP|基础协议|
|DTLS|UDP数据包传输层安全性协议|
|RTP|实时传输协议(音频视频)|
|SRTP|RTP + DTLS|
|RTCP|RTP传输控制协议监控数据传输质量并给予数据发送方反馈|
|SRTCP|RTCP + DTLS|
|SCTP|流控制传输协议(自定义的应用数据传输)|
|STUN/TURN|内网穿透协议|
订阅发布
## ICE/SIP/SDP
ICE信息的描述格式通常采用标准的SDP其全称为Session Description Protocol即会话描述协议。
SDP只是一种信息格式的描述标准不属于传输协议但是可以被其他传输协议用来交换必要的信息例如SIP、RTSP等等。
## 协议关系
```
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| HTTPS/WSS | | SCTP | SRTP/SRTCP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ICE/SIP/SDP +-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TLS | | DTLS |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| HTTP/WS | STUN/TURN | RTP/RTCP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TCP | UDP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| IPv4/IPv6 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
## 其他协议
|协议|描述|
|:--|:--|
|HLS|基于HTTP的自适应码率流媒体传输协议|
|RTSP|可以控制媒体(点播)|
|RTMP|实时消息传送协议|
## 通道
|通道类型|协议|
|:--|:--|
|信令通道|自己实现|
|会话通道|SIP/SDP|
|媒体通道|RTP/RTCP/SRTP/SRTCP|
## WebRTC资料
[GB28181](https://blog.csdn.net/jisuanji111111/article/details/121634199)
[WebRTC协议](http://www.manoner.com/post/音视频基础/WebRTC核心组件和协议栈/)
[WebRTC开源项目](https://blog.csdn.net/ababab12345/article/details/115585378)

View File

@@ -17,10 +17,18 @@
<description>WebRTC模块</description>
<modules>
<module>taoyao-webrtc-jni</module>
<module>taoyao-webrtc-sfu</module>
<module>taoyao-webrtc-mcu</module>
<module>taoyao-webrtc-mesh</module>
<module>taoyao-webrtc-jitsi</module>
<module>taoyao-webrtc-kurento</module>
</modules>
<dependencies>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-boot</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>taoyao-webrtc-jitsi</artifactId>
<packaging>jar</packaging>
<name>taoyao-webrtc-jitsi</name>
<description>WebRTC协议簇实现jitsi</description>
<dependencies>
<dependency>
<groupId>org.jitsi</groupId>
<artifactId>ice4j</artifactId>
</dependency>
<dependency>
<groupId>org.jitsi</groupId>
<artifactId>libjitsi</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -10,13 +10,17 @@
<version>1.0.0</version>
</parent>
<artifactId>taoyao-webrtc-jni</artifactId>
<artifactId>taoyao-webrtc-kurento</artifactId>
<packaging>jar</packaging>
<name>taoyao-webrtc-jni</name>
<description>WebRTC JNIWebRTC本地接口</description>
<name>taoyao-webrtc-kurento</name>
<description>WebRTC协议簇实现kurento</description>
<dependencies>
<dependency>
<groupId>org.kurento</groupId>
<artifactId>kurento-client</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -19,7 +19,11 @@
<dependencies>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-jni</artifactId>
<artifactId>taoyao-webrtc-jitsi</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-kurento</artifactId>
</dependency>
</dependencies>

View File

@@ -19,7 +19,11 @@
<dependencies>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-jni</artifactId>
<artifactId>taoyao-webrtc-jitsi</artifactId>
</dependency>
<dependency>
<groupId>com.acgist</groupId>
<artifactId>taoyao-webrtc-kurento</artifactId>
</dependency>
</dependencies>