Compare commits

...

8 Commits

Author SHA1 Message Date
Michelle Tang
2e0e8c2235 Bump version to 7.74.5 2026-02-21 19:28:33 -05:00
Greyson Parrelli
5c6962082a Only set HDR transcoder flags for HDR content. 2026-02-21 19:26:57 -05:00
Greyson Parrelli
701bed970b Route video GIF attachments to the GENERIC_TRANSCODE queue. 2026-02-21 19:26:51 -05:00
Greyson Parrelli
d0918dcb7b Fix video transcoding crash caused by premature codec API calls.
Move getParameterDescriptor and setParameters calls to after
configure/start, since they require the codec to be in the
Executing state. Always set KEY_COLOR_TRANSFER_REQUEST in the
format before configure as the primary tone-mapping mechanism.
2026-02-21 19:26:47 -05:00
Greyson Parrelli
36aae9823b Remove now-unused colorInfo parsing from video transcoding. 2026-02-21 19:26:42 -05:00
Greyson Parrelli
b1f5206680 Possible fix for some transcoding issues. 2026-02-21 19:26:33 -05:00
Alex Hart
38ed75fb64 Bump version to 7.74.4 2026-02-19 11:02:47 -04:00
Alex Hart
26ab20f860 Fix possible captcha race. 2026-02-19 10:53:59 -04:00
8 changed files with 90 additions and 135 deletions

View File

@@ -1,17 +1,7 @@
@file:Suppress("UnstableApiUsage")
import com.android.build.api.dsl.ManagedVirtualDevice
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import java.io.File
import java.util.Properties
plugins {
@@ -31,8 +21,8 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1651
val canonicalVersionName = "7.74.3"
val currentHotfixVersion = 0
val canonicalVersionName = "7.74.5"
val currentHotfixVersion = 2
val maxHotfixVersions = 100
// We don't want versions to ever end in 0 so that they don't conflict with nightly versions

View File

@@ -74,7 +74,7 @@ public final class AttachmentCompressionJob extends BaseJob {
int mmsSubscriptionId)
{
return new AttachmentCompressionJob(databaseAttachment.attachmentId,
MediaUtil.isVideo(databaseAttachment) && MediaConstraints.isVideoTranscodeAvailable(),
MediaUtil.isVideo(databaseAttachment) && !databaseAttachment.videoGif && MediaConstraints.isVideoTranscodeAvailable(),
mms,
mmsSubscriptionId);
}

View File

@@ -458,8 +458,8 @@ class RegistrationViewModel : ViewModel() {
}
fun submitCaptchaToken(context: Context) {
val e164 = getCurrentE164() ?: throw IllegalStateException("Can't submit captcha token if no phone number is set!")
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
val e164 = getCurrentE164() ?: return clearChallengesAndBail { Log.w(TAG, "Phone number was null when trying to submit captcha token.") }
val captchaToken = store.value.captchaToken ?: return bail { Log.w(TAG, "Captcha token was null when trying to submit captcha token.") }
store.update {
it.copy(captchaToken = null, challengeInProgress = true, inProgress = true)
@@ -486,7 +486,7 @@ class RegistrationViewModel : ViewModel() {
fun requestAndSubmitPushToken(context: Context) {
Log.v(TAG, "validatePushToken()")
val e164 = getCurrentE164() ?: throw IllegalStateException("Can't submit captcha token if no phone number is set!")
val e164 = getCurrentE164() ?: return clearChallengesAndBail { Log.w(TAG, "Phone number was null when trying to submit push token.") }
viewModelScope.launch {
Log.d(TAG, "Getting session in order to perform push token verification…")
@@ -1063,6 +1063,22 @@ class RegistrationViewModel : ViewModel() {
setInProgress(false)
}
/**
* Like [bail], but also clears challenge state. This is needed when challenge handling fails due to missing phone number,
* since otherwise the stale challenges would re-trigger the observer on every config change.
*/
private fun clearChallengesAndBail(logMessage: () -> Unit) {
logMessage()
store.update {
it.copy(
inProgress = false,
challengesRequested = emptyList(),
challengeInProgress = false,
captchaToken = null
)
}
}
fun registerWithBackupKey(context: Context, backupKey: String, e164: String?, pin: String?, aciIdentityKeyPair: IdentityKeyPair?, pniIdentityKeyPair: IdentityKeyPair?) {
setInProgress(true)

View File

@@ -600,6 +600,7 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_
private fun updateEnabledControls(showProgress: Boolean, isReRegister: Boolean) {
binding.countryCode.isEnabled = !showProgress
binding.number.isEnabled = !showProgress
countryPickerView.isEnabled = !showProgress
binding.cancelButton.visible = !showProgress && isReRegister
}

View File

@@ -150,6 +150,7 @@ public final class MediaConverter {
AudioTrackConverter audioTrackConverter = null;
long mdatContentLength = 0;
boolean muxerStarted = false;
boolean muxerStopped = false;
try {
@@ -162,13 +163,17 @@ public final class MediaConverter {
throw new EncodingException("No video and audio tracks");
}
doExtractDecodeEditEncodeMux(
muxerStarted = doExtractDecodeEditEncodeMux(
videoTrackConverter,
audioTrackConverter,
muxer);
mdatContentLength = muxer.stop();
muxerStopped = true;
if (muxerStarted) {
mdatContentLength = muxer.stop();
muxerStopped = true;
} else if (mCancelled) {
throw new EncodingException("Conversion cancelled before muxing started");
}
} catch (EncodingException | IOException e) {
Log.e(TAG, "error converting", e);
@@ -204,7 +209,7 @@ public final class MediaConverter {
}
try {
if (muxer != null) {
if (!muxerStopped) {
if (!muxerStopped && muxerStarted) {
muxer.stop();
}
muxer.release();
@@ -225,8 +230,10 @@ public final class MediaConverter {
/**
* Does the actual work for extracting, decoding, encoding and muxing.
*
* @return true if the muxer was started, false otherwise.
*/
private void doExtractDecodeEditEncodeMux(
private boolean doExtractDecodeEditEncodeMux(
final @Nullable VideoTrackConverter videoTrackConverter,
final @Nullable AudioTrackConverter audioTrackConverter,
final @NonNull Muxer muxer) throws IOException, TranscodingException {
@@ -249,7 +256,7 @@ public final class MediaConverter {
Log.d(TAG, "loop: " + currentState);
}
if (currentState.equals(oldState)) {
if (muxing && currentState.equals(oldState)) {
if (++stuckFrames >= STUCK_FRAME_THRESHOLD) {
mCancelled = true;
}
@@ -305,6 +312,8 @@ public final class MediaConverter {
}
// TODO: Check the generated output file.
return muxing;
}
static String getMimeTypeFor(MediaFormat format) {

View File

@@ -13,6 +13,7 @@ import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.video.interfaces.MediaInput;
import org.thoughtcrime.securesms.video.videoconverter.utils.MediaCodecCompat;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -88,19 +89,25 @@ final class VideoThumbnailsExtractor {
outputSurface = new OutputSurface(outputWidthRotated, outputHeightRotated, true);
decoder = MediaCodec.createDecoderByType(mime);
if (Build.VERSION.SDK_INT >= 31) {
final String VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY = "vendor.dolby.codec.transfer.value";
MediaCodec.ParameterDescriptor descriptor = decoder.getParameterDescriptor(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY);
if (descriptor != null) {
Bundle transferBundle = new Bundle();
transferBundle.putString(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY, "transfer.sdr.normal");
decoder.setParameters(transferBundle);
} else {
mediaFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
}
final boolean isHdr = MediaCodecCompat.isHdrVideo(mediaFormat);
if (Build.VERSION.SDK_INT >= 31 && isHdr) {
mediaFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
}
decoder.configure(mediaFormat, outputSurface.getSurface(), null, 0);
decoder.start();
if (Build.VERSION.SDK_INT >= 31 && isHdr) {
try {
final String VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY = "vendor.dolby.codec.transfer.value";
MediaCodec.ParameterDescriptor descriptor = decoder.getParameterDescriptor(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY);
if (descriptor != null) {
Bundle transferBundle = new Bundle();
transferBundle.putString(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY, "transfer.sdr.normal");
decoder.setParameters(transferBundle);
}
} catch (IllegalStateException e) {
Log.w(TAG, "Failed to set Dolby Vision transfer parameter", e);
}
}
long duration = 0;
@@ -217,4 +224,5 @@ final class VideoThumbnailsExtractor {
}
Log.i(TAG, "doExtract finished");
}
}

View File

@@ -150,11 +150,6 @@ final class VideoTrackConverter {
outputHeightRotated = outputHeight;
}
final ColorInfo colorInfo = preDecodeColorInfo(mVideoExtractor, inputVideoFormat);
// IMPORTANT: reset extractor after probing
mVideoExtractor.unselectTrack(videoInputTrack);
mVideoExtractor.selectTrack(videoInputTrack);
mVideoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
final MediaFormat outputVideoFormat = MediaFormat.createVideoFormat(videoCodec, outputWidthRotated, outputHeightRotated);
// Set some properties. Failing to specify some of these can cause the MediaCodec
@@ -192,82 +187,6 @@ final class VideoTrackConverter {
}
}
private ColorInfo preDecodeColorInfo(MediaExtractor extractor, MediaFormat inputFormat) throws IOException {
MediaCodec decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME));
decoder.configure(inputFormat, null, null, 0);
decoder.start();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean outputFormatKnown = false;
Integer colorStandard = null, colorTransfer = null, colorRange = null;
boolean inputDone = false;
while (!outputFormatKnown) {
// ---- FEED INPUT ----
if (!inputDone) {
int inIndex = decoder.dequeueInputBuffer(20000);
if (inIndex >= 0) {
ByteBuffer inputBuffer = decoder.getInputBuffer(inIndex);
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {
decoder.queueInputBuffer(
inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM
);
inputDone = true;
} else {
long pts = extractor.getSampleTime();
decoder.queueInputBuffer(inIndex, 0, sampleSize, pts, 0);
extractor.advance();
}
}
}
// ---- DRAIN OUTPUT ----
int outIndex = decoder.dequeueOutputBuffer(info, 20000);
if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat fmt = decoder.getOutputFormat();
if (fmt.containsKey(MediaFormat.KEY_COLOR_STANDARD))
colorStandard = fmt.getInteger(MediaFormat.KEY_COLOR_STANDARD);
if (fmt.containsKey(MediaFormat.KEY_COLOR_TRANSFER))
colorTransfer = fmt.getInteger(MediaFormat.KEY_COLOR_TRANSFER);
if (fmt.containsKey(MediaFormat.KEY_COLOR_RANGE))
colorRange = fmt.getInteger(MediaFormat.KEY_COLOR_RANGE);
outputFormatKnown = true;
} else if (outIndex >= 0) {
// We won't render, but must release output buffers
decoder.releaseOutputBuffer(outIndex, false);
}
// If EOS reached and still no format → decoder doesnt provide it
if (inputDone && outIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
break;
}
}
decoder.stop();
decoder.release();
return new ColorInfo(colorStandard, colorTransfer, colorRange);
}
private boolean isHdr(MediaFormat inputVideoFormat) {
if (Build.VERSION.SDK_INT < 24) {
return false;
}
try {
final int colorInfo = inputVideoFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER);
return colorInfo == MediaFormat.COLOR_TRANSFER_ST2084 || colorInfo == MediaFormat.COLOR_TRANSFER_HLG;
} catch (NullPointerException npe) {
// color transfer key does not exist, no color data supplied
return false;
}
}
void setMuxer(final @NonNull Muxer muxer) throws IOException {
mMuxer = muxer;
if (mEncoderOutputVideoFormat != null) {
@@ -562,20 +481,27 @@ final class VideoTrackConverter {
final Pair<MediaCodec, MediaFormat> decoderPair = MediaCodecCompat.findDecoder(inputFormat);
final MediaCodec decoder = decoderPair.getFirst();
// Try to use the Dolby Vision decoder, but if it doesn't support the transfer parameter, the decoded video buffer
// is HLG and in-app tone mapping has to be used instead
if (Build.VERSION.SDK_INT >= 31) {
MediaCodec.ParameterDescriptor descriptor = decoder.getParameterDescriptor(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY);
if (descriptor != null) {
Bundle transferBundle = new Bundle();
transferBundle.putString(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY, "transfer.sdr.normal");
decoder.setParameters(transferBundle);
} else {
decoderPair.getSecond().setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
}
// For HDR video, request SDR tone-mapping from the decoder. Only do this for HDR content
// (PQ or HLG transfer), as some hardware decoders (e.g. Qualcomm HEVC) crash when this is
// set on non-HDR video.
final boolean isHdr = MediaCodecCompat.isHdrVideo(decoderPair.getSecond());
if (Build.VERSION.SDK_INT >= 31 && isHdr) {
decoderPair.getSecond().setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
}
decoder.configure(decoderPair.getSecond(), surface, null, 0);
decoder.start();
if (Build.VERSION.SDK_INT >= 31 && isHdr) {
try {
MediaCodec.ParameterDescriptor descriptor = decoder.getParameterDescriptor(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY);
if (descriptor != null) {
Bundle transferBundle = new Bundle();
transferBundle.putString(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY, "transfer.sdr.normal");
decoder.setParameters(transferBundle);
}
} catch (IllegalStateException e) {
Log.w(TAG, "Failed to set Dolby Vision transfer parameter", e);
}
}
return decoder;
}
@@ -626,16 +552,5 @@ final class VideoTrackConverter {
return MediaConverter.getMimeTypeFor(format).startsWith("video/");
}
private class ColorInfo {
public final Integer colorStandard;
public final Integer colorTransfer;
public final Integer colorRange;
public ColorInfo(Integer colorSpace, Integer transfer, Integer primaries) {
this.colorStandard = colorSpace;
this.colorTransfer = transfer;
this.colorRange = primaries;
}
}
}

View File

@@ -149,4 +149,20 @@ object MediaCodecCompat {
} else {
null
}
/**
* Returns true if the given [MediaFormat] describes an HDR video (PQ or HLG color transfer).
* Some hardware decoders crash when tone-mapping parameters are set on non-HDR video.
*/
@JvmStatic
fun isHdrVideo(format: MediaFormat): Boolean {
return try {
val colorTransfer = format.getInteger(MediaFormat.KEY_COLOR_TRANSFER)
colorTransfer == MediaFormat.COLOR_TRANSFER_ST2084 || colorTransfer == MediaFormat.COLOR_TRANSFER_HLG
} catch (e: NullPointerException) {
false
} catch (e: ClassCastException) {
false
}
}
}