diff --git a/app/src/main/java/com/limelight/AppViewShortcutTrampoline.java b/app/src/main/java/com/limelight/AppViewShortcutTrampoline.java index a2b6101e..402ff076 100644 --- a/app/src/main/java/com/limelight/AppViewShortcutTrampoline.java +++ b/app/src/main/java/com/limelight/AppViewShortcutTrampoline.java @@ -91,7 +91,7 @@ public class AppViewShortcutTrampoline extends Activity { // If a game is running, we'll make the stream the top level activity if (details.runningGameId != 0) { intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this, - new NvApp("app", details.runningGameId), details, managerBinder)); + new NvApp("app", details.runningGameId, false), details, managerBinder)); } // Now start the activities diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index cfc9c92c..34c30483 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -125,6 +125,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, public static final String EXTRA_STREAMING_REMOTE = "Remote"; public static final String EXTRA_PC_UUID = "UUID"; public static final String EXTRA_PC_NAME = "PcName"; + public static final String EXTRA_APP_HDR = "HDR"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -191,7 +192,10 @@ public class Game extends Activity implements SurfaceHolder.Callback, } // Warn the user if they're on a metered connection - checkDataConnection(); + ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + if (connMgr.isActiveNetworkMetered()) { + displayTransientMessage(getResources().getString(R.string.conn_metered)); + } // Make sure Wi-Fi is fully powered up WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); @@ -206,6 +210,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false); String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID); String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME); + boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false); if (appId == StreamConfiguration.INVALID_APP_ID) { finish(); @@ -219,6 +224,42 @@ public class Game extends Activity implements SurfaceHolder.Callback, // Initialize the MediaCodec helper before creating the decoder MediaCodecHelper.initializeWithContext(this); + // Check if the user has enabled HDR + if (prefConfig.enableHdr) { + // Check if the app supports it + if (!willStreamHdr) { + Toast.makeText(this, "This game does not support HDR10", Toast.LENGTH_SHORT).show(); + } + // It does, so start our HDR checklist + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // We already know the app supports HDR if willStreamHdr is set. + Display display = getWindowManager().getDefaultDisplay(); + Display.HdrCapabilities hdrCaps = display.getHdrCapabilities(); + + // We must now ensure our display is compatible with HDR10 + boolean foundHdr10 = false; + for (int hdrType : hdrCaps.getSupportedHdrTypes()) { + if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) { + LimeLog.info("Display supports HDR10"); + foundHdr10 = true; + } + } + + if (!foundHdr10) { + // Nope, no HDR for us :( + willStreamHdr = false; + Toast.makeText(this, "Display does not support HDR10", Toast.LENGTH_LONG).show(); + } + } + else { + Toast.makeText(this, "HDR requires Android 7.0 or later", Toast.LENGTH_LONG).show(); + willStreamHdr = false; + } + } + else { + willStreamHdr = false; + } + decoderRenderer = new MediaCodecDecoderRenderer(prefConfig, new CrashListener() { @Override @@ -230,7 +271,16 @@ public class Game extends Activity implements SurfaceHolder.Callback, tombstonePrefs.edit().putInt("CrashCount", tombstonePrefs.getInt("CrashCount", 0) + 1).commit(); } }, - tombstonePrefs.getInt("CrashCount", 0)); + tombstonePrefs.getInt("CrashCount", 0), + connMgr.isActiveNetworkMetered(), + willStreamHdr + ); + + // Don't stream HDR if the decoder can't support it + if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) { + willStreamHdr = false; + Toast.makeText(this, "Decoder does not support HEVC Main10HDR10", Toast.LENGTH_LONG).show(); + } // Display a message to the user if H.265 was forced on but we still didn't find a decoder if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) { @@ -252,7 +302,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, StreamConfiguration config = new StreamConfiguration.Builder() .setResolution(prefConfig.width, prefConfig.height) .setRefreshRate(prefConfig.fps) - .setApp(new NvApp(appName, appId)) + .setApp(new NvApp(appName, appId, willStreamHdr)) .setBitrate(prefConfig.bitrate * 1000) .setEnableSops(prefConfig.enableSops) .enableLocalAudioPlayback(prefConfig.playHostAudio) @@ -260,6 +310,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, .setRemote(remote) .setHevcBitratePercentageMultiplier(75) .setHevcSupported(decoderRenderer.isHevcSupported()) + .setEnableHdr(willStreamHdr) .setAudioConfiguration(prefConfig.enable51Surround ? MoonBridge.AUDIO_CONFIGURATION_51_SURROUND : MoonBridge.AUDIO_CONFIGURATION_STEREO) @@ -409,14 +460,6 @@ public class Game extends Activity implements SurfaceHolder.Callback, } } - private void checkDataConnection() - { - ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - if (mgr.isActiveNetworkMetered()) { - displayTransientMessage(getResources().getString(R.string.conn_metered)); - } - } - @SuppressLint("InlinedApi") private final Runnable hideSystemUi = new Runnable() { @Override diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 5f9647db..b5686934 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -543,7 +543,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { return true; } - ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder); + ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder); return true; case QUIT_ID: @@ -558,7 +558,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { public void run() { ServerHelper.doQuit(PcView.this, ServerHelper.getCurrentAddressFromComputer(computer.details), - new NvApp("app", 0), managerBinder, null); + new NvApp("app", 0, false), managerBinder, null); } }, null); return true; diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index dd74a9d3..2db4151e 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -80,9 +80,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { return decoder; } - private MediaCodecInfo findHevcDecoder(int videoFormat) { + private MediaCodecInfo findHevcDecoder(PreferenceConfiguration prefs, boolean meteredNetwork, boolean requestedHdr) { // Don't return anything if H.265 is forced off - if (videoFormat == PreferenceConfiguration.FORCE_H265_OFF) { + if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_OFF) { return null; } @@ -93,10 +93,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { // for even required levels of HEVC. MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1); if (decoderInfo != null) { - if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName())) { + if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork, requestedHdr)) { LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName()); - if (videoFormat == PreferenceConfiguration.FORCE_H265_ON) { + if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_ON) { LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder"); } else { @@ -113,7 +113,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { } public MediaCodecDecoderRenderer(PreferenceConfiguration prefs, - CrashListener crashListener, int consecutiveCrashCount) { + CrashListener crashListener, int consecutiveCrashCount, + boolean meteredData, boolean requestedHdr) { //dumpDecoders(); this.prefs = prefs; @@ -136,7 +137,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { LimeLog.warning("No AVC decoder found"); } - hevcDecoder = findHevcDecoder(prefs.videoFormat); + hevcDecoder = findHevcDecoder(prefs, meteredData, requestedHdr); if (hevcDecoder != null) { LimeLog.info("Selected HEVC decoder: "+hevcDecoder.getName()); } @@ -175,6 +176,21 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer { return avcDecoder != null; } + public boolean isHevcMain10Hdr10Supported() { + if (hevcDecoder == null) { + return false; + } + + for (MediaCodecInfo.CodecProfileLevel profileLevel : hevcDecoder.getCapabilitiesForType("video/hevc").profileLevels) { + if (profileLevel.profile == MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10) { + LimeLog.info("HEVC decoder "+hevcDecoder.getName()+" supports HEVC Main10 HDR10"); + return true; + } + } + + return false; + } + public int getActiveVideoFormat() { return this.videoFormat; } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index 334a6f30..c503370f 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -28,6 +28,7 @@ public class MediaCodecHelper { private static final List blacklistedDecoderPrefixes; private static final List spsFixupBitstreamFixupDecoderPrefixes; private static final List whitelistedAdaptiveResolutionPrefixes; + private static final List deprioritizedHevcDecoders; private static final List baselineProfileHackPrefixes; private static final List directSubmitPrefixes; private static final List constrainedHighProfilePrefixes; @@ -113,8 +114,14 @@ public class MediaCodecHelper { // Exynos seems to be the only HEVC decoder that works reliably whitelistedHevcDecoders.add("omx.exynos"); - // TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames - //whitelistedHevcDecoders.add("omx.nvidia"); + // On Darcy (Shield 2017), HEVC runs fine with no fixups required. + // For some reason, other X1 implementations require bitstream fixups. + if (Build.DEVICE.equalsIgnoreCase("darcy")) { + whitelistedHevcDecoders.add("omx.nvidia"); + } + else { + // TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames + } // Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame). // I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem @@ -132,6 +139,14 @@ public class MediaCodecHelper { // during initialization to avoid SoCs with broken HEVC decoders. } + static { + deprioritizedHevcDecoders = new LinkedList<>(); + + // These are decoders that work but aren't used by default for various reasons. + + // Qualcomm is currently the only decoders in this group. + } + public static void initializeWithContext(Context context) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); @@ -163,11 +178,12 @@ public class MediaCodecHelper { // Unfortunately, it's not that easy to get that information here, so I'll use an // approximation by checking the GLES level (<= 3.0 is bad). if (configInfo.reqGlEsVersion > 0x30000) { - // FIXME: We prefer reference frame invalidation support (which is only doable on AVC on + // We prefer reference frame invalidation support (which is only doable on AVC on // older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings - // to force HEVC on. - //LimeLog.info("Added omx.qcom to supported HEVC decoders based on GLES 3.1+ support"); - //whitelistedHevcDecoders.add("omx.qcom"); + // to force HEVC on. If HDR or mobile data will be used, we'll override this and use + // HEVC anyway. + LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support"); + deprioritizedHevcDecoders.add("omx.qcom"); } else { blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc"); @@ -245,7 +261,7 @@ public class MediaCodecHelper { return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName); } - public static boolean decoderIsWhitelistedForHevc(String decoderName) { + public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData, boolean willStreamHdr) { // TODO: Shield Tablet K1/LTE? // // NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know @@ -277,6 +293,15 @@ public class MediaCodecHelper { return false; } + // Some devices have HEVC decoders that we prefer not to use + // typically because it can't support reference frame invalidation. + // However, we will use it for HDR and for streaming over mobile networks + // since it works fine otherwise. + if ((meteredData || willStreamHdr) && isDecoderInList(deprioritizedHevcDecoders, decoderName)) { + LimeLog.info("Selected deprioritized decoder"); + return true; + } + return isDecoderInList(whitelistedHevcDecoders, decoderName); } diff --git a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java index 129d1572..42154ca0 100644 --- a/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java +++ b/app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java @@ -25,6 +25,7 @@ public class PreferenceConfiguration { private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3"; private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver"; private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop"; + private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr"; private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_60 = 10; @@ -50,6 +51,7 @@ public class PreferenceConfiguration { private static final boolean ONLY_L3_R3_DEFAULT = false; private static final boolean DEFAULT_BATTERY_SAVER = false; private static final boolean DEFAULT_DISABLE_FRAME_DROP = false; + private static final boolean DEFAULT_ENABLE_HDR = false; public static final int FORCE_H265_ON = -1; public static final int AUTOSELECT_H265 = 0; @@ -66,6 +68,7 @@ public class PreferenceConfiguration { public boolean onlyL3R3; public boolean batterySaver; public boolean disableFrameDrop; + public boolean enableHdr; public static int getDefaultBitrate(String resFpsString) { if (resFpsString.equals("720p30")) { @@ -209,6 +212,7 @@ public class PreferenceConfiguration { config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT); config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER); config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP); + config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR); return config; } diff --git a/app/src/main/java/com/limelight/utils/ServerHelper.java b/app/src/main/java/com/limelight/utils/ServerHelper.java index 87883753..658ac242 100644 --- a/app/src/main/java/com/limelight/utils/ServerHelper.java +++ b/app/src/main/java/com/limelight/utils/ServerHelper.java @@ -28,6 +28,7 @@ public class ServerHelper { intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer)); intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName()); intent.putExtra(Game.EXTRA_APP_ID, app.getAppId()); + intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported()); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); intent.putExtra(Game.EXTRA_STREAMING_REMOTE, computer.reachability != ComputerDetails.Reachability.LOCAL); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3858241d..a8f156af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -148,5 +148,7 @@ May reduce micro-stuttering on some devices, but can increase latency Change H.265 settings H.265 lowers video bandwidth requirements but requires a very recent device + Enable HDR (Experimental) + Stream HDR when the game, client display, decoder, and GPU support it diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6bb88c40..562a35da 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -109,5 +109,10 @@ android:title="@string/title_disable_frame_drop" android:summary="@string/summary_disable_frame_drop" android:defaultValue="false" /> + diff --git a/moonlight-common b/moonlight-common index a5a40298..ad27fb7f 160000 --- a/moonlight-common +++ b/moonlight-common @@ -1 +1 @@ -Subproject commit a5a402981fc2524b0a6a71971b7cdf9f123ff457 +Subproject commit ad27fb7f2d05708facec18ab27dbaca484961fec