diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java index 68fbdd9..26d75e4 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java @@ -2,6 +2,7 @@ package com.acgist.taoyao.client; import android.Manifest; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.ColorStateList; @@ -18,6 +19,7 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.GridLayout; +import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -51,7 +53,7 @@ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle bundle) { - Log.i(MainActivity.class.getSimpleName(), "onCreate"); + Log.d(MainActivity.class.getSimpleName(), "onCreate"); super.onCreate(bundle); final Window window = this.getWindow(); // 强制横屏 @@ -76,19 +78,19 @@ public class MainActivity extends AppCompatActivity { @Override protected void onStart() { - Log.i(MainActivity.class.getSimpleName(), "onStart"); + Log.d(MainActivity.class.getSimpleName(), "onStart"); super.onStart(); } @Override protected void onStop() { - Log.i(MainActivity.class.getSimpleName(), "onStop"); + Log.d(MainActivity.class.getSimpleName(), "onStop"); super.onStop(); } @Override protected void onDestroy() { - Log.i(MainActivity.class.getSimpleName(), "onDestroy"); + Log.d(MainActivity.class.getSimpleName(), "onDestroy"); super.onDestroy(); // 资源释放 } @@ -123,10 +125,12 @@ public class MainActivity extends AppCompatActivity { Manifest.permission.RECEIVE_BOOT_COMPLETED, Manifest.permission.WRITE_EXTERNAL_STORAGE }; - if (Stream.of(permissions).map(this.getApplicationContext()::checkSelfPermission).allMatch(v -> v == PackageManager.PERMISSION_GRANTED)) { - Log.i(MediaService.class.getSimpleName(), "授权成功"); + final Context context = this.getApplicationContext(); + if (Stream.of(permissions).map(context::checkSelfPermission).allMatch(v -> v == PackageManager.PERMISSION_GRANTED)) { + Log.d(MediaService.class.getSimpleName(), "授权成功"); } else { ActivityCompat.requestPermissions(this, permissions, IdUtils.nextInt()); + Toast.makeText(context, "授权失败", Toast.LENGTH_SHORT).show(); } } @@ -158,7 +162,7 @@ public class MainActivity extends AppCompatActivity { new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK) { - Log.i(MediaManager.class.getSimpleName(), "屏幕捕获成功"); + Log.d(MediaManager.class.getSimpleName(), "屏幕捕获成功"); final Intent intent = new Intent(this, MediaService.class); intent.setAction(MediaService.Action.SCREEN_CAPTURE.name()); intent.putExtra("data", result.getData()); @@ -290,6 +294,7 @@ public class MainActivity extends AppCompatActivity { layoutParams.width = 0; layoutParams.height = 0; } else { + // 复用布局 layoutParams = this.removeLayoutParams.remove(0); } final SurfaceView surfaceView = (SurfaceView) message.obj; @@ -312,8 +317,9 @@ public class MainActivity extends AppCompatActivity { return; } video.removeViewAt(index); + // 缓存布局 this.removeLayoutParams.add(surfaceView.getLayoutParams()); } } -} \ No newline at end of file +} diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java index 6ceab8c..b8a6a8a 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java @@ -77,7 +77,7 @@ public class MediaService extends Service { @Override public void onCreate() { - Log.i(MediaService.class.getSimpleName(), "onCreate"); + Log.d(MediaService.class.getSimpleName(), "onCreate"); Log.i(MediaService.class.getSimpleName(), """ 庭院深深深几许,杨柳堆烟,帘幕无重数。玉勒雕鞍游冶处,楼高不见章台路。 雨横风狂三月暮,门掩黄昏,无计留春住。泪眼问花花不语,乱红飞过秋千去。 @@ -97,13 +97,13 @@ public class MediaService extends Service { @Override public IBinder onBind(Intent intent) { - Log.i(MediaService.class.getSimpleName(), "onBind"); + Log.d(MediaService.class.getSimpleName(), "onBind"); return new Binder(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { - Log.i(MediaService.class.getSimpleName(), "onStartCommand:" + intent.getAction()); + Log.d(MediaService.class.getSimpleName(), "onStartCommand:" + intent.getAction()); if (Action.BOOT.name().equals(intent.getAction())) { this.boot(); this.openConnect(); @@ -122,7 +122,7 @@ public class MediaService extends Service { @Override public void onDestroy() { - Log.i(MediaService.class.getSimpleName(), "onDestroy"); + Log.d(MediaService.class.getSimpleName(), "onDestroy"); super.onDestroy(); this.close(); } @@ -155,11 +155,11 @@ public class MediaService extends Service { resources.getInteger(R.integer.imageQuantity), resources.getString(R.string.audioQuantity), resources.getString(R.string.videoQuantity), + resources.getBoolean(R.bool.broadcaster), resources.getInteger(R.integer.channelCount), resources.getInteger(R.integer.iFrameInterval), resources.getString(R.string.storagePathImage), resources.getString(R.string.storagePathVideo), - resources.getBoolean(R.bool.broadcaster), resources.getString(R.string.watermark), VideoSourceType.valueOf(resources.getString(R.string.videoSourceType)) ); @@ -199,13 +199,20 @@ public class MediaService extends Service { final String password = sharedPreferences.getString("settings.password", "taoyao"); final Context context = this.getApplicationContext(); final Resources resources = this.getResources(); - this.close(); + if(this.taoyao != null) { + if(this.taoyao.needReconnect(port, host, name, clientId, username, password)) { + this.close(); + } else { + Log.d(MediaService.class.getSimpleName(), "配置没有改变忽略重连"); + return; + } + } // 连接信令 this.taoyao = new Taoyao( - port, host, resources.getString(R.string.version), - name, clientId, resources.getString(R.string.clientType), username, password, + resources.getString(R.string.version), resources.getString(R.string.clientType), + port, host, name, clientId, username, password, resources.getInteger(R.integer.timeout), resources.getString(R.string.encrypt), resources.getString(R.string.encryptSecret), - this.mainHandler, context, this.taoyaoListener + MediaService.mainHandler, context, this.taoyaoListener ); MediaManager.getInstance().initTaoyao(this.taoyao); Toast.makeText(context, "连接信令", Toast.LENGTH_SHORT).show(); @@ -268,11 +275,11 @@ public class MediaService extends Service { */ private void settingAudio() { final AudioManager audioManager = this.getApplicationContext().getSystemService(AudioManager.class); - Log.i(MediaService.class.getSimpleName(), "当前音频模式:" + audioManager.getMode()); - Log.i(MediaService.class.getSimpleName(), "当前音频音量:" + audioManager.getStreamVolume(audioManager.getMode())); -// Log.i(MediaService.class.getSimpleName(), "当前蓝牙是否打开:" + audioManager.isBluetoothScoOn()); -// Log.i(MediaService.class.getSimpleName(), "当前耳机是否打开:" + audioManager.isWiredHeadsetOn()); -// Log.i(MediaService.class.getSimpleName(), "当前电话扬声器是否打开:" + audioManager.isSpeakerphoneOn()); + Log.d(MediaService.class.getSimpleName(), "当前音频模式:" + audioManager.getMode()); + Log.d(MediaService.class.getSimpleName(), "当前音频音量:" + audioManager.getStreamVolume(audioManager.getMode())); +// Log.d(MediaService.class.getSimpleName(), "当前蓝牙是否打开:" + audioManager.isBluetoothScoOn()); +// Log.d(MediaService.class.getSimpleName(), "当前耳机是否打开:" + audioManager.isWiredHeadsetOn()); +// Log.d(MediaService.class.getSimpleName(), "当前电话扬声器是否打开:" + audioManager.isSpeakerphoneOn()); // audioManager.setStreamVolume(AudioManager.MODE_IN_COMMUNICATION, audioManager.getStreamMaxVolume(AudioManager.MODE_IN_COMMUNICATION), AudioManager.FLAG_PLAY_SOUND); } diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/SettingsActivity.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/SettingsActivity.java index b88b6be..1f6c6dd 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/SettingsActivity.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/SettingsActivity.java @@ -6,6 +6,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; @@ -22,18 +23,18 @@ public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { - Log.i(SettingsActivity.class.getSimpleName(), "onCreate"); + Log.d(SettingsActivity.class.getSimpleName(), "onCreate"); super.onCreate(savedInstanceState); - // 布局 this.binding = ActivitySettingsBinding.inflate(this.getLayoutInflater()); - this.setContentView(this.binding.getRoot()); - // 设置按钮 + final View root = this.binding.getRoot(); + root.setZ(100F); + this.setContentView(root); this.binding.connect.setOnClickListener(this::settingsPersistent); } @Override protected void onStart() { - Log.i(SettingsActivity.class.getSimpleName(), "onStart"); + Log.d(SettingsActivity.class.getSimpleName(), "onStart"); super.onStart(); // 回填配置 final SharedPreferences sharedPreferences = this.getSharedPreferences("settings", Context.MODE_PRIVATE); @@ -47,18 +48,18 @@ public class SettingsActivity extends AppCompatActivity { @Override protected void onStop() { - Log.i(SettingsActivity.class.getSimpleName(), "onStop"); + Log.d(SettingsActivity.class.getSimpleName(), "onStop"); super.onStop(); } @Override protected void onDestroy() { - Log.i(SettingsActivity.class.getSimpleName(), "onDestroy"); + Log.d(SettingsActivity.class.getSimpleName(), "onDestroy"); super.onDestroy(); } /** - * 持久化日志 + * 保存配置 * * @param view View */ @@ -86,16 +87,19 @@ public class SettingsActivity extends AppCompatActivity { editor.putString("settings.clientId", clientId); editor.putString("settings.username", username); editor.putString("settings.password", password); - editor.commit(); - // 重连信令 - final Intent serviceIntent = new Intent(this, MediaService.class); - serviceIntent.setAction(MediaService.Action.RECONNECT.name()); - this.startService(serviceIntent); - // 预览页面 - final Intent activityIntent = new Intent(this, MainActivity.class); - this.startActivity(activityIntent); - // 结束 - this.finish(); + if(editor.commit()) { + // 重连信令 + final Intent serviceIntent = new Intent(this, MediaService.class); + serviceIntent.setAction(MediaService.Action.RECONNECT.name()); + this.startService(serviceIntent); + // 预览页面 + final Intent activityIntent = new Intent(this, MainActivity.class); + this.startActivity(activityIntent); + // 结束 + this.finish(); + } else { + Toast.makeText(this.getApplicationContext(), "配置保存失败", Toast.LENGTH_SHORT).show(); + } } -} \ No newline at end of file +} diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/TaoyaoReceiver.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/TaoyaoReceiver.java index 85f2500..a6e5d22 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/TaoyaoReceiver.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/TaoyaoReceiver.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.util.Log; /** - * 开机启动 + * 开机广播 * * @author acgist */ @@ -14,7 +14,7 @@ public class TaoyaoReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - Log.i(TaoyaoReceiver.class.getSimpleName(), "onReceive:" + intent.getAction()); + Log.d(TaoyaoReceiver.class.getSimpleName(), "onReceive:" + intent.getAction()); if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { this.bootTaoyao(context); } else { diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java index 0847080..ada7a37 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java @@ -23,6 +23,7 @@ import com.acgist.taoyao.boot.utils.JSONUtils; import com.acgist.taoyao.boot.utils.MapUtils; import com.acgist.taoyao.client.R; import com.acgist.taoyao.media.MediaManager; +import com.acgist.taoyao.media.client.RecordClient; import com.acgist.taoyao.media.client.Room; import com.acgist.taoyao.media.client.SessionClient; import com.acgist.taoyao.media.config.MediaAudioProperties; @@ -60,17 +61,26 @@ import javax.crypto.spec.SecretKeySpec; public final class Taoyao implements ITaoyao { /** - * 端口 + * 心跳时间 */ - private final int port; - /** - * 地址 - */ - private final String host; + private static final long HEARTBEAT_DURATION = 30L * 1000; + /** * 信令版本 */ private final String version; + /** + * 终端类型 + */ + private final String clientType; + /** + * 信令端口 + */ + private final int port; + /** + * 信令地址 + */ + private final String host; /** * 终端名称 */ @@ -80,15 +90,11 @@ public final class Taoyao implements ITaoyao { */ private final String clientId; /** - * 终端类型 - */ - private final String clientType; - /** - * 桃夭帐号 + * 信令帐号 */ private final String username; /** - * 桃夭密码 + * 信令密码 */ private final String password; /** @@ -124,11 +130,11 @@ public final class Taoyao implements ITaoyao { */ private final Cipher decrypt; /** - * Handler + * MainHandler */ private final Handler mainHandler; /** - * 服务上下文 + * 上下文 */ private final Context context; /** @@ -136,7 +142,7 @@ public final class Taoyao implements ITaoyao { */ private final ITaoyaoListener taoyaoListener; /** - * Wifi管理器 + * WIFI管理器 */ private final WifiManager wifiManager; /** @@ -151,12 +157,9 @@ public final class Taoyao implements ITaoyao { * 位置管理器 */ private final LocationManager locationManager; - /** - * 请求消息:同步消息 - */ - private final Map requestMessage; /** * 消息Handler + * 接收消息、重连信令 */ private final Handler messageHandler; /** @@ -165,6 +168,7 @@ public final class Taoyao implements ITaoyao { private final HandlerThread messageThread; /** * 执行Handler + * 处理接收信令消息 */ private final Handler executeHandler; /** @@ -180,9 +184,13 @@ public final class Taoyao implements ITaoyao { */ private final HandlerThread heartbeatThread; /** - * 媒体来源管理器 + * 媒体管理器 */ private final MediaManager mediaManager; + /** + * 请求消息:同步消息 + */ + private final Map requestMessage; /** * 视频房间列表 */ @@ -196,52 +204,85 @@ public final class Taoyao implements ITaoyao { */ public static Taoyao taoyao; + /** + * @param version 信令版本 + * @param clientType 终端类型 + * @param port 信令端口 + * @param host 信令地址 + * @param name 终端名称 + * @param clientId 终端ID + * @param username 信令帐号 + * @param password 信令密码 + * @param timeout 超时时间 + * @param algo 加密算法 + * @param secret 加密密钥 + * @param mainHandler MainHandler + * @param context 上下文 + * @param taoyaoListener 桃夭监听 + */ public Taoyao( - int port, String host, String version, - String name, String clientId, String clientType, String username, String password, + String version, String clientType, int port, String host, + String name, String clientId, String username, String password, int timeout, String algo, String secret, Handler mainHandler, Context context, ITaoyaoListener taoyaoListener ) { - this.close = false; - this.connect = false; - this.port = port; - this.host = host; - this.version = version; - this.name = name; - this.clientId = clientId; + this.close = false; + this.connect = false; + this.version = version; this.clientType = clientType; - this.username = username; - this.password = password; - this.timeout = timeout; + this.port = port; + this.host = host; + this.name = name; + this.clientId = clientId; + this.username = username; + this.password = password; + this.timeout = timeout; final boolean plaintext = algo == null || algo.isEmpty() || algo.equals("PLAINTEXT"); - this.encrypt = plaintext ? null : this.buildCipher(Cipher.ENCRYPT_MODE, algo, secret); - this.decrypt = plaintext ? null : this.buildCipher(Cipher.DECRYPT_MODE, algo, secret); - this.mainHandler = mainHandler; - this.context = context; - this.taoyaoListener = taoyaoListener; - this.wifiManager = context.getSystemService(WifiManager.class); - this.powerManager = context.getSystemService(PowerManager.class); - this.batteryManager = context.getSystemService(BatteryManager.class); - this.locationManager = context.getSystemService(LocationManager.class); - this.requestMessage = new ConcurrentHashMap<>(); - this.messageThread = new HandlerThread("MessageThread"); - this.messageThread.setDaemon(true); - this.messageThread.start(); - this.messageHandler = new Handler(this.messageThread.getLooper()); - this.messageHandler.post(this::connect); - this.executeThread = new HandlerThread("ExecuteThread"); - this.executeThread.setDaemon(true); - this.executeThread.start(); - this.executeHandler = new Handler(this.executeThread.getLooper()); - this.heartbeatThread = new HandlerThread("HeartbeatThread"); - this.heartbeatThread.setDaemon(true); - this.heartbeatThread.start(); + this.encrypt = plaintext ? null : this.buildCipher(Cipher.ENCRYPT_MODE, algo, secret); + this.decrypt = plaintext ? null : this.buildCipher(Cipher.DECRYPT_MODE, algo, secret); + this.mainHandler = mainHandler; + this.context = context; + this.taoyaoListener = taoyaoListener; + this.wifiManager = context.getSystemService(WifiManager.class); + this.powerManager = context.getSystemService(PowerManager.class); + this.batteryManager = context.getSystemService(BatteryManager.class); + this.locationManager = context.getSystemService(LocationManager.class); + this.messageThread = this.buildHandlerThread("MessageThread"); + this.messageHandler = new Handler(this.messageThread.getLooper()); + this.executeThread = this.buildHandlerThread("ExecuteThread"); + this.executeHandler = new Handler(this.executeThread.getLooper()); + this.heartbeatThread = this.buildHandlerThread("HeartbeatThread"); this.heartbeatHandler = new Handler(this.heartbeatThread.getLooper()); - this.heartbeatHandler.postDelayed(this::heartbeat, 30L * 1000); - this.mediaManager = MediaManager.getInstance(); - this.rooms = new ConcurrentHashMap<>(); - this.sessions = new ConcurrentHashMap<>(); - Taoyao.taoyao = this; + this.mediaManager = MediaManager.getInstance(); + this.requestMessage = new ConcurrentHashMap<>(); + this.rooms = new ConcurrentHashMap<>(); + this.sessions = new ConcurrentHashMap<>(); + Taoyao.taoyao = this; + // 开始连接 + this.messageHandler.post(this::connect); + // 开始心跳 + this.heartbeatHandler.postDelayed(this::clientHeartbeat, HEARTBEAT_DURATION); + } + + /** + * @param port 信令端口 + * @param host 信令地址 + * @param name 终端名称 + * @param clientId 终端ID + * @param username 信令帐号 + * @param password 信令密码 + * + * @return 是否需要重连信令 + */ + public boolean needReconnect(int port, String host, String name, String clientId, String username, String password) { + return !( + port == this.port && + host.equals(this.host) && + name.equals(this.name) && + clientId.equals(this.clientId) && + username.equals(this.username) && + password.equals(this.password) + ); } /** @@ -249,22 +290,36 @@ public final class Taoyao implements ITaoyao { * @param name 算法名称 * @param secret 密钥 * - * @return 加解密工具 + * @return 加密解密工具 */ private Cipher buildCipher(int mode, String name, String secret) { try { - final String algo = name.equals("DES") ? "DES/ECB/PKCS5Padding" : "AES/ECB/PKCS5Padding"; + final String algo = "DES".equals(name) ? "DES/ECB/PKCS5Padding" : "AES/ECB/PKCS5Padding"; final Cipher cipher = Cipher.getInstance(algo); cipher.init(mode, new SecretKeySpec(Base64.getMimeDecoder().decode(secret), name)); return cipher; } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) { - Log.e(Taoyao.class.getSimpleName(), "创建加解密工具异常", e); + Log.e(Taoyao.class.getSimpleName(), "创建加密解密工具异常", e); } return null; } + /** + * @param name 线程名称 + * + * @return 线程 + */ + private HandlerThread buildHandlerThread(String name) { + final HandlerThread handlerThread = new HandlerThread(name); + handlerThread.setDaemon(true); + handlerThread.start(); + return handlerThread; + } + /** * 连接信令 + * + * @returns 是否连接成功 */ public synchronized boolean connect() { if(this.close) { @@ -273,7 +328,7 @@ public final class Taoyao implements ITaoyao { // 释放连接 this.disconnect(); // 开始连接 - Log.d(Taoyao.class.getSimpleName(), "连接信令:" + this.host + ":" + this.port); + Log.i(Taoyao.class.getSimpleName(), "连接信令:" + this.host + ":" + this.port); this.socket = new Socket(); this.taoyaoListener.onConnect(); try { @@ -282,8 +337,8 @@ public final class Taoyao implements ITaoyao { this.socket.connect(new InetSocketAddress(this.host, this.port), this.timeout); if (this.socket.isConnected()) { this.connect = true; - this.input = this.socket.getInputStream(); - this.output = this.socket.getOutputStream(); + this.input = this.socket.getInputStream(); + this.output = this.socket.getOutputStream(); this.clientRegister(); this.taoyaoListener.onConnected(); this.messageHandler.post(this::pull); @@ -303,13 +358,12 @@ public final class Taoyao implements ITaoyao { * 循环读取信令消息 */ private void pull() { - int length = 0; - short messageLength = 0; - final byte[] bytes = new byte[1024]; + int length = 0; + short messageLength = 0; + final byte[] bytes = new byte[1024]; final ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); while (!this.close && this.connect) { try { - // 读取 while (!this.close && (length = this.input.read(bytes)) >= 0) { buffer.put(bytes, 0, length); while (buffer.position() > 0) { @@ -337,7 +391,7 @@ public final class Taoyao implements ITaoyao { buffer.compact(); final String content = new String(this.decrypt.doFinal(message)); try { - Taoyao.this.on(content); + this.on(content); } catch (Exception e) { Log.e(Taoyao.class.getSimpleName(), "处理信令异常:" + content, e); this.taoyaoListener.onError(e); @@ -360,32 +414,29 @@ public final class Taoyao implements ITaoyao { * @param message 原始消息 * * @return 加密消息 + * + * @throws BadPaddingException 错误填充 + * @throws IllegalBlockSizeException 错误大小 */ - private byte[] encrypt(Message message) { + private byte[] encrypt(Message message) throws BadPaddingException, IllegalBlockSizeException { final byte[] bytes = message.toString().getBytes(); - if (this.encrypt != null) { - try { - // 加密 - final byte[] encryptBytes = this.encrypt.doFinal(bytes); - // 编码 - final ByteBuffer buffer = ByteBuffer.allocateDirect(Short.BYTES + encryptBytes.length); - buffer.putShort((short) encryptBytes.length); - buffer.put(encryptBytes); - buffer.flip(); - // 编码 - final byte[] encodingBytes = new byte[buffer.capacity()]; - buffer.get(encodingBytes); - return encodingBytes; - } catch (IllegalBlockSizeException | BadPaddingException e) { - Log.e(Taoyao.class.getSimpleName(), "加密异常:" + message, e); - } + if (this.encrypt == null) { + return bytes; } - return bytes; + final byte[] encryptBytes = this.encrypt.doFinal(bytes); + final ByteBuffer buffer = ByteBuffer.allocateDirect(Short.BYTES + encryptBytes.length); + final byte[] encodingBytes = new byte[buffer.capacity()]; + buffer.putShort((short) encryptBytes.length); + buffer.put(encryptBytes); + buffer.flip(); + buffer.get(encodingBytes); + return encodingBytes; } /** * @param message 信令消息 */ + @Override public void push(Message message) { if (this.output == null) { Log.w(Taoyao.class.getSimpleName(), "通道没有打开:" + message); @@ -406,8 +457,16 @@ public final class Taoyao implements ITaoyao { */ @Override public Message request(Message request) { + if(request == null) { + Log.w(Taoyao.class.getSimpleName(), "信令消息错误:" + request); + return null; + } final Header header = request.getHeader(); - final Long id = header.getId(); + if(header == null) { + Log.w(Taoyao.class.getSimpleName(), "信令消息错误:" + request); + return null; + } + final Long id = header.getId(); this.requestMessage.put(id, request); synchronized (request) { this.push(request); @@ -432,32 +491,34 @@ public final class Taoyao implements ITaoyao { if(!this.connect) { return; } + Log.i(Taoyao.class.getSimpleName(), "释放信令:" + this.host + ":" + this.port); this.taoyaoListener.onDisconnect(); - Log.d(Taoyao.class.getSimpleName(), "释放信令:" + this.host + ":" + this.port); this.connect = false; CloseableUtils.close(this.input); CloseableUtils.close(this.output); CloseableUtils.close(this.socket); - this.input = null; + this.input = null; this.output = null; this.socket = null; this.closeRoomMedia(); this.closeSessionMedia(); } + /** + * 释放所有视频房间 + */ private void closeRoomMedia() { Log.i(Taoyao.class.getSimpleName(), "释放所有视频房间"); - this.rooms.forEach((k, v) -> { - v.close(); - }); + this.rooms.forEach((k, v) -> v.close()); this.rooms.clear(); } + /** + * 释放所有视频会话 + */ private void closeSessionMedia() { Log.i(Taoyao.class.getSimpleName(), "释放所有视频会话"); - this.sessions.forEach((k, v) -> { - v.close(); - }); + this.sessions.forEach((k, v) -> v.close()); this.sessions.clear(); } @@ -468,14 +529,12 @@ public final class Taoyao implements ITaoyao { if(this.close) { return; } - Log.d(Taoyao.class.getSimpleName(), "关闭信令:" + this.host + ":" + this.port); + Log.i(Taoyao.class.getSimpleName(), "关闭信令:" + this.host + ":" + this.port); this.close = true; this.disconnect(); this.heartbeatThread.quitSafely(); this.messageThread.quitSafely(); this.executeThread.quitSafely(); - this.rooms.values().forEach(Room::close); - this.sessions.values().forEach(SessionClient::close); } /** @@ -484,7 +543,7 @@ public final class Taoyao implements ITaoyao { * * @return 消息 */ - public Message buildMessage(String signal, Object... args) { + public Message buildMessage(String signal, Object ... args) { final Map map = new HashMap<>(); if (ArrayUtils.isNotEmpty(args)) { for (int index = 0; index < args.length; index += 2) { @@ -520,16 +579,18 @@ public final class Taoyao implements ITaoyao { private void on(String content) { final Message message = JSONUtils.toJava(content, Message.class); if (message == null) { + Log.w(Taoyao.class.getSimpleName(), "信令消息错误:" + content); return; } final Header header = message.getHeader(); if (header == null) { + Log.w(Taoyao.class.getSimpleName(), "信令消息错误:" + content); return; } final Long id = header.getId(); final Message request = this.requestMessage.get(id); if (request != null) { - Log.d(Taoyao.class.getSimpleName(), "处理信令响应:" + content); + Log.i(Taoyao.class.getSimpleName(), "处理信令响应:" + content); // 同步处理:重新设置响应消息 this.requestMessage.put(id, message); // 唤醒等待线程 @@ -537,257 +598,436 @@ public final class Taoyao implements ITaoyao { request.notifyAll(); } } else { - Log.d(Taoyao.class.getSimpleName(), "处理信令异步:" + content); + Log.i(Taoyao.class.getSimpleName(), "处理信令异步:" + content); this.executeHandler.post(() -> { try { this.dispatch(content, header, message); } catch (Exception e) { Log.e(Taoyao.class.getSimpleName(), "处理信令异常:" + content, e); + this.taoyaoListener.onError(e); } }); } } + /** + * @param content 信令原始消息 + * @param header 信令消息头部 + * @param message 信令消息 + */ private void dispatch(final String content, final Header header, final Message message) { if(this.taoyaoListener.preOnMessage(message)) { return; } switch (header.getSignal()) { - case "control::config::audio" -> this.controlConfigAudio(message, message.body()); - case "control::config::video" -> this.controlConfigVideo(message, message.body()); - case "control::photograph" -> this.controlPhotograph(message, message.body()); - case "control::record" -> this.controlRecord(message, message.body()); - case "client::config" -> this.clientConfig(message, message.body()); - case "client::register" -> this.clientRegister(message, message.body()); - case "client::reboot" -> this.clientReboot(message, message.body()); - case "client::shutdown" -> this.clientShutdown(message, message.body()); - case "media::consume" -> this.mediaConsume(message, message.body()); - case "media::audio::volume" -> this.mediaAudioVolume(message, message.body()); - case "media::consumer::close" -> this.mediaConsumerClose(message, message.body()); - case "media::consumer::pause" -> this.mediaConsumerPause(message, message.body()); - case "media::consumer::request::key::frame" -> this.mediaConsumerRequestKeyFrame(message, message.body()); - case "media::consumer::resume" -> this.mediaConsumerResume(message, message.body()); - case "media::consumer::set::preferred::layers" -> this.mediaConsumerSetPreferredLayers(message, message.body()); - case "media::consumer::status" -> this.mediaConsumerStatus(message, message.body()); - case "media::producer::close" -> this.mediaProducerClose(message, message.body()); - case "media::producer::pause" -> this.mediaProducerPause(message, message.body()); - case "media::producer::resume" -> this.mediaProducerResume(message, message.body()); - case "media::producer::video::orientation:change" -> this.mediaVideoOrientationChange(message, message.body()); - case "room::client::list" -> this.roomClientList(message, message.body()); - case "room::close" -> this.roomClose(message, message.body()); - case "room::enter" -> this.roomEnter(message, message.body()); - case "room::expel" -> this.roomExpel(message, message.body()); - case "room::invite" -> this.roomInivte(message, message.body()); - case "room::leave" -> this.roomLeave(message, message.body()); - case "session::call" -> this.sessionCall(message, message.body()); - case "session::close" -> this.sessionClose(message, message.body()); - case "session::exchange" -> this.sessionExchange(message, message.body()); - case "session::pause" -> this.sessionPause(message, message.body()); - case "session::resume" -> this.sessionResume(message, message.body()); - default -> Log.d(Taoyao.class.getSimpleName(), "没有适配信令:" + content); + case "client::config" -> this.clientConfig(message, message.body()); + case "client::reboot" -> this.clientReboot(message, message.body()); + case "client::register" -> this.clientRegister(message, message.body()); + case "client::shutdown" -> this.clientShutdown(message, message.body()); + case "control::config::audio" -> this.controlConfigAudio(message, message.body()); + case "control::config::video" -> this.controlConfigVideo(message, message.body()); + case "control::photograph" -> this.controlPhotograph(message, message.body()); + case "control::record" -> this.controlRecord(message, message.body()); + case "media::audio::volume" -> this.mediaAudioVolume(message, message.body()); + case "media::consume" -> this.mediaConsume(message, message.body()); + case "media::consumer::close" -> this.mediaConsumerClose(message, message.body()); + case "media::consumer::pause" -> this.mediaConsumerPause(message, message.body()); + case "media::consumer::request::key::frame" -> this.mediaConsumerRequestKeyFrame(message, message.body()); + case "media::consumer::resume" -> this.mediaConsumerResume(message, message.body()); + case "media::consumer::set::preferred::layers" -> this.mediaConsumerSetPreferredLayers(message, message.body()); + case "media::consumer::status" -> this.mediaConsumerStatus(message, message.body()); + case "media::producer::close" -> this.mediaProducerClose(message, message.body()); + case "media::producer::pause" -> this.mediaProducerPause(message, message.body()); + case "media::producer::resume" -> this.mediaProducerResume(message, message.body()); + case "media::video::orientation::change" -> this.mediaVideoOrientationChange(message, message.body()); + case "room::client::list" -> this.roomClientList(message, message.body()); + case "room::close" -> this.roomClose(message, message.body()); + case "room::enter" -> this.roomEnter(message, message.body()); + case "room::expel" -> this.roomExpel(message, message.body()); + case "room::invite" -> this.roomInivte(message, message.body()); + case "room::leave" -> this.roomLeave(message, message.body()); + case "session::call" -> this.sessionCall(message, message.body()); + case "session::close" -> this.sessionClose(message, message.body()); + case "session::exchange" -> this.sessionExchange(message, message.body()); + case "session::pause" -> this.sessionPause(message, message.body()); + case "session::resume" -> this.sessionResume(message, message.body()); + default -> Log.d(Taoyao.class.getSimpleName(), "没有适配信令:" + content); } this.taoyaoListener.postOnMessage(message); } /** - * 注册 - */ - private void clientRegister() { - final Location location = this.location(); - this.push(this.buildMessage( - "client::register", - "username", this.username, - "password", this.password, - "name", this.name, - "clientId", this.clientId, - "clientType", this.clientType, - "latitude", location == null ? -1 : location.getLatitude(), - "longitude", location == null ? -1 : location.getLongitude(), - "signal", this.signal(), - "battery", this.battery(), - "charging", this.charging(), - "recording", this.mediaManager.isRecording() - )); - } - - private void controlConfigAudio(Message message, Map body) { - final MediaAudioProperties mediaAudioProperties = JSONUtils.toJava(JSONUtils.toJSON(body), MediaAudioProperties.class); - this.mediaManager.updateAudioConfig(mediaAudioProperties); - - } - - private void controlConfigVideo(Message message, Map body) { - final MediaVideoProperties mediaVideoProperties = JSONUtils.toJava(JSONUtils.toJSON(body), MediaVideoProperties.class); - this.mediaManager.updateVideoConfig(mediaVideoProperties); - } - - private void controlPhotograph(Message message, Map body) { - this.mediaManager.photograph(); - } - - private void controlRecord(Message message, Map body) { - final Boolean enabled = MapUtils.getBoolean(body, "enabled"); - if(Boolean.TRUE.equals(enabled)) { - this.mediaManager.startRecord(); - } else { - this.mediaManager.stopRecord(); - } - body.put("enabled", enabled); - this.push(message); - } - - /** - * @param message 消息 - * @param body 消息主体 + * 配置终端 + * + * @param message 信令消息 + * @param body 信令主体 */ private void clientConfig(Message message, Map body) { - final MediaProperties mediaProperties = JSONUtils.toJava(JSONUtils.toJSON(body.get("media")), MediaProperties.class); + final MediaProperties mediaProperties = JSONUtils.toJava(JSONUtils.toJSON(body.get("media")), MediaProperties.class); this.mediaManager.updateMediaConfig(mediaProperties); final WebrtcProperties webrtcProperties = JSONUtils.toJava(JSONUtils.toJSON(body.get("webrtc")), WebrtcProperties.class); this.mediaManager.updateWebrtcConfig(webrtcProperties); } /** + * 终端心跳信令 + */ + private void clientHeartbeat() { + this.heartbeatHandler.postDelayed(this::clientHeartbeat, HEARTBEAT_DURATION); + if(this.close || !this.connect) { + return; + } + final Location location = this.location(); + this.push(this.buildMessage( + "client::heartbeat", + "latitude", location == null ? -1 : location.getLatitude(), + "longitude", location == null ? -1 : location.getLongitude(), + "signal", this.signal(), + "battery", this.battery(), + "charging", this.charging(), + "recording", this.mediaManager.isRecording() + )); + } + + /** + * 重启终端 + * + * @param message 信令消息 + * @param body 信令主体 + */ + @SuppressLint("MissingPermission") + private void clientReboot(Message message, Map body) { + Log.i(Taoyao.class.getSimpleName(), "系统重启"); + this.powerManager.reboot("系统重启"); + Process.killProcess(Process.myPid()); + } + + /** + * 终端注册 + */ + private void clientRegister() { + final Location location = this.location(); + this.push(this.buildMessage( + "client::register", + "name", this.name, + "clientId", this.clientId, + "clientType", this.clientType, + "username", this.username, + "password", this.password, + "latitude", location == null ? -1 : location.getLatitude(), + "longitude", location == null ? -1 : location.getLongitude(), + "signal", this.signal(), + "battery", this.battery(), + "charging", this.charging(), + "recording", this.mediaManager.isRecording() + )); + } + + /** + * 终端注册 + * * @param message 消息 * @param body 消息主体 */ private void clientRegister(Message message, Map body) { - final Integer index = (Integer) body.get("index"); + final Integer index = MapUtils.getInteger(body, "index"); if (index == null) { return; } IdUtils.setClientIndex(index); } - private void clientReboot(Message message, Map body) { - Log.i(Taoyao.class.getSimpleName(), "系统重启"); -// this.powerManager.reboot("系统重启"); - Process.killProcess(Process.myPid()); - } - + /** + * 关闭终端 + * + * @param message 信令消息 + * @param body 信令主体 + */ + @SuppressLint("MissingPermission") private void clientShutdown(Message message, Map body) { Log.i(Taoyao.class.getSimpleName(), "系统关机"); - // 自行实现 -// this.powerManager.reboot("系统关机"); + this.powerManager.reboot("系统关机"); Process.killProcess(Process.myPid()); } + /** + * 更新音频配置 + * + * @param message 信令消息 + * @param body 信令主体 + */ + private void controlConfigAudio(Message message, Map body) { + final MediaAudioProperties mediaAudioProperties = JSONUtils.toJava(JSONUtils.toJSON(body), MediaAudioProperties.class); + this.mediaManager.updateAudioConfig(mediaAudioProperties); + } + + /** + * 更新视频配置 + * + * @param message 信令消息 + * @param body 信令主体 + */ + private void controlConfigVideo(Message message, Map body) { + final MediaVideoProperties mediaVideoProperties = JSONUtils.toJava(JSONUtils.toJSON(body), MediaVideoProperties.class); + this.mediaManager.updateVideoConfig(mediaVideoProperties); + } + + /** + * 拍照 + * + * @param message 信令消息 + * @param body 信令主体 + */ + private void controlPhotograph(Message message, Map body) { + final String filepath = this.mediaManager.photograph(); + body.put("filepath", filepath); + this.push(message); + } + + /** + * 录制 + * + * @param message 信令消息 + * @param body 信令主体 + */ + private void controlRecord(Message message, Map body) { + String filepath; + final Boolean enabled = MapUtils.getBoolean(body, "enabled"); + if(Boolean.TRUE.equals(enabled)) { + final RecordClient recordClient = this.mediaManager.startRecord(); + filepath = recordClient.getFilepath(); + } else { + filepath = this.mediaManager.stopRecord(); + } + body.put("enabled", enabled); + body.put("filepath", filepath); + this.push(message); + } + + /** + * 远程音量 + * + * @param message 信令消息 + * @param body 信令主体 + */ + private void mediaAudioVolume(Message message, Map body) { + // TODO:如果需要显示音量 + } + + /** + * 消费媒体信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaConsume(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.mediaConsume(message, body); } - private void mediaAudioVolume(Message message, Map body) { - - } - + /** + * 关闭消费者信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaConsumerClose(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.mediaConsumerClose(body); } + /** + * 暂停消费者信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaConsumerPause(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.mediaConsumerPause(body); } + /** + * 请求关键帧信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaConsumerRequestKeyFrame(Message message, Map body) { } + /** + * 恢复消费者信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaConsumerResume(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.mediaConsumerResume(body); } + /** + * 修改最佳空间层和时间层信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaConsumerSetPreferredLayers(Message message, Map body) { } + /** + * 查询消费者状态信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaConsumerStatus(Message message, Map body) { } + /** + * 关闭生产者信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaProducerClose(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.mediaProducerClose(body); } + /** + * 暂停生产者信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaProducerPause(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.mediaProducerPause(body); } - private void mediaVideoOrientationChange(Message message, Map body) { - - } - + /** + * 恢复生产者信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void mediaProducerResume(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.mediaProducerResume(body); } + /** + * 视频方向变化信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ + private void mediaVideoOrientationChange(Message message, Map body) { + + } + + /** + * 房间终端列表信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void roomClientList(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.newRemoteClientFromRoomClientList(body); } + /** + * 关闭房间信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void roomClose(Message message, Map body) { - final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.remove(roomId); + final String roomId = MapUtils.get(body, "roomId"); + final Room room = this.rooms.remove(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.close(); } + /** + * 进入房间信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void roomEnter(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.newRemoteClientFromRoomEnter(body); } + /** + * 进入房间信令 + * + * @param roomId 房间ID + * @param password 房间密码 + * + * @return 房间 + */ public Room roomEnter(String roomId, String password) { final Resources resources = this.context.getResources(); final Room room = this.rooms.computeIfAbsent( roomId, key -> new Room( - this.name, key, + roomId, this.name, this.clientId, password, this, this.mainHandler, resources.getBoolean(R.bool.preview), @@ -808,25 +1048,44 @@ public final class Taoyao implements ITaoyao { room.mediaProduce(); return room; } else { + Log.i(Taoyao.class.getSimpleName(), "进入房间失败:" + roomId); this.rooms.remove(roomId); return null; } } + /** + * 踢出房间信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void roomExpel(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); this.roomLeave(roomId); } + /** + * 邀请终端信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void roomInivte(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); final String password = MapUtils.get(body, "password"); this.roomEnter(roomId, password); } + /** + * 离开房间信令 + * + * @param roomId 房间ID + */ public void roomLeave(String roomId) { final Room room = this.rooms.remove(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } this.push(this.buildMessage( @@ -836,16 +1095,28 @@ public final class Taoyao implements ITaoyao { room.close(); } + /** + * 离开房间信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void roomLeave(Message message, Map body) { final String roomId = MapUtils.get(body, "roomId"); final String clientId = MapUtils.get(body, "clientId"); - final Room room = this.rooms.get(roomId); + final Room room = this.rooms.get(roomId); if(room == null) { + Log.w(Taoyao.class.getSimpleName(), "无效房间:" + roomId); return; } room.closeRemoteClient(clientId); } + /** + * 发起会话信令 + * + * @param clientId 终端ID + */ public void sessionCall(String clientId) { this.requestFuture( this.buildMessage( @@ -854,11 +1125,11 @@ public final class Taoyao implements ITaoyao { ), response -> { final Map body = response.body(); - final String name = MapUtils.get(body, "name"); - final String sessionId = MapUtils.get(body, "sessionId"); + final String name = MapUtils.get(body, "name"); + final String sessionId = MapUtils.get(body, "sessionId"); final Resources resources = this.context.getResources(); final SessionClient sessionClient = new SessionClient( - sessionId, name, MapUtils.get(body, "clientId"), this, this.mainHandler, + sessionId, name, clientId, this, this.mainHandler, resources.getBoolean(R.bool.preview), resources.getBoolean(R.bool.playAudio), resources.getBoolean(R.bool.playVideo), @@ -876,6 +1147,12 @@ public final class Taoyao implements ITaoyao { ); } + /** + * 发起会话信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void sessionCall(Message message, Map body) { final String name = MapUtils.get(body, "name"); final String clientId = MapUtils.get(body, "clientId"); @@ -900,85 +1177,72 @@ public final class Taoyao implements ITaoyao { sessionClient.offer(); } + /** + * 关闭媒体信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void sessionClose(Message message, Map body) { - final String sessionId = MapUtils.get(body, "sessionId"); + final String sessionId = MapUtils.get(body, "sessionId"); final SessionClient sessionClient = this.sessions.remove(sessionId); if(sessionClient == null) { + Log.w(Taoyao.class.getSimpleName(), "无效会话:" + sessionId); return; } sessionClient.close(); } + /** + * 媒体交换信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void sessionExchange(Message message, Map body) { final String sessionId = MapUtils.get(body, "sessionId"); final SessionClient sessionClient = this.sessions.get(sessionId); if(sessionClient == null) { - Log.w(Taoyao.class.getSimpleName(), "会话交换无效会话:" + sessionId); + Log.w(Taoyao.class.getSimpleName(), "无效会话:" + sessionId); return; } sessionClient.exchange(message, body); } + /** + * 暂停媒体信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void sessionPause(Message message, Map body) { final String sessionId = MapUtils.get(body, "sessionId"); final SessionClient sessionClient = this.sessions.get(sessionId); if(sessionClient == null) { + Log.w(Taoyao.class.getSimpleName(), "无效会话:" + sessionId); return; } final String type = MapUtils.get(body, "type"); sessionClient.pause(type); } + /** + * 恢复媒体信令 + * + * @param message 信令消息 + * @param body 信令主体 + */ private void sessionResume(Message message, Map body) { final String sessionId = MapUtils.get(body, "sessionId"); final SessionClient sessionClient = this.sessions.get(sessionId); if(sessionClient == null) { + Log.w(Taoyao.class.getSimpleName(), "无效会话:" + sessionId); return; } final String type = MapUtils.get(body, "type"); sessionClient.resume(type); } - /** - * 心跳 - */ - private void heartbeat() { - this.heartbeatHandler.postDelayed(this::heartbeat, 30L * 1000); - if(this.close || !this.connect) { - return; - } - final Location location = this.location(); - this.push(this.buildMessage( - "client::heartbeat", - "latitude", location == null ? -1 : location.getLatitude(), - "longitude", location == null ? -1 : location.getLongitude(), - "signal", this.signal(), - "battery", this.battery(), - "charging", this.charging(), - "recording", this.mediaManager.isRecording() - )); - } - - /** - * @return 电量百分比 - */ - private int battery() { - return - this.batteryManager == null ? - -1 : - this.batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); - } - - /** - * @return 充电状态 - */ - private boolean charging() { - return - this.batteryManager == null ? - false : - this.batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) == BatteryManager.BATTERY_STATUS_CHARGING; - } - /** * @return WIFI信号强度 */ @@ -990,12 +1254,25 @@ public final class Taoyao implements ITaoyao { if(wifiInfo == null) { return -1; } - final int signal = this.wifiManager.calculateSignalLevel(wifiInfo.getRssi()); - return signal / this.wifiManager.getMaxSignalLevel() * 100; + return this.wifiManager.calculateSignalLevel(wifiInfo.getRssi()) / this.wifiManager.getMaxSignalLevel() * 100; } /** - * @return 位置 + * @return 电量信息 + */ + private int battery() { + return this.batteryManager == null ? -1 : this.batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); + } + + /** + * @return 充电状态 + */ + private boolean charging() { + return this.batteryManager == null ? false : this.batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) == BatteryManager.BATTERY_STATUS_CHARGING; + } + + /** + * @return 位置信息 */ @SuppressLint("MissingPermission") private Location location() { @@ -1003,10 +1280,10 @@ public final class Taoyao implements ITaoyao { return null; } final Criteria criteria = new Criteria(); + criteria.setAccuracy(Criteria.ACCURACY_FINE); criteria.setCostAllowed(false); criteria.setBearingRequired(false); criteria.setAltitudeRequired(false); - criteria.setAccuracy(Criteria.ACCURACY_FINE); criteria.setPowerRequirement(Criteria.POWER_LOW); final String provider = this.locationManager.getBestProvider(criteria, true); if (provider == null) { diff --git a/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml b/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml index 00a86bf..640dee3 100644 --- a/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml +++ b/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml @@ -8,10 +8,10 @@ true 1.0.0 - - 5000 MOBILE + + 5000 DES @@ -34,14 +34,14 @@ true true - - false 100 fd-audio fd-video + + false 1 diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java index 313b11b..f05d67b 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java @@ -47,11 +47,9 @@ import java.util.Locale; import java.util.UUID; /** - * 媒体来源管理器 + * 媒体管理器 * * @author acgist - * - * TODO:动态码率(BITRATE_MODE_VBR、BITRATE_MODE) */ public final class MediaManager { @@ -85,6 +83,10 @@ public final class MediaManager { * 视频质量 */ private String videoQuantity; + /** + * 语音播报 + */ + private boolean broadcaster; /** * 通道数量 */ @@ -161,14 +163,14 @@ public final class MediaManager { * JavaAudioDeviceModule */ private JavaAudioDeviceModule javaAudioDeviceModule; + /** + * 语音播报 + */ + private TextToSpeech textToSpeech; /** * 视频处理 */ private VideoProcesser videoProcesser; - /** - * TTS - */ - private TextToSpeech textToSpeech; /** * 录屏等待锁 */ @@ -241,34 +243,35 @@ public final class MediaManager { * @param imageQuantity 图片质量 * @param audioQuantity 音频质量 * @param videoQuantity 视频质量 + * @param broadcaster 是否语音播报 * @param channelCount 音频通道数量 * @param iFrameInterval 关键帧频率 * @param imagePath 图片保存路径 * @param videoPath 视频保存路径 - * @param tts 是否加载TTS * @param watermark 水印信息 * @param videoSourceType 视频来源类型 */ public void initContext( Handler mainHandler, Context context, int imageQuantity, String audioQuantity, String videoQuantity, - int channelCount, int iFrameInterval, - String imagePath, String videoPath, boolean tts, + boolean broadcaster, int channelCount, int iFrameInterval, + String imagePath, String videoPath, String watermark, VideoSourceType videoSourceType ) { - this.mainHandler = mainHandler; - this.context = context; - this.imageQuantity = imageQuantity; - this.audioQuantity = audioQuantity; - this.videoQuantity = videoQuantity; - this.channelCount = channelCount; - this.iFrameInterval = iFrameInterval; - this.imagePath = imagePath; - this.videoPath = videoPath; - this.watermark = watermark; + this.mainHandler = mainHandler; + this.context = context; + this.imageQuantity = imageQuantity; + this.audioQuantity = audioQuantity; + this.videoQuantity = videoQuantity; + this.broadcaster = broadcaster; + this.channelCount = channelCount; + this.iFrameInterval = iFrameInterval; + this.imagePath = imagePath; + this.videoPath = videoPath; + this.watermark = watermark; this.videoSourceType = videoSourceType; - if(tts) { - this.initTTS(context); + synchronized (this) { + this.notifyAll(); } } @@ -277,29 +280,21 @@ public final class MediaManager { */ public void initTaoyao(ITaoyao taoyao) { this.taoyao = taoyao; + synchronized (this) { + this.notifyAll(); + } } - private void initTTS(Context context) { - if(this.textToSpeech != null) { + public synchronized void broadcast(String text) { + if(!this.broadcaster) { return; } - Log.i(MediaManager.class.getSimpleName(), "加载TTS"); - this.textToSpeech = new TextToSpeech(context, new MediaManager.TextToSpeechInitListener()); - } - - public void broadcast(String text) { if(this.textToSpeech == null) { - return; + this.textToSpeech = new TextToSpeech(this.context, new MediaManager.TextToSpeechInitListener()); } -// this.textToSpeech.stop(); this.textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, UUID.randomUUID().toString()); - } - - public void closeTTS() { - if(this.textToSpeech == null) { - return; - } - this.textToSpeech.shutdown(); +// this.textToSpeech.stop(); +// this.textToSpeech.shutdown(); } /** @@ -775,14 +770,16 @@ public final class MediaManager { } } - public void stopRecord() { + public String stopRecord() { synchronized (this) { if(this.recordClient == null) { - return; + return null; } else { + final String filepath = this.recordClient.getFilepath(); this.recordClient.close(); this.recordClient = null; this.mainHandler.obtainMessage(Config.WHAT_RECORD, Boolean.FALSE).sendToTarget(); + return filepath; } } } @@ -960,7 +957,7 @@ public final class MediaManager { private class TextToSpeechInitListener implements TextToSpeech.OnInitListener { @Override public void onInit(int status) { - Log.i(MediaManager.class.getSimpleName(), "加载TTS:" + status); + Log.i(MediaManager.class.getSimpleName(), "加载语音播报:" + status); if(status == TextToSpeech.SUCCESS) { MediaManager.this.textToSpeech.setLanguage(Locale.CANADA); MediaManager.this.textToSpeech.setPitch(1.0F); diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java index bd0539e..a66907e 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java @@ -183,8 +183,10 @@ public class RecordClient extends Client implements VideoSink { audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, this.audioBitRate); // 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, 1024 * 8 * 8); // 设置缓冲大小 + // 设置缓冲大小 +// audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024 * 8 * 8); this.audioCodec = MediaCodec.createEncoderByType(audioType); this.audioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); } catch (Exception e) { diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/Room.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/Room.java index 33528bc..fbfb99b 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/Room.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/Room.java @@ -46,6 +46,7 @@ public class Room extends CloseableClient implements RouterCallback { private final boolean dataProduce; private final boolean audioProduce; private final boolean videoProduce; + private boolean produce; private final MediaProperties mediaProperties; private final WebrtcProperties webrtcProperties; private final Map remoteClients; @@ -57,8 +58,8 @@ public class Room extends CloseableClient implements RouterCallback { private Object sctpCapabilities; /** - * @param name 房间名称 * @param roomId 房间ID + * @param name 终端名称 * @param clientId 当前终端ID * @param password 房间密码 * @param taoyao 信令 @@ -76,17 +77,17 @@ public class Room extends CloseableClient implements RouterCallback { * @param webrtcProperties WebRTC配置 */ public Room( - String name, String roomId, + String roomId, String name, String clientId, String password, ITaoyao taoyao, Handler mainHandler, - boolean preview, boolean playAudio, boolean playVideo, + boolean preview, boolean playAudio, boolean playVideo, boolean dataConsume, boolean audioConsume, boolean videoConsume, boolean dataProduce, boolean audioProduce, boolean videoProduce, MediaProperties mediaProperties, WebrtcProperties webrtcProperties ) { super(taoyao, mainHandler); - this.name = name; this.roomId = roomId; + this.name = name; this.clientId = clientId; this.password = password; this.preview = preview; @@ -98,9 +99,10 @@ public class Room extends CloseableClient implements RouterCallback { this.dataProduce = dataProduce; this.audioProduce = audioProduce; this.videoProduce = videoProduce; - this.mediaProperties = mediaProperties; + this.produce = false; + this.mediaProperties = mediaProperties; this.webrtcProperties = webrtcProperties; - this.remoteClients = new ConcurrentHashMap<>(); + this.remoteClients = new ConcurrentHashMap<>(); this.nativeRoomPointer = this.nativeNewRoom(roomId, this); } @@ -109,6 +111,7 @@ public class Room extends CloseableClient implements RouterCallback { if (this.init) { return true; } + Log.i(Room.class.getSimpleName(), "进入房间:" + this.roomId); super.init(); this.peerConnectionFactory = this.mediaManager.newClient(); this.localClient = new LocalClient(this.name, this.clientId, this.taoyao, this.mainHandler); @@ -141,6 +144,10 @@ public class Room extends CloseableClient implements RouterCallback { } public void mediaProduce() { + if(this.produce) { + return; + } + this.produce = true; if (this.audioProduce || this.videoProduce) { this.createSendTransport(); } diff --git a/taoyao-client-media/src/Taoyao.js b/taoyao-client-media/src/Taoyao.js index 1f2ad59..245a7fd 100644 --- a/taoyao-client-media/src/Taoyao.js +++ b/taoyao-client-media/src/Taoyao.js @@ -130,13 +130,13 @@ const signalChannel = { // TODO:电池信息 me.push( protocol.buildMessage("client::register", { - clientId: config.signal.clientId, name: config.signal.name, + clientId: config.signal.clientId, clientType: "MEDIA", - battery: 100, - charging: true, username: config.signal.username, password: config.signal.password, + battery: 100, + charging: true, }) ); me.reconnectionTimeout = me.minReconnectionDelay; diff --git a/taoyao-client-web/src/components/Taoyao.js b/taoyao-client-web/src/components/Taoyao.js index 3cf8dc6..1c1ab4d 100644 --- a/taoyao-client-web/src/components/Taoyao.js +++ b/taoyao-client-web/src/components/Taoyao.js @@ -142,13 +142,13 @@ const signalChannel = { const battery = await navigator.getBattery(); me.push( protocol.buildMessage("client::register", { - clientId: me.taoyao.clientId, name: me.taoyao.name, + clientId: me.taoyao.clientId, clientType: "WEB", - battery: battery.level * 100, - charging: battery.charging, username: me.taoyao.username, password: me.taoyao.password, + battery: battery.level * 100, + charging: battery.charging, }) ); me.reconnectionTimeout = me.minReconnectionDelay; @@ -1660,6 +1660,7 @@ class Taoyao extends RemoteClient { const me = this; const { clientId } = message.body; me.remoteClients.delete(clientId); + // TODO:close console.info("终端离开:", clientId); } /** @@ -2240,7 +2241,7 @@ class Taoyao extends RemoteClient { }) ); const { name, sessionId } = response.body; - const session = new Session({name, clientId: response.body.clientId, sessionId}); + const session = new Session({name, clientId, sessionId}); this.sessionClients.set(sessionId, session); } diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/ProtocolAdapter.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/ProtocolAdapter.java index d9066bf..5e9229f 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/ProtocolAdapter.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/ProtocolAdapter.java @@ -99,6 +99,10 @@ public abstract class ProtocolAdapter implements Protocol { if(id == null) { id = this.idService.buildId(); } + // 设置主体 + if(body == null) { + body = Map.of(); + } // 消息头部 final Header header = Header.builder() .v(this.taoyaoProperties.getVersion()) diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlPhotographProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlPhotographProtocol.java index f35965a..bdf30b7 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlPhotographProtocol.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlPhotographProtocol.java @@ -16,14 +16,21 @@ import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter; */ @Protocol @Description( - body = """ - { - "to": "目标终端ID" - } - """, + body = { + """ + { + "to": "目标终端ID" + } + """, + """ + { + "filepath": "图片文件路径" + } + """ + }, flow = { - "信令服务->终端", - "终端->信令服务->终端" + "信令服务->目标终端->信令服务", + "终端=>信令服务->目标终端->信令服务->终端" } ) public class ControlPhotographProtocol extends ProtocolControlAdapter { diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlRecordProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlRecordProtocol.java index 224dc9b..5c9f72c 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlRecordProtocol.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlRecordProtocol.java @@ -17,15 +17,23 @@ import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter; */ @Protocol @Description( - body = """ - { - "to": "目标终端ID", - "enabled": 是否录像(true|false) - } - """, + body = { + """ + { + "to": "目标终端ID", + "enabled": 是否录像(true|false) + } + """, + """ + { + "enabled": 是否录像(true|false), + "filepath": "视频文件路径" + } + """ + }, flow = { - "信令服务->终端", - "终端=>信令服务->终端" + "信令服务->目标终端->信令服务", + "终端=>信令服务->目标终端->信令服务->终端" } ) public class ControlRecordProtocol extends ProtocolControlAdapter { @@ -43,7 +51,7 @@ public class ControlRecordProtocol extends ProtocolControlAdapter { /** * @param clientId 终端ID - * @param enabled 状态 + * @param enabled 状态 * * @return 执行结果 */ diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaConsumerSetPreferredLayersProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaConsumerSetPreferredLayersProtocol.java index 75fbe35..d3ee5cd 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaConsumerSetPreferredLayersProtocol.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaConsumerSetPreferredLayersProtocol.java @@ -14,9 +14,12 @@ import com.acgist.taoyao.signal.protocol.ProtocolRoomAdapter; /** * 修改最佳空间层和时间层信令 + * * 空间层(spatialLayer):分辨率 * 时间层(temporalLayer):帧率 + * * 码率:数据大小和时间的比值 + * * 注意:只有simulcast和SVC消费者有效 * * @author acgist diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaConsumerSetPriorityProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaConsumerSetPriorityProtocol.java index 6873293..ba98987 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaConsumerSetPriorityProtocol.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaConsumerSetPriorityProtocol.java @@ -13,6 +13,8 @@ import com.acgist.taoyao.signal.protocol.ProtocolRoomAdapter; /** * 设置消费者优先级信令 * + * TODO:unsetPriority + * * @author acgist */ @Protocol diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/session/SessionCloseProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/session/SessionCloseProtocol.java index 04b215e..e17eae6 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/session/SessionCloseProtocol.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/session/SessionCloseProtocol.java @@ -45,7 +45,6 @@ public class SessionCloseProtocol extends ProtocolSessionAdapter implements Appl @Override public void execute(String clientId, ClientType clientType, Session session, Client client, Message message, Map body) { - session.push(message); session.close(); }