├── .gitignore ├── .idea ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── commons-codec-1.10-rep.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── las2mile │ │ └── scrcpy │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── scrcpy-server.jar │ │ └── scrcpy-server_old.jar │ ├── java │ │ └── org │ │ │ └── las2mile │ │ │ └── scrcpy │ │ │ ├── MainActivity.java │ │ │ ├── Scrcpy.java │ │ │ ├── SendCommands.java │ │ │ ├── adblib │ │ │ ├── AdbBase64.java │ │ │ ├── AdbConnection.java │ │ │ ├── AdbCrypto.java │ │ │ ├── AdbProtocol.java │ │ │ ├── AdbStream.java │ │ │ └── LICENSE │ │ │ ├── decoder │ │ │ └── VideoDecoder.java │ │ │ └── model │ │ │ ├── ByteUtils.java │ │ │ ├── MediaPacket.java │ │ │ └── VideoPacket.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout-land │ │ ├── surface_nav.xml │ │ └── surface_no_nav.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── surface_nav.xml │ │ └── surface_no_nav.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── org │ └── las2mile │ └── scrcpy │ └── ExampleUnitTest.java ├── build.gradle ├── config ├── android-checkstyle.gradle └── checkstyle │ └── checkstyle.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── release └── scrcpy-release.apk ├── server ├── .gitignore ├── LICENSE ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── android │ │ └── view │ │ └── IRotationWatcher.aidl │ └── java │ └── org │ └── las2mile │ └── scrcpy │ ├── Device.java │ ├── DisplayInfo.java │ ├── DroidConnection.java │ ├── EventController.java │ ├── Ln.java │ ├── Options.java │ ├── Position.java │ ├── ScreenEncoder.java │ ├── ScreenInfo.java │ ├── Server.java │ ├── Size.java │ ├── model │ ├── ByteUtils.java │ ├── MediaPacket.java │ └── VideoPacket.java │ └── wrappers │ ├── DisplayManager.java │ ├── InputManager.java │ ├── PowerManager.java │ ├── ServiceManager.java │ ├── SurfaceControl.java │ └── WindowManager.java └── settings.gradle /.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/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scrcpy-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 target android device to scrcpy-android device. 6 | 7 | - scrcpy-android uses ADB-Connect interface to connect to android device to be mirrored. 8 | 9 | 10 | ## Download 11 | 12 | [scrcpy-release-v1.2.apk](https://gitlab.com/las2mile/scrcpy-android/raw/master/release/scrcpy-release.apk) 13 | 14 | 15 | ## Instructions to use 16 | 17 | - Make sure both devices are on same local network. 18 | 19 | - Enable **ADB-connect/ADB-wireless/ADB over network** on the device to be mirrored. 20 | 21 | - Open scrcpy app and enter ip address of device to be mirrored. 22 | 23 | - Select display parameters and bitrate from drop-down menu(1280x720 and 2Mbps works best). 24 | 25 | - Set **Navbar** switch if the device to be mirrored has only hardware navigation buttons. 26 | 27 | - Hit **start** button. 28 | 29 | - Accept and trust(check always allow from this computer) the ADB connection prompt on target device(Some custom roms don't have this prompt). 30 | 31 | - Thats all! You should be seeing the screen of target android device. 32 | 33 | - To wake up device, **double tap anywhere on screen**. 34 | 35 | - To put device to sleep, **close proxmity sensor and double tap anywhere on the screen**. 36 | 37 | - To bring back the local android system navbar while mirroring the remote device, **swipe up from the bottom edge of screen**. 38 | 39 | 40 | ## Building with Gradle 41 | 42 | ./gradlew assembleDebug 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | evaluationDependsOn(':server') 2 | 3 | apply plugin: 'com.android.application' 4 | 5 | 6 | android { 7 | compileSdkVersion 28 8 | defaultConfig { 9 | applicationId "org.las2mile.scrcpy" 10 | minSdkVersion 19 11 | targetSdkVersion 28 12 | versionCode 3 13 | versionName "1.2" 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | setProperty("archivesBaseName", "scrcpy") 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation 'com.android.support.constraint:constraint-layout:1.1.2' 28 | testImplementation 'junit:junit:4.12' 29 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 30 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/libs/commons-codec-1.10-rep.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/libs/commons-codec-1.10-rep.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/org/las2mile/scrcpy/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("org.las2mile.scrcpy", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/assets/scrcpy-server.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/assets/scrcpy-server.jar -------------------------------------------------------------------------------- /app/src/main/assets/scrcpy-server_old.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/updeshxp/scrcpy-android/348b42c8095bc0212456f2aab4c598819e4aa3b8/app/src/main/assets/scrcpy-server_old.jar -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/MainActivity.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.ComponentName; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.ServiceConnection; 9 | import android.content.pm.ActivityInfo; 10 | import android.content.res.AssetManager; 11 | import android.hardware.Sensor; 12 | import android.hardware.SensorEvent; 13 | import android.hardware.SensorEventListener; 14 | import android.hardware.SensorManager; 15 | import android.os.Bundle; 16 | import android.os.IBinder; 17 | import android.os.SystemClock; 18 | import android.util.Base64; 19 | import android.util.DisplayMetrics; 20 | import android.util.Log; 21 | import android.view.Display; 22 | import android.view.MotionEvent; 23 | import android.view.Surface; 24 | import android.view.SurfaceView; 25 | import android.view.View; 26 | import android.view.ViewConfiguration; 27 | import android.widget.AdapterView; 28 | import android.widget.ArrayAdapter; 29 | import android.widget.Button; 30 | import android.widget.EditText; 31 | import android.widget.Spinner; 32 | import android.widget.Switch; 33 | import android.widget.Toast; 34 | 35 | import java.io.IOException; 36 | import java.io.InputStream; 37 | import java.net.Inet4Address; 38 | import java.net.Inet6Address; 39 | import java.net.InetAddress; 40 | import java.net.NetworkInterface; 41 | import java.net.SocketException; 42 | import java.util.Enumeration; 43 | 44 | 45 | public class MainActivity extends Activity implements Scrcpy.ServiceCallbacks, SensorEventListener { 46 | 47 | // private static final String TAG = "MainActivity"; 48 | private static final String PREFERENCE_KEY = "default"; 49 | private static final String PREFERENCE_SPINNER_RESOLUTION = "spinner_resolution"; 50 | private static final String PREFERENCE_SPINNER_BITRATE = "spinner_bitrate"; 51 | private static int screenWidth; 52 | private static int screenHeight; 53 | private static boolean landscape = false; 54 | private static boolean first_time = true; 55 | private static boolean resultofRotation = false; 56 | private static boolean serviceBound = false; 57 | private static boolean nav = false; 58 | SensorManager sensorManager; 59 | private static SendCommands sendCommands; 60 | private int videoBitrate; 61 | private String localip; 62 | private Context context; 63 | private String serverAdr = null; 64 | private InputStream inputStream; 65 | private SurfaceView surfaceView; 66 | private Surface surface; 67 | private Scrcpy scrcpy; 68 | private long timestamp = 0; 69 | private byte[] fileBase64; 70 | 71 | private ServiceConnection serviceConnection = new ServiceConnection() { 72 | @Override 73 | public void onServiceConnected(ComponentName componentName, IBinder iBinder) { 74 | scrcpy = ((Scrcpy.MyServiceBinder) iBinder).getService(); 75 | scrcpy.setServiceCallbacks(MainActivity.this); 76 | if (first_time) { 77 | scrcpy.start(surface, serverAdr, screenHeight, screenWidth); 78 | } else { 79 | scrcpy.setParms(surface, screenWidth, screenHeight); 80 | } 81 | first_time = false; 82 | serviceBound = true; 83 | } 84 | 85 | @Override 86 | public void onServiceDisconnected(ComponentName componentName) { 87 | serviceBound = false; 88 | } 89 | }; 90 | 91 | @Override 92 | protected void onCreate(Bundle savedInstanceState) { 93 | super.onCreate(savedInstanceState); 94 | if (first_time) { 95 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 96 | setContentView(R.layout.activity_main); 97 | final Button startButton = (Button) findViewById(R.id.button_start); 98 | AssetManager assetManager = getAssets(); 99 | try { 100 | inputStream = assetManager.open("scrcpy-server.jar"); 101 | byte[] buffer = new byte[inputStream.available()]; 102 | inputStream.read(buffer); 103 | fileBase64 = Base64.encode(buffer, 2); 104 | } catch (IOException e) { 105 | Log.e("Asset Manager", e.getMessage()); 106 | } 107 | sendCommands = new SendCommands(); 108 | startButton.setOnClickListener(new View.OnClickListener() { 109 | @Override 110 | public void onClick(View v) { 111 | localip = wifiIpAddress(); 112 | getAttributes(); 113 | if (!serverAdr.isEmpty()) { 114 | if (sendCommands.SendAdbCommands(context, fileBase64, serverAdr, localip, videoBitrate, Math.max(screenHeight, screenWidth)) == 0) { 115 | if (nav) { 116 | startwithNav(); 117 | } else { 118 | startwithoutNav(); 119 | } 120 | } else { 121 | Toast.makeText(context, "Network OR ADB connection failed", Toast.LENGTH_SHORT).show(); 122 | } 123 | } else { 124 | Toast.makeText(context, "Server Address Empty", Toast.LENGTH_SHORT).show(); 125 | } 126 | } 127 | }); 128 | 129 | this.context = this; 130 | final EditText editTextServerHost = (EditText) findViewById(R.id.editText_server_host); 131 | final Switch aSwitch = findViewById(R.id.switch1); 132 | editTextServerHost.setText(context.getSharedPreferences(PREFERENCE_KEY, 0).getString("Server Address", "")); 133 | aSwitch.setChecked(context.getSharedPreferences(PREFERENCE_KEY, 0).getBoolean("Nav Switch", false)); 134 | setSpinner(R.array.options_resolution_keys, R.id.spinner_video_resolution, PREFERENCE_SPINNER_RESOLUTION); 135 | setSpinner(R.array.options_bitrate_keys, R.id.spinner_video_bitrate, PREFERENCE_SPINNER_BITRATE); 136 | } else { 137 | this.context = this; 138 | if (nav) { 139 | startwithNav(); 140 | } else { 141 | startwithoutNav(); 142 | } 143 | 144 | } 145 | 146 | sensorManager = (SensorManager) this.getSystemService(SENSOR_SERVICE); 147 | Sensor proximity; 148 | proximity = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); 149 | sensorManager.registerListener(this, proximity, SensorManager.SENSOR_DELAY_NORMAL); 150 | 151 | } 152 | 153 | private void setSpinner(final int textArrayOptionResId, final int textViewResId, final String preferenceId) { 154 | 155 | final Spinner spinner = (Spinner) findViewById(textViewResId); 156 | ArrayAdapter arrayAdapter = ArrayAdapter.createFromResource(this, textArrayOptionResId, android.R.layout.simple_spinner_item); 157 | arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 158 | spinner.setAdapter(arrayAdapter); 159 | 160 | spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 161 | @Override 162 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 163 | context.getSharedPreferences(PREFERENCE_KEY, 0).edit().putInt(preferenceId, position).apply(); 164 | } 165 | 166 | @Override 167 | public void onNothingSelected(AdapterView parent) { 168 | context.getSharedPreferences(PREFERENCE_KEY, 0).edit().putInt(preferenceId, 0).apply(); 169 | } 170 | }); 171 | spinner.setSelection(context.getSharedPreferences(PREFERENCE_KEY, 0).getInt(preferenceId, 0)); 172 | } 173 | 174 | private void getAttributes() { 175 | 176 | final EditText editTextServerHost = (EditText) findViewById(R.id.editText_server_host); 177 | serverAdr = editTextServerHost.getText().toString(); 178 | context.getSharedPreferences(PREFERENCE_KEY, 0).edit().putString("Server Address", serverAdr).apply(); 179 | final Spinner videoResolutionSpinner = (Spinner) findViewById(R.id.spinner_video_resolution); 180 | final Spinner videoBitrateSpinner = (Spinner) findViewById(R.id.spinner_video_bitrate); 181 | final Switch aSwitch = findViewById(R.id.switch1); 182 | nav = aSwitch.isChecked(); 183 | context.getSharedPreferences(PREFERENCE_KEY, 0).edit().putBoolean("Nav Switch", nav).apply(); 184 | 185 | 186 | final String[] videoResolutions = getResources().getStringArray(R.array.options_resolution_values)[videoResolutionSpinner.getSelectedItemPosition()].split(","); 187 | screenHeight = Integer.parseInt(videoResolutions[0]); 188 | screenWidth = Integer.parseInt(videoResolutions[1]); 189 | videoBitrate = getResources().getIntArray(R.array.options_bitrate_values)[videoBitrateSpinner.getSelectedItemPosition()]; 190 | 191 | } 192 | 193 | 194 | private void swapDimensions() { 195 | int temp = screenHeight; 196 | screenHeight = screenWidth; 197 | screenWidth = temp; 198 | } 199 | 200 | 201 | @SuppressLint("ClickableViewAccessibility") 202 | private void startwithoutNav() { 203 | setContentView(R.layout.surface_no_nav); 204 | final View decorView = getWindow().getDecorView(); 205 | decorView.setSystemUiVisibility( 206 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 207 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 208 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 209 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 210 | | View.SYSTEM_UI_FLAG_FULLSCREEN 211 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 212 | 213 | surfaceView = (SurfaceView) findViewById(R.id.decoder_surface); 214 | surface = surfaceView.getHolder().getSurface(); 215 | startScrcpyservice(); 216 | DisplayMetrics metrics = new DisplayMetrics(); 217 | 218 | if (ViewConfiguration.get(context).hasPermanentMenuKey()) { 219 | getWindowManager().getDefaultDisplay().getMetrics(metrics); 220 | 221 | 222 | } else { 223 | final Display display = getWindowManager().getDefaultDisplay(); 224 | display.getRealMetrics(metrics); 225 | } 226 | final int height = metrics.heightPixels; 227 | final int width = metrics.widthPixels; 228 | 229 | 230 | surfaceView.setOnTouchListener(new View.OnTouchListener() { 231 | @Override 232 | public boolean onTouch(View v, MotionEvent event) { 233 | return scrcpy.touchevent(event, width, height); 234 | } 235 | }); 236 | 237 | 238 | } 239 | 240 | @SuppressLint("ClickableViewAccessibility") 241 | private void startwithNav() { 242 | 243 | setContentView(R.layout.surface_nav); 244 | final View decorView = getWindow().getDecorView(); 245 | decorView.setSystemUiVisibility( 246 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 247 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 248 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 249 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 250 | | View.SYSTEM_UI_FLAG_FULLSCREEN 251 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 252 | 253 | final Button backButton = (Button) findViewById(R.id.back_button); 254 | final Button homeButton = (Button) findViewById(R.id.home_button); 255 | final Button appswitchButton = (Button) findViewById(R.id.appswitch_button); 256 | 257 | surfaceView = (SurfaceView) findViewById(R.id.decoder_surface); 258 | surface = surfaceView.getHolder().getSurface(); 259 | startScrcpyservice(); 260 | DisplayMetrics metrics = new DisplayMetrics(); 261 | int offset = 0; 262 | 263 | if (ViewConfiguration.get(context).hasPermanentMenuKey()) { 264 | final Display display = getWindowManager().getDefaultDisplay(); 265 | display.getRealMetrics(metrics); 266 | offset = 100; 267 | 268 | } else { 269 | getWindowManager().getDefaultDisplay().getMetrics(metrics); 270 | } 271 | 272 | final int height = metrics.heightPixels - offset; 273 | final int width = metrics.widthPixels; 274 | surfaceView.setOnTouchListener(new View.OnTouchListener() { 275 | @Override 276 | public boolean onTouch(View v, MotionEvent event) { 277 | return scrcpy.touchevent(event, width, height); 278 | 279 | } 280 | }); 281 | 282 | backButton.setOnClickListener(new View.OnClickListener() { 283 | @Override 284 | public void onClick(View v) { 285 | scrcpy.sendKeyevent(4); 286 | 287 | } 288 | }); 289 | 290 | homeButton.setOnClickListener(new View.OnClickListener() { 291 | @Override 292 | public void onClick(View v) { 293 | scrcpy.sendKeyevent(3); 294 | 295 | } 296 | }); 297 | 298 | appswitchButton.setOnClickListener(new View.OnClickListener() { 299 | @Override 300 | public void onClick(View v) { 301 | scrcpy.sendKeyevent(187); 302 | 303 | } 304 | }); 305 | 306 | 307 | } 308 | 309 | 310 | protected String wifiIpAddress() { 311 | //https://stackoverflow.com/questions/6064510/how-to-get-ip-address-of-the-device-from-code 312 | try { 313 | InetAddress ipv4 = null; 314 | InetAddress ipv6 = null; 315 | 316 | for (Enumeration en = NetworkInterface 317 | .getNetworkInterfaces(); en.hasMoreElements(); ) { 318 | NetworkInterface intf = en.nextElement(); 319 | for (Enumeration enumIpAddr = intf 320 | .getInetAddresses(); enumIpAddr.hasMoreElements(); ) { 321 | InetAddress inetAddress = enumIpAddr.nextElement(); 322 | if (inetAddress instanceof Inet6Address) { 323 | ipv6 = inetAddress; 324 | continue; 325 | } 326 | if (inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) { 327 | ipv4 = inetAddress; 328 | continue; 329 | } 330 | return inetAddress.getHostAddress(); 331 | } 332 | } 333 | if (ipv6 != null) { 334 | return ipv6.getHostAddress(); 335 | } 336 | if (ipv4 != null) { 337 | return ipv4.getHostAddress(); 338 | } 339 | return null; 340 | 341 | } catch (SocketException ex) { 342 | ex.printStackTrace(); 343 | } 344 | return null; 345 | 346 | } 347 | 348 | 349 | private void startScrcpyservice() { 350 | Intent intent = new Intent(this, Scrcpy.class); 351 | startService(intent); 352 | bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); 353 | } 354 | 355 | 356 | @Override 357 | public void loadNewRotation() { 358 | unbindService(serviceConnection); 359 | serviceBound = false; 360 | resultofRotation = true; 361 | landscape = !landscape; 362 | swapDimensions(); 363 | if (landscape) { 364 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 365 | } else { 366 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 367 | } 368 | 369 | } 370 | 371 | 372 | @Override 373 | protected void onPause() { 374 | super.onPause(); 375 | if (serviceBound) { 376 | scrcpy.pause(); 377 | } 378 | } 379 | 380 | 381 | @Override 382 | protected void onResume() { 383 | super.onResume(); 384 | if (!first_time && !resultofRotation) { 385 | final View decorView = getWindow().getDecorView(); 386 | decorView.setSystemUiVisibility( 387 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 388 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 389 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 390 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 391 | | View.SYSTEM_UI_FLAG_FULLSCREEN 392 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 393 | if (serviceBound) { 394 | scrcpy.resume(); 395 | } 396 | } 397 | resultofRotation = false; 398 | } 399 | 400 | @Override 401 | public void onBackPressed() { 402 | if (timestamp == 0) { 403 | timestamp = SystemClock.uptimeMillis(); 404 | Toast.makeText(context, "Press again to exit", Toast.LENGTH_SHORT).show(); 405 | } else { 406 | long now = SystemClock.uptimeMillis(); 407 | if (now < timestamp + 1000) { 408 | timestamp = 0; 409 | if (serviceBound) { 410 | scrcpy.StopService(); 411 | unbindService(serviceConnection); 412 | } 413 | android.os.Process.killProcess(android.os.Process.myPid()); 414 | System.exit(1); 415 | } 416 | timestamp = 0; 417 | } 418 | 419 | } 420 | 421 | 422 | @Override 423 | public void onSensorChanged(SensorEvent sensorEvent) { 424 | if (sensorEvent.sensor.getType() == Sensor.TYPE_PROXIMITY) { 425 | if (sensorEvent.values[0] == 0) { 426 | if (serviceBound) { 427 | scrcpy.sendKeyevent(28); 428 | } 429 | } else { 430 | if (serviceBound) { 431 | scrcpy.sendKeyevent(29); 432 | } 433 | } 434 | } 435 | } 436 | 437 | @Override 438 | public void onAccuracyChanged(Sensor sensor, int i) { 439 | 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/Scrcpy.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.Binder; 6 | import android.os.IBinder; 7 | import android.view.MotionEvent; 8 | import android.view.Surface; 9 | 10 | import org.las2mile.scrcpy.decoder.VideoDecoder; 11 | import org.las2mile.scrcpy.model.ByteUtils; 12 | import org.las2mile.scrcpy.model.MediaPacket; 13 | import org.las2mile.scrcpy.model.VideoPacket; 14 | 15 | import java.io.DataInputStream; 16 | import java.io.DataOutputStream; 17 | import java.io.IOException; 18 | import java.net.Socket; 19 | import java.util.concurrent.atomic.AtomicBoolean; 20 | 21 | 22 | public class Scrcpy extends Service { 23 | 24 | private String serverAdr; 25 | private Surface surface; 26 | private int screenWidth; 27 | private int screenHeight; 28 | private byte[] event = null; 29 | private VideoDecoder videoDecoder; 30 | private AtomicBoolean updateAvailable = new AtomicBoolean(false); 31 | private IBinder mBinder = new MyServiceBinder(); 32 | private boolean first_time = true; 33 | private AtomicBoolean LetServceRunning = new AtomicBoolean(true); 34 | private ServiceCallbacks serviceCallbacks; 35 | 36 | 37 | @Override 38 | public IBinder onBind(Intent intent) { 39 | return mBinder; 40 | } 41 | 42 | public void setServiceCallbacks(ServiceCallbacks callbacks) { 43 | serviceCallbacks = callbacks; 44 | } 45 | 46 | 47 | public void setParms(Surface NewSurface, int NewWidth, int NewHeight) { 48 | this.screenWidth = NewWidth; 49 | this.screenHeight = NewHeight; 50 | this.surface = NewSurface; 51 | videoDecoder.start(); 52 | updateAvailable.set(true); 53 | 54 | } 55 | 56 | public void start(Surface surface, String serverAdr, int screenHeight, int screenWidth) { 57 | this.videoDecoder = new VideoDecoder(); 58 | videoDecoder.start(); 59 | this.serverAdr = serverAdr; 60 | this.screenHeight = screenHeight; 61 | this.screenWidth = screenWidth; 62 | this.surface = surface; 63 | Thread thread = new Thread(new Runnable() { 64 | @Override 65 | public void run() { 66 | startConnection(); 67 | } 68 | }); 69 | thread.start(); 70 | } 71 | 72 | public void pause() { 73 | videoDecoder.stop(); 74 | 75 | } 76 | 77 | public void resume() { 78 | videoDecoder.start(); 79 | updateAvailable.set(true); 80 | } 81 | 82 | public void StopService() { 83 | LetServceRunning.set(false); 84 | stopSelf(); 85 | } 86 | 87 | 88 | public boolean touchevent(MotionEvent touch_event, int displayW, int displayH) { 89 | 90 | int[] buf = new int[]{touch_event.getAction(), touch_event.getButtonState(), (int) touch_event.getX() * screenWidth / displayW, (int) touch_event.getY() * screenHeight / displayH}; 91 | final byte[] array = new byte[buf.length * 4]; // https://stackoverflow.com/questions/2183240/java-integer-to-byte-array 92 | for (int j = 0; j < buf.length; j++) { 93 | final int c = buf[j]; 94 | array[j * 4] = (byte) ((c & 0xFF000000) >> 24); 95 | array[j * 4 + 1] = (byte) ((c & 0xFF0000) >> 16); 96 | array[j * 4 + 2] = (byte) ((c & 0xFF00) >> 8); 97 | array[j * 4 + 3] = (byte) (c & 0xFF); 98 | } 99 | event = array; 100 | return true; 101 | } 102 | 103 | public void sendKeyevent(int keycode) { 104 | int[] buf = new int[]{keycode}; 105 | 106 | final byte[] array = new byte[buf.length * 4]; // https://stackoverflow.com/questions/2183240/java-integer-to-byte-array 107 | for (int j = 0; j < buf.length; j++) { 108 | final int c = buf[j]; 109 | array[j * 4] = (byte) ((c & 0xFF000000) >> 24); 110 | array[j * 4 + 1] = (byte) ((c & 0xFF0000) >> 16); 111 | array[j * 4 + 2] = (byte) ((c & 0xFF00) >> 8); 112 | array[j * 4 + 3] = (byte) (c & 0xFF); 113 | } 114 | event = array; 115 | } 116 | 117 | private void startConnection() { 118 | videoDecoder = new VideoDecoder(); 119 | videoDecoder.start(); 120 | DataInputStream dataInputStream; 121 | DataOutputStream dataOutputStream; 122 | Socket socket = null; 123 | VideoPacket.StreamSettings streamSettings = null; 124 | int attempts = 50; 125 | while (attempts != 0) { 126 | try { 127 | socket = new Socket(serverAdr, 7007); 128 | dataInputStream = new DataInputStream(socket.getInputStream()); 129 | dataOutputStream = new DataOutputStream(socket.getOutputStream()); 130 | 131 | byte[] packetSize; 132 | attempts = 0; 133 | while (LetServceRunning.get()) { 134 | try { 135 | if (event != null) { 136 | dataOutputStream.write(event, 0, event.length); 137 | event = null; 138 | } 139 | 140 | if (dataInputStream.available() > 0) { 141 | 142 | packetSize = new byte[4]; 143 | dataInputStream.readFully(packetSize, 0, 4); 144 | 145 | int size = ByteUtils.bytesToInt(packetSize); 146 | byte[] packet = new byte[size]; 147 | dataInputStream.readFully(packet, 0, size); 148 | VideoPacket videoPacket = VideoPacket.fromArray(packet); 149 | if (videoPacket.type == MediaPacket.Type.VIDEO) { 150 | byte[] data = videoPacket.data; 151 | if (videoPacket.flag == VideoPacket.Flag.CONFIG || updateAvailable.get()) { 152 | if (!updateAvailable.get()) { 153 | streamSettings = VideoPacket.getStreamSettings(data); 154 | if (!first_time) { 155 | if (serviceCallbacks != null) { 156 | serviceCallbacks.loadNewRotation(); 157 | } 158 | while (!updateAvailable.get()) { 159 | // Waiting for new surface 160 | try { 161 | Thread.sleep(100); 162 | } catch (InterruptedException e) { 163 | e.printStackTrace(); 164 | } 165 | 166 | } 167 | 168 | } 169 | } 170 | updateAvailable.set(false); 171 | first_time = false; 172 | videoDecoder.configure(surface, screenWidth, screenHeight, streamSettings.sps, streamSettings.pps); 173 | } else if (videoPacket.flag == VideoPacket.Flag.END) { 174 | // need close stream 175 | } else { 176 | videoDecoder.decodeSample(data, 0, data.length, 0, videoPacket.flag.getFlag()); 177 | 178 | } 179 | } 180 | 181 | } 182 | 183 | 184 | } catch (IOException e) { 185 | } 186 | } 187 | 188 | 189 | } catch (IOException e) { 190 | try { 191 | attempts = attempts - 1; 192 | Thread.sleep(100); 193 | } catch (InterruptedException ignore) { 194 | } 195 | // Log.e("Scrcpy", e.getMessage()); 196 | } finally { 197 | if (socket != null) { 198 | try { 199 | socket.close(); 200 | } catch (IOException e) { 201 | e.printStackTrace(); 202 | } 203 | } 204 | 205 | } 206 | 207 | 208 | } 209 | 210 | } 211 | 212 | public interface ServiceCallbacks { 213 | void loadNewRotation(); 214 | } 215 | 216 | public class MyServiceBinder extends Binder { 217 | public Scrcpy getService() { 218 | return Scrcpy.this; 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/SendCommands.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import org.las2mile.scrcpy.adblib.AdbBase64; 7 | import org.las2mile.scrcpy.adblib.AdbConnection; 8 | import org.las2mile.scrcpy.adblib.AdbCrypto; 9 | import org.las2mile.scrcpy.adblib.AdbStream; 10 | 11 | import java.io.FileInputStream; 12 | import java.io.FileOutputStream; 13 | import java.io.IOException; 14 | import java.net.ConnectException; 15 | import java.net.NoRouteToHostException; 16 | import java.net.Socket; 17 | import java.net.UnknownHostException; 18 | import java.security.NoSuchAlgorithmException; 19 | import java.security.spec.InvalidKeySpecException; 20 | 21 | import static android.org.apache.commons.codec.binary.Base64.encodeBase64String; 22 | 23 | //Uses code from https://github.com/Jolanrensen/ADBPlugin 24 | 25 | 26 | public class SendCommands { 27 | 28 | private Thread thread = null; 29 | private Context context; 30 | private int status; 31 | 32 | 33 | public SendCommands() { 34 | 35 | } 36 | 37 | public static AdbBase64 getBase64Impl() { 38 | return new AdbBase64() { 39 | @Override 40 | public String encodeToString(byte[] arg0) { 41 | return encodeBase64String(arg0); 42 | } 43 | }; 44 | } 45 | 46 | private AdbCrypto setupCrypto() 47 | throws NoSuchAlgorithmException, IOException { 48 | 49 | AdbCrypto c = null; 50 | try { 51 | FileInputStream privIn = context.openFileInput("priv.key"); 52 | FileInputStream pubIn = context.openFileInput("pub.key"); 53 | c = AdbCrypto.loadAdbKeyPair(getBase64Impl(), privIn, pubIn); 54 | } catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException | NullPointerException e) { 55 | // Failed to read from file 56 | c = null; 57 | } 58 | 59 | 60 | if (c == null) { 61 | // We couldn't load a key, so let's generate a new one 62 | c = AdbCrypto.generateAdbKeyPair(getBase64Impl()); 63 | 64 | // Save it 65 | FileOutputStream privOut = context.openFileOutput("priv.key", Context.MODE_PRIVATE); 66 | FileOutputStream pubOut = context.openFileOutput("pub.key", Context.MODE_PRIVATE); 67 | 68 | c.saveAdbKeyPair(privOut, pubOut); 69 | //Generated new keypair 70 | } else { 71 | //Loaded existing keypair 72 | } 73 | 74 | return c; 75 | } 76 | 77 | 78 | public int SendAdbCommands(Context context, final byte[] fileBase64, final String ip, String localip, int bitrate, int size) { 79 | this.context = context; 80 | status = 1; 81 | final StringBuilder command = new StringBuilder(); 82 | command.append(" CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / org.las2mile.scrcpy.Server "); 83 | command.append(" /" + localip + " " + Long.toString(size) + " " + Long.toString(bitrate) + ";"); 84 | 85 | thread = new Thread(new Runnable() { 86 | @Override 87 | public void run() { 88 | try { 89 | adbWrite(ip, fileBase64, command.toString()); 90 | } catch (IOException e) { 91 | e.printStackTrace(); 92 | } 93 | } 94 | }); 95 | thread.start(); 96 | 97 | while (status == 1) { 98 | Log.e("ADB", "Connecting..."); 99 | for (int i = 0; i < 1000000; i++) { 100 | } 101 | } 102 | return status; 103 | } 104 | 105 | 106 | private void adbWrite(String ip, byte[] fileBase64, String command) throws IOException { 107 | 108 | AdbConnection adb = null; 109 | Socket sock = null; 110 | AdbCrypto crypto; 111 | AdbStream stream = null; 112 | 113 | try { 114 | crypto = setupCrypto(); 115 | } catch (NoSuchAlgorithmException e) { 116 | e.printStackTrace(); 117 | return; 118 | } catch (IOException e) { 119 | e.printStackTrace(); 120 | throw new IOException("Couldn't read/write keys"); 121 | } 122 | 123 | try { 124 | sock = new Socket(ip, 5555); 125 | } catch (UnknownHostException e) { 126 | status = 2; 127 | throw new UnknownHostException(ip + " is no valid ip address"); 128 | } catch (ConnectException e) { 129 | status = 2; 130 | throw new ConnectException("Device at " + ip + ":" + 5555 + " has no adb enabled or connection is refused"); 131 | } catch (NoRouteToHostException e) { 132 | status = 2; 133 | throw new NoRouteToHostException("Couldn't find adb device at " + ip + ":" + 5555); 134 | } catch (IOException e) { 135 | e.printStackTrace(); 136 | status = 2; 137 | } 138 | 139 | if (sock != null) { 140 | try { 141 | adb = AdbConnection.create(sock, crypto); 142 | adb.connect(); 143 | } catch (IllegalStateException e) { 144 | e.printStackTrace(); 145 | } catch (IOException | InterruptedException e) { 146 | e.printStackTrace(); 147 | return; 148 | } 149 | } 150 | 151 | if (adb != null) { 152 | 153 | try { 154 | stream = adb.open("shell:"); 155 | } catch (IOException | InterruptedException e) { 156 | e.printStackTrace(); 157 | return; 158 | } 159 | } 160 | 161 | if (stream != null) { 162 | try { 163 | stream.write("" + '\n'); 164 | } catch (IOException | InterruptedException e) { 165 | e.printStackTrace(); 166 | return; 167 | } 168 | } 169 | 170 | 171 | String responses = ""; 172 | boolean done = false; 173 | while (!done && stream != null) { 174 | try { 175 | byte[] responseBytes = stream.read(); 176 | String response = new String(responseBytes, "US-ASCII"); 177 | if (response.substring(response.length() - 2).equals("$ ") || 178 | response.substring(response.length() - 2).equals("# ")) { 179 | done = true; 180 | responses += response; 181 | break; 182 | } else { 183 | responses += response; 184 | } 185 | } catch (InterruptedException | IOException e) { 186 | e.printStackTrace(); 187 | } 188 | } 189 | 190 | 191 | if (stream != null) { 192 | int len = fileBase64.length; 193 | byte[] filePart = new byte[4056]; 194 | int sourceOffset = 0; 195 | try { 196 | stream.write(" cd data/local/tmp " + '\n'); 197 | while (sourceOffset < len) { 198 | if (len - sourceOffset >= 4056) { 199 | System.arraycopy(fileBase64, sourceOffset, filePart, 0, 4056); //Writing in 4KB pieces. 4096-40 ---> 40 Bytes for actual command text. 200 | sourceOffset = sourceOffset + 4056; 201 | String ServerBase64part = new String(filePart, "US-ASCII"); 202 | stream.write(" echo " + ServerBase64part + " >> serverBase64" + '\n'); 203 | done = false; 204 | while (!done) { 205 | byte[] responseBytes = stream.read(); 206 | String response = new String(responseBytes, "US-ASCII"); 207 | if (response.endsWith("$ ") || response.endsWith("# ")) { 208 | done = true; 209 | } 210 | } 211 | } else { 212 | int rem = len - sourceOffset; 213 | byte[] remPart = new byte[rem]; 214 | System.arraycopy(fileBase64, sourceOffset, remPart, 0, rem); 215 | sourceOffset = sourceOffset + rem; 216 | String ServerBase64part = new String(remPart, "US-ASCII"); 217 | stream.write(" echo " + ServerBase64part + " >> serverBase64" + '\n'); 218 | done = false; 219 | while (!done) { 220 | byte[] responseBytes = stream.read(); 221 | String response = new String(responseBytes, "US-ASCII"); 222 | if (response.endsWith("$ ") || response.endsWith("# ")) { 223 | done = true; 224 | } 225 | } 226 | } 227 | } 228 | stream.write(" base64 -d < serverBase64 > scrcpy-server.jar && rm serverBase64" + '\n'); 229 | stream.write(command + '\n'); 230 | } catch (IOException | InterruptedException e) { 231 | e.printStackTrace(); 232 | return; 233 | } 234 | } 235 | 236 | status = 0; 237 | 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/adblib/AdbBase64.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy.adblib; 2 | 3 | /** 4 | * This interface specifies the required functions for AdbCrypto to 5 | * perform Base64 encoding of its public key. 6 | * 7 | * @author Cameron Gutman 8 | */ 9 | public interface AdbBase64 { 10 | /** 11 | * This function must encoded the specified data as a base 64 string, without 12 | * appending any extra newlines or other characters. 13 | * 14 | * @param data Data to encode 15 | * @return String containing base 64 encoded data 16 | */ 17 | public String encodeToString(byte[] data); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/adblib/AdbConnection.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy.adblib; 2 | 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.ConnectException; 10 | import java.net.Socket; 11 | import java.util.HashMap; 12 | 13 | /** 14 | * This class represents an ADB connection. 15 | * 16 | * @author Cameron Gutman 17 | */ 18 | public class AdbConnection implements Closeable { 19 | 20 | /** 21 | * The output stream that this class uses to read from 22 | * the socket. 23 | */ 24 | OutputStream outputStream; 25 | /** 26 | * The underlying socket that this class uses to 27 | * communicate with the target device. 28 | */ 29 | private Socket socket; 30 | /** 31 | * The last allocated local stream ID. The ID 32 | * chosen for the next stream will be this value + 1. 33 | */ 34 | private int lastLocalId; 35 | /** 36 | * The input stream that this class uses to read from 37 | * the socket. 38 | */ 39 | private InputStream inputStream; 40 | /** 41 | * The backend thread that handles responding to ADB packets. 42 | */ 43 | private Thread connectionThread; 44 | 45 | /** 46 | * Specifies whether a connect has been attempted 47 | */ 48 | private boolean connectAttempted; 49 | 50 | /** 51 | * Specifies whether a CNXN packet has been received from the peer. 52 | */ 53 | private boolean connected; 54 | 55 | /** 56 | * Specifies the maximum amount data that can be sent to the remote peer. 57 | * This is only valid after connect() returns successfully. 58 | */ 59 | private int maxData; 60 | 61 | /** 62 | * An initialized ADB crypto object that contains a key pair. 63 | */ 64 | private AdbCrypto crypto; 65 | 66 | /** 67 | * Specifies whether this connection has already sent a signed token. 68 | */ 69 | private boolean sentSignature; 70 | 71 | /** 72 | * A hash map of our open streams indexed by local ID. 73 | **/ 74 | private HashMap openStreams; 75 | 76 | /** 77 | * Internal constructor to initialize some internal state 78 | */ 79 | private AdbConnection() { 80 | openStreams = new HashMap(); 81 | lastLocalId = 0; 82 | connectionThread = createConnectionThread(); 83 | } 84 | 85 | /** 86 | * Creates a AdbConnection object associated with the socket and 87 | * crypto object specified. 88 | * 89 | * @param socket The socket that the connection will use for communcation. 90 | * @param crypto The crypto object that stores the key pair for authentication. 91 | * @return A new AdbConnection object. 92 | * @throws IOException If there is a socket error 93 | */ 94 | public static AdbConnection create(Socket socket, AdbCrypto crypto) throws IOException { 95 | AdbConnection newConn = new AdbConnection(); 96 | 97 | newConn.crypto = crypto; 98 | 99 | newConn.socket = socket; 100 | newConn.inputStream = socket.getInputStream(); 101 | newConn.outputStream = socket.getOutputStream(); 102 | 103 | /* Disable Nagle because we're sending tiny packets */ 104 | socket.setTcpNoDelay(true); 105 | 106 | return newConn; 107 | } 108 | 109 | /** 110 | * Creates a new connection thread. 111 | * 112 | * @return A new connection thread. 113 | */ 114 | private Thread createConnectionThread() { 115 | @SuppressWarnings("resource") final AdbConnection conn = this; 116 | return new Thread(new Runnable() { 117 | @Override 118 | public void run() { 119 | while (!connectionThread.isInterrupted()) { 120 | try { 121 | /* Read and parse a message off the socket's input stream */ 122 | AdbProtocol.AdbMessage msg = AdbProtocol.AdbMessage.parseAdbMessage(inputStream); 123 | 124 | /* Verify magic and checksum */ 125 | if (!AdbProtocol.validateMessage(msg)) 126 | continue; 127 | 128 | switch (msg.command) { 129 | /* Stream-oriented commands */ 130 | case AdbProtocol.CMD_OKAY: 131 | case AdbProtocol.CMD_WRTE: 132 | case AdbProtocol.CMD_CLSE: 133 | /* We must ignore all packets when not connected */ 134 | if (!conn.connected) 135 | continue; 136 | 137 | /* Get the stream object corresponding to the packet */ 138 | AdbStream waitingStream = openStreams.get(msg.arg1); 139 | if (waitingStream == null) 140 | continue; 141 | 142 | synchronized (waitingStream) { 143 | if (msg.command == AdbProtocol.CMD_OKAY) { 144 | /* We're ready for writes */ 145 | waitingStream.updateRemoteId(msg.arg0); 146 | waitingStream.readyForWrite(); 147 | 148 | /* Unwait an open/write */ 149 | waitingStream.notify(); 150 | } else if (msg.command == AdbProtocol.CMD_WRTE) { 151 | /* Got some data from our partner */ 152 | waitingStream.addPayload(msg.payload); 153 | 154 | /* Tell it we're ready for more */ 155 | waitingStream.sendReady(); 156 | } else if (msg.command == AdbProtocol.CMD_CLSE) { 157 | /* He doesn't like us anymore :-( */ 158 | conn.openStreams.remove(msg.arg1); 159 | 160 | /* Notify readers and writers */ 161 | waitingStream.notifyClose(); 162 | } 163 | } 164 | 165 | break; 166 | 167 | case AdbProtocol.CMD_AUTH: 168 | 169 | byte[] packet; 170 | 171 | if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN) { 172 | /* This is an authentication challenge */ 173 | if (conn.sentSignature) { 174 | /* We've already tried our signature, so send our public key */ 175 | packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC, 176 | conn.crypto.getAdbPublicKeyPayload()); 177 | } else { 178 | /* We'll sign the token */ 179 | packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE, 180 | conn.crypto.signAdbTokenPayload(msg.payload)); 181 | conn.sentSignature = true; 182 | } 183 | 184 | /* Write the AUTH reply */ 185 | conn.outputStream.write(packet); 186 | conn.outputStream.flush(); 187 | } 188 | break; 189 | 190 | case AdbProtocol.CMD_CNXN: 191 | synchronized (conn) { 192 | /* We need to store the max data size */ 193 | conn.maxData = msg.arg1; 194 | 195 | /* Mark us as connected and unwait anyone waiting on the connection */ 196 | conn.connected = true; 197 | conn.notifyAll(); 198 | } 199 | break; 200 | 201 | default: 202 | /* Unrecognized packet, just drop it */ 203 | break; 204 | } 205 | } catch (Exception e) { 206 | /* The cleanup is taken care of by a combination of this thread 207 | * and close() */ 208 | break; 209 | } 210 | } 211 | 212 | /* This thread takes care of cleaning up pending streams */ 213 | synchronized (conn) { 214 | cleanupStreams(); 215 | conn.notifyAll(); 216 | conn.connectAttempted = false; 217 | } 218 | } 219 | }); 220 | } 221 | 222 | /** 223 | * Gets the max data size that the remote client supports. 224 | * A connection must have been attempted before calling this routine. 225 | * This routine will block if a connection is in progress. 226 | * 227 | * @return The maximum data size indicated in the connect packet. 228 | * @throws InterruptedException If a connection cannot be waited on. 229 | * @throws IOException if the connection fails 230 | */ 231 | public int getMaxData() throws InterruptedException, IOException { 232 | if (!connectAttempted) 233 | throw new IllegalStateException("connect() must be called first"); 234 | 235 | synchronized (this) { 236 | /* Block if a connection is pending, but not yet complete */ 237 | if (!connected) 238 | wait(); 239 | 240 | if (!connected) { 241 | throw new IOException("Connection failed"); 242 | } 243 | } 244 | 245 | return maxData; 246 | } 247 | 248 | /** 249 | * Connects to the remote device. This routine will block until the connection 250 | * completes. 251 | * 252 | * @throws IOException If the socket fails while connecting 253 | * @throws InterruptedException If we are unable to wait for the connection to finish 254 | */ 255 | public void connect() throws IOException, InterruptedException { 256 | if (connected) 257 | throw new IllegalStateException("Already connected"); 258 | 259 | /* Write the CONNECT packet */ 260 | outputStream.write(AdbProtocol.generateConnect()); 261 | outputStream.flush(); 262 | 263 | /* Start the connection thread to respond to the peer */ 264 | connectAttempted = true; 265 | connectionThread.start(); 266 | 267 | /* Wait for the connection to go live */ 268 | synchronized (this) { 269 | if (!connected) 270 | wait(); 271 | 272 | if (!connected) { 273 | throw new IOException("Connection failed"); 274 | } 275 | } 276 | } 277 | 278 | /** 279 | * Opens an AdbStream object corresponding to the specified destination. 280 | * This routine will block until the connection completes. 281 | * 282 | * @param destination The destination to open on the target 283 | * @return AdbStream object corresponding to the specified destination 284 | * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8 285 | * @throws IOException If the stream fails while sending the packet 286 | * @throws InterruptedException If we are unable to wait for the connection to finish 287 | */ 288 | public AdbStream open(String destination) throws UnsupportedEncodingException, IOException, InterruptedException { 289 | int localId = ++lastLocalId; 290 | 291 | if (!connectAttempted) 292 | throw new IllegalStateException("connect() must be called first"); 293 | 294 | /* Wait for the connect response */ 295 | synchronized (this) { 296 | if (!connected) 297 | wait(); 298 | 299 | if (!connected) { 300 | throw new IOException("Connection failed"); 301 | } 302 | } 303 | 304 | /* Add this stream to this list of half-open streams */ 305 | AdbStream stream = new AdbStream(this, localId); 306 | openStreams.put(localId, stream); 307 | 308 | /* Send the open */ 309 | outputStream.write(AdbProtocol.generateOpen(localId, destination)); 310 | outputStream.flush(); 311 | 312 | /* Wait for the connection thread to receive the OKAY */ 313 | synchronized (stream) { 314 | stream.wait(); 315 | } 316 | 317 | /* Check if the open was rejected */ 318 | if (stream.isClosed()) 319 | throw new ConnectException("Stream open actively rejected by remote peer"); 320 | 321 | /* We're fully setup now */ 322 | return stream; 323 | } 324 | 325 | /** 326 | * This function terminates all I/O on streams associated with this ADB connection 327 | */ 328 | private void cleanupStreams() { 329 | /* Close all streams on this connection */ 330 | for (AdbStream s : openStreams.values()) { 331 | /* We handle exceptions for each close() call to avoid 332 | * terminating cleanup for one failed close(). */ 333 | try { 334 | s.close(); 335 | } catch (IOException e) { 336 | } 337 | } 338 | 339 | /* No open streams anymore */ 340 | openStreams.clear(); 341 | } 342 | 343 | /** 344 | * This routine closes the Adb connection and underlying socket 345 | * 346 | * @throws IOException if the socket fails to close 347 | */ 348 | @Override 349 | public void close() throws IOException { 350 | /* If the connection thread hasn't spawned yet, there's nothing to do */ 351 | if (connectionThread == null) 352 | return; 353 | 354 | /* Closing the socket will kick the connection thread */ 355 | socket.close(); 356 | 357 | /* Wait for the connection thread to die */ 358 | connectionThread.interrupt(); 359 | try { 360 | connectionThread.join(); 361 | } catch (InterruptedException e) { 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/adblib/AdbCrypto.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy.adblib; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.math.BigInteger; 7 | import java.nio.ByteBuffer; 8 | import java.nio.ByteOrder; 9 | import java.security.GeneralSecurityException; 10 | import java.security.KeyFactory; 11 | import java.security.KeyPair; 12 | import java.security.KeyPairGenerator; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.security.interfaces.RSAPublicKey; 15 | import java.security.spec.EncodedKeySpec; 16 | import java.security.spec.InvalidKeySpecException; 17 | import java.security.spec.PKCS8EncodedKeySpec; 18 | import java.security.spec.X509EncodedKeySpec; 19 | 20 | import javax.crypto.Cipher; 21 | 22 | /** 23 | * This class encapsulates the ADB cryptography functions and provides 24 | * an interface for the storage and retrieval of keys. 25 | * 26 | * @author Cameron Gutman 27 | */ 28 | public class AdbCrypto { 29 | 30 | /** 31 | * The ADB RSA key length in bits 32 | */ 33 | public static final int KEY_LENGTH_BITS = 2048; 34 | /** 35 | * The ADB RSA key length in bytes 36 | */ 37 | public static final int KEY_LENGTH_BYTES = KEY_LENGTH_BITS / 8; 38 | /** 39 | * The ADB RSA key length in words 40 | */ 41 | public static final int KEY_LENGTH_WORDS = KEY_LENGTH_BYTES / 4; 42 | /** 43 | * The RSA signature padding as an int array 44 | */ 45 | public static final int[] SIGNATURE_PADDING_AS_INT = new int[] 46 | { 47 | 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 48 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 49 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 50 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 51 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 52 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 53 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 54 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 55 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 56 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 57 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 58 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 59 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 60 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 61 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 62 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 63 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 64 | 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 65 | 0x04, 0x14 66 | }; 67 | /** 68 | * The RSA signature padding as a byte array 69 | */ 70 | public static byte[] SIGNATURE_PADDING; 71 | 72 | static { 73 | SIGNATURE_PADDING = new byte[SIGNATURE_PADDING_AS_INT.length]; 74 | 75 | for (int i = 0; i < SIGNATURE_PADDING.length; i++) 76 | SIGNATURE_PADDING[i] = (byte) SIGNATURE_PADDING_AS_INT[i]; 77 | } 78 | 79 | /** 80 | * An RSA keypair encapsulated by the AdbCrypto object 81 | */ 82 | private KeyPair keyPair; 83 | /** 84 | * The base 64 conversion interface to use 85 | */ 86 | private AdbBase64 base64; 87 | 88 | /** 89 | * Converts a standard RSAPublicKey object to the special ADB format 90 | * 91 | * @param pubkey RSAPublicKey object to convert 92 | * @return Byte array containing the converted RSAPublicKey object 93 | */ 94 | private static byte[] convertRsaPublicKeyToAdbFormat(RSAPublicKey pubkey) { 95 | /* 96 | * ADB literally just saves the RSAPublicKey struct to a file. 97 | * 98 | * typedef struct RSAPublicKey { 99 | * int len; // Length of n[] in number of uint32_t 100 | * uint32_t n0inv; // -1 / n[0] mod 2^32 101 | * uint32_t n[RSANUMWORDS]; // modulus as little endian array 102 | * uint32_t rr[RSANUMWORDS]; // R^2 as little endian array 103 | * int exponent; // 3 or 65537 104 | * } RSAPublicKey; 105 | */ 106 | 107 | /* ------ This part is a Java-ified version of RSA_to_RSAPublicKey from adb_host_auth.c ------ */ 108 | BigInteger r32, r, rr, rem, n, n0inv; 109 | 110 | r32 = BigInteger.ZERO.setBit(32); 111 | n = pubkey.getModulus(); 112 | r = BigInteger.ZERO.setBit(KEY_LENGTH_WORDS * 32); 113 | rr = r.modPow(BigInteger.valueOf(2), n); 114 | rem = n.remainder(r32); 115 | n0inv = rem.modInverse(r32); 116 | 117 | int myN[] = new int[KEY_LENGTH_WORDS]; 118 | int myRr[] = new int[KEY_LENGTH_WORDS]; 119 | BigInteger res[]; 120 | for (int i = 0; i < KEY_LENGTH_WORDS; i++) { 121 | res = rr.divideAndRemainder(r32); 122 | rr = res[0]; 123 | rem = res[1]; 124 | myRr[i] = rem.intValue(); 125 | 126 | res = n.divideAndRemainder(r32); 127 | n = res[0]; 128 | rem = res[1]; 129 | myN[i] = rem.intValue(); 130 | } 131 | 132 | /* ------------------------------------------------------------------------------------------- */ 133 | 134 | ByteBuffer bbuf = ByteBuffer.allocate(524).order(ByteOrder.LITTLE_ENDIAN); 135 | 136 | 137 | bbuf.putInt(KEY_LENGTH_WORDS); 138 | bbuf.putInt(n0inv.negate().intValue()); 139 | for (int i : myN) 140 | bbuf.putInt(i); 141 | for (int i : myRr) 142 | bbuf.putInt(i); 143 | 144 | bbuf.putInt(pubkey.getPublicExponent().intValue()); 145 | return bbuf.array(); 146 | } 147 | 148 | 149 | public static AdbCrypto loadAdbKeyPair(AdbBase64 base64, FileInputStream privIn, FileInputStream pubIn) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { 150 | AdbCrypto crypto = new AdbCrypto(); 151 | 152 | byte[] privKeyBytes = new byte[privIn.available()]; 153 | byte[] pubKeyBytes = new byte[pubIn.available()]; 154 | 155 | privIn.read(privKeyBytes); 156 | pubIn.read(pubKeyBytes); 157 | 158 | privIn.close(); 159 | pubIn.close(); 160 | 161 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 162 | EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes); 163 | EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(pubKeyBytes); 164 | 165 | crypto.keyPair = new KeyPair(keyFactory.generatePublic(publicKeySpec), 166 | keyFactory.generatePrivate(privateKeySpec)); 167 | crypto.base64 = base64; 168 | 169 | return crypto; 170 | } 171 | 172 | 173 | /** 174 | * Creates a new AdbCrypto object by generating a new key pair. 175 | * 176 | * @param base64 Implementation of base 64 conversion interface required by ADB 177 | * @return A new AdbCrypto object 178 | * @throws NoSuchAlgorithmException If an RSA key factory cannot be found 179 | */ 180 | public static AdbCrypto generateAdbKeyPair(AdbBase64 base64) throws NoSuchAlgorithmException { 181 | AdbCrypto crypto = new AdbCrypto(); 182 | 183 | KeyPairGenerator rsaKeyPg = KeyPairGenerator.getInstance("RSA"); 184 | rsaKeyPg.initialize(KEY_LENGTH_BITS); 185 | 186 | crypto.keyPair = rsaKeyPg.genKeyPair(); 187 | crypto.base64 = base64; 188 | 189 | return crypto; 190 | } 191 | 192 | /** 193 | * Signs the ADB SHA1 payload with the private key of this object. 194 | * 195 | * @param payload SHA1 payload to sign 196 | * @return Signed SHA1 payload 197 | * @throws GeneralSecurityException If signing fails 198 | */ 199 | public byte[] signAdbTokenPayload(byte[] payload) throws GeneralSecurityException { 200 | Cipher c = Cipher.getInstance("RSA/ECB/NoPadding"); 201 | 202 | c.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate()); 203 | 204 | c.update(SIGNATURE_PADDING); 205 | 206 | return c.doFinal(payload); 207 | } 208 | 209 | /** 210 | * Gets the RSA public key in ADB format. 211 | * 212 | * @return Byte array containing the RSA public key in ADB format. 213 | * @throws IOException If the key cannot be retrived 214 | */ 215 | public byte[] getAdbPublicKeyPayload() throws IOException { 216 | byte[] convertedKey = convertRsaPublicKeyToAdbFormat((RSAPublicKey) keyPair.getPublic()); 217 | StringBuilder keyString = new StringBuilder(720); 218 | 219 | /* The key is base64 encoded with a user@host suffix and terminated with a NUL */ 220 | keyString.append(base64.encodeToString(convertedKey)); 221 | keyString.append(" unknown@unknown"); 222 | keyString.append('\0'); 223 | 224 | return keyString.toString().getBytes("UTF-8"); 225 | } 226 | 227 | 228 | public void saveAdbKeyPair(FileOutputStream privOut, FileOutputStream pubOut) throws IOException { 229 | 230 | privOut.write(keyPair.getPrivate().getEncoded()); 231 | pubOut.write(keyPair.getPublic().getEncoded()); 232 | 233 | privOut.close(); 234 | pubOut.close(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/adblib/AdbProtocol.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy.adblib; 2 | 3 | 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.UnsupportedEncodingException; 7 | import java.nio.ByteBuffer; 8 | import java.nio.ByteOrder; 9 | 10 | 11 | /** 12 | * This class provides useful functions and fields for ADB protocol details. 13 | * 14 | * @author Cameron Gutman 15 | */ 16 | public class AdbProtocol { 17 | 18 | /** 19 | * The length of the ADB message header 20 | */ 21 | public static final int ADB_HEADER_LENGTH = 24; 22 | 23 | public static final int CMD_SYNC = 0x434e5953; 24 | 25 | /** 26 | * CNXN is the connect message. No messages (except AUTH) 27 | * are valid before this message is received. 28 | */ 29 | public static final int CMD_CNXN = 0x4e584e43; 30 | 31 | /** 32 | * The current version of the ADB protocol 33 | */ 34 | public static final int CONNECT_VERSION = 0x01000000; 35 | 36 | /** 37 | * The maximum data payload supported by the ADB implementation 38 | */ 39 | public static final int CONNECT_MAXDATA = 4096; 40 | /** 41 | * AUTH is the authentication message. It is part of the 42 | * RSA public key authentication added in Android 4.2.2. 43 | */ 44 | public static final int CMD_AUTH = 0x48545541; 45 | /** 46 | * This authentication type represents a SHA1 hash to sign 47 | */ 48 | public static final int AUTH_TYPE_TOKEN = 1; 49 | /** 50 | * This authentication type represents the signed SHA1 hash 51 | */ 52 | public static final int AUTH_TYPE_SIGNATURE = 2; 53 | /** 54 | * This authentication type represents a RSA public key 55 | */ 56 | public static final int AUTH_TYPE_RSA_PUBLIC = 3; 57 | /** 58 | * OPEN is the open stream message. It is sent to open 59 | * a new stream on the target device. 60 | */ 61 | public static final int CMD_OPEN = 0x4e45504f; 62 | /** 63 | * OKAY is a success message. It is sent when a write is 64 | * processed successfully. 65 | */ 66 | public static final int CMD_OKAY = 0x59414b4f; 67 | /** 68 | * CLSE is the close stream message. It it sent to close an 69 | * existing stream on the target device. 70 | */ 71 | public static final int CMD_CLSE = 0x45534c43; 72 | /** 73 | * WRTE is the write stream message. It is sent with a payload 74 | * that is the data to write to the stream. 75 | */ 76 | public static final int CMD_WRTE = 0x45545257; 77 | /** 78 | * The payload sent with the connect message 79 | */ 80 | public static byte[] CONNECT_PAYLOAD; 81 | 82 | static { 83 | try { 84 | CONNECT_PAYLOAD = "host::\0".getBytes("UTF-8"); 85 | } catch (UnsupportedEncodingException e) { 86 | } 87 | } 88 | 89 | /** 90 | * This function performs a checksum on the ADB payload data. 91 | * 92 | * @param payload Payload to checksum 93 | * @return The checksum of the payload 94 | */ 95 | private static int getPayloadChecksum(byte[] payload) { 96 | int checksum = 0; 97 | 98 | for (byte b : payload) { 99 | /* We have to manually "unsign" these bytes because Java sucks */ 100 | if (b >= 0) 101 | checksum += b; 102 | else 103 | checksum += b + 256; 104 | } 105 | 106 | return checksum; 107 | } 108 | 109 | /** 110 | * This function validate the ADB message by checking 111 | * its command, magic, and payload checksum. 112 | * 113 | * @param msg ADB message to validate 114 | * @return True if the message was valid, false otherwise 115 | */ 116 | public static boolean validateMessage(AdbMessage msg) { 117 | /* Magic is cmd ^ 0xFFFFFFFF */ 118 | if (msg.command != (msg.magic ^ 0xFFFFFFFF)) 119 | return false; 120 | 121 | if (msg.payloadLength != 0) { 122 | if (getPayloadChecksum(msg.payload) != msg.checksum) 123 | return false; 124 | } 125 | 126 | return true; 127 | } 128 | 129 | /** 130 | * This function generates an ADB message given the fields. 131 | * 132 | * @param cmd Command identifier 133 | * @param arg0 First argument 134 | * @param arg1 Second argument 135 | * @param payload Data payload 136 | * @return Byte array containing the message 137 | */ 138 | public static byte[] generateMessage(int cmd, int arg0, int arg1, byte[] payload) { 139 | /* struct message { 140 | * unsigned command; // command identifier constant 141 | * unsigned arg0; // first argument 142 | * unsigned arg1; // second argument 143 | * unsigned data_length; // length of payload (0 is allowed) 144 | * unsigned data_check; // checksum of data payload 145 | * unsigned magic; // command ^ 0xffffffff 146 | * }; 147 | */ 148 | 149 | ByteBuffer message; 150 | 151 | if (payload != null) { 152 | message = ByteBuffer.allocate(ADB_HEADER_LENGTH + payload.length).order(ByteOrder.LITTLE_ENDIAN); 153 | } else { 154 | message = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN); 155 | } 156 | 157 | message.putInt(cmd); 158 | message.putInt(arg0); 159 | message.putInt(arg1); 160 | 161 | if (payload != null) { 162 | message.putInt(payload.length); 163 | message.putInt(getPayloadChecksum(payload)); 164 | } else { 165 | message.putInt(0); 166 | message.putInt(0); 167 | } 168 | 169 | message.putInt(cmd ^ 0xFFFFFFFF); 170 | 171 | if (payload != null) { 172 | message.put(payload); 173 | } 174 | 175 | return message.array(); 176 | } 177 | 178 | /** 179 | * Generates a connect message with default parameters. 180 | * 181 | * @return Byte array containing the message 182 | */ 183 | public static byte[] generateConnect() { 184 | return generateMessage(CMD_CNXN, CONNECT_VERSION, CONNECT_MAXDATA, CONNECT_PAYLOAD); 185 | } 186 | 187 | /** 188 | * Generates an auth message with the specified type and payload. 189 | * 190 | * @param type Authentication type (see AUTH_TYPE_* constants) 191 | * @param data The payload for the message 192 | * @return Byte array containing the message 193 | */ 194 | public static byte[] generateAuth(int type, byte[] data) { 195 | return generateMessage(CMD_AUTH, type, 0, data); 196 | } 197 | 198 | /** 199 | * Generates an open stream message with the specified local ID and destination. 200 | * 201 | * @param localId A unique local ID identifying the stream 202 | * @param dest The destination of the stream on the target 203 | * @return Byte array containing the message 204 | * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8 205 | */ 206 | public static byte[] generateOpen(int localId, String dest) throws UnsupportedEncodingException { 207 | ByteBuffer bbuf = ByteBuffer.allocate(dest.length() + 1); 208 | bbuf.put(dest.getBytes("UTF-8")); 209 | bbuf.put((byte) 0); 210 | return generateMessage(CMD_OPEN, localId, 0, bbuf.array()); 211 | } 212 | 213 | /** 214 | * Generates a write stream message with the specified IDs and payload. 215 | * 216 | * @param localId The unique local ID of the stream 217 | * @param remoteId The unique remote ID of the stream 218 | * @param data The data to provide as the write payload 219 | * @return Byte array containing the message 220 | */ 221 | public static byte[] generateWrite(int localId, int remoteId, byte[] data) { 222 | return generateMessage(CMD_WRTE, localId, remoteId, data); 223 | } 224 | 225 | /** 226 | * Generates a close stream message with the specified IDs. 227 | * 228 | * @param localId The unique local ID of the stream 229 | * @param remoteId The unique remote ID of the stream 230 | * @return Byte array containing the message 231 | */ 232 | public static byte[] generateClose(int localId, int remoteId) { 233 | return generateMessage(CMD_CLSE, localId, remoteId, null); 234 | } 235 | 236 | /** 237 | * Generates an okay message with the specified IDs. 238 | * 239 | * @param localId The unique local ID of the stream 240 | * @param remoteId The unique remote ID of the stream 241 | * @return Byte array containing the message 242 | */ 243 | public static byte[] generateReady(int localId, int remoteId) { 244 | return generateMessage(CMD_OKAY, localId, remoteId, null); 245 | } 246 | 247 | /** 248 | * This class provides an abstraction for the ADB message format. 249 | * 250 | * @author Cameron Gutman 251 | */ 252 | final static class AdbMessage { 253 | /** 254 | * The command field of the message 255 | */ 256 | public int command; 257 | /** 258 | * The arg0 field of the message 259 | */ 260 | public int arg0; 261 | /** 262 | * The arg1 field of the message 263 | */ 264 | public int arg1; 265 | /** 266 | * The payload length field of the message 267 | */ 268 | public int payloadLength; 269 | /** 270 | * The checksum field of the message 271 | */ 272 | public int checksum; 273 | /** 274 | * The magic field of the message 275 | */ 276 | public int magic; 277 | /** 278 | * The payload of the message 279 | */ 280 | public byte[] payload; 281 | 282 | /** 283 | * Read and parse an ADB message from the supplied input stream. 284 | * This message is NOT validated. 285 | * 286 | * @param in InputStream object to read data from 287 | * @return An AdbMessage object represented the message read 288 | * @throws IOException If the stream fails while reading 289 | */ 290 | public static AdbMessage parseAdbMessage(InputStream in) throws IOException { 291 | AdbMessage msg = new AdbMessage(); 292 | ByteBuffer packet = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN); 293 | 294 | /* Read the header first */ 295 | int dataRead = 0; 296 | do { 297 | int bytesRead = in.read(packet.array(), dataRead, 24 - dataRead); 298 | 299 | if (bytesRead < 0) 300 | throw new IOException("Stream closed"); 301 | else 302 | dataRead += bytesRead; 303 | } 304 | while (dataRead < ADB_HEADER_LENGTH); 305 | 306 | /* Pull out header fields */ 307 | msg.command = packet.getInt(); 308 | msg.arg0 = packet.getInt(); 309 | msg.arg1 = packet.getInt(); 310 | msg.payloadLength = packet.getInt(); 311 | msg.checksum = packet.getInt(); 312 | msg.magic = packet.getInt(); 313 | 314 | /* If there's a payload supplied, read that too */ 315 | if (msg.payloadLength != 0) { 316 | msg.payload = new byte[msg.payloadLength]; 317 | 318 | dataRead = 0; 319 | do { 320 | int bytesRead = in.read(msg.payload, dataRead, msg.payloadLength - dataRead); 321 | 322 | if (bytesRead < 0) 323 | throw new IOException("Stream closed"); 324 | else 325 | dataRead += bytesRead; 326 | } 327 | while (dataRead < msg.payloadLength); 328 | } 329 | 330 | return msg; 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/adblib/AdbStream.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.scrcpy.adblib; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | import java.util.Queue; 6 | import java.util.concurrent.ConcurrentLinkedQueue; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | 9 | /** 10 | * This class abstracts the underlying ADB streams 11 | * 12 | * @author Cameron Gutman 13 | */ 14 | public class AdbStream implements Closeable { 15 | 16 | /** 17 | * The AdbConnection object that the stream communicates over 18 | */ 19 | private AdbConnection adbConn; 20 | 21 | /** 22 | * The local ID of the stream 23 | */ 24 | private int localId; 25 | 26 | /** 27 | * The remote ID of the stream 28 | */ 29 | private int remoteId; 30 | 31 | /** 32 | * Indicates whether a write is currently allowed 33 | */ 34 | private AtomicBoolean writeReady; 35 | 36 | /** 37 | * A queue of data from the target's write packets 38 | */ 39 | private Queue readQueue; 40 | 41 | /** 42 | * Indicates whether the connection is closed already 43 | */ 44 | private boolean isClosed; 45 | 46 | /** 47 | * Creates a new AdbStream object on the specified AdbConnection 48 | * with the given local ID. 49 | * 50 | * @param adbConn AdbConnection that this stream is running on 51 | * @param localId Local ID of the stream 52 | */ 53 | public AdbStream(AdbConnection adbConn, int localId) { 54 | this.adbConn = adbConn; 55 | this.localId = localId; 56 | this.readQueue = new ConcurrentLinkedQueue(); 57 | this.writeReady = new AtomicBoolean(false); 58 | this.isClosed = false; 59 | } 60 | 61 | /** 62 | * Called by the connection thread to indicate newly received data. 63 | * 64 | * @param payload Data inside the write message 65 | */ 66 | void addPayload(byte[] payload) { 67 | synchronized (readQueue) { 68 | readQueue.add(payload); 69 | readQueue.notifyAll(); 70 | } 71 | } 72 | 73 | /** 74 | * Called by the connection thread to send an OKAY packet, allowing the 75 | * other side to continue transmission. 76 | * 77 | * @throws IOException If the connection fails while sending the packet 78 | */ 79 | void sendReady() throws IOException { 80 | /* Generate and send a READY packet */ 81 | byte[] packet = AdbProtocol.generateReady(localId, remoteId); 82 | adbConn.outputStream.write(packet); 83 | adbConn.outputStream.flush(); 84 | } 85 | 86 | /** 87 | * Called by the connection thread to update the remote ID for this stream 88 | * 89 | * @param remoteId New remote ID 90 | */ 91 | void updateRemoteId(int remoteId) { 92 | this.remoteId = remoteId; 93 | } 94 | 95 | /** 96 | * Called by the connection thread to indicate the stream is okay to send data. 97 | */ 98 | void readyForWrite() { 99 | writeReady.set(true); 100 | } 101 | 102 | /** 103 | * Called by the connection thread to notify that the stream was closed by the peer. 104 | */ 105 | void notifyClose() { 106 | /* We don't call close() because it sends another CLOSE */ 107 | isClosed = true; 108 | 109 | /* Unwait readers and writers */ 110 | synchronized (this) { 111 | notifyAll(); 112 | } 113 | synchronized (readQueue) { 114 | readQueue.notifyAll(); 115 | } 116 | } 117 | 118 | /** 119 | * Reads a pending write payload from the other side. 120 | * 121 | * @return Byte array containing the payload of the write 122 | * @throws InterruptedException If we are unable to wait for data 123 | * @throws IOException If the stream fails while waiting 124 | */ 125 | public byte[] read() throws InterruptedException, IOException { 126 | byte[] data = null; 127 | 128 | synchronized (readQueue) { 129 | /* Wait for the connection to close or data to be received */ 130 | while (!isClosed && (data = readQueue.poll()) == null) { 131 | readQueue.wait(); 132 | } 133 | 134 | if (isClosed) { 135 | throw new IOException("Stream closed"); 136 | } 137 | } 138 | 139 | return data; 140 | } 141 | 142 | /** 143 | * Sends a write packet with a given String payload. 144 | * 145 | * @param payload Payload in the form of a String 146 | * @throws IOException If the stream fails while sending data 147 | * @throws InterruptedException If we are unable to wait to send data 148 | */ 149 | public void write(String payload) throws IOException, InterruptedException { 150 | /* ADB needs null-terminated strings */ 151 | write(payload.getBytes("UTF-8"), false); 152 | write(new byte[]{0}, true); 153 | } 154 | 155 | /** 156 | * Sends a write packet with a given byte array payload. 157 | * 158 | * @param payload Payload in the form of a byte array 159 | * @throws IOException If the stream fails while sending data 160 | * @throws InterruptedException If we are unable to wait to send data 161 | */ 162 | public void write(byte[] payload) throws IOException, InterruptedException { 163 | write(payload, true); 164 | } 165 | 166 | /** 167 | * Queues a write packet and optionally sends it immediately. 168 | * 169 | * @param payload Payload in the form of a byte array 170 | * @param flush Specifies whether to send the packet immediately 171 | * @throws IOException If the stream fails while sending data 172 | * @throws InterruptedException If we are unable to wait to send data 173 | */ 174 | public void write(byte[] payload, boolean flush) throws IOException, InterruptedException { 175 | synchronized (this) { 176 | /* Make sure we're ready for a write */ 177 | while (!isClosed && !writeReady.compareAndSet(true, false)) 178 | wait(); 179 | 180 | if (isClosed) { 181 | throw new IOException("Stream closed"); 182 | } 183 | } 184 | 185 | /* Generate a WRITE packet and send it */ 186 | byte[] packet = AdbProtocol.generateWrite(localId, remoteId, payload); 187 | adbConn.outputStream.write(packet); 188 | 189 | if (flush) 190 | adbConn.outputStream.flush(); 191 | } 192 | 193 | /** 194 | * Closes the stream. This sends a close message to the peer. 195 | * 196 | * @throws IOException If the stream fails while sending the close message. 197 | */ 198 | @Override 199 | public void close() throws IOException { 200 | synchronized (this) { 201 | /* This may already be closed by the remote host */ 202 | if (isClosed) 203 | return; 204 | 205 | /* Notify readers/writers that we've closed */ 206 | notifyClose(); 207 | } 208 | 209 | byte[] packet = AdbProtocol.generateClose(localId, remoteId); 210 | adbConn.outputStream.write(packet); 211 | adbConn.outputStream.flush(); 212 | } 213 | 214 | /** 215 | * Retreives whether the stream is closed or not 216 | * 217 | * @return True if the stream is close, false if not 218 | */ 219 | public boolean isClosed() { 220 | return isClosed; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/adblib/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Cameron Gutman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/decoder/VideoDecoder.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.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 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | public class VideoDecoder { 13 | private MediaCodec mCodec; 14 | private Worker mWorker; 15 | private AtomicBoolean mIsConfigured = new AtomicBoolean(false); 16 | 17 | public void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) { 18 | if (mWorker != null) { 19 | mWorker.decodeSample(data, offset, size, presentationTimeUs, flags); 20 | } 21 | } 22 | 23 | public void configure(Surface surface, int width, int height, ByteBuffer csd0, ByteBuffer csd1) { 24 | if (mWorker != null) { 25 | mWorker.configure(surface, width, height, csd0, csd1); 26 | } 27 | } 28 | 29 | 30 | public void start() { 31 | if (mWorker == null) { 32 | mWorker = new Worker(); 33 | mWorker.setRunning(true); 34 | mWorker.start(); 35 | } 36 | } 37 | 38 | public void stop() { 39 | if (mWorker != null) { 40 | mWorker.setRunning(false); 41 | mWorker = null; 42 | mIsConfigured.set(false); 43 | mCodec.stop(); 44 | } 45 | } 46 | 47 | private class Worker extends Thread { 48 | 49 | private AtomicBoolean mIsRunning = new AtomicBoolean(false); 50 | 51 | Worker() { 52 | } 53 | 54 | private void setRunning(boolean isRunning) { 55 | mIsRunning.set(isRunning); 56 | } 57 | 58 | private void configure(Surface surface, int width, int height, ByteBuffer csd0, ByteBuffer csd1) { 59 | if (mIsConfigured.get()) { 60 | mIsConfigured.set(false); 61 | mCodec.stop(); 62 | 63 | } 64 | MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); 65 | format.setByteBuffer("csd-0", csd0); 66 | format.setByteBuffer("csd-1", csd1); 67 | try { 68 | mCodec = MediaCodec.createDecoderByType("video/avc"); 69 | } catch (IOException e) { 70 | throw new RuntimeException("Failed to create codec", e); 71 | } 72 | mCodec.configure(format, surface, null, 0); 73 | mCodec.start(); 74 | mIsConfigured.set(true); 75 | } 76 | 77 | 78 | @SuppressWarnings("deprecation") 79 | public void decodeSample(byte[] data, int offset, int size, long presentationTimeUs, int flags) { 80 | if (mIsConfigured.get() && mIsRunning.get()) { 81 | int index = mCodec.dequeueInputBuffer(-1); 82 | if (index >= 0) { 83 | ByteBuffer buffer; 84 | 85 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 86 | buffer = mCodec.getInputBuffers()[index]; 87 | buffer.clear(); 88 | } else { 89 | buffer = mCodec.getInputBuffer(index); 90 | } 91 | if (buffer != null) { 92 | buffer.put(data, offset, size); 93 | mCodec.queueInputBuffer(index, 0, size, 0, flags); 94 | } 95 | } 96 | } 97 | } 98 | 99 | @Override 100 | public void run() { 101 | try { 102 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 103 | while (mIsRunning.get()) { 104 | if (mIsConfigured.get()) { 105 | int index = mCodec.dequeueOutputBuffer(info, 0); 106 | if (index >= 0) { 107 | // setting true is telling system to render frame onto Surface 108 | mCodec.releaseOutputBuffer(index, true); 109 | if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) { 110 | break; 111 | } 112 | } 113 | } else { 114 | // just waiting to be configured, then decode and render 115 | try { 116 | Thread.sleep(10); 117 | } catch (InterruptedException ignore) { 118 | } 119 | } 120 | } 121 | } catch (IllegalStateException e) { 122 | } 123 | 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /app/src/main/java/org/las2mile/scrcpy/model/ByteUtils.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.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/las2mile/scrcpy/model/MediaPacket.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.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/las2mile/scrcpy/model/VideoPacket.java: -------------------------------------------------------------------------------- 1 | package org.las2mile.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 | // create byte array 53 | public static byte[] toArray(Type type, Flag flag, long presentationTimeStamp, byte[] data) { 54 | 55 | // should be 4 bytes for packet size 56 | byte[] bytes = ByteUtils.intToBytes(10 + data.length); 57 | 58 | int packetSize = 14 + data.length; // 4 - inner packet size 1 - type + 1 - flag + 8 - timeStamp + data.length 59 | byte[] values = new byte[packetSize]; 60 | 61 | System.arraycopy(bytes, 0, values, 0, 4); 62 | 63 | // set type value 64 | values[4] = type.getType(); 65 | // set flag value 66 | values[5] = flag.getFlag(); 67 | // set timeStamp 68 | byte[] longToBytes = ByteUtils.longToBytes(presentationTimeStamp); 69 | System.arraycopy(longToBytes, 0, values, 6, longToBytes.length); 70 | 71 | // set data array 72 | System.arraycopy(data, 0, values, 14, data.length); 73 | return values; 74 | } 75 | 76 | // should call on inner packet 77 | public static boolean isVideoPacket(byte[] values) { 78 | return values[0] == Type.VIDEO.getType(); 79 | } 80 | 81 | public static StreamSettings getStreamSettings(byte[] buffer) { 82 | byte[] sps, pps; 83 | 84 | ByteBuffer spsPpsBuffer = ByteBuffer.wrap(buffer); 85 | if (spsPpsBuffer.getInt() == 0x00000001) { 86 | System.out.println("parsing sps/pps"); 87 | } else { 88 | System.out.println("something is amiss?"); 89 | } 90 | int ppsIndex = 0; 91 | while (!(spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x00 && spsPpsBuffer.get() == 0x01)) { 92 | 93 | } 94 | ppsIndex = spsPpsBuffer.position(); 95 | sps = new byte[ppsIndex - 4]; 96 | System.arraycopy(buffer, 0, sps, 0, sps.length); 97 | ppsIndex -= 4; 98 | pps = new byte[buffer.length - ppsIndex]; 99 | System.arraycopy(buffer, ppsIndex, pps, 0, pps.length); 100 | 101 | // sps buffer 102 | ByteBuffer spsBuffer = ByteBuffer.wrap(sps, 0, sps.length); 103 | 104 | // pps buffer 105 | ByteBuffer ppsBuffer = ByteBuffer.wrap(pps, 0, pps.length); 106 | 107 | StreamSettings streamSettings = new StreamSettings(); 108 | streamSettings.sps = spsBuffer; 109 | streamSettings.pps = ppsBuffer; 110 | 111 | return streamSettings; 112 | } 113 | 114 | public byte[] toByteArray() { 115 | return toArray(type, flag, presentationTimeStamp, data); 116 | } 117 | 118 | public enum Flag { 119 | 120 | FRAME((byte) 0), KEY_FRAME((byte) 1), CONFIG((byte) 2), END((byte) 4); 121 | 122 | private byte type; 123 | 124 | Flag(byte type) { 125 | this.type = type; 126 | } 127 | 128 | public static Flag getFlag(byte value) { 129 | for (Flag type : Flag.values()) { 130 | if (type.getFlag() == value) { 131 | return type; 132 | } 133 | } 134 | 135 | return null; 136 | } 137 | 138 | public byte getFlag() { 139 | return type; 140 | } 141 | } 142 | 143 | public static class StreamSettings { 144 | public ByteBuffer pps; 145 | public ByteBuffer sps; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /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/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/layout-land/surface_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 24 | 25 | 26 |