feat: 添加远端视频录制功能
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
package com.ttstd.controller;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -20,8 +23,10 @@ import android.widget.Switch;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.Gravity;
|
||||
import com.ttstd.controller.webrtc.SelfCodecDecoder;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -36,6 +41,8 @@ import com.ttstd.controller.signaling.SignalMessage;
|
||||
import com.ttstd.controller.signaling.WebSocketClient;
|
||||
import com.ttstd.controller.view.RemoteTouchView;
|
||||
import com.ttstd.controller.utils.DeviceUtils;
|
||||
import com.ttstd.controller.webrtc.SelfCodecDecoder;
|
||||
import com.ttstd.controller.webrtc.VideoRecorder;
|
||||
import com.ttstd.controller.webrtc.WebRtcClient;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
@@ -72,6 +79,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
private SurfaceHolder.Callback surfaceCallback;
|
||||
private boolean programmaticSwitch = false;
|
||||
|
||||
// 屏幕录制相关
|
||||
private static final int REQ_RECORD_PERMISSION = 1001;
|
||||
private Switch switchRecord;
|
||||
private TextView tvRecordStatus;
|
||||
private VideoRecorder videoRecorder;
|
||||
private boolean isSelfCodecMode = false; // 当前是否为自编码串流模式
|
||||
private boolean pendingRecordStart = false;
|
||||
|
||||
private WebSocketClient wsClient;
|
||||
private WebRtcClient webRtcClient;
|
||||
private EglBase eglBase;
|
||||
@@ -180,6 +195,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
updateUI(false);
|
||||
|
||||
setupSelfCodecUi();
|
||||
setupRecordUi();
|
||||
}
|
||||
|
||||
private void adjustVideoSize(int videoWidth, int videoHeight, int rotation) {
|
||||
@@ -396,6 +412,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
webRtcClient = new WebRtcClient(this, wsClient, myDeviceId);
|
||||
webRtcClient.initialize(eglBase);
|
||||
webRtcClient.setSelfCodecDecoder(selfCodecDecoder);
|
||||
webRtcClient.setRecorder(videoRecorder);
|
||||
webRtcClient.setStreamModeReportListener(mode -> runOnUiThread(() -> syncStreamModeUi(mode)));
|
||||
webRtcClient.setConnectionListener(new WebRtcClient.ConnectionListener() {
|
||||
@Override
|
||||
@@ -687,6 +704,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (connected) {
|
||||
statsHandler.post(statsRunnable);
|
||||
if (switchStreamMode != null) switchStreamMode.setEnabled(true);
|
||||
if (switchRecord != null) switchRecord.setEnabled(true);
|
||||
} else {
|
||||
statsHandler.removeCallbacks(statsRunnable);
|
||||
if (switchStreamMode != null) {
|
||||
@@ -697,6 +715,17 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
if (tvStreamMode != null) tvStreamMode.setText("串流: WebRTC");
|
||||
if (selfCodecDecoder != null) selfCodecDecoder.release();
|
||||
// 断开连接时停止录制
|
||||
if (videoRecorder != null && videoRecorder.isRecording()) {
|
||||
stopRecording();
|
||||
}
|
||||
if (switchRecord != null) {
|
||||
switchRecord.setEnabled(false);
|
||||
programmaticSwitch = true;
|
||||
switchRecord.setChecked(false);
|
||||
programmaticSwitch = false;
|
||||
}
|
||||
if (tvRecordStatus != null) tvRecordStatus.setText("未录制");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,6 +760,139 @@ public class MainActivity extends AppCompatActivity {
|
||||
});
|
||||
}
|
||||
|
||||
/** 初始化"屏幕录制"UI,绑定录制开关并处理保存逻辑。 */
|
||||
private void setupRecordUi() {
|
||||
switchRecord = findViewById(R.id.switch_record);
|
||||
tvRecordStatus = findViewById(R.id.tv_record_status);
|
||||
switchRecord.setEnabled(false);
|
||||
|
||||
videoRecorder = new VideoRecorder();
|
||||
videoRecorder.setOnRecordingListener(new VideoRecorder.OnRecordingListener() {
|
||||
@Override
|
||||
public void onStarted() {
|
||||
runOnUiThread(() -> tvRecordStatus.setText("录制中"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped(String savedLocation) {
|
||||
runOnUiThread(() -> {
|
||||
tvRecordStatus.setText("未录制");
|
||||
Toast.makeText(MainActivity.this,
|
||||
"视频已保存: " + savedLocation, Toast.LENGTH_LONG).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoppedEmpty() {
|
||||
runOnUiThread(() -> {
|
||||
tvRecordStatus.setText("未录制");
|
||||
Toast.makeText(MainActivity.this, "本次未录制到画面", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String message) {
|
||||
runOnUiThread(() -> {
|
||||
tvRecordStatus.setText("录制失败");
|
||||
programmaticSwitch = true;
|
||||
switchRecord.setChecked(false);
|
||||
programmaticSwitch = false;
|
||||
Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
switchRecord.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (programmaticSwitch) return;
|
||||
if (isChecked) {
|
||||
startRecording();
|
||||
} else {
|
||||
stopRecording();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 开始录制:先校验存储权限,必要时切回 WebRTC 模式再开始。 */
|
||||
private void startRecording() {
|
||||
if (!hasStoragePermission()) {
|
||||
pendingRecordStart = true;
|
||||
requestStoragePermission();
|
||||
return;
|
||||
}
|
||||
doStartRecording();
|
||||
}
|
||||
|
||||
private void doStartRecording() {
|
||||
if (videoRecorder == null) return;
|
||||
videoRecorder.start(this);
|
||||
if (webRtcClient != null) {
|
||||
webRtcClient.setRecorder(videoRecorder);
|
||||
webRtcClient.setRecording(true);
|
||||
}
|
||||
// 自编码模式下没有 WebRTC 媒体轨道,自动切回 WebRTC 才能录到画面
|
||||
if (isSelfCodecMode) {
|
||||
applyStreamMode(false);
|
||||
}
|
||||
tvRecordStatus.setText("录制中");
|
||||
}
|
||||
|
||||
private void stopRecording() {
|
||||
if (webRtcClient != null) {
|
||||
webRtcClient.setRecording(false);
|
||||
}
|
||||
if (videoRecorder != null) {
|
||||
videoRecorder.stop();
|
||||
}
|
||||
tvRecordStatus.setText("保存中...");
|
||||
}
|
||||
|
||||
/** Android 10+ 使用作用域存储,写入应用自身的媒体集合无需存储权限。 */
|
||||
private boolean hasStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
return true;
|
||||
}
|
||||
return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return;
|
||||
String[] perms;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
perms = new String[]{
|
||||
Manifest.permission.READ_MEDIA_VIDEO,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
};
|
||||
} else {
|
||||
perms = new String[]{
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
};
|
||||
}
|
||||
ActivityCompat.requestPermissions(this, perms, REQ_RECORD_PERMISSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode,
|
||||
@NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQ_RECORD_PERMISSION) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (pendingRecordStart) {
|
||||
pendingRecordStart = false;
|
||||
doStartRecording();
|
||||
}
|
||||
} else {
|
||||
pendingRecordStart = false;
|
||||
programmaticSwitch = true;
|
||||
switchRecord.setChecked(false);
|
||||
programmaticSwitch = false;
|
||||
Toast.makeText(this, "需要存储权限才能保存录制视频", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户切换串流模式:发送信令并切换本地视图。 */
|
||||
private void applyStreamMode(boolean self) {
|
||||
if (webRtcClient != null && webRtcClient.isDataChannelOpen()) {
|
||||
@@ -755,6 +917,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
/** 收到被控端回报的当前模式:同步 UI 与视图(防止切换失败不一致)。 */
|
||||
private void syncStreamModeUi(int mode) {
|
||||
boolean self = (mode == WebRtcClient.STREAM_MODE_SELF_CODEC);
|
||||
this.isSelfCodecMode = self;
|
||||
programmaticSwitch = true;
|
||||
switchStreamMode.setChecked(self);
|
||||
programmaticSwitch = false;
|
||||
|
||||
Reference in New Issue
Block a user