[+] 视频捕获:前置、后置、屏幕

This commit is contained in:
acgist
2023-04-01 02:28:33 +08:00
parent b7c9a0e4c1
commit ed66875a47
26 changed files with 756 additions and 136 deletions

View File

@@ -23,14 +23,22 @@
|:--|:--|:--|
|taoyao-client-web|Web终端|Web终端|
|taoyao-client-media|媒体终端|媒体服务|
|taoyao-client-android|安卓终端|安卓终端|
|taoyao-signal-server|信令服务|终端信令控制|
|taoyao-client-android|安卓终端|安卓终端|
## 功能
### Web终端功能
Web、信令已经完成大部分音视频功能还有部分视频质量调整功能没有完成。
|功能|是否支持|是否实现|描述|
|P2P|支持|暂未实现|P2P监控模式|
|WebRTC|支持|实现|Web终端不能同时进入多个房间|
Android还在学习之中...
### 安卓终端功能
|功能|是否支持|是否实现|描述|
|P2P|支持|暂未实现|P2P监控模式|
|WebRTC|支持|暂未实现|安卓终端支持同时进入多个房间|
|RTP|支持|暂未实现|支持房间RTP推流不会拉流|
||||
## 证书
@@ -54,8 +62,6 @@ Android还在学习之中...
## TODO
* P2P
* RTP
* 标识 -> ID
* 所有字段获取 -> get
* 优化JS错误回调 -> platform::error

View File

@@ -403,6 +403,17 @@ firewall-cmd --list-ports
```
mkdir /data/certs
cd /data/certs
# CA证书
openssl genrsa -out ca.key 2048
openssl req -x509 -new -key ca.key -out ca.crt -days 3650
openssl x509 -in ca.crt -subject -issuer -noout
# subject= /C=cn/ST=gd/L=gz/O=acgist/OU=acgist/CN=acgist.com
# issuer= /C=cn/ST=gd/L=gz/O=acgist/OU=acgist/CN=acgist.com
# Server证书信息
vim server.ext
---
@@ -422,58 +433,19 @@ DNS.3=www.acgist.com
DNS.4=taoyao.acgist.com
---
# CA
openssl genrsa -out ca.key 2048
openssl req -x509 -new -key ca.key -out ca.crt -days 3650
openssl x509 -in ca.crt -subject -issuer -noout
# subject= /C=cn/ST=gd/L=gz/O=acgist/OU=acgist/CN=acgist.com
# issuer= /C=cn/ST=gd/L=gz/O=acgist/OU=acgist/CN=acgist.com
# Server
# Server证书
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
# 设置信息:-subj "/C=cn/ST=gd/L=gz/O=acgist/OU=taoyao/CN=taoyao.acgist.com"
openssl x509 -req -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -extfile server.ext
openssl x509 -in server.crt -subject -issuer -noout
# subject= /C=cn/ST=gd/L=gz/O=acgist/OU=taoyao/CN=taoyao.acgist.com
# issuer= /C=cn/ST=gd/L=gz/O=acgist/OU=acgist/CN=acgist.com
openssl pkcs12 -export -clcerts -in server.crt -inkey server.key -out server.p12 -name taoyao
```
## licenses
```
List of licenses:
webrtc,
abseil-cpp,
android_deps,
android_deps:com_android_support_support_annotations.*,
android_ndk,
android_sdk,
androidx,
base64,
boringssl,
crc32c,
fft,
fiat,
g711,
g722,
ijar,
jdk,
libaom,
libevent,
libjpeg_turbo,
libsrtp,
libvpx,
libyuv,
nasm,
ooura,
opus,
pffft,
protobuf,
rnnoise,
sigslot,
spl_sqrt_floor,
usrsctp,
zlib
# 不要导出ca证书-clcerts
# 设置密码:-passout pass:123456
# keytool -importkeystore -v -srckeystore server.p12 -srcstoretype pkcs12 -destkeystore server.jks -deststoretype jks
# 原始密码:-srcstorepass 123456
# 设置密码:-deststorepass 123456
```

View File

@@ -87,3 +87,41 @@ cmake . -B build \
make -C build
make install -C build
```
## licenses
```
List of licenses:
webrtc,
abseil-cpp,
android_deps,
android_deps:com_android_support_support_annotations.*,
android_ndk,
android_sdk,
androidx,
base64,
boringssl,
crc32c,
fft,
fiat,
g711,
g722,
ijar,
jdk,
libaom,
libevent,
libjpeg_turbo,
libsrtp,
libvpx,
libyuv,
nasm,
ooura,
opus,
pffft,
protobuf,
rnnoise,
sigslot,
spl_sqrt_floor,
usrsctp,
zlib
```

View File

@@ -11,6 +11,7 @@
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:enabled="true"
android:exported="true"
android:label="@string/title_activity_main"
android:launchMode="singleTask"
@@ -22,6 +23,7 @@
</activity>
<activity
android:name=".SettingsActivity"
android:enabled="true"
android:exported="false"
android:label="@string/title_activity_settings"
android:theme="@style/Theme.Taoyao" />
@@ -29,7 +31,9 @@
<service
android:name=".MediaService"
android:enabled="true"
android:exported="false" />
android:exported="false"
android:foregroundServiceType="mediaProjection" />
<receiver
android:name=".BootReceiver"
android:enabled="true"

View File

@@ -1,8 +1,11 @@
package com.acgist.taoyao.client;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
@@ -11,19 +14,24 @@ import android.util.Log;
import android.view.Display;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import com.acgist.taoyao.client.databinding.ActivityMainBinding;
import com.acgist.taoyao.config.Config;
import com.acgist.taoyao.media.MediaManager;
import com.acgist.taoyao.media.MediaRecorder;
import java.io.Serializable;
import java.util.stream.Stream;
/**
* 预览界面
@@ -34,6 +42,8 @@ public class MainActivity extends AppCompatActivity implements Serializable {
private MainHandler mainHandler;
private ActivityMainBinding binding;
private ActivityResultLauncher<Intent> activityResultLauncher;
private MediaProjectionManager mediaProjectionManager;
@Override
protected void onCreate(Bundle bundle) {
@@ -52,7 +62,8 @@ public class MainActivity extends AppCompatActivity implements Serializable {
// 布局
this.binding = ActivityMainBinding.inflate(this.getLayoutInflater());
this.setContentView(this.binding.getRoot());
// 设置按钮
this.registerMediaProjection();
this.binding.record.setOnClickListener(this::switchRecord);
this.binding.settings.setOnClickListener(this::launchSettings);
}
@@ -83,27 +94,18 @@ public class MainActivity extends AppCompatActivity implements Serializable {
Manifest.permission.INTERNET,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.FOREGROUND_SERVICE,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.RECEIVE_BOOT_COMPLETED,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
boolean allGranted = true;
for (String permission : permissions) {
if(this.getApplicationContext().checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
Log.i(MediaService.class.getSimpleName(), "授权成功:" + permission);
} else {
allGranted = false;
Log.w(MediaService.class.getSimpleName(), "授权失败:" + permission);
}
if(Stream.of(permissions).map(this.getApplicationContext()::checkSelfPermission).allMatch(v -> v == PackageManager.PERMISSION_GRANTED)) {
Log.i(MediaService.class.getSimpleName(), "授权成功");
} else {
ActivityCompat.requestPermissions(this, permissions, 0);
}
if(!allGranted) {
Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show();
}
MediaManager.getInstance().init(this.getApplicationContext());
MediaManager.getInstance().initAudio();
MediaManager.getInstance().initVideo();
}
/**
@@ -129,6 +131,40 @@ public class MainActivity extends AppCompatActivity implements Serializable {
}
}
private void registerMediaProjection() {
if(this.activityResultLauncher != null && this.mediaProjectionManager != null) {
return;
}
this.mediaProjectionManager = this.getApplicationContext().getSystemService(MediaProjectionManager.class);
this.activityResultLauncher = this.registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if(result.getResultCode() == Activity.RESULT_OK) {
Log.i(MediaManager.class.getSimpleName(), "屏幕捕获成功");
final Intent intent = new Intent(this, MediaService.class);
intent.setAction(MediaService.Action.SCREEN_RECORD.name());
intent.putExtra("data", result.getData());
intent.putExtra("code", result.getResultCode());
this.startService(intent);
} else {
Log.w(MainActivity.class.getSimpleName(), "屏幕捕获失败:" + result.getResultCode());
}
}
);
}
private void switchRecord(View view) {
final MediaRecorder mediaRecorder = MediaRecorder.getInstance();
if(mediaRecorder.isActive()) {
mediaRecorder.stop();
} else {
MediaManager.getInstance().init(this.mainHandler, this.getApplicationContext());
MediaManager.getInstance().initAudio();
MediaManager.getInstance().initVideo();
mediaRecorder.init(System.currentTimeMillis() + ".mp4", null, null, 1, 1);
}
}
/**
* 拉起设置页面
*
@@ -158,7 +194,7 @@ public class MainActivity extends AppCompatActivity implements Serializable {
Log.d(MainHandler.class.getSimpleName(), "Handler消息" + message.what + " - " + message.obj);
switch(message.what) {
case Config.WHAT_SCREEN_CAPTURE -> this.mainActivity.screenCapture(message);
case Config.WHAT_NEW_CLIENT_VIDEO -> this.mainActivity.newClientVideo(message);
case Config.WHAT_NEW_LOCAL_VIDEO -> this.mainActivity.newLocalVideo(message);
}
}
@@ -170,18 +206,7 @@ public class MainActivity extends AppCompatActivity implements Serializable {
* @param message 消息
*/
private void screenCapture(Message message) {
final ActivityResultLauncher<Intent> activityResultLauncher = this.registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if(result.getResultCode() == RESULT_OK) {
Log.i(MediaManager.class.getSimpleName(), "开始屏幕捕获");
message.getCallback().run();
} else {
Log.w(MainActivity.class.getSimpleName(), "屏幕捕获失败:" + result.getResultCode());
}
}
);
activityResultLauncher.launch((Intent) message.obj);
this.activityResultLauncher.launch(this.mediaProjectionManager.createScreenCaptureIntent());
}
/**
@@ -189,7 +214,9 @@ public class MainActivity extends AppCompatActivity implements Serializable {
*
* @param message 消息
*/
private void newClientVideo(Message message) {
private void newLocalVideo(Message message) {
final SurfaceView surfaceView = (SurfaceView) message.obj;
this.addContentView(surfaceView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
}

View File

@@ -1,12 +1,15 @@
package com.acgist.taoyao.client;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.location.LocationManager;
import android.net.wifi.WifiManager;
import android.os.BatteryManager;
@@ -16,7 +19,10 @@ import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import com.acgist.taoyao.client.signal.Taoyao;
import com.acgist.taoyao.media.MediaManager;
/**
* 媒体服务
@@ -40,7 +46,9 @@ public class MediaService extends Service {
// 连接
CONNECT,
// 重连
RECONNECT;
RECONNECT,
// 屏幕录制
SCREEN_RECORD;
}
@@ -63,16 +71,11 @@ public class MediaService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(MediaService.class.getSimpleName(), "onStartCommand");
if (Action.CONNECT.name().equals(intent.getAction())) {
if (this.taoyao == null) {
Log.d(MediaService.class.getSimpleName(), "打开信令连接");
this.mainHandler = (Handler) intent.getSerializableExtra("mainHandler");
this.connect();
} else {
Log.d(MediaService.class.getSimpleName(), "信令已经连接");
}
this.openConnect(intent);
} else if (Action.RECONNECT.name().equals(intent.getAction())) {
Log.d(MediaService.class.getSimpleName(), "重新连接信令");
this.connect();
this.reconnect();
} else if (Action.SCREEN_RECORD.name().equals(intent.getAction())) {
this.screenRecord(intent);
} else {
Log.w(MediaService.class.getSimpleName(), "未知动作:" + intent.getAction());
}
@@ -86,6 +89,21 @@ public class MediaService extends Service {
this.close();
}
private void openConnect(Intent intent) {
if (this.taoyao == null) {
Log.d(MediaService.class.getSimpleName(), "打开信令连接");
this.mainHandler = (Handler) intent.getSerializableExtra("mainHandler");
this.connect();
} else {
Log.d(MediaService.class.getSimpleName(), "信令已经连接");
}
}
private void reconnect() {
Log.d(MediaService.class.getSimpleName(), "重新连接信令");
this.connect();
}
/**
* 连接信令
*/
@@ -125,4 +143,23 @@ public class MediaService extends Service {
this.taoyao = null;
}
public void screenRecord(Intent intent) {
final Intent notificationIntent = new Intent(this, MediaService.class);
final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "NOTIFICATION_CHANNEL_ID")
.setSmallIcon(R.mipmap.ic_launcher_foreground)
.setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher_foreground))
.setTicker("NOTIFICATION_TICKER")
.setContentTitle("屏幕录制")
.setContentText("屏幕录制共享")
.setContentIntent(pendingIntent);
final Notification notification = notificationBuilder.build();
final NotificationChannel channel = new NotificationChannel("NOTIFICATION_CHANNEL_ID", "NOTIFICATION_CHANNEL_NAME", NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("NOTIFICATION_CHANNEL_DESC");
final NotificationManager notificationManager = this.getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
this.startForeground((int) System.currentTimeMillis(), notification);
MediaManager.getInstance().screenRecord(intent.getParcelableExtra("data"));
}
}

View File

@@ -1,4 +0,0 @@
package com.acgist.taoyao.client.media;
public class RoomClientManager {
}

View File

@@ -1,4 +0,0 @@
package com.acgist.taoyao.client.p2p;
public class P2PClientManager {
}

View File

@@ -22,6 +22,8 @@ import com.acgist.taoyao.boot.utils.CloseableUtils;
import com.acgist.taoyao.boot.utils.JSONUtils;
import com.acgist.taoyao.media.MediaRecorder;
import com.acgist.taoyao.client.utils.IdUtils;
import com.acgist.taoyao.media.P2PClient;
import com.acgist.taoyao.media.Room;
import org.apache.commons.lang3.ArrayUtils;
@@ -34,8 +36,10 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -156,6 +160,14 @@ public final class Taoyao {
* 定时任务线程池
*/
private final ScheduledExecutorService scheduled;
/**
* 房间列表
*/
private final List<Room> roomList;
/**
* P2P终端列表
*/
private final List<P2PClient> p2pClientList;
public Taoyao(
int port, String host, String version,
@@ -191,6 +203,8 @@ public final class Taoyao {
this.scheduled = Executors.newScheduledThreadPool(1);
this.executor.submit(this::loopMessage);
this.scheduled.scheduleWithFixedDelay(this::heartbeat, 30, 30, TimeUnit.SECONDS);
this.roomList = new CopyOnWriteArrayList<>();
this.p2pClientList = new CopyOnWriteArrayList<>();
}
/**
@@ -416,6 +430,8 @@ public final class Taoyao {
this.disconnect();
this.executor.shutdown();
this.scheduled.shutdown();
this.roomList.forEach(Room::close);
this.p2pClientList.forEach(P2PClient::close);
}
/**

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,10.48V6c0,-1.1 -0.9,-2 -2,-2H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-4.48l4,3.98v-11L18,10.48zM12,12l3,1.73l-1,1.73l-3,-1.73V17H9v-3.27l-3,1.73l-1,-1.73L8,12l-3,-1.73l1,-1.73l3,1.73V7h2v3.27l3,-1.73l1,1.73L12,12z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View File

@@ -7,6 +7,23 @@
android:layout_height="match_parent"
tools:context="com.acgist.taoyao.client.MainActivity">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/record"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/button_record"
android:src="@drawable/record"
app:borderWidth="16dp"
app:fabSize="normal"
app:layout_constraintBottom_toTopOf="@+id/settings"
app:layout_constraintHorizontal_bias="0.04"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/settings"
android:layout_width="wrap_content"

View File

@@ -15,4 +15,8 @@
<!-- 文件存储目录 -->
<string name="storagePathImage">/taoyao/image</string>
<string name="storagePathVideo">/taoyao/video</string>
<!-- 传输类型RTP|WEBRTC -->
<string name="transportType">WEBRTC</string>
<string name="audioRecordFormat"></string>
<string name="videoRecordFormat"></string>
</resources>

View File

@@ -3,6 +3,7 @@
<string name="app_name">桃夭</string>
<string name="title_activity_main">桃夭终端预览</string>
<string name="title_activity_settings">桃夭终端设置</string>
<string name="button_record">录制</string>
<string name="button_setting">设置</string>
<string name="button_connect">连接</string>
<string name="signal_port">信令端口</string>

View File

@@ -5,10 +5,14 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- adb shell getprop ro.opengles.version -->
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
</manifest>

View File

@@ -12,8 +12,20 @@ public class Config {
*/
public static final int WHAT_SCREEN_CAPTURE = 1000;
/**
* 新建用户视
* 新建本地音
*/
public static final int WHAT_NEW_CLIENT_VIDEO = 1001;
public static final int WHAT_NEW_LOCAL_AUDIO = 2000;
/**
* 新建本地视频
*/
public static final int WHAT_NEW_LOCAL_VIDEO = 2001;
/**
* 新建远程音频
*/
public static final int WHAT_NEW_REMOTE_AUDIO = 2002;
/**
* 新建远程视频
*/
public static final int WHAT_NEW_REMOTE_VIDEO = 2003;
}

View File

@@ -9,8 +9,25 @@ import java.util.logging.Handler;
*/
public class LocalClient extends RoomClient {
/**
* 传输类型
*
* @author acgist
*/
public enum TransportType {
RTP,
WEBRTC;
}
public LocalClient(String name, String clientId, Handler handler) {
super(name, clientId, handler);
}
@Override
public void close() {
super.close();
}
}

View File

@@ -2,9 +2,11 @@ package com.acgist.taoyao.media;
import android.content.Context;
import android.content.Intent;
import android.media.AudioRecord;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
@@ -15,18 +17,29 @@ import org.webrtc.AudioTrack;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.EglBase;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RendererCommon;
import org.webrtc.ScreenCapturerAndroid;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoDecoderFactory;
import org.webrtc.VideoEncoderFactory;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import org.webrtc.audio.JavaAudioDeviceModule;
import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioRecord;
import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
@@ -34,6 +47,18 @@ import java.util.List;
* 媒体来源管理器
*
* @author acgist
*
* https://zhuanlan.zhihu.com/p/82446482
* https://www.jianshu.com/p/97acd9a51909
* https://blog.csdn.net/nanoage/article/details/127406494
* https://webrtc.org.cn/20190419_tutorial3_webrtc_android
* https://blog.csdn.net/CSDN_Mew/article/details/103406781
* https://blog.csdn.net/Tong_Hou/article/details/112116349
* https://blog.csdn.net/u011418943/article/details/127108642
* https://blog.csdn.net/csdn_shen0221/article/details/120331004
* https://blog.csdn.net/csdn_shen0221/article/details/119982257
*
* TODO动态码率BITRATE_MODE_VBR、BITRATE_MODE
*/
public class MediaManager {
@@ -84,6 +109,14 @@ public class MediaManager {
* 上下文
*/
private Context context;
/**
* 本地终端
*/
private LocalClient localClient;
/**
*
*/
private EglBase eglBase;
/**
* 媒体流:声音、主码流(预览流)、次码流
*/
@@ -107,6 +140,8 @@ public class MediaManager {
WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(true);
// 回声小丑
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
// 使用OpenSL ES
// WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
}
/**
@@ -114,16 +149,38 @@ public class MediaManager {
*
* @param context 上下文
*/
public void init(Context context) {
public void init(Handler handler, Context context) {
this.type = Type.BACK;
this.handler = handler;
this.context = context;
this.eglBase = EglBase.create();
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(this.context)
.setEnableInternalTracer(true)
.createInitializationOptions()
);
this.peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
final VideoDecoderFactory videoDecoderFactory = new DefaultVideoDecoderFactory(this.eglBase.getEglBaseContext());
final VideoEncoderFactory videoEncoderFactory = new DefaultVideoEncoderFactory(this.eglBase.getEglBaseContext(), true, true);
final JavaAudioDeviceModule javaAudioDeviceModule = JavaAudioDeviceModule.builder(this.context)
// 本地音频
.setSamplesReadyCallback(MediaRecorder.getInstance().audioRecoder)
// 远程音频
// .setAudioTrackStateCallback()
// .setUseHardwareNoiseSuppressor(true)
// .setUseHardwareAcousticEchoCanceler(true)
.createAudioDeviceModule();
this.peerConnectionFactory = PeerConnectionFactory.builder()
// .setAudioProcessingFactory()
// .setAudioDecoderFactoryFactory()
// .setAudioEncoderFactoryFactory()
.setAudioDeviceModule(javaAudioDeviceModule)
.setVideoDecoderFactory(videoDecoderFactory)
.setVideoEncoderFactory(videoEncoderFactory)
.createPeerConnectionFactory();
this.mediaStream = this.peerConnectionFactory.createLocalMediaStream("ARDAMS");
Arrays.stream(videoEncoderFactory.getSupportedCodecs()).forEach(v -> {
Log.i(MediaManager.class.getSimpleName(), "支持的视频解码器:" + v.name);
});
}
/**
@@ -163,6 +220,25 @@ public class MediaManager {
this.closeAudioTrack();
// 加载音频
final MediaConstraints mediaConstraints = new MediaConstraints();
// 高音过滤
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
// // 自动增益
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
// // 回声消除
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
// // 噪音处理
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
// 更多
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation2", "true"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googDAEchoCancellation", "true"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl2", "true"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAudioMirroring", "false"));
// mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
final AudioSource audioSource = this.peerConnectionFactory.createAudioSource(mediaConstraints);
final AudioTrack audioTrack = this.peerConnectionFactory.createAudioTrack("ARDAMSa0", audioSource);
audioTrack.setEnabled(true);
@@ -192,26 +268,25 @@ public class MediaManager {
Log.d(MediaManager.class.getSimpleName(), "忽略摄像头:" + name);
}
}
this.initVideoTrack();
} else if(this.type == Type.FILE) {
Log.i(MediaManager.class.getSimpleName(), "加载视频(文件)");
} else if(this.type == Type.SCREEN) {
Log.i(MediaManager.class.getSimpleName(), "加载视频(录屏)");
final MediaProjectionManager mediaProjectionManager = this.context.getSystemService(MediaProjectionManager.class);
final Intent intent = mediaProjectionManager.createScreenCaptureIntent();
final Message message = Message.obtain(this.handler, () -> {
this.videoCapturer = new ScreenCapturerAndroid(intent, new MediaProjection.Callback() {
@Override
public void onStop() {
super.onStop();
Log.i(MediaManager.class.getSimpleName(), "停止屏幕捕获");
}
});
});
message.obj = intent;
final Message message = new Message();
message.what = Config.WHAT_SCREEN_CAPTURE;
this.handler.sendMessage(message);
// this.handler.dispatchMessage(message);
}
}
public void screenRecord(Intent intent) {
this.videoCapturer = new ScreenCapturerAndroid(intent, new MediaProjection.Callback() {
@Override
public void onStop() {
super.onStop();
Log.i(MediaManager.class.getSimpleName(), "停止屏幕捕获");
}
});
this.initVideoTrack();
}
@@ -219,20 +294,42 @@ public class MediaManager {
* 加载视频
*/
private void initVideoTrack() {
// 设置预览
final SurfaceViewRenderer surfaceViewRenderer = new SurfaceViewRenderer(this.context);
surfaceViewRenderer.setMirror(true);
surfaceViewRenderer.setEnableHardwareScaler(true);
final SurfaceViewRenderer surfaceViewRenderer = this.preview();
// 加载视频
final EglBase eglBase = EglBase.create();
final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("MediaVideoThread", eglBase.getEglBaseContext());
final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("MediaVideoThread", this.eglBase.getEglBaseContext());
final VideoSource videoSource = this.peerConnectionFactory.createVideoSource(this.videoCapturer.isScreencast());
this.videoCapturer.initialize(surfaceTextureHelper, this.context, videoSource.getCapturerObserver());
this.videoCapturer.startCapture(640, 480, 30);
final VideoTrack videoTrack = this.peerConnectionFactory.createVideoTrack("ARDAMSv0", videoSource);
videoTrack.addSink(surfaceViewRenderer);
videoTrack.addSink(MediaRecorder.getInstance().videoRecoder);
videoTrack.setEnabled(true);
this.mediaStream.addTrack(videoTrack);
Log.i(MediaManager.class.getSimpleName(), "加载视频:" + videoTrack.id());
// 二次处理VideoProcessor
}
private SurfaceViewRenderer preview() {
// 设置预览
final SurfaceViewRenderer surfaceViewRenderer = new SurfaceViewRenderer(this.context);
// 视频反转
surfaceViewRenderer.setMirror(false);
// 视频拉伸
surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
// 硬件拉伸
surfaceViewRenderer.setEnableHardwareScaler(true);
// 加载
surfaceViewRenderer.init(this.eglBase.getEglBaseContext(), null);
// 事件
// surfaceViewRenderer.setOnClickListener();
// TODO迁移localvideo
// surfaceViewRenderer.release();
// 页面加载
final Message message = new Message();
message.obj = surfaceViewRenderer;
message.what = Config.WHAT_NEW_LOCAL_VIDEO;
this.handler.sendMessage(message);
return surfaceViewRenderer;
}
public void pauseAudio() {

View File

@@ -1,23 +1,102 @@
package com.acgist.taoyao.media;
import android.media.AudioFormat;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.Environment;
import android.util.Log;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
import org.webrtc.audio.JavaAudioDeviceModule;
import org.webrtc.voiceengine.WebRtcAudioRecord;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* 录像机
*
* https://blog.csdn.net/m0_60259116/article/details/126875532
*
* @author acgist
*/
public final class MediaRecorder {
/**
* 是否正在录像
*/
private boolean active;
private static final MediaRecorder INSTANCE = new MediaRecorder();
private MediaRecorder() {
final MediaCodecList mediaCodecList = new MediaCodecList(-1);
for (MediaCodecInfo mediaCodecInfo : mediaCodecList.getCodecInfos()) {
if (mediaCodecInfo.isEncoder()) {
final String[] supportedTypes = mediaCodecInfo.getSupportedTypes();
Log.d(MediaRecorder.class.getSimpleName(), "编码器名称:" + mediaCodecInfo.getName());
Log.d(MediaRecorder.class.getSimpleName(), "编码器类型:" + String.join(" , ", supportedTypes));
for (String supportType : supportedTypes) {
final MediaCodecInfo.CodecCapabilities codecCapabilities = mediaCodecInfo.getCapabilitiesForType(supportType);
final int[] colorFormats = codecCapabilities.colorFormats;
Log.d(MediaRecorder.class.getSimpleName(), "编码器格式:" + codecCapabilities.getMimeType());
// MediaCodecInfo.CodecCapabilities.COLOR_*
Log.d(MediaRecorder.class.getSimpleName(), "编码器支持格式:" + IntStream.of(colorFormats).boxed().map(String::valueOf).collect(Collectors.joining(" , ")));
}
}
}
this.audioRecoder = audioSamples -> {
Log.d(MediaRecorder.class.getSimpleName(), audioSamples + " - 音频");
};
this.videoRecoder = videoFrame -> {
// Log.d(MediaRecorder.class.getSimpleName(), videoFrame + " - 视频");
if(this.active && this.videoActive) {
final VideoFrame.Buffer buffer = videoFrame.getBuffer();
final VideoFrame.I420Buffer i420Buffer = buffer.toI420();
i420Buffer.getDataU();
// this.putVideo(videoFrame.getBuffer(), videoFrame.getTimestampNs());
}
};
}
public static final MediaRecorder getInstance() {
return INSTANCE;
}
/**
* 是否正在录像
*/
private volatile boolean active;
private volatile boolean audioActive;
private volatile boolean videoActive;
private volatile long pts;
/**
* 音频编码
*/
private MediaCodec audioCodec;
private Thread audioThread;
/**
* 视频编码
*/
private MediaCodec videoCodec;
private Thread videoThread;
/**
* 媒体合成器
*/
private MediaMuxer mediaMuxer;
/**
* 音频录制
*/
public final JavaAudioDeviceModule.SamplesReadyCallback audioRecoder;
/**
* 视频录制
*/
public final VideoSink videoRecoder;
/**
* @return 是否正在录像
*/
@@ -25,4 +104,212 @@ public final class MediaRecorder {
return this.active;
}
public void init(String file, String audioFormat, String videoFormat, int width, int height) {
synchronized (MediaRecorder.INSTANCE) {
this.active = true;
if(
this.audioThread == null || !this.audioThread.isAlive() ||
this.videoThread == null || !this.videoThread.isAlive()
) {
this.initMediaMuxer(file);
this.initAudioThread(MediaFormat.MIMETYPE_AUDIO_AAC, 96000, 44100, 1);
this.initVideoThread(MediaFormat.MIMETYPE_VIDEO_AVC, 2500 * 1000, 30, 1, 1920, 1080);
}
// this.audioCodec = MediaCodec.createByCodecName();
}
}
/**
* @param audioType 类型
* @param bitRate 比特率96 * 1000 | 128 * 1000 | 256 * 1000
* @param sampleRate 采样率32000 | 44100 | 48000
* @param channelCount 通道数量
*/
private void initAudioThread(String audioType, int bitRate, int sampleRate, int channelCount) {
try {
this.audioCodec = MediaCodec.createEncoderByType(audioType);
final MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, sampleRate, channelCount);
// audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, AudioFormat.ENCODING_PCM_16BIT);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
audioFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 8 * 1024);
this.audioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (Exception e) {
Log.e(MediaRecorder.class.getSimpleName(), "加载音频录制线程异常", e);
}
final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
this.audioThread = new Thread(() -> {
int trackIndex;
int outputIndex;
synchronized (MediaRecorder.INSTANCE) {
Log.i(MediaRecorder.class.getSimpleName(), "开始录制音频");
this.audioCodec.start();
this.audioActive = true;
trackIndex = this.mediaMuxer.addTrack(this.audioCodec.getOutputFormat());
if(this.videoActive) {
Log.i(MediaRecorder.class.getSimpleName(), "开始录制文件");
this.pts = System.currentTimeMillis();
this.mediaMuxer.start();
MediaRecorder.INSTANCE.notifyAll();
} else {
try {
MediaRecorder.INSTANCE.wait();
} catch (InterruptedException e) {
}
}
}
while(this.active) {
outputIndex = this.audioCodec.dequeueOutputBuffer(info, 1000L * 1000);
if(outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if(outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
} else {
final ByteBuffer outputBuffer = this.audioCodec.getOutputBuffer(outputIndex);
outputBuffer.position(info.offset);
outputBuffer.limit(info.offset + info.size);
info.presentationTimeUs = info.presentationTimeUs - this.pts;
this.mediaMuxer.writeSampleData(trackIndex, outputBuffer, info);
this.audioCodec.releaseOutputBuffer(outputIndex, false);
}
}
synchronized (MediaRecorder.INSTANCE) {
if(this.audioCodec != null) {
Log.i(MediaRecorder.class.getSimpleName(), "结束录制音频");
this.audioCodec.stop();
this.audioCodec.release();
this.audioCodec = null;
}
this.audioActive = false;
if(this.mediaMuxer != null && !this.videoActive) {
Log.i(MediaRecorder.class.getSimpleName(), "结束录制文件");
this.mediaMuxer.stop();
this.mediaMuxer.release();
this.mediaMuxer = null;
}
}
});
this.audioThread.setName("AudioRecoder");
this.audioThread.start();
}
public void putAudio(byte[] bytes) {
}
/**
* @param videoType 视频格式
* @param bitRate 比特率800 * 1000 | 1600 * 1000 | 2500 * 1000
* @param frameRate 帧率30
* @param iFrameInterval 关键帧频率1
* @param width 宽度1920
* @param height 高度1080
*/
private void initVideoThread(String videoType, int bitRate, int frameRate, int iFrameInterval, int width, int height) {
try {
this.videoCodec = MediaCodec.createEncoderByType(videoType);
final MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 2500000);
// videoFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel32);
// videoFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
// videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
this.videoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (Exception e) {
Log.e(MediaRecorder.class.getSimpleName(), "加载视频录制线程异常", e);
}
final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
this.videoThread = new Thread(() -> {
int trackIndex;
int outputIndex;
synchronized (MediaRecorder.INSTANCE) {
Log.i(MediaRecorder.class.getSimpleName(), "开始录制视频");
this.videoCodec.start();
this.videoActive = true;
trackIndex = this.mediaMuxer.addTrack(this.videoCodec.getOutputFormat());
if(this.audioActive) {
Log.i(MediaRecorder.class.getSimpleName(), "开始录制文件");
this.pts = System.currentTimeMillis();
this.mediaMuxer.start();
MediaRecorder.INSTANCE.notifyAll();
} else {
try {
MediaRecorder.INSTANCE.wait();
} catch (InterruptedException e) {
}
}
}
while(this.active) {
outputIndex = this.videoCodec.dequeueOutputBuffer(info, 1000L * 1000);
if(outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if(outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
} else {
Log.i(MediaRecorder.class.getSimpleName(), "======" + info.size);
final ByteBuffer outputBuffer = this.audioCodec.getOutputBuffer(outputIndex);
outputBuffer.position(info.offset);
outputBuffer.limit(info.offset + info.size);
info.presentationTimeUs = info.presentationTimeUs - this.pts;
this.mediaMuxer.writeSampleData(trackIndex, outputBuffer, info);
this.audioCodec.releaseOutputBuffer(outputIndex, false);
// if(info.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
// } else if(info.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
// } else if(info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
// } else if(info.flags == MediaCodec.BUFFER_FLAG_PARTIAL_FRAME) {
// }
}
}
synchronized (MediaRecorder.INSTANCE) {
if(this.videoCodec != null) {
Log.i(MediaRecorder.class.getSimpleName(), "结束录制视频");
this.videoCodec.stop();
this.videoCodec.release();
this.videoCodec = null;
}
this.videoActive = false;
if(this.mediaMuxer != null && !this.audioActive) {
Log.i(MediaRecorder.class.getSimpleName(), "结束录制文件");
this.mediaMuxer.stop();
this.mediaMuxer.release();
this.mediaMuxer = null;
}
}
});
this.videoThread.setName("VideoRecoder");
this.videoThread.start();
}
public void putVideo(byte[] bytes, long pts) {
while(this.active && this.videoActive) {
final int index = this.videoCodec.dequeueInputBuffer(1000L * 1000);
if(index < 0) {
continue;
}
final ByteBuffer byteBuffer = this.videoCodec.getInputBuffer(index);
byteBuffer.put(bytes);
this.videoCodec.queueInputBuffer(index, 0, bytes.length, pts, 0);
}
}
private void initMediaMuxer(String file) {
try {
this.mediaMuxer = new MediaMuxer(
Paths.get(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath(), file).toAbsolutePath().toString(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
);
// 设置方向
// this.mediaMuxer.setOrientationHint();
} catch (IOException e) {
Log.e(MediaManager.class.getSimpleName(), "加载媒体合成器异常", e);
}
}
public void stop() {
synchronized(MediaRecorder.INSTANCE) {
this.active = false;
this.audioThread = null;
this.videoThread = null;
}
}
}

View File

@@ -1,10 +1,36 @@
package com.acgist.taoyao.media;
import android.util.Log;
import java.io.Closeable;
import java.io.IOException;
/**
* P2P终端
* 使用安卓SDK + WebRTC实现P2P会话
*
* @author acgist
*/
public class P2PClient {
public class P2PClient implements Closeable {
private final String clientId;
public P2PClient(String clientId) {
this.clientId = clientId;
}
// 配置STUN穿透服务器 转发服务器
// iceServers = new ArrayList<>();
// PeerConnection.IceServer iceServer = PeerConnection.IceServer.builder(Constant.STUN).createIceServer();
// iceServers.add(iceServer);
// streamList = new ArrayList<>();
// PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(iceServers);
// PeerConnectionObserver connectionObserver = getObserver();
// peerConnection = peerConnectionFactory.createPeerConnection(configuration, connectionObserver);
@Override
public void close() {
Log.i(Room.class.getSimpleName(), "关闭终端:" + this.clientId);
}
}

View File

@@ -1,9 +1,23 @@
package com.acgist.taoyao.media;
import java.io.Closeable;
import java.io.IOException;
import java.util.logging.Handler;
/**
* 房间远程终端
*
* @author acgist
*/
public class RemoteClient {
public class RemoteClient extends RoomClient {
public RemoteClient(String name, String clientId, Handler handler) {
super(name, clientId, handler);
}
@Override
public void close() {
super.close();
}
}

View File

@@ -1,4 +1,33 @@
package com.acgist.taoyao.media;
public class Room {
import android.util.Log;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
/**
* 房间
*
* @author acgist
*/
public class Room implements Closeable {
private final String id;
public Room(String id) {
this.id = id;
}
/**
* 远程终端列表
*/
private List<RemoteClient> remoteClientList;
@Override
public void close() {
Log.i(Room.class.getSimpleName(), "关闭房间:" + this.id);
this.remoteClientList.forEach(RemoteClient::close);
}
}

View File

@@ -1,18 +1,22 @@
package com.acgist.taoyao.media;
import android.util.Log;
import org.webrtc.AudioTrack;
import org.webrtc.MediaStream;
import org.webrtc.VideoTrack;
import java.io.Closeable;
import java.io.IOException;
import java.util.logging.Handler;
/**
* 房间终端
* 使用NDK + Mediasoup实现多人会话
* 使用SDK + NDK + Mediasoup实现多人会话
*
* @author acgist
*/
public class RoomClient {
public class RoomClient implements Closeable {
protected final String name;
protected final String clientId;
@@ -34,4 +38,9 @@ public class RoomClient {
}
@Override
public void close() {
Log.i(Room.class.getSimpleName(), "关闭终端:" + this.clientId);
}
}

View File

@@ -109,6 +109,7 @@ public final class NetUtils {
if(Boolean.FALSE.equals(NetUtils.ipRewriteProperties.getEnabled())) {
return sourceIp;
}
log.debug("重写地址:{} - {}", sourceIp, clientIp);
try {
final InetAddress sourceAddress = NetUtils.realAddress(sourceIp);
final InetAddress clientAddress = NetUtils.realAddress(clientIp);