-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
dc750a4
commit dd07737
Showing
5 changed files
with
578 additions
and
9 deletions.
There are no files selected for viewing
50 changes: 50 additions & 0 deletions
50
server/src/main/java/org/cagnulein/android_remote/VideoCodec.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package org.cagnulein.android_remote; | ||
|
||
import android.annotation.SuppressLint; | ||
import android.media.MediaFormat; | ||
|
||
public enum VideoCodec implements Codec { | ||
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC), | ||
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC), | ||
@SuppressLint("InlinedApi") // introduced in API 29 | ||
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1); | ||
|
||
private final int id; // 4-byte ASCII representation of the name | ||
private final String name; | ||
private final String mimeType; | ||
|
||
VideoCodec(int id, String name, String mimeType) { | ||
this.id = id; | ||
this.name = name; | ||
this.mimeType = mimeType; | ||
} | ||
|
||
@Override | ||
public Type getType() { | ||
return Type.VIDEO; | ||
} | ||
|
||
@Override | ||
public int getId() { | ||
return id; | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return name; | ||
} | ||
|
||
@Override | ||
public String getMimeType() { | ||
return mimeType; | ||
} | ||
|
||
public static VideoCodec findByName(String name) { | ||
for (VideoCodec codec : values()) { | ||
if (codec.name.equals(name)) { | ||
return codec; | ||
} | ||
} | ||
return null; | ||
} | ||
} |
339 changes: 339 additions & 0 deletions
339
server/src/main/java/org/cagnulein/android_remote/Workarounds.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,339 @@ | ||
package org.cagnulein.android_remote; | ||
|
||
import android.annotation.SuppressLint; | ||
import android.annotation.TargetApi; | ||
import android.app.Application; | ||
import android.content.AttributionSource; | ||
import android.content.Context; | ||
import android.content.ContextWrapper; | ||
import android.content.pm.ApplicationInfo; | ||
import android.media.AudioAttributes; | ||
import android.media.AudioManager; | ||
import android.media.AudioRecord; | ||
import android.os.Build; | ||
import android.os.Looper; | ||
import android.os.Parcel; | ||
|
||
import java.lang.ref.WeakReference; | ||
import java.lang.reflect.Constructor; | ||
import java.lang.reflect.Field; | ||
import java.lang.reflect.Method; | ||
|
||
@SuppressLint("PrivateApi,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") | ||
public final class Workarounds { | ||
|
||
private static final Class<?> ACTIVITY_THREAD_CLASS; | ||
private static final Object ACTIVITY_THREAD; | ||
|
||
static { | ||
prepareMainLooper(); | ||
|
||
try { | ||
// ActivityThread activityThread = new ActivityThread(); | ||
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); | ||
Constructor<?> activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor(); | ||
activityThreadConstructor.setAccessible(true); | ||
ACTIVITY_THREAD = activityThreadConstructor.newInstance(); | ||
|
||
// ActivityThread.sCurrentActivityThread = activityThread; | ||
Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); | ||
sCurrentActivityThreadField.setAccessible(true); | ||
sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); | ||
} catch (Exception e) { | ||
throw new AssertionError(e); | ||
} | ||
} | ||
|
||
private Workarounds() { | ||
// not instantiable | ||
} | ||
|
||
public static void apply(boolean audio, boolean camera) { | ||
boolean mustFillConfigurationController = false; | ||
boolean mustFillAppInfo = false; | ||
boolean mustFillAppContext = false; | ||
|
||
if (Build.BRAND.equalsIgnoreCase("meizu")) { | ||
// Workarounds must be applied for Meizu phones: | ||
// - <https://github.com/Genymobile/scrcpy/issues/240> | ||
// - <https://github.com/Genymobile/scrcpy/issues/365> | ||
// - <https://github.com/Genymobile/scrcpy/issues/2656> | ||
// | ||
// But only apply when strictly necessary, since workarounds can cause other issues: | ||
// - <https://github.com/Genymobile/scrcpy/issues/940> | ||
// - <https://github.com/Genymobile/scrcpy/issues/994> | ||
mustFillAppInfo = true; | ||
} else if (Build.BRAND.equalsIgnoreCase("honor")) { | ||
// More workarounds must be applied for Honor devices: | ||
// - <https://github.com/Genymobile/scrcpy/issues/4015> | ||
// | ||
// The system context must not be set for all devices, because it would cause other problems: | ||
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142> | ||
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031> | ||
mustFillAppInfo = true; | ||
mustFillAppContext = true; | ||
} | ||
|
||
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { | ||
// Before Android 11, audio is not supported. | ||
// Since Android 12, we can properly set a context on the AudioRecord. | ||
// Only on Android 11 we must fill the application context for the AudioRecord to work. | ||
mustFillAppContext = true; | ||
} | ||
|
||
if (camera) { | ||
mustFillAppInfo = true; | ||
mustFillAppContext = true; | ||
} | ||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
// On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), | ||
// which requires a non-null ConfigurationController. | ||
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. | ||
// <https://github.com/Genymobile/scrcpy/issues/4467> | ||
mustFillConfigurationController = true; | ||
} | ||
|
||
if (mustFillConfigurationController) { | ||
// Must be call before fillAppContext() because it is necessary to get a valid system context | ||
fillConfigurationController(); | ||
} | ||
if (mustFillAppInfo) { | ||
fillAppInfo(); | ||
} | ||
if (mustFillAppContext) { | ||
fillAppContext(); | ||
} | ||
} | ||
|
||
@SuppressWarnings("deprecation") | ||
private static void prepareMainLooper() { | ||
// Some devices internally create a Handler when creating an input Surface, causing an exception: | ||
// "Can't create handler inside thread that has not called Looper.prepare()" | ||
// <https://github.com/Genymobile/scrcpy/issues/240> | ||
// | ||
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: | ||
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' | ||
// on a null object reference" | ||
// <https://github.com/Genymobile/scrcpy/issues/921> | ||
Looper.prepareMainLooper(); | ||
} | ||
|
||
private static void fillAppInfo() { | ||
try { | ||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); | ||
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); | ||
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); | ||
appBindDataConstructor.setAccessible(true); | ||
Object appBindData = appBindDataConstructor.newInstance(); | ||
|
||
ApplicationInfo applicationInfo = new ApplicationInfo(); | ||
applicationInfo.packageName = FakeContext.PACKAGE_NAME; | ||
|
||
// appBindData.appInfo = applicationInfo; | ||
Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); | ||
appInfoField.setAccessible(true); | ||
appInfoField.set(appBindData, applicationInfo); | ||
|
||
// activityThread.mBoundApplication = appBindData; | ||
Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication"); | ||
mBoundApplicationField.setAccessible(true); | ||
mBoundApplicationField.set(ACTIVITY_THREAD, appBindData); | ||
} catch (Throwable throwable) { | ||
// this is a workaround, so failing is not an error | ||
Ln.d("Could not fill app info: " + throwable.getMessage()); | ||
} | ||
} | ||
|
||
private static void fillAppContext() { | ||
try { | ||
Application app = new Application(); | ||
Field baseField = ContextWrapper.class.getDeclaredField("mBase"); | ||
baseField.setAccessible(true); | ||
baseField.set(app, FakeContext.get()); | ||
|
||
// activityThread.mInitialApplication = app; | ||
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); | ||
mInitialApplicationField.setAccessible(true); | ||
mInitialApplicationField.set(ACTIVITY_THREAD, app); | ||
} catch (Throwable throwable) { | ||
// this is a workaround, so failing is not an error | ||
Ln.d("Could not fill app context: " + throwable.getMessage()); | ||
} | ||
} | ||
|
||
private static void fillConfigurationController() { | ||
try { | ||
Class<?> configurationControllerClass = Class.forName("android.app.ConfigurationController"); | ||
Class<?> activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); | ||
Constructor<?> configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); | ||
configurationControllerConstructor.setAccessible(true); | ||
Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); | ||
|
||
Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); | ||
configurationControllerField.setAccessible(true); | ||
configurationControllerField.set(ACTIVITY_THREAD, configurationController); | ||
} catch (Throwable throwable) { | ||
Ln.d("Could not fill configuration: " + throwable.getMessage()); | ||
} | ||
} | ||
|
||
static Context getSystemContext() { | ||
try { | ||
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); | ||
return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD); | ||
} catch (Throwable throwable) { | ||
// this is a workaround, so failing is not an error | ||
Ln.d("Could not get system context: " + throwable.getMessage()); | ||
return null; | ||
} | ||
} | ||
|
||
@TargetApi(Build.VERSION_CODES.R) | ||
@SuppressLint("WrongConstant,MissingPermission") | ||
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { | ||
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. | ||
// | ||
// This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses | ||
// reflections to initialize it like the normal constructor do (or the `AudioRecord.Builder.build()` method do). | ||
// As a result, the modified code was not executed. | ||
try { | ||
// AudioRecord audioRecord = new AudioRecord(0L); | ||
Constructor<AudioRecord> audioRecordConstructor = AudioRecord.class.getDeclaredConstructor(long.class); | ||
audioRecordConstructor.setAccessible(true); | ||
AudioRecord audioRecord = audioRecordConstructor.newInstance(0L); | ||
|
||
// audioRecord.mRecordingState = RECORDSTATE_STOPPED; | ||
Field mRecordingStateField = AudioRecord.class.getDeclaredField("mRecordingState"); | ||
mRecordingStateField.setAccessible(true); | ||
mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED); | ||
|
||
Looper looper = Looper.myLooper(); | ||
if (looper == null) { | ||
looper = Looper.getMainLooper(); | ||
} | ||
|
||
// audioRecord.mInitializationLooper = looper; | ||
Field mInitializationLooperField = AudioRecord.class.getDeclaredField("mInitializationLooper"); | ||
mInitializationLooperField.setAccessible(true); | ||
mInitializationLooperField.set(audioRecord, looper); | ||
|
||
// Create `AudioAttributes` with fixed capture preset | ||
int capturePreset = source; | ||
AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder(); | ||
Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset", int.class); | ||
setInternalCapturePresetMethod.invoke(audioAttributesBuilder, capturePreset); | ||
AudioAttributes attributes = audioAttributesBuilder.build(); | ||
|
||
// audioRecord.mAudioAttributes = attributes; | ||
Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes"); | ||
mAudioAttributesField.setAccessible(true); | ||
mAudioAttributesField.set(audioRecord, attributes); | ||
|
||
// audioRecord.audioParamCheck(capturePreset, sampleRate, encoding); | ||
Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class, int.class); | ||
audioParamCheckMethod.setAccessible(true); | ||
audioParamCheckMethod.invoke(audioRecord, capturePreset, sampleRate, encoding); | ||
|
||
// audioRecord.mChannelCount = channels | ||
Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount"); | ||
mChannelCountField.setAccessible(true); | ||
mChannelCountField.set(audioRecord, channels); | ||
|
||
// audioRecord.mChannelMask = channelMask | ||
Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask"); | ||
mChannelMaskField.setAccessible(true); | ||
mChannelMaskField.set(audioRecord, channelMask); | ||
|
||
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding); | ||
int bufferSizeInBytes = minBufferSize * 8; | ||
|
||
// audioRecord.audioBuffSizeCheck(bufferSizeInBytes) | ||
Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class); | ||
audioBuffSizeCheckMethod.setAccessible(true); | ||
audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes); | ||
|
||
final int channelIndexMask = 0; | ||
|
||
int[] sampleRateArray = new int[]{sampleRate}; | ||
int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; | ||
|
||
int initResult; | ||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { | ||
// private native final int native_setup(Object audiorecord_this, | ||
// Object /*AudioAttributes*/ attributes, | ||
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, | ||
// int buffSizeInBytes, int[] sessionId, String opPackageName, | ||
// long nativeRecordInJavaObj); | ||
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, | ||
int.class, int.class, int.class, int[].class, String.class, long.class); | ||
nativeSetupMethod.setAccessible(true); | ||
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray, | ||
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, FakeContext.get().getOpPackageName(), | ||
0L); | ||
} else { | ||
// Assume `context` is never `null` | ||
AttributionSource attributionSource = FakeContext.get().getAttributionSource(); | ||
|
||
// Assume `attributionSource.getPackageName()` is never null | ||
|
||
// ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState() | ||
Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState"); | ||
asScopedParcelStateMethod.setAccessible(true); | ||
|
||
try (AutoCloseable attributionSourceState = (AutoCloseable) asScopedParcelStateMethod.invoke(attributionSource)) { | ||
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); | ||
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); | ||
|
||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { | ||
// private native int native_setup(Object audiorecordThis, | ||
// Object /*AudioAttributes*/ attributes, | ||
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, | ||
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, | ||
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); | ||
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, | ||
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); | ||
nativeSetupMethod.setAccessible(true); | ||
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, | ||
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, | ||
attributionSourceParcel, 0L, 0); | ||
} else { | ||
// Android 14 added a new int parameter "halInputFlags" | ||
// <https://github.com/aosp-mirror/platform_frameworks_base/commit/f6135d75db79b1d48fad3a3b3080d37be20a2313> | ||
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, | ||
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class); | ||
nativeSetupMethod.setAccessible(true); | ||
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, | ||
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, | ||
attributionSourceParcel, 0L, 0, 0); | ||
} | ||
} | ||
} | ||
|
||
if (initResult != AudioRecord.SUCCESS) { | ||
Ln.e("Error code " + initResult + " when initializing native AudioRecord object."); | ||
throw new RuntimeException("Cannot create AudioRecord"); | ||
} | ||
|
||
// mSampleRate = sampleRate[0] | ||
Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate"); | ||
mSampleRateField.setAccessible(true); | ||
mSampleRateField.set(audioRecord, sampleRateArray[0]); | ||
|
||
// audioRecord.mSessionId = session[0] | ||
Field mSessionIdField = AudioRecord.class.getDeclaredField("mSessionId"); | ||
mSessionIdField.setAccessible(true); | ||
mSessionIdField.set(audioRecord, session[0]); | ||
|
||
// audioRecord.mState = AudioRecord.STATE_INITIALIZED | ||
Field mStateField = AudioRecord.class.getDeclaredField("mState"); | ||
mStateField.setAccessible(true); | ||
mStateField.set(audioRecord, AudioRecord.STATE_INITIALIZED); | ||
|
||
return audioRecord; | ||
} catch (Exception e) { | ||
Ln.e("Failed to invoke AudioRecord.<init>.", e); | ||
throw new RuntimeException("Cannot create AudioRecord"); | ||
} | ||
} | ||
} |
Oops, something went wrong.