增加webrtc,增加显示屏幕信息

This commit is contained in:
2025-12-24 09:17:19 +08:00
parent c00251ad64
commit 9e602a6cd1
7 changed files with 757 additions and 28 deletions

View File

@@ -100,6 +100,9 @@ dependencies {
/*https://github.com/JeremyLiao/LiveEventBus*/ /*https://github.com/JeremyLiao/LiveEventBus*/
implementation 'com.jeremyliao:live-event-bus-x:1.7.3' implementation 'com.jeremyliao:live-event-bus-x:1.7.3'
implementation 'org.webrtc:google-webrtc:1.0.32006'
implementation 'org.java-websocket:Java-WebSocket:1.5.3'
//MMKV //MMKV
implementation 'com.tencent:mmkv-static:1.2.14' implementation 'com.tencent:mmkv-static:1.2.14'
//bugly //bugly

View File

@@ -10,7 +10,8 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission <uses-permission
android:name="android.permission.POST_NOTIFICATIONS" android:name="android.permission.POST_NOTIFICATIONS"
@@ -33,6 +34,9 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".activity.WebRTCScreenCaptureActivity"
android:exported="true"/>
<!-- 屏幕采集服务 --> <!-- 屏幕采集服务 -->
<service <service
@@ -41,6 +45,10 @@
<!-- 指令执行服务 --> <!-- 指令执行服务 -->
<service android:name=".service.ControlService" /> <service android:name=".service.ControlService" />
<service
android:name=".service.ScreenCaptureService2"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
</application> </application>

View File

@@ -0,0 +1,620 @@
package com.ttstd.remoteservice.activity;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.ttstd.remoteservice.R;
import com.ttstd.remoteservice.service.ScreenCaptureService2;
import org.webrtc.CapturerObserver;
import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import java.util.ArrayList;
import java.util.List;
public class WebRTCScreenCaptureActivity extends AppCompatActivity {
private static final String TAG = "WebRTCScreenCaptureActivity";
// 请求码
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1001;
private static final int REQUEST_CODE_PERMISSIONS = 1002;
// WebRTC核心组件
private PeerConnectionFactory peerConnectionFactory;
private PeerConnection peerConnection;
private VideoSource videoSource;
private VideoTrack videoTrack;
private EglBase rootEglBase;
// 屏幕采集相关
private MediaProjectionManager mediaProjectionManager;
private MediaProjection mediaProjection;
private VirtualDisplay virtualDisplay;
private ScreenVideoCapturer screenVideoCapturer; // 自定义屏幕采集器
// 屏幕参数
private int screenWidth;
private int screenHeight;
private int screenDpi;
// UI控件
private Button btnStartPush;
private SurfaceViewRenderer localRender; // 本地预览(可选)
private TextureView textureView;
private VirtualDisplay mVirtualDisplay;
// 远端信令信息(示例:实际需从信令服务器获取)
private String remoteSdp = ""; // 替换为远端实际SDP
private List<IceCandidate> remoteIceCandidates = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webrtc_screen_capture);
// 初始化控件
btnStartPush = findViewById(R.id.btn_start_push);
localRender = findViewById(R.id.local_render);
textureView = findViewById(R.id.textureView);
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
Log.e(TAG, "onSurfaceTextureAvailable: ");
// SurfaceTexture准备就绪开始屏幕捕捉
setupVirtualDisplay();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
Log.e(TAG, "onSurfaceTextureSizeChanged: ");
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
Log.e(TAG, "onSurfaceTextureDestroyed: ");
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// Log.e(TAG, "onSurfaceTextureUpdated: ");
}
});
// 初始化屏幕参数
screenWidth = getResources().getDisplayMetrics().widthPixels;
screenHeight = getResources().getDisplayMetrics().heightPixels;
screenDpi = getResources().getDisplayMetrics().densityDpi;
// 初始化MediaProjectionManager
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
// 检查权限
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
// 初始化WebRTC EGL环境
rootEglBase = EglBase.create();
localRender.init(rootEglBase.getEglBaseContext(), null);
localRender.setMirror(true);
localRender.setEnableHardwareScaler(true);
}
private static final String FOREGROUND_SERVICE_MEDIA_PROJECTION = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
/**
* 检查权限并启动采集+推流
*/
private void checkPermissionsAndStart() {
List<String> neededPermissions = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
neededPermissions.add(Manifest.permission.INTERNET);
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
// ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
// neededPermissions.add(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION);
// }
if (Build.VERSION.SDK_INT >= 33 &&
ContextCompat.checkSelfPermission(this, FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
neededPermissions.add(FOREGROUND_SERVICE_MEDIA_PROJECTION);
}
if (!neededPermissions.isEmpty()) {
ActivityCompat.requestPermissions(this, neededPermissions.toArray(new String[0]), REQUEST_CODE_PERMISSIONS);
} else {
// 启动前台服务
startForegroundService();
// 权限已满足,请求屏幕录制授权
startScreenCaptureAuthorization();
}
}
/**
* 发起屏幕录制授权请求
*/
private void startScreenCaptureAuthorization() {
Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
}
/**
* 初始化WebRTC核心组件
*/
private void initWebRTC() {
// 1. 初始化PeerConnectionFactory
PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions.builder(this)
.setEnableInternalTracer(true)
.setFieldTrials("WebRTC-H264HighProfile/Enabled/") // 可选启用H264
.createInitializationOptions();
PeerConnectionFactory.initialize(initOptions);
// 2. 创建PeerConnectionFactory实例
PeerConnectionFactory.Options factoryOptions = new PeerConnectionFactory.Options();
peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(factoryOptions)
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(
EglBase.create().getEglBaseContext(), true, true))
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(
EglBase.create().getEglBaseContext()))
.createPeerConnectionFactory();
// 3. 创建自定义屏幕采集器
screenVideoCapturer = new ScreenVideoCapturer();
// 4. 创建VideoSource和VideoTrack
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("ScreenCaptureThread", rootEglBase.getEglBaseContext());
videoSource = peerConnectionFactory.createVideoSource(screenVideoCapturer.isScreencast());
screenVideoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());
screenVideoCapturer.startCapture(screenWidth, screenHeight, 30); // 30fps
// 5. 创建视频轨道并绑定到本地渲染
videoTrack = peerConnectionFactory.createVideoTrack("screen_video_track", videoSource);
videoTrack.addSink(localRender);
// 6. 创建PeerConnection需替换为实际的ICE服务器配置
List<PeerConnection.IceServer> iceServers = getIceServers();
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
// 配置 ICE 传输策略(可选,根据网络环境调整)
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; // 通常禁用 TCP 候选,使用 UDP
rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE; // 推荐启用 BUNDLE 以节省资源
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; // 持续收集 ICE 候选
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
Log.d(TAG, "PeerConnection onSignalingChange: " + signalingState);
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.d(TAG, "PeerConnection onIceConnectionChange: " + iceConnectionState);
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
runOnUiThread(() -> Toast.makeText(WebRTCScreenCaptureActivity.this, "WebRTC连接成功", Toast.LENGTH_SHORT).show());
}
}
@Override
public void onIceConnectionReceivingChange(boolean b) {
Log.d(TAG, "PeerConnection onIceConnectionReceivingChange: " + b);
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
Log.d(TAG, "PeerConnection onIceGatheringChange: " + iceGatheringState);
// ICE候选收集完成后可将本地SDP发送给远端
if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) {
Log.d(TAG, "ICE候选收集完成可发送本地SDP给远端");
}
}
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
Log.d(TAG, "PeerConnection onIceCandidate: " + iceCandidate);
// 将ICE候选发送给远端实际需通过信令服务器
sendIceCandidateToRemote(iceCandidate);
}
@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
Log.d(TAG, "PeerConnection onIceCandidatesRemoved");
}
@Override
public void onAddStream(MediaStream mediaStream) {
Log.d(TAG, "PeerConnection onAddStream: 收到远端流");
}
@Override
public void onRemoveStream(MediaStream mediaStream) {
Log.d(TAG, "PeerConnection onRemoveStream");
}
@Override
public void onDataChannel(org.webrtc.DataChannel dataChannel) {
Log.d(TAG, "PeerConnection onDataChannel");
}
@Override
public void onRenegotiationNeeded() {
Log.d(TAG, "PeerConnection onRenegotiationNeeded");
}
@Override
public void onAddTrack(org.webrtc.RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
Log.d(TAG, "PeerConnection onAddTrack");
}
});
// 7. 创建MediaStream并添加视频轨道
MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream("screen_stream");
mediaStream.addTrack(videoTrack);
peerConnection.addStream(mediaStream);
// 8. 创建Offer发起端
createOffer();
}
/**
* 创建WebRTC Offer
*/
private void createOffer() {
MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")); // 只推流,不接收视频
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")); // 不接收音频
peerConnection.createOffer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
// 设置本地SDP
peerConnection.setLocalDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
}
@Override
public void onSetSuccess() {
// 本地SDP设置成功通过信令服务器发送给远端
Log.d(TAG, "本地Offer创建成功: " + sessionDescription.description);
sendSdpToRemote(sessionDescription);
// 将本地SDP发送给远端实际需通过信令服务器
remoteSdp = sessionDescription.description; // 示例:实际需发送到远端
}
@Override
public void onCreateFailure(String s) {
Log.e(TAG, "创建Offer失败: " + s);
}
@Override
public void onSetFailure(String s) {
Log.e(TAG, "设置本地SDP失败: " + s);
}
}, sessionDescription);
}
@Override
public void onSetSuccess() {
}
@Override
public void onCreateFailure(String s) {
Log.e(TAG, "创建Offer失败: " + s);
}
@Override
public void onSetFailure(String s) {
}
}, constraints);
}
private List<PeerConnection.IceServer> getIceServers() {
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
// 添加用户指定的 STUN 服务器
PeerConnection.IceServer stunServer = PeerConnection.IceServer.builder("stun:47.242.112.133:3478")
.setUsername("tt")
.setPassword("tongtong")
.createIceServer();
iceServers.add(stunServer);
// 强烈建议添加备用的公共 STUN 服务器
PeerConnection.IceServer publicStunServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302")
.createIceServer();
iceServers.add(publicStunServer);
// 如果您的部署包含 TURN 服务器(用于可靠的中继转发),也应在此添加
// PeerConnection.IceServer turnServer = PeerConnection.IceServer.builder("turn:your_turn_server:3478")
// .setUsername("your_username")
// .setPassword("your_password")
// .createIceServer();
// iceServers.add(turnServer);
return iceServers;
}
private void sendSdpToRemote(SessionDescription sdp) {
// TODO: 实现信令发送逻辑例如通过WebSocket发送SDP字符串
}
/**
* 自定义屏幕视频采集器核心对接MediaProjection和WebRTC
*/
private class ScreenVideoCapturer implements VideoCapturer {
private CapturerObserver capturerObserver;
private SurfaceTextureHelper surfaceTextureHelper;
private Surface surface;
@Override
public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context context, CapturerObserver capturerObserver) {
this.surfaceTextureHelper = surfaceTextureHelper;
this.capturerObserver = capturerObserver;
this.surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
// 创建虚拟显示器将屏幕内容输出到WebRTC的Surface
createVirtualDisplay();
}
@Override
public void startCapture(int width, int height, int framerate) {
capturerObserver.onCapturerStarted(true);
Log.d(TAG, "Screen capturer started: " + width + "x" + height + " " + framerate + "fps");
}
@Override
public void stopCapture() throws InterruptedException {
capturerObserver.onCapturerStopped();
releaseVirtualDisplay();
}
@Override
public void changeCaptureFormat(int width, int height, int framerate) {
// 适配分辨率变化
releaseVirtualDisplay();
screenWidth = width;
screenHeight = height;
createVirtualDisplay();
}
@Override
public void dispose() {
surfaceTextureHelper.dispose();
releaseVirtualDisplay();
stopForegroundService();
}
@Override
public boolean isScreencast() {
return true; // 标记为屏幕采集(非摄像头)
}
private void createVirtualDisplay() {
if (mediaProjection == null || surface == null) {
return;
}
virtualDisplay = mediaProjection.createVirtualDisplay(
"WebRTC-ScreenCapture",
screenWidth,
screenHeight,
screenDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface,
null,
null
);
}
private void releaseVirtualDisplay() {
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
}
}
/**
* 启动前台服务Android 10+ 必须)
*/
private void startForegroundService() {
Intent serviceIntent = new Intent(this, ScreenCaptureService2.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent);
} else {
startService(serviceIntent);
}
}
/**
* 停止前台服务
*/
private void stopForegroundService() {
stopService(new Intent(this, ScreenCaptureService2.class));
}
/**
* 释放虚拟显示器
*/
private void releaseVirtualDisplay() {
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
}
/**
* 发送ICE候选到远端示例实际需通过WebSocket/HTTP信令服务器
*/
private void sendIceCandidateToRemote(IceCandidate iceCandidate) {
// 这里仅做日志打印,实际需替换为信令发送逻辑
Log.d(TAG, "Send ICE Candidate to Remote: " + iceCandidate.sdpMid + " " + iceCandidate.sdpMLineIndex + " " + iceCandidate.sdp);
// 示例将iceCandidate转为JSON发送到远端服务器
}
/**
* 处理权限请求结果
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
startScreenCaptureAuthorization();
} else {
Toast.makeText(this, "权限申请失败,无法启动推流", Toast.LENGTH_SHORT).show();
}
}
}
/**
* 处理屏幕录制授权结果
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
if (resultCode == RESULT_OK && data != null) {
// 授权成功获取MediaProjection
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
mediaProjection.registerCallback(new MediaProjection.Callback() {
@Override
public void onStop() {
super.onStop();
Toast.makeText(WebRTCScreenCaptureActivity.this, "屏幕采集已停止", Toast.LENGTH_SHORT).show();
releaseResources();
}
}, null);
setupVirtualDisplay();
// 初始化WebRTC并开始推流
initWebRTC();
// 更新按钮状态
btnStartPush.setText("停止推流");
btnStartPush.setOnClickListener(v -> releaseResources());
} else {
Toast.makeText(this, "屏幕录制授权被拒绝", Toast.LENGTH_SHORT).show();
}
}
}
private void setupVirtualDisplay() {
if (mediaProjection == null) return;
// 获取屏幕尺寸和密度
Log.e(TAG, "setupVirtualDisplay: ");
// 从TextureView的SurfaceTexture创建Surface
Surface surface = new Surface(textureView.getSurfaceTexture());
// 创建虚拟显示器将屏幕内容投射到Surface
mVirtualDisplay = mediaProjection.createVirtualDisplay(
"ScreenCapture",
screenWidth, screenHeight, screenDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, null, null);
}
/**
* 释放所有资源
*/
private void releaseResources() {
// 停止WebRTC
if (peerConnection != null) {
peerConnection.close();
peerConnection = null;
}
if (videoTrack != null) {
videoTrack.dispose();
videoTrack = null;
}
if (videoSource != null) {
videoSource.dispose();
videoSource = null;
}
if (peerConnectionFactory != null) {
peerConnectionFactory.dispose();
peerConnectionFactory = null;
}
// 停止屏幕采集
if (screenVideoCapturer != null) {
try {
screenVideoCapturer.stopCapture();
} catch (InterruptedException e) {
e.printStackTrace();
}
screenVideoCapturer.dispose();
screenVideoCapturer = null;
}
// 释放MediaProjection
if (mediaProjection != null) {
mediaProjection.stop();
mediaProjection = null;
}
// 停止前台服务
stopForegroundService();
// 释放渲染
if (localRender != null) {
localRender.release();
}
// if (rootEglBase != null) {
// rootEglBase.release();
// }
// 重置按钮
btnStartPush.setText("开始推流");
btnStartPush.setOnClickListener(v -> checkPermissionsAndStart());
}
@Override
protected void onDestroy() {
super.onDestroy();
releaseResources();
}
}

View File

@@ -12,6 +12,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.ttstd.remoteservice.R; import com.ttstd.remoteservice.R;
import com.ttstd.remoteservice.activity.WebRTCScreenCaptureActivity;
import com.ttstd.remoteservice.base.mvvm.BaseMvvmActivity; import com.ttstd.remoteservice.base.mvvm.BaseMvvmActivity;
import com.ttstd.remoteservice.databinding.ActivityMainBinding; import com.ttstd.remoteservice.databinding.ActivityMainBinding;
import com.ttstd.remoteservice.service.ControlService; import com.ttstd.remoteservice.service.ControlService;
@@ -85,8 +86,10 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
// } // }
// } // }
// 申请屏幕采集权限 // 申请屏幕采集权限
Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent(); // Intent captureIntent = mMediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE); // startActivityForResult(captureIntent, REQUEST_CODE_SCREEN_CAPTURE);
startActivity(new Intent(MainActivity.this, WebRTCScreenCaptureActivity.class));
} }
} }

View File

@@ -6,6 +6,7 @@ import android.app.NotificationManager;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
@@ -32,6 +33,12 @@ import android.view.WindowManager;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.webrtc.AudioTrack;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoTrack;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@@ -41,22 +48,20 @@ import java.nio.ByteBuffer;
public class ScreenCaptureService extends Service { public class ScreenCaptureService extends Service {
private static final String TAG = "ScreenCaptureService"; private static final String TAG = "ScreenCaptureService";
private static final int NOTIFICATION_ID = 7897; // 前台服务通知ID private static final int NOTIFICATION_ID = 100011; // 前台服务通知ID
private static final String CHANNEL_ID = "SCREEN_CAPTURE_CHANNEL"; // 通知渠道ID private static final String CHANNEL_ID = "SCREEN_CAPTURE_CHANNEL"; // 通知渠道ID
private static final String CHANNEL_NAME = "屏幕采集服务"; // 通知渠道名称 private static final String CHANNEL_NAME = "屏幕采集服务"; // 通知渠道名称
// 编码参数配置 // 编码参数配置
private static final String MIME_TYPE = "video/avc"; // H.264编码 private static final String MIME_TYPE = "video/avc"; // H.264编码
private static final int BIT_RATE = 2 * 1024 * 1024; // 码率2Mbps private static final int BIT_RATE = 2 * 1024 * 1024; // 码率2Mbps
private static final int FRAME_RATE = 15; // 帧率15fps private static final int FRAME_RATE = 15; // 帧率15fps
private static final int I_FRAME_INTERVAL = 5; // 关键帧间隔5秒 private static final int I_FRAME_INTERVAL = 5; // 关键帧间隔5秒
private static final String CONTROL_HOST = "47.242.112.133"; // 控制端IP需替换为实际IP
private static final int CONTROL_PORT = 3478; // 控制端视频接收端口
private static final String CONTROL_HOST = "192.168.1.100"; // 控制端IP需替换为实际IP private static final int IMAGE_FORMAT = ImageFormat.YUV_420_888;
private static final int CONTROL_PORT = 9999; // 控制端视频接收端口
private int mScreenWidth, mScreenHeight, mScreenDensity; private int mScreenWidth, mScreenHeight, mScreenDensity;
@@ -69,9 +74,6 @@ public class ScreenCaptureService extends Service {
private boolean isEncoding = false; private boolean isEncoding = false;
// 采集参数
private static final int IMAGE_FORMAT = ImageFormat.YUV_420_888; // 兼容大部分设备的格式
private ImageReader imageReader; private ImageReader imageReader;
private Handler captureHandler; // 帧回调处理线程 private Handler captureHandler; // 帧回调处理线程
private boolean isCapturing = false; private boolean isCapturing = false;
@@ -80,6 +82,14 @@ public class ScreenCaptureService extends Service {
// 用于存储提取的单帧Bitmap可根据需求改为回调/文件存储) // 用于存储提取的单帧Bitmap可根据需求改为回调/文件存储)
private Bitmap currentFrameBitmap; private Bitmap currentFrameBitmap;
// WebRTC核心组件
private PeerConnectionFactory peerConnectionFactory;
private PeerConnection peerConnection;
private VideoCapturer videoCapturer;
private VideoTrack localVideoTrack;
private AudioTrack localAudioTrack;
@Nullable @Nullable
@Override @Override
@@ -120,10 +130,10 @@ public class ScreenCaptureService extends Service {
// 此处可扩展添加VirtualDisplay + MediaCodec编码屏幕画面为H.264 // 此处可扩展添加VirtualDisplay + MediaCodec编码屏幕画面为H.264
// 并通过Socket将编码后的视频流发送到控制端 // 并通过Socket将编码后的视频流发送到控制端
// 初始化编码器并开始采集 // 初始化编码器并开始采集
// new Thread(this::initEncoderAndCapture).start(); new Thread(this::initEncoderAndCapture).start();
// 初始化ImageReader并开始采集 // 初始化ImageReader并开始采集
initImageReaderAndCapture(); // initImageReaderAndCapture();
} else { } else {
Log.e(TAG, "MediaProjection初始化失败"); Log.e(TAG, "MediaProjection初始化失败");
@@ -154,14 +164,6 @@ public class ScreenCaptureService extends Service {
isEncoding = true; isEncoding = true;
Log.d(TAG, "MediaCodec编码器初始化成功"); Log.d(TAG, "MediaCodec编码器初始化成功");
// 1. 创建ImageReader用于读取屏幕帧
imageReader = ImageReader.newInstance(
mScreenWidth,
mScreenHeight,
IMAGE_FORMAT,
2 // 缓冲区数量2帧避免帧丢失
);
// 2. 创建虚拟显示将屏幕画面投射到编码Surface // 2. 创建虚拟显示将屏幕画面投射到编码Surface
virtualDisplay = mediaProjection.createVirtualDisplay( virtualDisplay = mediaProjection.createVirtualDisplay(
"ScreenCaptureDisplay", "ScreenCaptureDisplay",
@@ -518,7 +520,7 @@ public class ScreenCaptureService extends Service {
// 第三步启动前台服务关键指定MediaProjection类型 // 第三步启动前台服务关键指定MediaProjection类型
if (Build.VERSION.SDK_INT >= 31) { if (Build.VERSION.SDK_INT >= 31) {
// Android 12+ 必须指定前台服务类型 // Android 12+ 必须指定前台服务类型
startForeground(NOTIFICATION_ID, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
} else { } else {
// 低版本直接启动 // 低版本直接启动
startForeground(NOTIFICATION_ID, notification); startForeground(NOTIFICATION_ID, notification);

View File

@@ -0,0 +1,67 @@
package com.ttstd.remoteservice.service;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
public class ScreenCaptureService2 extends Service {
private static final String TAG = "ScreenCaptureService2";
private static final String CHANNEL_ID = "ScreenCaptureChannel";
private static final int NOTIFICATION_ID = 1001;
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG, "onCreate: ");
createNotificationChannel();
if (Build.VERSION.SDK_INT >= 31) {
// Android 12+ 必须指定前台服务类型
startForeground(NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
} else {
// 低版本直接启动
startForeground(NOTIFICATION_ID, createNotification());
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY; // 服务被杀死后尝试重启
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"屏幕采集服务",
NotificationManager.IMPORTANCE_LOW
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
}
private Notification createNotification() {
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("屏幕采集中")
.setContentText("正在将屏幕内容推流到WebRTC")
.setSmallIcon(android.R.drawable.ic_media_play)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build();
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_start_push"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始推流" />
<!-- WebRTC本地预览显示采集的屏幕内容 -->
<org.webrtc.SurfaceViewRenderer
android:id="@+id/local_render"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="1" />
<TextureView
android:id="@+id/textureView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_weight="1" />
</LinearLayout>