Merge branch 'dev' into release
This commit is contained in:
@@ -10,14 +10,14 @@ Maven >= 3.8.0
|
|||||||
CMake >= 3.26.0
|
CMake >= 3.26.0
|
||||||
NodeJS >= v18.16.0
|
NodeJS >= v18.16.0
|
||||||
Python >= 3.8.0 with PIP
|
Python >= 3.8.0 with PIP
|
||||||
ffmpeg >= 4.3.0
|
FFmpeg >= 4.3.0
|
||||||
gcc/g++ >= 10.2.0
|
GCC/G++ >= 10.2.0
|
||||||
Android >= 9.0
|
Android >= 9.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Debian
|
## Debian
|
||||||
|
|
||||||
`CentOS 7`实在是太旧了,软件更新非常麻烦,所以直接使用`Debian`作为测试,系统配置全部使用`root`用户。
|
`CentOS 7`实在是太旧了,软件更新非常麻烦,所以直接使用`Debian`作为测试。
|
||||||
|
|
||||||
### 系统参数
|
### 系统参数
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ set nocompatible
|
|||||||
vi /etc/network/interfaces
|
vi /etc/network/interfaces
|
||||||
|
|
||||||
---
|
---
|
||||||
|
auto enp0s3
|
||||||
iface enp0s3 inet static
|
iface enp0s3 inet static
|
||||||
address 192.168.1.110
|
address 192.168.1.110
|
||||||
gateway 192.168.1.1
|
gateway 192.168.1.1
|
||||||
@@ -80,6 +81,17 @@ ifup enp0s3
|
|||||||
### 设置国内镜像
|
### 设置国内镜像
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# DNS
|
||||||
|
sudo vim /etc/systemd/resolved.conf
|
||||||
|
|
||||||
|
---
|
||||||
|
DNS=233.5.5.5 233.6.6.6 114.114.114.114 8.8.8.8
|
||||||
|
---
|
||||||
|
|
||||||
|
sudo systemctl restart systemd-resolved
|
||||||
|
sudo systemctl enable systemd-resolved
|
||||||
|
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
|
||||||
|
|
||||||
# 配置
|
# 配置
|
||||||
vi /etc/apt/sources.list
|
vi /etc/apt/sources.list
|
||||||
|
|
||||||
@@ -96,6 +108,7 @@ deb-src https://mirrors.aliyun.com/debian/ bullseye-backports main non-free cont
|
|||||||
|
|
||||||
# 更新系统
|
# 更新系统
|
||||||
apt update
|
apt update
|
||||||
|
apt upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
### 安装依赖
|
### 安装依赖
|
||||||
@@ -177,7 +190,7 @@ sudo apt install git
|
|||||||
git --version
|
git --version
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装gcc/g++
|
## 安装GCC/G++
|
||||||
|
|
||||||
```
|
```
|
||||||
# 安装
|
# 安装
|
||||||
@@ -335,7 +348,7 @@ trusted-host = mirrors.aliyun.com
|
|||||||
pip config list
|
pip config list
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装ffmpeg
|
## 安装FFmpeg
|
||||||
|
|
||||||
```
|
```
|
||||||
mkdir -p /data/dev/ffmpeg ; cd $_
|
mkdir -p /data/dev/ffmpeg ; cd $_
|
||||||
@@ -407,6 +420,15 @@ PKG_CONFIG_PATH="/usr/local/lib/pkgconfig/"
|
|||||||
--enable-encoder=libvpx_vp9 --enable-decoder=vp9 --enable-parser=vp9
|
--enable-encoder=libvpx_vp9 --enable-decoder=vp9 --enable-parser=vp9
|
||||||
make && sudo make install
|
make && sudo make install
|
||||||
|
|
||||||
|
# 链接文件
|
||||||
|
vim /etc/ld.so.conf
|
||||||
|
|
||||||
|
---
|
||||||
|
/usr/local/lib/
|
||||||
|
---
|
||||||
|
|
||||||
|
ldconfig
|
||||||
|
|
||||||
# 验证
|
# 验证
|
||||||
ffmpeg -version
|
ffmpeg -version
|
||||||
ffmpeg -decoders
|
ffmpeg -decoders
|
||||||
@@ -488,7 +510,7 @@ pm2 start|stop|restart taoyao-client-media
|
|||||||
|
|
||||||
> 下载依赖建议备份方便再次编译使用
|
> 下载依赖建议备份方便再次编译使用
|
||||||
|
|
||||||
### Mediasoup单独编译
|
### Mediasoup单独编译(旧版)
|
||||||
|
|
||||||
编译媒体服务时会自动编译`mediasoup`所以忽略单独编译
|
编译媒体服务时会自动编译`mediasoup`所以忽略单独编译
|
||||||
|
|
||||||
@@ -502,6 +524,16 @@ make
|
|||||||
make clean
|
make clean
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Mediasoup单独编译(新版)
|
||||||
|
|
||||||
|
* 需要`python3`和`pip3`
|
||||||
|
* 源码[mediasoup-3.13.16.zip](https://pan.baidu.com/s/1E_DXv32D9ODyj5J-o-ji_g?pwd=hudc)(包含依赖)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
node npm-scripts.mjs worker:build
|
||||||
|
```
|
||||||
|
|
||||||
## 安装Web终端
|
## 安装Web终端
|
||||||
|
|
||||||
`Nginx`和`PM2`选择一种启动即可
|
`Nginx`和`PM2`选择一种启动即可
|
||||||
@@ -625,7 +657,7 @@ openssl pkcs12 -export -clcerts -in server.crt -inkey server.key -out server.p12
|
|||||||
# 设置密码:-deststorepass 123456
|
# 设置密码:-deststorepass 123456
|
||||||
```
|
```
|
||||||
|
|
||||||
## gcc/g++路径配置
|
## GCC/G++路径配置
|
||||||
|
|
||||||
```
|
```
|
||||||
# 安装路径
|
# 安装路径
|
||||||
|
|||||||
@@ -37,6 +37,12 @@
|
|||||||
* WebRtcAudioRecord
|
* WebRtcAudioRecord
|
||||||
* WebRtcAudioTrack
|
* WebRtcAudioTrack
|
||||||
|
|
||||||
|
## 动态修改码率
|
||||||
|
|
||||||
|
```
|
||||||
|
HardwareVideoEncoder#updateBitrate
|
||||||
|
```
|
||||||
|
|
||||||
## 学习资料
|
## 学习资料
|
||||||
|
|
||||||
* https://developer.android.google.cn/docs?hl=zh-cn
|
* https://developer.android.google.cn/docs?hl=zh-cn
|
||||||
|
|||||||
@@ -175,6 +175,14 @@ namespace acgist {
|
|||||||
* @param env JNIEnv
|
* @param env JNIEnv
|
||||||
*/
|
*/
|
||||||
void closeRoom(JNIEnv* env);
|
void closeRoom(JNIEnv* env);
|
||||||
|
/**
|
||||||
|
* 设置码率
|
||||||
|
*
|
||||||
|
* @param maxFramerate 最大帧率
|
||||||
|
* @param minBitrate 最小码率
|
||||||
|
* @param maxBitrate 最大码率
|
||||||
|
*/
|
||||||
|
void setBitrate(int maxFramerate, int minBitrate, int maxBitrate);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -342,6 +342,8 @@ namespace acgist {
|
|||||||
}
|
}
|
||||||
this->factory = factory;
|
this->factory = factory;
|
||||||
this->rtcConfiguration = new webrtc::PeerConnectionInterface::RTCConfiguration(rtcConfiguration);
|
this->rtcConfiguration = new webrtc::PeerConnectionInterface::RTCConfiguration(rtcConfiguration);
|
||||||
|
// this->rtcConfiguration->set_cpu_adaptation(false);
|
||||||
|
// this->rtcConfiguration->set_experiment_cpu_load_estimator(false);
|
||||||
mediasoupclient::PeerConnection::Options options;
|
mediasoupclient::PeerConnection::Options options;
|
||||||
options.config = rtcConfiguration;
|
options.config = rtcConfiguration;
|
||||||
options.factory = factory;
|
options.factory = factory;
|
||||||
@@ -418,7 +420,7 @@ namespace acgist {
|
|||||||
nlohmann::json codecOptions =
|
nlohmann::json codecOptions =
|
||||||
{
|
{
|
||||||
// x-google-start-bitrate
|
// x-google-start-bitrate
|
||||||
{ "videoGoogleStartBitrate", 400 },
|
{ "videoGoogleStartBitrate", 1200 },
|
||||||
// x-google-min-bitrate
|
// x-google-min-bitrate
|
||||||
{ "videoGoogleMinBitrate", 800 },
|
{ "videoGoogleMinBitrate", 800 },
|
||||||
// x-google-max-bitrate
|
// x-google-max-bitrate
|
||||||
@@ -552,6 +554,53 @@ namespace acgist {
|
|||||||
this->closeRoomCallback(env);
|
this->closeRoomCallback(env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Room::setBitrate(int maxFramerate, int minBitrate, int maxBitrate) {
|
||||||
|
// if(
|
||||||
|
// this->sendTransport == nullptr ||
|
||||||
|
// this->sendTransport->sendHandler == nullptr ||
|
||||||
|
// this->sendTransport->sendHandler->pc == nullptr ||
|
||||||
|
// this->sendTransport->sendHandler->pc->pc == nullptr
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// webrtc::BitrateSettings settings;
|
||||||
|
// settings.min_bitrate_bps = minBitrate;
|
||||||
|
// settings.max_bitrate_bps = maxBitrate;
|
||||||
|
// settings.start_bitrate_bps = minBitrate;
|
||||||
|
// this->sendTransport->sendHandler->pc->pc->SetBitrate(settings);
|
||||||
|
webrtc::RtpSenderInterface* rtpSender;
|
||||||
|
if(
|
||||||
|
this->videoProducer == nullptr ||
|
||||||
|
(rtpSender = this->videoProducer->GetRtpSender()) == nullptr
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
webrtc::RtpParameters rtpParameters = rtpSender->GetParameters();
|
||||||
|
auto& encodings = rtpParameters.encodings;
|
||||||
|
for(
|
||||||
|
auto iterator = encodings.begin();
|
||||||
|
iterator != encodings.end();
|
||||||
|
++iterator
|
||||||
|
) {
|
||||||
|
if(maxFramerate > 0) {
|
||||||
|
LOG_I("当前最大帧率:%d - %d", maxFramerate, iterator->max_framerate);
|
||||||
|
iterator->max_framerate = maxFramerate;
|
||||||
|
}
|
||||||
|
if(minBitrate > 0) {
|
||||||
|
LOG_I("当前最小码率:%d - %d", minBitrate, iterator->min_bitrate_bps);
|
||||||
|
iterator->min_bitrate_bps = minBitrate;
|
||||||
|
}
|
||||||
|
if(maxBitrate > 0) {
|
||||||
|
LOG_I("当前最大码率:%d - %d", maxBitrate, iterator->max_bitrate_bps);
|
||||||
|
iterator->max_bitrate_bps = maxBitrate;
|
||||||
|
}
|
||||||
|
// iterator->bitrate_priority = 4.0;
|
||||||
|
// iterator->network_priority = webrtc::Priority::kHigh;
|
||||||
|
// iterator->scale_resolution_down_by = 2;
|
||||||
|
}
|
||||||
|
rtpSender->SetParameters(rtpParameters);
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT jlong JNICALL
|
extern "C" JNIEXPORT jlong JNICALL
|
||||||
Java_com_acgist_taoyao_media_client_Room_nativeNewRoom(
|
Java_com_acgist_taoyao_media_client_Room_nativeNewRoom(
|
||||||
JNIEnv* env, jobject me,
|
JNIEnv* env, jobject me,
|
||||||
@@ -727,4 +776,13 @@ namespace acgist {
|
|||||||
env->ReleaseStringUTFChars(jConsumerId, consumerId);
|
env->ReleaseStringUTFChars(jConsumerId, consumerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_com_acgist_taoyao_media_client_Room_nativeSetBitrate(JNIEnv* env, jobject me, jlong nativeRoomPointer, jint maxFramerate, jint minBitrate, jint maxBitrate) {
|
||||||
|
Room* room = (Room*) nativeRoomPointer;
|
||||||
|
if(room == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
room->setBitrate(maxFramerate, minBitrate, maxBitrate);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ public class Room extends CloseableClient implements RouterCallback {
|
|||||||
iceServers = new ArrayList<>();
|
iceServers = new ArrayList<>();
|
||||||
}
|
}
|
||||||
this.rtcConfiguration = new PeerConnection.RTCConfiguration(iceServers);
|
this.rtcConfiguration = new PeerConnection.RTCConfiguration(iceServers);
|
||||||
|
// this.rtcConfiguration.enableCpuOveruseDetection = true;
|
||||||
// 开始协商
|
// 开始协商
|
||||||
return this.taoyao.requestFuture(
|
return this.taoyao.requestFuture(
|
||||||
this.taoyao.buildMessage("media::router::rtp::capabilities", "roomId", this.roomId),
|
this.taoyao.buildMessage("media::router::rtp::capabilities", "roomId", this.roomId),
|
||||||
@@ -288,7 +289,7 @@ public class Room extends CloseableClient implements RouterCallback {
|
|||||||
"forceTcp", false,
|
"forceTcp", false,
|
||||||
"producing", false,
|
"producing", false,
|
||||||
"consuming", true,
|
"consuming", true,
|
||||||
"sctpCapabilities", this.dataProduce ? this.sctpCapabilities : null
|
"sctpCapabilities", this.dataConsume ? this.sctpCapabilities : null
|
||||||
),
|
),
|
||||||
response -> {
|
response -> {
|
||||||
this.nativeCreateRecvTransport(this.nativeRoomPointer, JSONUtils.toJSON(response.body()));
|
this.nativeCreateRecvTransport(this.nativeRoomPointer, JSONUtils.toJSON(response.body()));
|
||||||
@@ -382,6 +383,19 @@ public class Room extends CloseableClient implements RouterCallback {
|
|||||||
remoteClient.close();
|
remoteClient.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态设置帧率码率
|
||||||
|
*
|
||||||
|
* HardwareVideoEncoder#updateBitrate()
|
||||||
|
*
|
||||||
|
* @param maxFramerate 最大帧率
|
||||||
|
* @param minBitrate 最小码率
|
||||||
|
* @param maxBitrate 最大码率
|
||||||
|
*/
|
||||||
|
public void setBitrate(int maxFramerate, int minBitrate, int maxBitrate) {
|
||||||
|
this.nativeSetBitrate(this.nativeRoomPointer, maxFramerate, minBitrate, maxBitrate);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
@@ -773,4 +787,14 @@ public class Room extends CloseableClient implements RouterCallback {
|
|||||||
*/
|
*/
|
||||||
private native void nativeMediaConsumerClose(long nativeRoomPointer, String consumerId);
|
private native void nativeMediaConsumerClose(long nativeRoomPointer, String consumerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mediasoup设置码率
|
||||||
|
*
|
||||||
|
* @param nativeRoomPointer 房间指针
|
||||||
|
* @param maxFramerate 最大帧率
|
||||||
|
* @param minBitrate 最小码率
|
||||||
|
* @param maxBitrate 最大码率
|
||||||
|
*/
|
||||||
|
private native void nativeSetBitrate(long nativeRoomPointer, int maxFramerate, int minBitrate, int maxBitrate);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2329,6 +2329,9 @@ class Taoyao extends RemoteClient {
|
|||||||
videoSource: this.videoSource
|
videoSource: this.videoSource
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// let oldParameters = this.videoProducer.rtpSender.getParameters();
|
||||||
|
// oldParameters.encodings[0].maxBitrate = 800000;
|
||||||
|
// this.videoProducer.rtpSender.setParameters(oldParameters);
|
||||||
this.callbackTrack(this.clientId, track);
|
this.callbackTrack(this.clientId, track);
|
||||||
if (this.proxy && this.proxy.media) {
|
if (this.proxy && this.proxy.media) {
|
||||||
this.proxy.media(track, this.videoProducer);
|
this.proxy.media(track, this.videoProducer);
|
||||||
@@ -2839,7 +2842,7 @@ class Taoyao extends RemoteClient {
|
|||||||
forceTcp : this.forceTcp,
|
forceTcp : this.forceTcp,
|
||||||
producing : false,
|
producing : false,
|
||||||
consuming : true,
|
consuming : true,
|
||||||
sctpCapabilities: this.dataProduce ? this.mediasoupDevice.sctpCapabilities : undefined,
|
sctpCapabilities: this.dataConsume ? this.mediasoupDevice.sctpCapabilities : undefined,
|
||||||
}));
|
}));
|
||||||
const {
|
const {
|
||||||
transportId,
|
transportId,
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ public final class DateUtils {
|
|||||||
*
|
*
|
||||||
* @return 日期字符串
|
* @return 日期字符串
|
||||||
*/
|
*/
|
||||||
public static String format(LocalDate localDate, DateStyle format) {
|
public static final String format(LocalDate localDate, DateStyle format) {
|
||||||
return localDate != null && format != null ? format.getDateTimeFormatter().format(localDate) : null;
|
return localDate != null && format != null ? format.getDateTimeFormatter().format(localDate) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ public final class DateUtils {
|
|||||||
*
|
*
|
||||||
* @return 时间字符串
|
* @return 时间字符串
|
||||||
*/
|
*/
|
||||||
public static String format(LocalTime localTime, TimeStyle format) {
|
public static final String format(LocalTime localTime, TimeStyle format) {
|
||||||
return localTime != null && format != null ? format.getDateTimeFormatter().format(localTime) : null;
|
return localTime != null && format != null ? format.getDateTimeFormatter().format(localTime) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ public final class DateUtils {
|
|||||||
*
|
*
|
||||||
* @return 日期时间字符串
|
* @return 日期时间字符串
|
||||||
*/
|
*/
|
||||||
public static String format(LocalDateTime localDateTime, DateTimeStyle format) {
|
public static final String format(LocalDateTime localDateTime, DateTimeStyle format) {
|
||||||
return localDateTime != null && format != null ? format.getDateTimeFormatter().format(localDateTime) : null;
|
return localDateTime != null && format != null ? format.getDateTimeFormatter().format(localDateTime) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,11 +102,14 @@ public final class ErrorUtils {
|
|||||||
final MessageCode messageCode = messageCodeException.getMessageCode();
|
final MessageCode messageCode = messageCodeException.getMessageCode();
|
||||||
status = messageCode.getStatus();
|
status = messageCode.getStatus();
|
||||||
message = Message.fail(messageCode, messageCodeException.getMessage());
|
message = Message.fail(messageCode, messageCodeException.getMessage());
|
||||||
} else if(rootError instanceof Throwable throwable) {
|
} else if(
|
||||||
|
rootError instanceof Throwable rootThrowable &&
|
||||||
|
globalError instanceof Throwable globalThrowable
|
||||||
|
) {
|
||||||
// 未知异常:异常转换
|
// 未知异常:异常转换
|
||||||
final MessageCode messageCode = ErrorUtils.messageCode(status, throwable);
|
final MessageCode messageCode = ErrorUtils.messageCode(status, globalThrowable, rootThrowable);
|
||||||
status = messageCode.getStatus();
|
status = messageCode.getStatus();
|
||||||
message = Message.fail(messageCode, ErrorUtils.message(messageCode, throwable));
|
message = Message.fail(messageCode, ErrorUtils.message(messageCode, rootThrowable));
|
||||||
} else {
|
} else {
|
||||||
// 没有异常
|
// 没有异常
|
||||||
final MessageCode messageCode = MessageCode.of(status);
|
final MessageCode messageCode = MessageCode.of(status);
|
||||||
@@ -193,20 +196,35 @@ public final class ErrorUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param status 原始状态编码
|
* @see #messageCode(int, Throwable, Throwable)
|
||||||
* @param throwable 异常
|
*/
|
||||||
|
public static final MessageCode messageCode(int status, Throwable throwable) {
|
||||||
|
return ErrorUtils.messageCode(status, throwable, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param status 原始状态
|
||||||
|
* @param globalThrowable 外层异常
|
||||||
|
* @param rootThrowable 原始异常
|
||||||
*
|
*
|
||||||
* @return 状态编码
|
* @return 响应状态
|
||||||
*
|
*
|
||||||
* @see ResponseEntityExceptionHandler
|
* @see ResponseEntityExceptionHandler
|
||||||
* @see DefaultHandlerExceptionResolver
|
* @see DefaultHandlerExceptionResolver
|
||||||
*/
|
*/
|
||||||
public static final MessageCode messageCode(int status, Throwable throwable) {
|
public static final MessageCode messageCode(int status, Throwable globalThrowable, Throwable rootThrowable) {
|
||||||
final Class<?> clazz = throwable.getClass();
|
if(rootThrowable == null || globalThrowable == null) {
|
||||||
|
return MessageCode.CODE_9999;
|
||||||
|
}
|
||||||
|
final Class<?> rootClazz = rootThrowable.getClass();
|
||||||
|
final Class<?> globalClazz = globalThrowable.getClass();
|
||||||
return CODE_MAPPING.entrySet().stream()
|
return CODE_MAPPING.entrySet().stream()
|
||||||
.filter(entry -> {
|
.filter(entry -> {
|
||||||
final Class<?> mappingClazz = entry.getKey();
|
final Class<?> mappingClazz = entry.getKey();
|
||||||
return mappingClazz.equals(clazz) || mappingClazz.isAssignableFrom(clazz);
|
return mappingClazz.equals(globalClazz) ||
|
||||||
|
mappingClazz.isAssignableFrom(globalClazz) ||
|
||||||
|
mappingClazz.equals(rootClazz) ||
|
||||||
|
mappingClazz.isAssignableFrom(rootClazz);
|
||||||
})
|
})
|
||||||
.map(Map.Entry::getValue)
|
.map(Map.Entry::getValue)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
@@ -271,7 +289,7 @@ public final class ErrorUtils {
|
|||||||
if(cause instanceof MessageCodeException) {
|
if(cause instanceof MessageCodeException) {
|
||||||
return cause;
|
return cause;
|
||||||
}
|
}
|
||||||
} while(cause != null && (cause = cause.getCause()) != null);
|
} while(cause != null && cause.getCause() != null && (cause = cause.getCause()) != null);
|
||||||
// 返回原始异常
|
// 返回原始异常
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user