feat: 添加远端视频录制功能

This commit is contained in:
TongTongStudio
2026-07-29 16:20:35 +08:00
parent 1e37aa64cc
commit 7f545359d9
5 changed files with 691 additions and 3 deletions

View File

@@ -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;