diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 9625fcdf..5cff3e6d 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -2073,9 +2073,9 @@ public class Game extends Activity implements SurfaceHolder.Callback, } @Override - public void setHdrMode(boolean enabled) { + public void setHdrMode(boolean enabled, byte[] hdrMetadata) { LimeLog.info("Display HDR mode: " + (enabled ? "enabled" : "disabled")); - decoderRenderer.setHdrMode(enabled); + decoderRenderer.setHdrMode(enabled, hdrMetadata); } @Override 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 c6b11cdc..bc180d2e 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -2,6 +2,8 @@ package com.limelight.binding.video; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; @@ -49,6 +51,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C private byte[] ppsBuffer; private boolean submittedCsd; private boolean submitCsdNextCall; + private byte[] currentHdrMetadata; private int nextInputBufferIndex = -1; private ByteBuffer nextInputBuffer; @@ -379,10 +382,64 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, initialHeight); } + // Android 7.0 adds color options to the MediaFormat + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + videoFormat.setInteger(MediaFormat.KEY_COLOR_RANGE, + getPreferredColorRange() == MoonBridge.COLOR_RANGE_FULL ? + MediaFormat.COLOR_RANGE_FULL : MediaFormat.COLOR_RANGE_LIMITED); + + // If the stream is HDR-capable, the decoder will detect transitions in color standards + // rather than us hardcoding them into the MediaFormat. + if (getActiveVideoFormat() != MoonBridge.VIDEO_FORMAT_H265_MAIN10) { + // Set color format keys when not in HDR mode, since we know they won't change + videoFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_SDR_VIDEO); + switch (getPreferredColorSpace()) { + case MoonBridge.COLORSPACE_REC_601: + videoFormat.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT601_NTSC); + break; + case MoonBridge.COLORSPACE_REC_709: + videoFormat.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT709); + break; + case MoonBridge.COLORSPACE_REC_2020: + videoFormat.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT2020); + break; + } + } + } + return videoFormat; } private void configureAndStartDecoder(MediaFormat format) { + // Set HDR metadata if present + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (currentHdrMetadata != null) { + ByteBuffer hdrStaticInfo = ByteBuffer.allocate(25).order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer hdrMetadata = ByteBuffer.wrap(currentHdrMetadata).order(ByteOrder.LITTLE_ENDIAN); + + // Create a HDMI Dynamic Range and Mastering InfoFrame as defined by CTA-861.3 + hdrStaticInfo.put((byte) 0); // Metadata type + hdrStaticInfo.putShort(hdrMetadata.getShort()); // RX + hdrStaticInfo.putShort(hdrMetadata.getShort()); // RY + hdrStaticInfo.putShort(hdrMetadata.getShort()); // GX + hdrStaticInfo.putShort(hdrMetadata.getShort()); // GY + hdrStaticInfo.putShort(hdrMetadata.getShort()); // BX + hdrStaticInfo.putShort(hdrMetadata.getShort()); // BY + hdrStaticInfo.putShort(hdrMetadata.getShort()); // White X + hdrStaticInfo.putShort(hdrMetadata.getShort()); // White Y + hdrStaticInfo.putShort(hdrMetadata.getShort()); // Max mastering luminance + hdrStaticInfo.putShort(hdrMetadata.getShort()); // Min mastering luminance + hdrStaticInfo.putShort(hdrMetadata.getShort()); // Max content luminance + hdrStaticInfo.putShort(hdrMetadata.getShort()); // Max frame average luminance + + hdrStaticInfo.rewind(); + format.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, hdrStaticInfo); + } + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + format.removeKey(MediaFormat.KEY_HDR_STATIC_INFO); + } + } + LimeLog.info("Configuring with format: "+format); videoDecoder.configure(format, renderTarget.getSurface(), null, 0); @@ -1140,8 +1197,33 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer implements C } @Override - public void setHdrMode(boolean enabled) { - // TODO: Set HDR metadata? + public void setHdrMode(boolean enabled, byte[] hdrMetadata) { + // HDR metadata is only supported in Android 7.0 and later, so don't bother + // restarting the codec on anything earlier than that. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (currentHdrMetadata != null && (!enabled || hdrMetadata == null)) { + currentHdrMetadata = null; + } + else if (enabled && hdrMetadata != null && !Arrays.equals(currentHdrMetadata, hdrMetadata)) { + currentHdrMetadata = hdrMetadata; + } + else { + // Nothing to do + return; + } + + // If we reach this point, we need to restart the MediaCodec instance to + // pick up the HDR metadata change. This will happen on the next input + // or output buffer. + + // HACK: Reset codec recovery attempt counter, since this is an expected "recovery" + codecRecoveryAttempts = 0; + + // Promote None/Flush to Restart and leave Reset alone + if (!codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_NONE, CR_RECOVERY_TYPE_RESTART)) { + codecRecoveryType.compareAndSet(CR_RECOVERY_TYPE_FLUSH, CR_RECOVERY_TYPE_RESTART); + } + } } private boolean queueNextInputBuffer(long timestampUs, int codecFlags) { diff --git a/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java b/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java index f61ecec1..945f18a3 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java @@ -14,5 +14,5 @@ public interface NvConnectionListener { void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor); - void setHdrMode(boolean enabled); + void setHdrMode(boolean enabled, byte[] hdrMetadata); } diff --git a/app/src/main/java/com/limelight/nvstream/av/video/VideoDecoderRenderer.java b/app/src/main/java/com/limelight/nvstream/av/video/VideoDecoderRenderer.java index e0484d91..397d8f4e 100644 --- a/app/src/main/java/com/limelight/nvstream/av/video/VideoDecoderRenderer.java +++ b/app/src/main/java/com/limelight/nvstream/av/video/VideoDecoderRenderer.java @@ -16,5 +16,5 @@ public abstract class VideoDecoderRenderer { public abstract int getCapabilities(); - public abstract void setHdrMode(boolean enabled); + public abstract void setHdrMode(boolean enabled, byte[] hdrMetadata); } diff --git a/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java b/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java index d74c6f46..ca08002c 100644 --- a/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java +++ b/app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java @@ -253,9 +253,9 @@ public class MoonBridge { } } - public static void bridgeClSetHdrMode(boolean enabled) { + public static void bridgeClSetHdrMode(boolean enabled, byte[] hdrMetadata) { if (connectionListener != null) { - connectionListener.setHdrMode(enabled); + connectionListener.setHdrMode(enabled, hdrMetadata); } } diff --git a/app/src/main/jni/moonlight-core/callbacks.c b/app/src/main/jni/moonlight-core/callbacks.c index c542e5f5..6edc32c8 100644 --- a/app/src/main/jni/moonlight-core/callbacks.c +++ b/app/src/main/jni/moonlight-core/callbacks.c @@ -93,7 +93,7 @@ Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) { BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(I)V"); BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V"); BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V"); - BridgeClSetHdrModeMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetHdrMode", "(Z)V"); + BridgeClSetHdrModeMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClSetHdrMode", "(Z[B)V"); } int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) { @@ -331,7 +331,16 @@ void BridgeClConnectionStatusUpdate(int connectionStatus) { void BridgeClSetHdrMode(bool enabled) { JNIEnv* env = GetThreadEnv(); - (*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClSetHdrModeMethod, enabled); + jbyteArray hdrMetadataByteArray = NULL; + SS_HDR_METADATA hdrMetadata; + + // Check if HDR metadata was provided + if (enabled && LiGetHdrMetadata(&hdrMetadata)) { + hdrMetadataByteArray = (*env)->NewByteArray(env, sizeof(SS_HDR_METADATA)); + (*env)->SetByteArrayRegion(env, hdrMetadataByteArray, 0, sizeof(SS_HDR_METADATA), (jbyte*)&hdrMetadata); + } + + (*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClSetHdrModeMethod, enabled, hdrMetadataByteArray); if ((*env)->ExceptionCheck(env)) { // We will crash here (*JVM)->DetachCurrentThread(JVM);