增加webrtc,增加显示屏幕信息
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/src/main/res/layout/activity_webrtc_screen_capture.xml
Normal file
26
app/src/main/res/layout/activity_webrtc_screen_capture.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user