Demystifying iPhone’s Amber Flashlight

Source: The Verge

Back in 2013, iPhone 5s was launched. It introduces True Tone Flash feature to make photos “right”. An additional amber LED is added to the iSight camera for this purpose. Once you take a photo with flash on, the camera automatically balances the amount of white light and the amber (orange) light based on the temperature of the scene, making the resulting photo not too white or too bright due to the unexpected intensity of the white LED.

While this can be a cool feature for many iPhone photographers, Apple does not seem to provide any public APIs to control each LED separately. Quite a few posts in StackOverFlow (like this one) sought for this possibility but there has been no exact answer. Apple engineers only told us to make an enhancement request to no avail. The best the developers could do is adjusting the intensity of only the white LED, not something like alternating between white and amber light at will.

One may argue that we already have a workaround. There used to be a way to utilize the bug that activates both LEDs. The thing is, Apple fixed this bug since the release of iOS 9, and no one was able to find another solution since then.

The functionality of True Tone Flash lies deep in a private low-level API for the iSight camera, making it impossible for typical developers to manipulate by any means without some hack. Yes, we need to first jailbreak the iPhone if we ever want to have some fun with it.

There are plenty of iOS libraries that coordinate access to this physical camera. For this amber magic, I am not talking aboutAVFoundation.framework, Celestial.framework andMediaToolbox.framework. It is a mysterious HXISP.mediacapture library where X is the version of the iSight camera, sort of. To my knowledge, X is an integer starting from 4. H4ISP cameras are found in iPhone 4 and iPod touch 4G. H6ISP cameras are found in iPhone SE. H10ISP cameras are found in iPhone X.

Regardless of the iSight camera version, they share similar API, while differences can exist because of the new features added to the newer cameras (newer iPhone models). By using a reverse engineering tool like IDA Pro, we can work out how the amber flashlight could be activated by disassembling (and decompiling) the aforementioned library.

signed __int64 __fastcall SetTorchColor(__int64 infoDict, __int64 streamRef, __int64 deviceRef)
{
__int64 infoDictCFType; // x22
__int64 percentileCF; // x0
unsigned __int16 normalizedWarmLEDPercentile; // w8
__int64 functionResult; // x0 MAPDST
signed __int64 result; // x0
unsigned int warmLEDPercentile; // [xsp+1Ch] [xbp-24h]
if ( !infoDict )
return 4294954516LL;
infoDictCFType = CFGetTypeID(infoDict);
if ( infoDictCFType != CFDictionaryGetTypeID() )
return 4294954516LL;
percentileCF = CFDictionaryGetValue(infoDict, CFSTR("WarmLEDPercentile"));
if ( !percentileCF )
return 4294954516LL;
CFNumberGetValue(percentileCF, 9LL, &warmLEDPercentile);
normalizedWarmLEDPercentile = warmLEDPercentile;
if ( warmLEDPercentile >= 101 )
{
normalizedWarmLEDPercentile = 100;
warmLEDPercentile = 100;
}
functionResult = H6ISP::H6ISPDevice::SetTorchColorMode(
*(deviceRef + 24),
*(streamRef + 92),
2u,
normalizedWarmLEDPercentile);

H6ISPLogger(6u, "H6ISPCaptureDevice: SetTorchColor, warmLEDPercentile=%d, result=0x%08X\n", warmLEDPercentile, functionResult);
if ( functionResult )
result = 4294954516LL;
else
result = 0LL;
return result;
}

SetTorchColor(CFMutableDictionaryRef infoDict, HXISPCaptureStreamRef streamRef, HXISPCaptureDeviceRef deviceRef) is the key function that sets the color of the LED. We can guess right away that the value of WarmLEDPercentile has something to do with the amber light (“warm”). Note that the value shall not exceed 100 (because it is a percentile and because of the boundary check).

While the function uses the term “Torch” as its name, I will still refer to it as “Flashlight” to at least prevent confusion.

The problem is that it does not appear that SetTorchColor() is being used anywhere or in any higher-level APIs. This confirms us that why no one could do such a thing with the amber LED. However, with jailbreaking and ability to inject any code at run-time, we can invoke this function somewhere (some other function) that is guaranteed to be used by a higher-level API. I found that the easiest location that SetTorchColor() will work properly is within SetTorchLevel(CFNumberRef level, HXISPCaptureStreamRef streamRef, HXISPCaptureDeviceRef deviceRef) function. We will decompile this function anyway to better understand how this will work.

signed __int64 __fastcall SetTorchLevel(__int64 level, __int64 streamRef, __int64 deviceRef)
{
__int64 levelCFType; // x22
unsigned int rawLevelMult; // w8
unsigned __int64 normalizedTorchLevel; // x22
signed __int64 result; // x19
unsigned int *v10; // x19
__int64 enableTorchResult; // x21
unsigned int v12; // t1
float rawLevel; // [xsp+1Ch] [xbp-24h]
levelCFType = CFGetTypeID(level);
if ( levelCFType != CFNumberGetTypeID() || !*(streamRef + 528) )
return 4294954516LL;
CFNumberGetValue(level, 12LL, &rawLevel);
if ( rawLevel >= 1.0 )
{
normalizedTorchLevel = 6LL;
}
else
{
if ( rawLevel <= 0.0 )
{
TURN_OFF:
enableTorchResult = H6ISP::H6ISPDevice::DisableTorch(*(deviceRef + 24), *(streamRef + 92));
if ( !*(streamRef + 96) )
H6ISP::H6ISPDevice::ISP_EnableSensorPower(*(deviceRef + 24), *(streamRef + 92), 0, 0);
normalizedTorchLevel = 0LL;
goto FINALLY;
}
rawLevelMult = (rawLevel * 6.0);
if ( rawLevelMult )
normalizedTorchLevel = rawLevelMult;
else
normalizedTorchLevel = 1LL;
}
if ( normalizedTorchLevel && gCaptureDeviceCFPrefs )
{
normalizedTorchLevel = gCaptureDeviceCFPrefs <= 0xF ? gCaptureDeviceCFPrefs : 15LL;
H6ISPLogger(6u, "H6ISPCaptureDevice: Torch Level Override: %d.\n", normalizedTorchLevel);
if ( !normalizedTorchLevel )
goto TURN_OFF;
}
if ( *(streamRef + 96) )
{
v10 = (streamRef + 92);
}
else
{
v12 = *(streamRef + 92);
v10 = (streamRef + 92);
H6ISP::H6ISPDevice::ISP_EnableSensorPower(*(deviceRef + 24), v12, 1, 1);
}
enableTorchResult = H6ISP::H6ISPDevice::EnableTorch(*(deviceRef + 24), *v10, normalizedTorchLevel);
FINALLY:
if ( enableTorchResult )
result = 0xFFFFCE14LL;
else
result = 0LL;
H6ISPLogger(6u, "H6ISPCaptureDevice: Setting torch level to %d. result=0x%08X\n", normalizedTorchLevel, enableTorchResult);
return result;
}

As the name suggests, this function is invoked when there is a request to turn on the flashlight or to change the brightness level of the flashlight. For the sake of simplicity, we will ignore gCaptureDeviceCFPrefs which overrides the torch level. We developers know for the fact that the level (intensity) is a real number within [0.0, 1.0]. The level will be multiplied by 6 and rounded to an integer before actually issuing a command to the physical camera. The absolute maximum level will not exceed 6.

What we can do is to set the flashlight color to amber after setting the flashlight intensity level. The camera will then alternate to the amber light automatically — or says, magically. Programmatically speaking, we can fake the scene information (set our own LEDWarmPercentile value) to SetTorchColor() after the flashlight is on via SetTorchLevel().

Someone might ask then, is it possible to turn on both LEDs to achieve the maximum possible intensity? The answer is yes. We can hack up SetTorchColorMode() by overriding the mode to be both-LEDs (integer of 1). The resulting brightness seems not to be much brighter than that of the white LED alone, though. One explanation may be that the non-white light could never be brighter than the white light, given the same intensity.

However, this technique no longer works as of H9ISP cameras. For an uninitiated, H9ISP cameras are of the devices that come with quad LEDs (iPhone 7 and newer). Faking the scene condition would result in the flashlight not being activated at all. Luckily, Apple do provide an alternative but neat way to control the color, and even the LEDs themselves!

Inside SetTorchLevel(), they use SetIndividualTorchLEDLevels(?, ?, unsigned int) function to enable the flashlight. The last argument (unsigned integer) is where the fun begins. This value is 32-bit and each consecutive 8 bits represents the brightness level of a single LED. By default, Apple ensures that the value is in the form 0xhh00hh00. The two non-zero chunks are of the white LEDs. From the raw brightness level within the range [0.0, 1.0], they do this:

intLevel = rawLevel ? (unsigned int)(rawLevel * 0xFF) : 1;
finalLevels = (intLevel>>1) ? ((intLevel<<7) & 0xFF00) | ((intLevel>>1) << 24) : 0;

The maximum possible levels from this expression is 0x7f007f00, although there’s nothing totally wrong going up to0xff00ff00. Now, what can you do to amber-ify it?

Simple, when you hook this function, do finalLevels >>= 8 to make the pattern be 0x00hh00hh. But, if you want all to turn on, finalLevels = finalLevels | (finalLevels >> 8).

You can take a look at my GitHub project that demonstrates this whole finding to better understand what is going on.

Let’s compare these two LEDs (using my own iPhone SE) with courtesy of a mirror.

It is clear that one single LED can be powered on at a time, and the amber light is very amber. iPhone users usually appreciate the amber tone only when both LEDs are active because the scene is warm. If they jailbreak their device, they will be able to have this magic, of course in exchange to warranty void.

With the power of Activator or one of the first tweaks come to mind of those jailbreakers, you can turn your LEDs into some sort of fancy blinking two-color bulb. For those who use LED flash for notifications, this also opens up a possibility to distinguish types or importance levels based on the LED color that blinks, i.e., white for general stuffs and amber for important stuffs.

Why don’t Apple implement this to iPhone already?

Thatchapon Unprasert

Written by

Dedicated Technologist.