[+] 视频捕获:前置、后置、屏幕
This commit is contained in:
18
README.md
18
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.acgist.taoyao.client.media;
|
||||
|
||||
public class RoomClientManager {
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.acgist.taoyao.client.p2p;
|
||||
|
||||
public class P2PClientManager {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user