├── .github └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ └── client │ │ │ └── scrcpy │ │ │ ├── App.java │ │ │ ├── Constant.java │ │ │ ├── Dialog.java │ │ │ ├── DisplayWindow.java │ │ │ ├── FloatService.java │ │ │ ├── MainActivity.java │ │ │ ├── Scrcpy.java │ │ │ ├── ScrcpyHost.java │ │ │ ├── SendCommands.java │ │ │ ├── decoder │ │ │ ├── AudioDecoder.java │ │ │ └── VideoDecoder.java │ │ │ ├── model │ │ │ ├── AudioPacket.java │ │ │ ├── ByteUtils.java │ │ │ ├── MediaPacket.java │ │ │ └── VideoPacket.java │ │ │ └── utils │ │ │ ├── ExecUtil.java │ │ │ ├── FileUtils.java │ │ │ ├── HttpRequest.java │ │ │ ├── PreUtils.java │ │ │ ├── ProcessHelper.java │ │ │ ├── Progress.java │ │ │ ├── ThreadUtils.java │ │ │ └── Util.java │ ├── jniLibs │ │ ├── LICENSE │ │ ├── arm64-v8a │ │ │ └── libadb.so │ │ ├── armeabi-v7a │ │ │ └── libadb.so │ │ ├── x86 │ │ │ └── libadb.so │ │ └── x86_64 │ │ │ └── libadb.so │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── btn_click.xml │ │ ├── btn_global.xml │ │ ├── btn_selector.xml │ │ ├── close.png │ │ ├── down.png │ │ ├── edit_background.xml │ │ ├── ic_launcher_background.xml │ │ └── minis.png │ │ ├── layout-land │ │ ├── surface.xml │ │ ├── surface_nav.xml │ │ └── surface_no_nav.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── surface.xml │ │ ├── surface_nav.xml │ │ ├── surface_no_nav.xml │ │ └── window_display.xml │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-zh │ │ └── strings.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── scrcpy │ ├── AndroidManifest.xml │ └── res │ └── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png ├── build.gradle ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ └── home.jpg │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── home.jpg ├── server ├── .gitignore ├── LICENSE ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── android │ │ └── view │ │ └── IRotationWatcher.aidl │ └── java │ ├── android │ └── content │ │ └── IContentProvider.java │ └── org │ └── server │ └── scrcpy │ ├── AudioEncoder.java │ ├── Device.java │ ├── DisplayInfo.java │ ├── DroidConnection.java │ ├── EventController.java │ ├── Ln.java │ ├── Options.java │ ├── Position.java │ ├── ScreenCapture.java │ ├── ScreenEncoder.java │ ├── ScreenInfo.java │ ├── Server.java │ ├── Size.java │ ├── audio │ ├── AudioCapture.java │ ├── AudioCaptureException.java │ ├── AudioConfig.java │ ├── AudioDirectCapture.java │ ├── AudioRecordReader.java │ └── AudioSource.java │ ├── control │ ├── Pointer.java │ └── PointersState.java │ ├── device │ └── Point.java │ ├── model │ ├── AudioPacket.java │ ├── ByteUtils.java │ ├── MediaPacket.java │ └── VideoPacket.java │ ├── util │ ├── Command.java │ ├── FakeContext.java │ ├── IO.java │ ├── SettingsException.java │ └── Workarounds.java │ └── wrappers │ ├── ActivityManager.java │ ├── ContentProvider.java │ ├── DisplayManager.java │ ├── InputManager.java │ ├── PowerManager.java │ ├── ServiceManager.java │ ├── SurfaceControl.java │ └── WindowManager.java └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Decode Keystore 24 | env: 25 | ENCODED_STRING: ${{ secrets.SIGNING_KEY }} # Uses the encoded signing key stored in GitHub Secrets. 26 | run: | 27 | echo $ENCODED_STRING | base64 -di > app/scrcpy.jks 28 | 29 | - name: Grant execute permission for gradlew 30 | run: chmod +x gradlew 31 | - name: Build with Gradle 32 | run: ./gradlew assembleScrcpyRelease 33 | env: 34 | SIGNING_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }} 35 | SIGNING_KEY_ALIAS: ${{ secrets.ALIAS }} 36 | SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} 37 | 38 | - name: Upload Signed Release APK 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: ScrcpyForAndroid-signed-release 42 | path: | 43 | app/build/outputs/apk/scrcpy/release/*.apk 44 | retention-days: 90 # Sets the artifact retention period to 90 days. 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | /.idea 12 | app/src/main/assets/scrcpy-server.jar 13 | *.jks 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scrcpy for Android 2 | 3 | - This application is android port to desktop applicaton [**Scrcpy**](https://github.com/Genymobile/scrcpy). 4 | 5 | - This application mirrors display and touch controls from a remote android device to android device. 6 | 7 | - Scrcpy for Android uses ADB-Connect interface to connect to android device to be mirrored. 8 | 9 | 10 | 11 | ## Download 12 | 13 | [scrcpy-release.apk](https://github.com/zwc456baby/ScrcpyForAndroid/releases) 14 | 15 | 16 | ![home](home.jpg) 17 | 18 | 19 | 20 | ## Instructions to use 21 | 22 | - Make sure both devices are on same local network. 23 | - Enable **ADB-connect/ADB-wireless/ADB over network** on the device to be mirrored. 24 | - Open scrcpy-android app and enter ip address of device to be mirrored. 25 | - Select display parameters and bitrate from drop-down menu(1280x720 and 2Mbps works best). 26 | - Set **Navbar** switch if the device to be mirrored has only hardware navigation buttons. 27 | - Hit **start** button. 28 | - Accept and trust(check always allow from this computer) the ADB connection prompt on target device(Some custom roms don't have this prompt). 29 | - Thats all! You should be seeing the screen of remote android device. 30 | - To wake up the remote device, **double tap anywhere on screen**. 31 | - To put the remote device to sleep, **close proxmity sensor and double tap anywhere on the screen**. 32 | - To bring back the local android system navbar while mirroring the remote device, **swipe up from the bottom edge of screen**. 33 | 34 | 35 | 36 | ## Connecting to public network devices 37 | 38 | 39 | 40 | > The public network port of the device needs to be open for access 41 | 42 | 43 | 44 | ### Connection Example 45 | 46 | - 192.168.1.222 47 | 48 | - host.example.com:5555 49 | 50 | - [2000:2000:2000:2000::2000]:5555 51 | 52 | ## Code Reference 53 | 54 | - [scrcpy-android](https://gitlab.com/las2mile/scrcpy-android) 55 | - [scrcpy](https://github.com/Genymobile/scrcpy) 56 | 57 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | evaluationDependsOn(':server') 2 | 3 | apply plugin: 'com.android.application' 4 | 5 | android { 6 | namespace 'org.client.scrcpy' 7 | flavorDimensions = ["default"] 8 | 9 | compileSdkVersion 31 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 31 13 | versionCode 9 14 | versionName "1.1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | setProperty("archivesBaseName", "scrcpy") 17 | } 18 | signingConfigs { 19 | release { 20 | storeFile file("scrcpy.jks") 21 | 22 | def properties = new Properties() 23 | if (file("../local.properties").exists()){ 24 | file("../local.properties").withInputStream { stream -> 25 | properties.load(stream) 26 | } 27 | } 28 | 29 | storePassword System.getenv("SIGNING_STORE_PASSWORD") ?: properties.getProperty("SIGNING_STORE_PASSWORD") 30 | keyAlias System.getenv("SIGNING_KEY_ALIAS") ?: properties.getProperty("SIGNING_KEY_ALIAS") 31 | keyPassword System.getenv("SIGNING_KEY_PASSWORD") ?: properties.getProperty("SIGNING_KEY_PASSWORD") 32 | } 33 | } 34 | 35 | buildTypes { 36 | release { 37 | minifyEnabled false 38 | signingConfig signingConfigs.release 39 | } 40 | } 41 | 42 | productFlavors { 43 | scrcpy { 44 | applicationId "org.client.scrcpy" 45 | } 46 | } 47 | 48 | 49 | tasks.whenTaskAdded { task -> 50 | def buildType = gradle.startParameter.taskNames.any { it.endsWith('Release') } ? 'Release' : 'Debug' 51 | 52 | task.dependsOn ":server:assemble${buildType}" 53 | task.dependsOn ":server:copyServer" 54 | } 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.13.2' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 18 | 19 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/App.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.app.Application; 6 | import android.content.Context; 7 | import android.os.Bundle; 8 | import android.text.TextUtils; 9 | import android.util.Log; 10 | 11 | import org.client.scrcpy.utils.ExecUtil; 12 | import org.client.scrcpy.utils.PreUtils; 13 | import org.client.scrcpy.utils.ThreadUtils; 14 | 15 | import java.util.HashMap; 16 | import java.util.LinkedList; 17 | import java.util.UUID; 18 | 19 | public class App extends Application implements Application.ActivityLifecycleCallbacks { 20 | 21 | @SuppressLint("StaticFieldLeak") 22 | public static Context mContext; 23 | 24 | private final static LinkedList activityList = new LinkedList(); 25 | 26 | private static boolean startAdbRun = false; 27 | 28 | @Override 29 | public void onCreate() { 30 | super.onCreate(); 31 | init(); // 初始化id 数据 32 | startAdbServer(); 33 | } 34 | 35 | @Override 36 | protected void attachBaseContext(Context base) { 37 | super.attachBaseContext(base); 38 | mContext = base; 39 | registerActivityLifecycleCallbacks(this); 40 | } 41 | 42 | private void init() { 43 | String userId = PreUtils.get(this, Constant.USER_ID, ""); 44 | if (TextUtils.isEmpty(userId)) { 45 | PreUtils.put(this, Constant.USER_ID, UUID.randomUUID().toString()); 46 | } 47 | } 48 | 49 | public static Activity getCurActivity() { 50 | // 获取最新的一个 activity 51 | try { 52 | return activityList.getFirst(); 53 | } catch (Exception ignore) { 54 | return null; 55 | } 56 | } 57 | 58 | /** 59 | * 启动 adb 服务 60 | */ 61 | public static void startAdbServer() { 62 | if (startAdbRun) { 63 | // 当前正在启动过程中,退出 64 | return; 65 | } 66 | startAdbRun = true; 67 | ThreadUtils.execute(() -> { 68 | // 启动 adb 服务 69 | Log.i("Scrcpy", "start adb server ..."); 70 | adbCmd("kill-server"); 71 | adbCmd("start-server"); 72 | // 启动完毕,重置为false,使其下次可以被重新调用 73 | startAdbRun = false; 74 | }); 75 | } 76 | 77 | 78 | public static String adbCmd(String... cmd) { 79 | if (cmd == null) { 80 | return ""; 81 | } 82 | String[] cmds = new String[cmd.length + 1]; 83 | cmds[0] = mContext.getApplicationInfo().nativeLibraryDir + "/libadb.so"; 84 | System.arraycopy(cmd, 0, cmds, 1, cmd.length); 85 | 86 | HashMap env = new HashMap<>(); 87 | env.put("HOME", mContext.getFilesDir().getAbsolutePath()); 88 | env.put("TMPDIR", mContext.getCacheDir().getAbsolutePath()); 89 | env.put("ANDROID_ADB_SERVER_PORT", "5137"); 90 | 91 | return ExecUtil.adbCommend(cmds, env, mContext.getFilesDir()); 92 | } 93 | 94 | @Override 95 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 96 | activityList.addFirst(activity); 97 | } 98 | 99 | @Override 100 | public void onActivityStarted(Activity activity) { 101 | 102 | } 103 | 104 | @Override 105 | public void onActivityResumed(Activity activity) { 106 | 107 | } 108 | 109 | @Override 110 | public void onActivityPaused(Activity activity) { 111 | 112 | } 113 | 114 | @Override 115 | public void onActivityStopped(Activity activity) { 116 | 117 | } 118 | 119 | @Override 120 | public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 121 | 122 | } 123 | 124 | @Override 125 | public void onActivityDestroyed(Activity activity) { 126 | activityList.remove(activity); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/Constant.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy; 2 | 3 | 4 | public class Constant { 5 | public static final String CONTROL_NAV = "control_nav"; 6 | public static final String CONTROL_NO = "no_control"; 7 | public static final String CONTROL_REMOTE_ADDR = "control_remote_addr"; 8 | 9 | public static final String PREFERENCE_SPINNER_RESOLUTION = "spinner_resolution"; 10 | public static final String PREFERENCE_SPINNER_BITRATE = "spinner_bitrate"; 11 | 12 | public static final String PREFERENCE_SPINNER_DELAY = "delay_control"; 13 | 14 | public static final String HISTORY_LIST_KEY = "history_list_key"; 15 | public static final String USER_ID = "user_id"; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/DisplayWindow.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.util.DisplayMetrics; 6 | import android.util.Log; 7 | import android.view.Display; 8 | import android.view.LayoutInflater; 9 | import android.view.MotionEvent; 10 | import android.view.Surface; 11 | import android.view.SurfaceView; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.view.WindowManager; 15 | import android.widget.FrameLayout; 16 | 17 | public class DisplayWindow extends FrameLayout { 18 | private static final String TAG = "DisplayWindow"; 19 | OnClickListener closeListener; 20 | OnMoveCallback moveCallback; 21 | OnActionCallback actionCallback; 22 | OnTouchListener onDisplayTouchListener; 23 | 24 | private float oldX; 25 | private float oldY; 26 | 27 | ViewGroup header; 28 | ViewGroup container; 29 | SurfaceView surfaceView; 30 | ViewGroup actionbar; 31 | 32 | public DisplayWindow(Context context) { 33 | super(context); 34 | init(); 35 | } 36 | 37 | public DisplayWindow(Context context, AttributeSet attrs) { 38 | super(context, attrs); 39 | init(); 40 | } 41 | 42 | public DisplayWindow(Context context, AttributeSet attrs, int defStyleAttr) { 43 | super(context, attrs, defStyleAttr); 44 | init(); 45 | } 46 | 47 | private void init(){ 48 | setClipChildren(false); 49 | setClipToPadding(false); 50 | LayoutInflater.from(getContext()).inflate(R.layout.window_display,this,true); 51 | 52 | container = findViewById(R.id.container); 53 | surfaceView = findViewById(R.id.surface); 54 | actionbar = findViewById(R.id.actionbar); 55 | 56 | findViewById(R.id.iv_close).setOnTouchListener(new OnTouchListener() { 57 | @Override 58 | public boolean onTouch(View view, MotionEvent motionEvent) { 59 | if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN){ 60 | closeListener.onClick(view); 61 | } 62 | return false; 63 | } 64 | }); 65 | 66 | header = findViewById(R.id.header); 67 | header.setOnTouchListener(new OnTouchListener() { 68 | @Override 69 | public boolean onTouch(View view, MotionEvent motionEvent) { 70 | final int action = motionEvent.getAction(); 71 | switch (action) { 72 | case MotionEvent.ACTION_DOWN: 73 | oldX = motionEvent.getRawX(); 74 | oldY = motionEvent.getRawY(); 75 | break; 76 | case MotionEvent.ACTION_MOVE: 77 | float disX = motionEvent.getRawX()-oldX; 78 | float disY = motionEvent.getRawY()-oldY; 79 | moveCallback.onMove(disX,disY); 80 | oldX = motionEvent.getRawX(); 81 | oldY = motionEvent.getRawY(); 82 | break; 83 | case MotionEvent.ACTION_UP: 84 | case MotionEvent.ACTION_CANCEL: 85 | 86 | break; 87 | } 88 | return true; 89 | } 90 | }); 91 | findViewById(R.id.iv_mini).setOnTouchListener(new OnTouchListener() { 92 | @Override 93 | public boolean onTouch(View view, MotionEvent motionEvent) { 94 | if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN){ 95 | if (container.isShown()){ 96 | container.setVisibility(View.GONE); 97 | actionbar.setVisibility(View.GONE); 98 | }else{ 99 | container.setVisibility(View.VISIBLE); 100 | actionbar.setVisibility(View.VISIBLE); 101 | } 102 | } 103 | return false; 104 | } 105 | }); 106 | findViewById(R.id.action_back).setOnTouchListener(new OnTouchListener() { 107 | @Override 108 | public boolean onTouch(View view, MotionEvent motionEvent) { 109 | if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN){ 110 | actionCallback.onAction(0); 111 | } 112 | return false; 113 | } 114 | }); 115 | findViewById(R.id.action_home).setOnTouchListener(new OnTouchListener() { 116 | @Override 117 | public boolean onTouch(View view, MotionEvent motionEvent) { 118 | if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN){ 119 | actionCallback.onAction(1); 120 | } 121 | return false; 122 | } 123 | }); 124 | findViewById(R.id.action_menu).setOnTouchListener(new OnTouchListener() { 125 | @Override 126 | public boolean onTouch(View view, MotionEvent motionEvent) { 127 | if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN){ 128 | actionCallback.onAction(2); 129 | } 130 | return false; 131 | } 132 | }); 133 | container.setOnTouchListener(new OnTouchListener() { 134 | @Override 135 | public boolean onTouch(View view, MotionEvent motionEvent) { 136 | return onDisplayTouchListener.onTouch(view,motionEvent); 137 | } 138 | }); 139 | } 140 | 141 | public void setCloseListener(OnClickListener closeListener) { 142 | this.closeListener = closeListener; 143 | } 144 | 145 | public void setMoveCallback(OnMoveCallback moveCallback) { 146 | this.moveCallback = moveCallback; 147 | } 148 | 149 | public void setActionCallback(OnActionCallback actionCallback) { 150 | this.actionCallback = actionCallback; 151 | } 152 | 153 | public void setOnDisplayTouchListener(OnTouchListener onDisplayTouchListener) { 154 | this.onDisplayTouchListener = onDisplayTouchListener; 155 | } 156 | 157 | public SurfaceView getSurfaceView() { 158 | return surfaceView; 159 | } 160 | 161 | public void setRemote(int w,int h){ 162 | DisplayMetrics metrics = new DisplayMetrics(); 163 | WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); 164 | final Display display = windowManager.getDefaultDisplay(); 165 | display.getRealMetrics(metrics); 166 | float this_dev_height = metrics.heightPixels; 167 | float this_dev_width = Math.min(metrics.heightPixels,metrics.widthPixels); 168 | 169 | post(new Runnable() { 170 | @Override 171 | public void run() { 172 | //根据比例设置高度 173 | ViewGroup.LayoutParams lp = container.getLayoutParams(); 174 | float rate = (float)w/h; 175 | Log.d(TAG, "setRemote: "+w+","+h+" %->"+rate); 176 | //高度屏幕的80%,然后宽度按比例 177 | lp.height = (int)(this_dev_height * 0.95 - actionbar.getMeasuredHeight() - header.getMeasuredHeight()-50); 178 | lp.width = (int) (lp.height * rate); 179 | container.setLayoutParams(lp); 180 | 181 | ViewGroup.LayoutParams lp2 = header.getLayoutParams(); 182 | lp2.width = lp.width; 183 | header.setLayoutParams(lp2); 184 | requestLayout(); 185 | } 186 | }); 187 | 188 | } 189 | 190 | public void hideHintTip(){ 191 | findViewById(R.id.hint).setVisibility(GONE); 192 | } 193 | 194 | public Surface getDisplaySurface(){ 195 | return surfaceView.getHolder().getSurface(); 196 | } 197 | 198 | public int getSurfaceWidth(){ 199 | return container.getMeasuredWidth(); 200 | } 201 | 202 | public int getSurfaceHeight(){ 203 | return container.getMeasuredHeight(); 204 | } 205 | 206 | public interface OnMoveCallback { 207 | void onMove(float x,float y); 208 | } 209 | public interface OnActionCallback{ 210 | void onAction(int actionType); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/FloatService.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy; 2 | 3 | import android.app.Service; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.res.Configuration; 7 | import android.os.IBinder; 8 | import android.util.Log; 9 | import android.view.Gravity; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.view.WindowManager; 13 | 14 | public class FloatService extends Service { 15 | private static final String TAG = "FloatService"; 16 | 17 | DisplayWindow displayWindow; 18 | WindowManager windowManager; 19 | WindowManager.LayoutParams lp; 20 | 21 | ScrcpyHost scrcpyHost; 22 | 23 | int w; 24 | int h; 25 | 26 | @Override 27 | public IBinder onBind(Intent intent) { 28 | return null; 29 | } 30 | 31 | @Override 32 | public int onStartCommand(Intent intent, int flags, int startId) { 33 | if (intent == null) { 34 | return super.onStartCommand(intent, flags, startId); 35 | } 36 | String ip = intent.getStringExtra("ip"); 37 | w = intent.getIntExtra("w", 1080); 38 | h = intent.getIntExtra("h", 1920); 39 | int b = intent.getIntExtra("b", 1024000); 40 | 41 | Log.d(TAG, "onStartCommand: " + w + "," + h + "|" + b + " ->" + ip); 42 | displayWindow.setRemote(w, h); 43 | 44 | new Thread(new Runnable() { 45 | @Override 46 | public void run() { 47 | startCopy(ip, w, h, b); 48 | } 49 | }).start(); 50 | 51 | return super.onStartCommand(intent, flags, startId); 52 | } 53 | 54 | @Override 55 | public void onCreate() { 56 | super.onCreate(); 57 | setupDisplay(); 58 | windowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); 59 | lp = new WindowManager.LayoutParams(); 60 | lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 61 | lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 62 | lp.gravity = Gravity.TOP | Gravity.LEFT; 63 | lp.width = WindowManager.LayoutParams.WRAP_CONTENT; 64 | lp.height = WindowManager.LayoutParams.WRAP_CONTENT; 65 | lp.verticalMargin = 0; 66 | lp.horizontalMargin = 0; 67 | windowManager.addView(displayWindow, lp); 68 | 69 | // startCopy(); 70 | 71 | } 72 | 73 | private void startCopy(String ip, int width, int height, int bitrate) { 74 | scrcpyHost = new ScrcpyHost(); 75 | scrcpyHost.setConnectCallBack(new ScrcpyHost.ConnectCallBack() { 76 | @Override 77 | public void onConnect(float w, float h) { 78 | displayWindow.setRemote((int) w, (int) h); 79 | displayWindow.hideHintTip(); 80 | } 81 | }); 82 | scrcpyHost.connect(getApplicationContext(), ip, width, height, bitrate, displayWindow.getDisplaySurface()); 83 | } 84 | 85 | @Override 86 | public void onConfigurationChanged(Configuration newConfig) { 87 | super.onConfigurationChanged(newConfig); 88 | Log.d(TAG, "onConfigurationChanged: "); 89 | displayWindow.setRemote(w, h); 90 | } 91 | 92 | @Override 93 | public void onDestroy() { 94 | super.onDestroy(); 95 | scrcpyHost.destroy(); 96 | System.exit(0); 97 | } 98 | 99 | private void setupDisplay() { 100 | displayWindow = new DisplayWindow(getApplicationContext()); 101 | displayWindow.setCloseListener(v -> { 102 | Log.d(TAG, "close"); 103 | windowManager.removeView(displayWindow); 104 | stopSelf(); 105 | }); 106 | displayWindow.setMoveCallback((x, y) -> { 107 | lp.x += x; 108 | lp.y += y; 109 | windowManager.updateViewLayout(displayWindow, lp); 110 | }); 111 | displayWindow.setActionCallback(new DisplayWindow.OnActionCallback() { 112 | @Override 113 | public void onAction(int actionType) { 114 | switch (actionType) { 115 | case 0: 116 | scrcpyHost.keyEvent(4); 117 | break; 118 | case 1: 119 | scrcpyHost.keyEvent(3); 120 | break; 121 | case 2: 122 | scrcpyHost.keyEvent(187); 123 | break; 124 | } 125 | } 126 | }); 127 | displayWindow.setOnDisplayTouchListener(new View.OnTouchListener() { 128 | @Override 129 | public boolean onTouch(View view, MotionEvent motionEvent) { 130 | return scrcpyHost.touch(motionEvent, displayWindow.getSurfaceWidth(), displayWindow.getSurfaceHeight()); 131 | } 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/SendCommands.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy; 2 | 3 | 4 | import android.content.Context; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | 8 | import org.client.scrcpy.utils.ThreadUtils; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.net.ConnectException; 13 | import java.net.NoRouteToHostException; 14 | import java.net.Socket; 15 | import java.net.UnknownHostException; 16 | import java.nio.charset.StandardCharsets; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.security.spec.InvalidKeySpecException; 19 | 20 | public class SendCommands { 21 | 22 | private Context context; 23 | private int status; 24 | 25 | 26 | public SendCommands() { 27 | 28 | } 29 | 30 | public int SendAdbCommands(Context context, final String ip, int port, int forwardport, String localip, int bitrate, int size) { 31 | return this.SendAdbCommands(context, null, ip, port, forwardport, localip, bitrate, size); 32 | } 33 | 34 | public int SendAdbCommands(Context context, final byte[] fileBase64, final String ip, int port, int forwardport, String localip, int bitrate, int size) { 35 | this.context = context; 36 | status = 1; 37 | String[] commands = new String[]{ 38 | "-s", ip + ":" + port, 39 | "shell", 40 | " CLASSPATH=/data/local/tmp/scrcpy-server.jar", 41 | "app_process", 42 | "/", 43 | "org.server.scrcpy.Server", 44 | "/" + localip, 45 | Long.toString(size), 46 | Long.toString(bitrate) + ";" 47 | }; 48 | ThreadUtils.execute(() -> { 49 | try { 50 | // 新版的复制方式 51 | newAdbServerStart(context, ip, localip, port, forwardport, commands); 52 | } catch (Exception e) { 53 | e.printStackTrace(); 54 | } 55 | }); 56 | int count = 0; 57 | while (status == 1 && count < 50) { 58 | Log.e("ADB", "Connecting..."); 59 | try { 60 | Thread.sleep(100); 61 | count++; 62 | } catch (InterruptedException e) { 63 | e.printStackTrace(); 64 | } 65 | } 66 | if (count >= 50) { 67 | status = 2; 68 | return status; 69 | } 70 | if (status == 0) { 71 | count = 0; 72 | // 检测程序是否已经启动,如果启动了,该文件会被删除 73 | while (status == 0 && count < 10) { 74 | String adbTextCmd = App.adbCmd("-s", ip + ":" + port, "shell", "ls", "-alh", "/data/local/tmp/scrcpy-server.jar"); 75 | if (TextUtils.isEmpty(adbTextCmd)) { 76 | break; 77 | } else { 78 | try { 79 | Thread.sleep(100); 80 | count++; 81 | } catch (InterruptedException e) { 82 | e.printStackTrace(); 83 | } 84 | } 85 | } 86 | } 87 | return status; 88 | } 89 | 90 | 91 | private void newAdbServerStart(Context context, String ip, String localip, int port, int serverport, String[] command) { 92 | App.adbCmd("connect", ip + ":" + port); 93 | 94 | Log.i("Scrcpy", "adb devices: " + App.adbCmd("devices")); 95 | // 复制server端到可执行目录 96 | String pushRet = App.adbCmd("-s", ip + ":" + port, "push", new File( 97 | context.getExternalFilesDir("scrcpy"), "scrcpy-server.jar" 98 | ).getAbsolutePath(), "/data/local/tmp/scrcpy-server.jar"); 99 | 100 | Log.i("Scrcpy", "pushRet: " + pushRet); 101 | 102 | String adbTextCmd = App.adbCmd("-s", ip + ":" + port, "shell", "ls", "-alh", "/data/local/tmp/scrcpy-server.jar"); 103 | if (TextUtils.isEmpty(adbTextCmd)) { 104 | status = 2; 105 | return; 106 | } 107 | // 开启本地端口 forward 转发 108 | Log.i("Scrcpy", "开启本地端口转发"); 109 | App.adbCmd("-s", ip + ":" + port, "forward", "tcp:" + serverport, "tcp:" + 7007); 110 | 111 | status = 0; 112 | // 执行启动命令 113 | App.adbCmd(command); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/decoder/AudioDecoder.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.decoder; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.AudioManager; 5 | import android.media.AudioTrack; 6 | import android.media.MediaCodec; 7 | import android.media.MediaFormat; 8 | import android.os.Build; 9 | import android.util.Log; 10 | import android.view.Surface; 11 | 12 | 13 | import java.io.IOException; 14 | import java.nio.ByteBuffer; 15 | import java.util.concurrent.atomic.AtomicBoolean; 16 | 17 | public class AudioDecoder { 18 | 19 | public static final String MIMETYPE_AUDIO_AAC = "audio/mp4a-latm"; 20 | 21 | private MediaCodec mCodec; 22 | private Worker mWorker; 23 | private AtomicBoolean mIsConfigured = new AtomicBoolean(false); 24 | 25 | private AudioTrack audioTrack; 26 | private final int SAMPLE_RATE = 48000; 27 | 28 | private void initAudioTrack() { 29 | int bufferSizeInBytes = AudioTrack.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT); 30 | audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, 31 | bufferSizeInBytes, AudioTrack.MODE_STREAM); 32 | } 33 | 34 | public void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) { 35 | if (mWorker != null) { 36 | mWorker.decodeSample(data, offset, size, presentationTimeUs, flags); 37 | } 38 | } 39 | 40 | public void configure(byte[] data) { 41 | if (mWorker != null) { 42 | mWorker.configure(data); 43 | } 44 | } 45 | 46 | 47 | public void start() { 48 | if (mWorker == null) { 49 | mWorker = new Worker(); 50 | mWorker.setRunning(true); 51 | mWorker.start(); 52 | } 53 | } 54 | 55 | public void stop() { 56 | if (mWorker != null) { 57 | mWorker.setRunning(false); 58 | mWorker = null; 59 | mIsConfigured.set(false); 60 | if (mCodec != null) { 61 | mCodec.stop(); 62 | } 63 | if (audioTrack != null) { 64 | audioTrack.stop(); 65 | } 66 | } 67 | } 68 | 69 | private class Worker extends Thread { 70 | 71 | private AtomicBoolean mIsRunning = new AtomicBoolean(false); 72 | 73 | Worker() { 74 | } 75 | 76 | private void setRunning(boolean isRunning) { 77 | mIsRunning.set(isRunning); 78 | } 79 | 80 | private void configure(byte[] data) { 81 | if (mIsConfigured.get()) { 82 | mIsConfigured.set(false); 83 | if (mCodec != null) { 84 | mCodec.stop(); 85 | } 86 | if (audioTrack != null) { 87 | audioTrack.stop(); 88 | } 89 | } 90 | MediaFormat format = MediaFormat.createAudioFormat(MIMETYPE_AUDIO_AAC, SAMPLE_RATE, 2); 91 | // 设置比特率 92 | format.setInteger(MediaFormat.KEY_BIT_RATE, 128000); 93 | // adts 0 94 | // format.setInteger(MediaFormat.KEY_IS_ADTS, 1); 95 | format.setByteBuffer("csd-0", ByteBuffer.wrap(data)); 96 | 97 | try { 98 | mCodec = MediaCodec.createDecoderByType(MIMETYPE_AUDIO_AAC); 99 | } catch (IOException e) { 100 | throw new RuntimeException("Failed to create codec", e); 101 | } 102 | mCodec.configure(format, null, null, 0); 103 | mCodec.start(); 104 | mIsConfigured.set(true); 105 | 106 | // 初始化音频播放器 107 | initAudioTrack(); 108 | // audio track 启动 109 | audioTrack.play(); 110 | } 111 | 112 | 113 | @SuppressWarnings("deprecation") 114 | public void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) { 115 | if (mIsConfigured.get() && mIsRunning.get()) { 116 | int index = mCodec.dequeueInputBuffer(-1); 117 | if (index >= 0) { 118 | ByteBuffer buffer; 119 | 120 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 121 | buffer = mCodec.getInputBuffers()[index]; 122 | buffer.clear(); 123 | } else { 124 | buffer = mCodec.getInputBuffer(index); 125 | } 126 | if (buffer != null) { 127 | buffer.put(data, offset, size); 128 | mCodec.queueInputBuffer(index, 0, size, presentationTimeUs, flags); 129 | } 130 | } 131 | } 132 | } 133 | 134 | @Override 135 | public void run() { 136 | try { 137 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 138 | while (mIsRunning.get()) { 139 | if (mIsConfigured.get()) { 140 | int index = mCodec.dequeueOutputBuffer(info, 0); 141 | // Log.e("Scrcpy", "Audio Decoder: " + index); 142 | if (index >= 0) { 143 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) { 144 | break; 145 | } 146 | // Log.e("Scrcpy", "Audio success get frame: " + index); 147 | 148 | // 读取 pcm 数据,写入 audiotrack 播放 149 | ByteBuffer outputBuffer = mCodec.getOutputBuffer(index); 150 | if (outputBuffer != null) { 151 | byte[] data = new byte[info.size]; 152 | outputBuffer.get(data); 153 | outputBuffer.clear(); 154 | audioTrack.write(data, 0, info.size); 155 | } 156 | // release 157 | mCodec.releaseOutputBuffer(index, true); 158 | } 159 | } else { 160 | // just waiting to be configured, then decode and render 161 | try { 162 | Thread.sleep(5); 163 | } catch (InterruptedException ignore) { 164 | } 165 | } 166 | } 167 | } catch (IllegalStateException e) { 168 | } 169 | 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/decoder/VideoDecoder.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.decoder; 2 | 3 | import android.media.MediaCodec; 4 | import android.media.MediaFormat; 5 | import android.os.Build; 6 | import android.view.Surface; 7 | 8 | 9 | import java.io.IOException; 10 | import java.nio.ByteBuffer; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | public class VideoDecoder { 14 | private MediaCodec mCodec; 15 | private Worker mWorker; 16 | private AtomicBoolean mIsConfigured = new AtomicBoolean(false); 17 | 18 | public void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) { 19 | if (mWorker != null) { 20 | mWorker.decodeSample(data, offset, size, presentationTimeUs, flags); 21 | } 22 | } 23 | 24 | public void configure(Surface surface, int width, int height, ByteBuffer csd0, ByteBuffer csd1) { 25 | if (mWorker != null) { 26 | mWorker.configure(surface, width, height, csd0, csd1); 27 | } 28 | } 29 | 30 | 31 | public void start() { 32 | if (mWorker == null) { 33 | mWorker = new Worker(); 34 | mWorker.setRunning(true); 35 | mWorker.start(); 36 | } 37 | } 38 | 39 | public void stop() { 40 | if (mWorker != null) { 41 | mWorker.setRunning(false); 42 | mWorker = null; 43 | mIsConfigured.set(false); 44 | if (mCodec != null) { 45 | mCodec.stop(); 46 | } 47 | } 48 | } 49 | 50 | private class Worker extends Thread { 51 | 52 | private AtomicBoolean mIsRunning = new AtomicBoolean(false); 53 | 54 | Worker() { 55 | } 56 | 57 | private void setRunning(boolean isRunning) { 58 | mIsRunning.set(isRunning); 59 | } 60 | 61 | private void configure(Surface surface, int width, int height, ByteBuffer csd0, ByteBuffer csd1) { 62 | if (mIsConfigured.get()) { 63 | mIsConfigured.set(false); 64 | if (mCodec != null) { 65 | mCodec.stop(); 66 | } 67 | 68 | } 69 | MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); 70 | format.setByteBuffer("csd-0", csd0); 71 | format.setByteBuffer("csd-1", csd1); 72 | try { 73 | mCodec = MediaCodec.createDecoderByType("video/avc"); 74 | } catch (IOException e) { 75 | throw new RuntimeException("Failed to create codec", e); 76 | } 77 | mCodec.configure(format, surface, null, 0); 78 | mCodec.start(); 79 | mIsConfigured.set(true); 80 | } 81 | 82 | 83 | @SuppressWarnings("deprecation") 84 | public void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) { 85 | if (mIsConfigured.get() && mIsRunning.get()) { 86 | int index = mCodec.dequeueInputBuffer(-1); 87 | if (index >= 0) { 88 | ByteBuffer buffer; 89 | 90 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 91 | buffer = mCodec.getInputBuffers()[index]; 92 | buffer.clear(); 93 | } else { 94 | buffer = mCodec.getInputBuffer(index); 95 | } 96 | if (buffer != null) { 97 | buffer.put(data, offset, size); 98 | mCodec.queueInputBuffer(index, 0, size, presentationTimeUs, flags); 99 | } 100 | } 101 | } 102 | } 103 | 104 | @Override 105 | public void run() { 106 | try { 107 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 108 | while (mIsRunning.get()) { 109 | if (mIsConfigured.get()) { 110 | int index = mCodec.dequeueOutputBuffer(info, 0); 111 | if (index >= 0) { 112 | // setting true is telling system to render frame onto Surface 113 | mCodec.releaseOutputBuffer(index, true); 114 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) { 115 | break; 116 | } 117 | } 118 | } else { 119 | // just waiting to be configured, then decode and render 120 | try { 121 | Thread.sleep(5); 122 | } catch (InterruptedException ignore) { 123 | } 124 | } 125 | } 126 | } catch (IllegalStateException e) { 127 | } 128 | 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/model/AudioPacket.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.model; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | /** 6 | * Created by Alexandr Golovach on 27.06.16. 7 | * https://www.github.com/alexmprog/VideoCodec 8 | */ 9 | 10 | public class AudioPacket extends MediaPacket { 11 | 12 | public Flag flag; 13 | public long presentationTimeStamp; 14 | public byte[] data; 15 | 16 | public AudioPacket() { 17 | } 18 | 19 | public AudioPacket(Type type, Flag flag, long presentationTimeStamp, byte[] data) { 20 | this.type = type; 21 | this.flag = flag; 22 | this.presentationTimeStamp = presentationTimeStamp; 23 | this.data = data; 24 | } 25 | 26 | // create packet from byte array 27 | public static AudioPacket fromArray(byte[] values) { 28 | AudioPacket videoPacket = new AudioPacket(); 29 | 30 | // should be a type value - 1 byte 31 | byte typeValue = values[0]; 32 | // should be a flag value - 1 byte 33 | byte flagValue = values[1]; 34 | 35 | videoPacket.type = Type.getType(typeValue); 36 | videoPacket.flag = Flag.getFlag(flagValue); 37 | 38 | // should be 8 bytes for timestamp 39 | byte[] timeStamp = new byte[8]; 40 | System.arraycopy(values, 2, timeStamp, 0, 8); 41 | videoPacket.presentationTimeStamp = ByteUtils.bytesToLong(timeStamp); 42 | 43 | // all other bytes is data 44 | int dataLength = values.length - 10; 45 | byte[] data = new byte[dataLength]; 46 | System.arraycopy(values, 10, data, 0, dataLength); 47 | videoPacket.data = data; 48 | 49 | return videoPacket; 50 | } 51 | 52 | public static AudioPacket readHead(byte[] values) { 53 | AudioPacket videoPacket = new AudioPacket(); 54 | 55 | // should be a type value - 1 byte 56 | byte typeValue = values[0]; 57 | // should be a flag value - 1 byte 58 | byte flagValue = values[1]; 59 | 60 | videoPacket.type = Type.getType(typeValue); 61 | videoPacket.flag = Flag.getFlag(flagValue); 62 | 63 | // should be 8 bytes for timestamp 64 | byte[] timeStamp = new byte[8]; 65 | System.arraycopy(values, 2, timeStamp, 0, 8); 66 | videoPacket.presentationTimeStamp = ByteUtils.bytesToLong(timeStamp); 67 | 68 | videoPacket.data = null; 69 | 70 | return videoPacket; 71 | } 72 | 73 | public static int getHeadLen(){ 74 | return 10; 75 | } 76 | 77 | // create byte array 78 | public static byte[] toArray(Type type, Flag flag, long presentationTimeStamp, byte[] data) { 79 | 80 | // should be 4 bytes for packet size 81 | byte[] bytes = ByteUtils.intToBytes(10 + data.length); 82 | 83 | int packetSize = 14 + data.length; // 4 - inner packet size 1 - type + 1 - flag + 8 - timeStamp + data.length 84 | byte[] values = new byte[packetSize]; 85 | 86 | System.arraycopy(bytes, 0, values, 0, 4); 87 | 88 | // set type value 89 | values[4] = type.getType(); 90 | // set flag value 91 | values[5] = flag.getFlag(); 92 | // set timeStamp 93 | byte[] longToBytes = ByteUtils.longToBytes(presentationTimeStamp); 94 | System.arraycopy(longToBytes, 0, values, 6, longToBytes.length); 95 | 96 | // set data array 97 | System.arraycopy(data, 0, values, 14, data.length); 98 | return values; 99 | } 100 | 101 | // should call on inner packet 102 | public static boolean isVideoPacket(byte[] values) { 103 | return values[0] == Type.VIDEO.getType(); 104 | } 105 | 106 | public static StreamSettings getStreamSettings(byte[] buffer) { 107 | byte[] sps, pps; 108 | 109 | ByteBuffer spsPpsBuffer = ByteBuffer.wrap(buffer); 110 | if (spsPpsBuffer.getInt() == 0x00000001) { 111 | System.out.println("parsing sps/pps"); 112 | } else { 113 | System.out.println("something is amiss?"); 114 | } 115 | int ppsIndex = 0; 116 | while (!(spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x01)) { 117 | 118 | } 119 | ppsIndex = spsPpsBuffer.position(); 120 | sps = new byte[ppsIndex - 4]; 121 | System.arraycopy(buffer, 0, sps, 0, sps.length); 122 | ppsIndex -= 4; 123 | pps = new byte[buffer.length - ppsIndex]; 124 | System.arraycopy(buffer, ppsIndex, pps, 0, pps.length); 125 | 126 | // sps buffer 127 | ByteBuffer spsBuffer = ByteBuffer.wrap(sps, 0, sps.length); 128 | 129 | // pps buffer 130 | ByteBuffer ppsBuffer = ByteBuffer.wrap(pps, 0, pps.length); 131 | 132 | StreamSettings streamSettings = new StreamSettings(); 133 | streamSettings.sps = spsBuffer; 134 | streamSettings.pps = ppsBuffer; 135 | 136 | return streamSettings; 137 | } 138 | 139 | public byte[] toByteArray() { 140 | return toArray(type, flag, presentationTimeStamp, data); 141 | } 142 | 143 | public enum Flag { 144 | 145 | FRAME((byte) 0), KEY_FRAME((byte) 1), CONFIG((byte) 2), END((byte) 4); 146 | 147 | private byte type; 148 | 149 | Flag(byte type) { 150 | this.type = type; 151 | } 152 | 153 | public static Flag getFlag(byte value) { 154 | for (Flag type : Flag.values()) { 155 | if (type.getFlag() == value) { 156 | return type; 157 | } 158 | } 159 | 160 | return null; 161 | } 162 | 163 | public byte getFlag() { 164 | return type; 165 | } 166 | } 167 | 168 | public static class StreamSettings { 169 | public ByteBuffer pps; 170 | public ByteBuffer sps; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/model/ByteUtils.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.model; 2 | 3 | import java.math.BigInteger; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * Created by Alexandr Golovach on 27.06.16. 8 | * https://www.github.com/alexmprog/VideoCodec 9 | */ 10 | 11 | public class ByteUtils { 12 | 13 | public static byte[] longToBytes(long x) { 14 | ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / 8); 15 | buffer.putLong(0, x); 16 | return buffer.array(); 17 | } 18 | 19 | public static long bytesToLong(byte[] bytes) { 20 | return new BigInteger(bytes).longValue(); 21 | } 22 | 23 | public static byte[] intToBytes(int x) { 24 | ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / 8); 25 | buffer.putInt(0, x); 26 | return buffer.array(); 27 | } 28 | 29 | public static int bytesToInt(byte[] bytes) { 30 | return new BigInteger(bytes).intValue(); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/model/MediaPacket.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.model; 2 | 3 | /** 4 | * Created by Alexandr Golovach on 27.06.16. 5 | * https://www.github.com/alexmprog/VideoCodec 6 | */ 7 | public class MediaPacket { 8 | 9 | public Type type; 10 | 11 | public enum Type { 12 | 13 | VIDEO((byte) 1), AUDIO((byte) 0); 14 | 15 | private byte type; 16 | 17 | Type(byte type) { 18 | this.type = type; 19 | } 20 | 21 | public static Type getType(byte value) { 22 | for (Type type : Type.values()) { 23 | if (type.getType() == value) { 24 | return type; 25 | } 26 | } 27 | 28 | return null; 29 | } 30 | 31 | public byte getType() { 32 | return type; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/model/VideoPacket.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.model; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | /** 6 | * Created by Alexandr Golovach on 27.06.16. 7 | * https://www.github.com/alexmprog/VideoCodec 8 | */ 9 | 10 | public class VideoPacket extends MediaPacket { 11 | 12 | public Flag flag; 13 | public long presentationTimeStamp; 14 | public byte[] data; 15 | 16 | public VideoPacket() { 17 | } 18 | 19 | public VideoPacket(Type type, Flag flag, long presentationTimeStamp, byte[] data) { 20 | this.type = type; 21 | this.flag = flag; 22 | this.presentationTimeStamp = presentationTimeStamp; 23 | this.data = data; 24 | } 25 | 26 | // create packet from byte array 27 | public static VideoPacket fromArray(byte[] values) { 28 | VideoPacket videoPacket = new VideoPacket(); 29 | 30 | // should be a type value - 1 byte 31 | byte typeValue = values[0]; 32 | // should be a flag value - 1 byte 33 | byte flagValue = values[1]; 34 | 35 | videoPacket.type = Type.getType(typeValue); 36 | videoPacket.flag = Flag.getFlag(flagValue); 37 | 38 | // should be 8 bytes for timestamp 39 | byte[] timeStamp = new byte[8]; 40 | System.arraycopy(values, 2, timeStamp, 0, 8); 41 | videoPacket.presentationTimeStamp = ByteUtils.bytesToLong(timeStamp); 42 | 43 | // all other bytes is data 44 | int dataLength = values.length - 10; 45 | byte[] data = new byte[dataLength]; 46 | System.arraycopy(values, 10, data, 0, dataLength); 47 | videoPacket.data = data; 48 | 49 | return videoPacket; 50 | } 51 | 52 | public static VideoPacket readHead(byte[] values) { 53 | VideoPacket videoPacket = new VideoPacket(); 54 | 55 | // should be a type value - 1 byte 56 | byte typeValue = values[0]; 57 | // should be a flag value - 1 byte 58 | byte flagValue = values[1]; 59 | 60 | videoPacket.type = Type.getType(typeValue); 61 | videoPacket.flag = VideoPacket.Flag.getFlag(flagValue); 62 | 63 | // should be 8 bytes for timestamp 64 | byte[] timeStamp = new byte[8]; 65 | System.arraycopy(values, 2, timeStamp, 0, 8); 66 | videoPacket.presentationTimeStamp = ByteUtils.bytesToLong(timeStamp); 67 | 68 | videoPacket.data = null; 69 | 70 | return videoPacket; 71 | } 72 | 73 | public static int getHeadLen(){ 74 | return 10; 75 | } 76 | 77 | // create byte array 78 | public static byte[] toArray(Type type, Flag flag, long presentationTimeStamp, byte[] data) { 79 | 80 | // should be 4 bytes for packet size 81 | byte[] bytes = ByteUtils.intToBytes(10 + data.length); 82 | 83 | int packetSize = 14 + data.length; // 4 - inner packet size 1 - type + 1 - flag + 8 - timeStamp + data.length 84 | byte[] values = new byte[packetSize]; 85 | 86 | System.arraycopy(bytes, 0, values, 0, 4); 87 | 88 | // set type value 89 | values[4] = type.getType(); 90 | // set flag value 91 | values[5] = flag.getFlag(); 92 | // set timeStamp 93 | byte[] longToBytes = ByteUtils.longToBytes(presentationTimeStamp); 94 | System.arraycopy(longToBytes, 0, values, 6, longToBytes.length); 95 | 96 | // set data array 97 | System.arraycopy(data, 0, values, 14, data.length); 98 | return values; 99 | } 100 | 101 | // should call on inner packet 102 | public static boolean isVideoPacket(byte[] values) { 103 | return values[0] == Type.VIDEO.getType(); 104 | } 105 | 106 | public static StreamSettings getStreamSettings(byte[] buffer) { 107 | byte[] sps, pps; 108 | 109 | ByteBuffer spsPpsBuffer = ByteBuffer.wrap(buffer); 110 | if (spsPpsBuffer.getInt() == 0x00000001) { 111 | System.out.println("parsing sps/pps"); 112 | } else { 113 | System.out.println("something is amiss?"); 114 | } 115 | int ppsIndex = 0; 116 | while (!(spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x01)) { 117 | 118 | } 119 | ppsIndex = spsPpsBuffer.position(); 120 | sps = new byte[ppsIndex - 4]; 121 | System.arraycopy(buffer, 0, sps, 0, sps.length); 122 | ppsIndex -= 4; 123 | pps = new byte[buffer.length - ppsIndex]; 124 | System.arraycopy(buffer, ppsIndex, pps, 0, pps.length); 125 | 126 | // sps buffer 127 | ByteBuffer spsBuffer = ByteBuffer.wrap(sps, 0, sps.length); 128 | 129 | // pps buffer 130 | ByteBuffer ppsBuffer = ByteBuffer.wrap(pps, 0, pps.length); 131 | 132 | StreamSettings streamSettings = new StreamSettings(); 133 | streamSettings.sps = spsBuffer; 134 | streamSettings.pps = ppsBuffer; 135 | 136 | return streamSettings; 137 | } 138 | 139 | public byte[] toByteArray() { 140 | return toArray(type, flag, presentationTimeStamp, data); 141 | } 142 | 143 | public enum Flag { 144 | 145 | FRAME((byte) 0), KEY_FRAME((byte) 1), CONFIG((byte) 2), END((byte) 4); 146 | 147 | private byte type; 148 | 149 | Flag(byte type) { 150 | this.type = type; 151 | } 152 | 153 | public static Flag getFlag(byte value) { 154 | for (Flag type : Flag.values()) { 155 | if (type.getFlag() == value) { 156 | return type; 157 | } 158 | } 159 | 160 | return null; 161 | } 162 | 163 | public byte getFlag() { 164 | return type; 165 | } 166 | } 167 | 168 | public static class StreamSettings { 169 | public ByteBuffer pps; 170 | public ByteBuffer sps; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/utils/ExecUtil.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.utils; 2 | 3 | import android.util.Log; 4 | 5 | 6 | import java.io.BufferedReader; 7 | import java.io.DataOutputStream; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.util.Map; 12 | 13 | 14 | public class ExecUtil { 15 | 16 | public static String execCommend(String cmd, String[] env, File workDir) { 17 | Process process = null; 18 | DataOutputStream os = null; 19 | BufferedReader successResult = null; 20 | BufferedReader errorResult = null; 21 | try { 22 | process = Runtime.getRuntime().exec("sh", env, workDir); 23 | os = new DataOutputStream(process.getOutputStream()); 24 | os.write(cmd.getBytes()); 25 | os.writeBytes("\n"); 26 | os.flush(); 27 | os.writeBytes("exit\n"); 28 | os.flush(); 29 | int result = process.waitFor(); 30 | // get command result 31 | StringBuilder successMsg = new StringBuilder(); 32 | StringBuilder errorMsg = new StringBuilder(); 33 | successResult = new BufferedReader(new InputStreamReader( 34 | process.getInputStream())); 35 | errorResult = new BufferedReader(new InputStreamReader( 36 | process.getErrorStream())); 37 | String s; 38 | while ((s = successResult.readLine()) != null) { 39 | successMsg.append(s); 40 | } 41 | while ((s = errorResult.readLine()) != null) { 42 | errorMsg.append(s); 43 | } 44 | return successMsg.toString(); 45 | } catch (Exception e) { 46 | Log.i("TaskPrint", e.toString()); 47 | } finally { 48 | try { 49 | if (os != null) { 50 | os.close(); 51 | } 52 | if (successResult != null) { 53 | successResult.close(); 54 | } 55 | if (errorResult != null) { 56 | errorResult.close(); 57 | } 58 | } catch (IOException e) { 59 | e.printStackTrace(); 60 | } 61 | if (process != null) { 62 | process.destroy(); 63 | } 64 | } 65 | return ""; 66 | } 67 | 68 | public static String adbCommend(String[] cmd, Map env, File workDir) { 69 | Process process = null; 70 | DataOutputStream os = null; 71 | BufferedReader successResult = null; 72 | BufferedReader errorResult = null; 73 | try { 74 | ProcessBuilder processBuilder = new ProcessBuilder(cmd).directory(workDir); 75 | Map envs = processBuilder.environment(); 76 | for (String s : env.keySet()) { 77 | envs.put(s, env.get(s)); 78 | } 79 | process = processBuilder.start(); 80 | os = new DataOutputStream(process.getOutputStream()); 81 | os.flush(); 82 | int result = process.waitFor(); 83 | // get command result 84 | StringBuilder successMsg = new StringBuilder(); 85 | StringBuilder errorMsg = new StringBuilder(); 86 | successResult = new BufferedReader(new InputStreamReader( 87 | process.getInputStream())); 88 | boolean successFirst = true; 89 | errorResult = new BufferedReader(new InputStreamReader( 90 | process.getErrorStream())); 91 | boolean errorFirst = true; 92 | String s; 93 | while ((s = successResult.readLine()) != null) { 94 | if (!successFirst) { 95 | successMsg.append("\n"); 96 | } else { 97 | successFirst = false; 98 | } 99 | successMsg.append(s); 100 | } 101 | while ((s = errorResult.readLine()) != null) { 102 | if (!errorFirst) { 103 | errorMsg.append("\n"); 104 | } else { 105 | errorFirst = false; 106 | } 107 | errorMsg.append(s); 108 | } 109 | Log.i("Scrcpy", errorMsg.toString()); 110 | return successMsg.toString(); 111 | } catch (Exception e) { 112 | Log.i("TaskPrint", e.toString()); 113 | } finally { 114 | try { 115 | if (os != null) { 116 | os.close(); 117 | } 118 | if (successResult != null) { 119 | successResult.close(); 120 | } 121 | if (errorResult != null) { 122 | errorResult.close(); 123 | } 124 | } catch (IOException e) { 125 | e.printStackTrace(); 126 | } 127 | if (process != null) { 128 | process.destroy(); 129 | } 130 | } 131 | return ""; 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/utils/HttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.utils; 2 | 3 | 4 | import java.io.*; 5 | import java.net.HttpURLConnection; 6 | import java.net.URL; 7 | import java.net.URLConnection; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.zip.GZIPInputStream; 11 | public class HttpRequest { 12 | /** 13 | * 向指定URL发送GET方法的请求 14 | * 15 | * @param url 发送请求的URL 16 | * @param params 请求参数,一个 Map 列表 17 | * @return URL 所代表远程资源的响应结果 18 | */ 19 | public static String sendGet(String url, Map params) { 20 | StringBuilder result = new StringBuilder(); 21 | InputStream is = null; 22 | try { 23 | String urlNameString = url + (params == null ? "" : ("?" + Util.getParamUrl(params))); 24 | URL realUrl = new URL(urlNameString); 25 | // 打开和URL之间的连接 26 | HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection(); 27 | connection.setConnectTimeout(5000); 28 | connection.setReadTimeout(5000); 29 | // 设置通用的请求属性 30 | connection.setRequestProperty("accept", "*/*"); 31 | connection.setRequestProperty("connection", "Keep-Alive"); 32 | connection.setRequestProperty("user-agent", 33 | "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); 34 | // 自动重定向 35 | connection.setInstanceFollowRedirects(true); 36 | // 建立实际的连接 37 | connection.connect(); 38 | if (connection.getContentEncoding() != null && 39 | !"".equals(connection.getContentEncoding())) { 40 | String encode = connection.getContentEncoding().toLowerCase(); 41 | if (encode.contains("gzip")) { 42 | is = new GZIPInputStream(connection.getInputStream()); 43 | } 44 | } 45 | if (null == is) { 46 | is = connection.getInputStream(); 47 | } 48 | // 获取所有响应头字段 49 | Map> map = connection.getHeaderFields(); 50 | // 遍历所有的响应头字段 51 | // for (String key : map.keySet()) { 52 | // System.out.println(key + "--->" + map.get(key)); 53 | // } 54 | // 手动处理重定向 55 | if (connection.getResponseCode() >= 300 && connection.getResponseCode() < 400) { 56 | String location = ""; 57 | if (map.get("Location") != null && !map.get("Location").isEmpty()) { 58 | location = map.get("Location").get(0); 59 | } 60 | if (map.get("location") != null && !map.get("location").isEmpty()) { 61 | location = map.get("location").get(0); 62 | } 63 | if (location != null && !location.isEmpty()) { 64 | if (location.startsWith("/")) { 65 | return sendGet(url + location, null); 66 | } 67 | return sendGet(location, null); 68 | } 69 | } 70 | // 定义 BufferedReader输入流来读取URL的响应 71 | BufferedReader in = new BufferedReader(new InputStreamReader(is)); 72 | String line = ""; 73 | while ((line = in.readLine()) != null) { 74 | if (result.length() > 0) { 75 | result.append("\n"); 76 | } 77 | result.append(line); 78 | } 79 | } catch (Exception e) { 80 | System.out.println("发送GET请求出现异常!" + e); 81 | e.printStackTrace(); 82 | } 83 | // 使用finally块来关闭输入流 84 | finally { 85 | try { 86 | if (is != null) { 87 | is.close(); 88 | } 89 | } catch (Exception e2) { 90 | e2.printStackTrace(); 91 | } 92 | } 93 | return result.toString(); 94 | } 95 | 96 | /** 97 | * 向指定 URL 发送POST方法的请求 98 | * 99 | * @param url 发送请求的 URL 100 | * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 101 | * @return 所代表远程资源的响应结果 102 | */ 103 | public static String sendPost(String url, String param) { 104 | PrintWriter out = null; 105 | BufferedReader in = null; 106 | String result = ""; 107 | try { 108 | URL realUrl = new URL(url); 109 | // 打开和URL之间的连接 110 | URLConnection conn = realUrl.openConnection(); 111 | // 设置通用的请求属性 112 | conn.setRequestProperty("accept", "*/*"); 113 | conn.setRequestProperty("connection", "Keep-Alive"); 114 | conn.setRequestProperty("user-agent", 115 | "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); 116 | // 发送POST请求必须设置如下两行 117 | conn.setDoOutput(true); 118 | conn.setDoInput(true); 119 | // 获取URLConnection对象对应的输出流 120 | out = new PrintWriter(conn.getOutputStream()); 121 | // 发送请求参数 122 | out.print(param); 123 | // flush输出流的缓冲 124 | out.flush(); 125 | // 定义BufferedReader输入流来读取URL的响应 126 | in = new BufferedReader( 127 | new InputStreamReader(conn.getInputStream())); 128 | String line; 129 | while ((line = in.readLine()) != null) { 130 | result += line; 131 | } 132 | } catch (Exception e) { 133 | System.out.println("发送 POST 请求出现异常!" + e); 134 | e.printStackTrace(); 135 | } 136 | //使用finally块来关闭输出流、输入流 137 | finally { 138 | try { 139 | if (out != null) { 140 | out.close(); 141 | } 142 | if (in != null) { 143 | in.close(); 144 | } 145 | } catch (IOException ex) { 146 | ex.printStackTrace(); 147 | } 148 | } 149 | return result; 150 | } 151 | } -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/utils/PreUtils.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.utils; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | 7 | /** 8 | * Created by hasee on 2017/6/10. 9 | */ 10 | @SuppressWarnings({"unused", "WeakerAccess"}) 11 | public class PreUtils { 12 | private static final String User = "default"; 13 | 14 | public static boolean put(Context context, String key, int value) { 15 | SharedPreferences preferences = context.getSharedPreferences(User, Context.MODE_PRIVATE); 16 | SharedPreferences.Editor editor = preferences.edit(); 17 | editor.putInt(key, value); 18 | return editor.commit(); 19 | } 20 | 21 | public static int get(Context context, String key, int defaultInt) { 22 | SharedPreferences preferences = context.getSharedPreferences(User, Context.MODE_PRIVATE); 23 | return preferences.getInt(key, defaultInt); 24 | 25 | } 26 | 27 | public static boolean put(Context context, String key, long value) { 28 | SharedPreferences preferences = context.getSharedPreferences(User, Context.MODE_PRIVATE); 29 | SharedPreferences.Editor editor = preferences.edit(); 30 | editor.putLong(key, value); 31 | return editor.commit(); 32 | } 33 | 34 | public static long get(Context context, String key, long defaultInt) { 35 | SharedPreferences preferences = context.getSharedPreferences(User, Context.MODE_PRIVATE); 36 | return preferences.getLong(key, defaultInt); 37 | } 38 | 39 | public static boolean put(Context context, String key, String value) { 40 | SharedPreferences preferences = context.getSharedPreferences(User, Context.MODE_PRIVATE); 41 | SharedPreferences.Editor editor = preferences.edit(); 42 | editor.putString(key, value); 43 | return editor.commit(); 44 | } 45 | 46 | public static String get(Context context, String key, String defaultStr) { 47 | SharedPreferences preferences = context.getSharedPreferences(User, Context.MODE_PRIVATE); 48 | return preferences.getString(key, defaultStr); 49 | } 50 | 51 | public static boolean put(Context context, String key, boolean value) { 52 | SharedPreferences preferences = context.getSharedPreferences(User, Context.MODE_PRIVATE); 53 | SharedPreferences.Editor editor = preferences.edit(); 54 | editor.putBoolean(key, value); 55 | return editor.commit(); 56 | } 57 | 58 | public static boolean get(Context context, String key, boolean defaultStr) { 59 | SharedPreferences preferences = context.getSharedPreferences(User, Context.MODE_PRIVATE); 60 | return preferences.getBoolean(key, defaultStr); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/utils/ProcessHelper.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.utils; 2 | 3 | import android.os.Build; 4 | 5 | 6 | import java.io.BufferedReader; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.io.OutputStream; 10 | import java.nio.charset.StandardCharsets; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * 执行命令,且命令会不断的在后台运行,直到调用 callback close 15 | */ 16 | public class ProcessHelper { 17 | protected volatile boolean isClose; 18 | // server 19 | private Process process = null; 20 | private OutputStream os = null; 21 | private BufferedReader successResult = null; 22 | private BufferedReader errorResult = null; 23 | 24 | public static ProcessHelper runComand(String command, 25 | StatuCallback statuCallback) { 26 | return new ProcessHelper(command, statuCallback); 27 | } 28 | 29 | public void writeCommand(String newCommand) { 30 | writeCommand(newCommand, false); 31 | } 32 | 33 | public void writeCommand(String newCommand, boolean needEnter) { 34 | write(newCommand.getBytes(StandardCharsets.UTF_8)); 35 | if (needEnter) { 36 | write("\n".getBytes(StandardCharsets.UTF_8)); 37 | } 38 | } 39 | 40 | public void write(byte[] bytes) { 41 | if (!isClose && os != null) { 42 | try { 43 | os.write(bytes); 44 | os.flush(); 45 | } catch (IOException e) { 46 | e.printStackTrace(); 47 | } 48 | } else { 49 | // 进程已经被中断,不再允许其写入数据 50 | throw new RuntimeException("process is close"); 51 | } 52 | } 53 | 54 | public void stopCommand() { 55 | isClose = true; 56 | streamClose(); 57 | } 58 | 59 | public boolean isRunning() { 60 | return !isClose; 61 | } 62 | 63 | public ProcessHelper() { 64 | this.isClose = false; 65 | } 66 | 67 | private ProcessHelper(String command, StatuCallback statuCallback) { 68 | this.isClose = false; 69 | try { 70 | process = Runtime.getRuntime().exec(command); 71 | os = process.getOutputStream(); 72 | // 读取输出 73 | successResult = new BufferedReader(new InputStreamReader( 74 | process.getInputStream())); 75 | errorResult = new BufferedReader(new InputStreamReader( 76 | process.getErrorStream())); 77 | if (statuCallback != null) { 78 | statuCallback.processOpen(this, process, os); 79 | } 80 | // 关闭流之后,销毁 Process 81 | Thread outputThread = new Thread(new Runnable() { 82 | @Override 83 | public void run() { 84 | String s; 85 | while (!isClose) { 86 | try { 87 | if ((s = successResult.readLine()) != null) { 88 | if (statuCallback != null) { 89 | statuCallback.onOutput(ProcessHelper.this, s); 90 | } 91 | } else { 92 | isClose = true; 93 | } 94 | } catch (IOException ignored) { 95 | isClose = true; 96 | } catch (Exception ignored) { 97 | } 98 | } 99 | streamClose(); 100 | // 关闭流之后,销毁 Process 101 | if (process != null) { 102 | process.destroy(); 103 | } 104 | int exitCode = -1; 105 | if (process != null) { 106 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 107 | if (process.isAlive()) { 108 | try { 109 | // 等 1s 后看进程是否结束 110 | if (process.waitFor(1, TimeUnit.SECONDS)) { 111 | exitCode = process.exitValue(); 112 | } 113 | } catch (InterruptedException ignore) { 114 | } 115 | } else { 116 | exitCode = process.exitValue(); 117 | } 118 | } else { 119 | try { 120 | exitCode = process.exitValue(); 121 | } catch (Exception ignore) { 122 | } 123 | } 124 | } 125 | if (statuCallback != null) { 126 | // statuCallback.processClose(ProcessHelper.this, process, process == null ? -1 : process.exitValue()); 127 | statuCallback.processClose(ProcessHelper.this, process, exitCode); 128 | } 129 | } 130 | }); 131 | Thread errorThread = new Thread(new Runnable() { 132 | @Override 133 | public void run() { 134 | String s; 135 | while (!isClose) { 136 | try { 137 | if ((s = errorResult.readLine()) != null) { 138 | if (statuCallback != null) { 139 | statuCallback.onError(ProcessHelper.this, s); 140 | } 141 | } else { 142 | isClose = true; 143 | } 144 | } catch (IOException ignored) { 145 | isClose = true; 146 | } catch (Exception ignored) { 147 | } 148 | } 149 | streamClose(); 150 | } 151 | }); 152 | outputThread.start(); 153 | errorThread.start(); 154 | } catch (Exception e) { 155 | // 如果启动过程发生异常,将其置为关闭状态 156 | this.isClose = true; 157 | if (statuCallback != null) { 158 | statuCallback.processError(this, process, e); 159 | } 160 | } 161 | } 162 | 163 | private synchronized void streamClose() { 164 | if (os != null) { 165 | try { 166 | os.close(); 167 | } catch (IOException e) { 168 | e.printStackTrace(); 169 | } finally { 170 | os = null; 171 | } 172 | } 173 | if (successResult != null) { 174 | try { 175 | successResult.close(); 176 | } catch (IOException e) { 177 | e.printStackTrace(); 178 | } finally { 179 | successResult = null; 180 | } 181 | } 182 | if (errorResult != null) { 183 | try { 184 | errorResult.close(); 185 | } catch (IOException e) { 186 | e.printStackTrace(); 187 | } finally { 188 | errorResult = null; 189 | } 190 | } 191 | } 192 | 193 | public static class StatuCallback { 194 | public void processOpen(ProcessHelper processHelper, Process process, OutputStream outputStream) { 195 | 196 | } 197 | 198 | public void processError(ProcessHelper processHelper, Process process, Exception e) { 199 | 200 | } 201 | 202 | public void processClose(ProcessHelper processHelper, Process process, int exitCode) { 203 | 204 | } 205 | 206 | public void onOutput(ProcessHelper processHelper, String output) { 207 | } 208 | 209 | public void onError(ProcessHelper processHelper, String error) { 210 | } 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/utils/Progress.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.app.ProgressDialog; 6 | import android.content.Context; 7 | import android.text.TextUtils; 8 | 9 | public final class Progress { 10 | 11 | private static ProgressDialog progressDialog; 12 | 13 | public static void showDialog(Activity context, String title) { 14 | showDialog(context, title, false); 15 | } 16 | 17 | public static void showDialog(Activity context, String title, String msg) { 18 | showDialog(context, title, msg, false); 19 | } 20 | 21 | public static void showDialog(Activity context, String title, boolean setup) { 22 | showDialog(context, title, "", setup); 23 | } 24 | 25 | @SuppressLint("InvalidWakeLockTag") 26 | public static void showDialog(Activity context, String title, String msg, boolean setup) { 27 | ThreadUtils.post(() -> { 28 | if (context == null || context.isFinishing()) { 29 | return; 30 | } 31 | if (progressDialog != null) { 32 | progressDialog.dismiss(); 33 | progressDialog = null; 34 | } 35 | Context application = context.getApplication(); 36 | progressDialog = new ProgressDialog(context); 37 | // 设置ProgressDialog 提示信息 38 | if (!TextUtils.isEmpty(msg)) { 39 | progressDialog.setTitle(title); 40 | progressDialog.setMessage(msg); 41 | } else { 42 | // 设置ProgressDialog 标题 43 | progressDialog.setMessage(title); 44 | } 45 | // 设置ProgressDialog 是否可以按退回按键取消 46 | progressDialog.setCancelable(false); 47 | // 取消或者关闭弹窗时,置空 48 | 49 | if (setup) { 50 | progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 51 | progressDialog.setMax(100); 52 | } 53 | progressDialog.show(); 54 | }); 55 | } 56 | 57 | public static void updateMessage(String msg) { 58 | ThreadUtils.post(() -> { 59 | if (progressDialog != null) { 60 | progressDialog.setMessage(msg); 61 | } 62 | }); 63 | } 64 | 65 | public static void updateDialog(String title) { 66 | ThreadUtils.post(() -> { 67 | if (progressDialog != null) { 68 | progressDialog.setTitle(title); 69 | // progressDialog.setMessage(msg); 70 | } 71 | }); 72 | } 73 | 74 | public static boolean isShowing() { 75 | return progressDialog != null && progressDialog.isShowing(); 76 | } 77 | 78 | public static void updateDialog(int progress) { 79 | ThreadUtils.post(() -> { 80 | if (progressDialog != null) { 81 | progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 82 | progressDialog.setMax(100); 83 | progressDialog.setProgress(progress); 84 | } 85 | }); 86 | } 87 | 88 | public static void closeDialog() { 89 | ThreadUtils.post(() -> { 90 | if (progressDialog != null) { 91 | if (progressDialog.isShowing()) { 92 | try { 93 | progressDialog.dismiss(); 94 | } catch (IllegalArgumentException e) { 95 | e.printStackTrace(); 96 | } 97 | } 98 | progressDialog = null; 99 | } 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/org/client/scrcpy/utils/Util.java: -------------------------------------------------------------------------------- 1 | package org.client.scrcpy.utils; 2 | 3 | import android.content.res.Resources; 4 | import android.graphics.Bitmap; 5 | import android.text.TextUtils; 6 | import android.util.TypedValue; 7 | 8 | import org.client.scrcpy.Scrcpy; 9 | 10 | import java.io.UnsupportedEncodingException; 11 | import java.net.URLEncoder; 12 | import java.util.Hashtable; 13 | import java.util.Map; 14 | 15 | public class Util { 16 | 17 | public static String[] getServerHostAndPort(String serverAdr) { 18 | if (TextUtils.isEmpty(serverAdr)) { 19 | return new String[]{"127.0.0.1", String.valueOf(Scrcpy.DEFAULT_ADB_PORT)}; 20 | } 21 | String serverHost = serverAdr; 22 | String serverPort = String.valueOf(Scrcpy.DEFAULT_ADB_PORT); 23 | if (serverAdr.contains(":")) { 24 | int lastIndex = serverAdr.lastIndexOf(":"); 25 | 26 | try { 27 | serverHost = serverAdr.substring(0, lastIndex); 28 | serverPort = String.valueOf(Integer.parseInt( 29 | serverAdr.substring(lastIndex + 1) 30 | )); 31 | // ipv6 不能去除前后的 [] 32 | // if (serverHost.startsWith("[") && serverHost.endsWith("]")) { 33 | // // 截取掉 ipv6 的 [] 34 | // serverHost = serverHost.substring(1, serverHost.length() - 1); 35 | // } 36 | } catch (Exception e) { 37 | e.printStackTrace(); 38 | } 39 | } 40 | return new String[]{serverHost, serverPort}; 41 | } 42 | 43 | public static int dpToPx(int dp) { 44 | return (int) TypedValue.applyDimension( 45 | TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics() 46 | ); 47 | } 48 | 49 | /** 50 | * 拼接 url 请求参数 51 | * 52 | * @param params 参数列表 53 | * @return 拼接后的url 54 | */ 55 | public static String getParamUrl(Map params) { 56 | if (params == null || params.isEmpty()) return ""; 57 | boolean start = true; 58 | StringBuilder urlBuilder = new StringBuilder(); 59 | for (Map.Entry entry : params.entrySet()) { 60 | try { 61 | urlBuilder.append(start ? "" : "&") 62 | .append(URLEncoder.encode(entry.getKey(), "UTF-8")) 63 | .append("=") 64 | .append(URLEncoder.encode(entry.getValue(), "UTF-8")); 65 | if (start) { 66 | start = false; 67 | } 68 | } catch (UnsupportedEncodingException e) { 69 | e.printStackTrace(); 70 | } 71 | 72 | } 73 | return urlBuilder.toString(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/jniLibs/arm64-v8a/libadb.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/app/src/main/jniLibs/arm64-v8a/libadb.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/libadb.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/app/src/main/jniLibs/armeabi-v7a/libadb.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/x86/libadb.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/app/src/main/jniLibs/x86/libadb.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/x86_64/libadb.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/app/src/main/jniLibs/x86_64/libadb.so -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_click.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_global.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/app/src/main/res/drawable/close.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/app/src/main/res/drawable/down.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/minis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwc456baby/ScrcpyForAndroid/40996ba5dd79a175034178a744d204c4df566599/app/src/main/res/drawable/minis.png -------------------------------------------------------------------------------- /app/src/main/res/layout-land/surface.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 24 | 25 | 26 |