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") @file:Suppress("UnstableApiUsage")
import com.android.build.api.dsl.ManagedVirtualDevice 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 org.gradle.api.tasks.testing.logging.TestExceptionFormat
import java.io.File
import java.util.Properties import java.util.Properties
plugins { plugins {
@@ -31,8 +21,8 @@ plugins {
apply(from = "static-ips.gradle.kts") apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1651 val canonicalVersionCode = 1651
val canonicalVersionName = "7.74.3" val canonicalVersionName = "7.74.5"
val currentHotfixVersion = 0 val currentHotfixVersion = 2
val maxHotfixVersions = 100 val maxHotfixVersions = 100
// We don't want versions to ever end in 0 so that they don't conflict with nightly versions // 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) int mmsSubscriptionId)
{ {
return new AttachmentCompressionJob(databaseAttachment.attachmentId, return new AttachmentCompressionJob(databaseAttachment.attachmentId,
MediaUtil.isVideo(databaseAttachment) && MediaConstraints.isVideoTranscodeAvailable(), MediaUtil.isVideo(databaseAttachment) && !databaseAttachment.videoGif && MediaConstraints.isVideoTranscodeAvailable(),
mms, mms,
mmsSubscriptionId); mmsSubscriptionId);
} }

View File

@@ -458,8 +458,8 @@ class RegistrationViewModel : ViewModel() {
} }
fun submitCaptchaToken(context: Context) { fun submitCaptchaToken(context: Context) {
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 captcha token.") }
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!") val captchaToken = store.value.captchaToken ?: return bail { Log.w(TAG, "Captcha token was null when trying to submit captcha token.") }
store.update { store.update {
it.copy(captchaToken = null, challengeInProgress = true, inProgress = true) it.copy(captchaToken = null, challengeInProgress = true, inProgress = true)
@@ -486,7 +486,7 @@ class RegistrationViewModel : ViewModel() {
fun requestAndSubmitPushToken(context: Context) { fun requestAndSubmitPushToken(context: Context) {
Log.v(TAG, "validatePushToken()") 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 { viewModelScope.launch {
Log.d(TAG, "Getting session in order to perform push token verification…") Log.d(TAG, "Getting session in order to perform push token verification…")
@@ -1063,6 +1063,22 @@ class RegistrationViewModel : ViewModel() {
setInProgress(false) 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?) { fun registerWithBackupKey(context: Context, backupKey: String, e164: String?, pin: String?, aciIdentityKeyPair: IdentityKeyPair?, pniIdentityKeyPair: IdentityKeyPair?) {
setInProgress(true) setInProgress(true)

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.video.interfaces.MediaInput; import org.thoughtcrime.securesms.video.interfaces.MediaInput;
import org.thoughtcrime.securesms.video.videoconverter.utils.MediaCodecCompat;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
@@ -88,19 +89,25 @@ final class VideoThumbnailsExtractor {
outputSurface = new OutputSurface(outputWidthRotated, outputHeightRotated, true); outputSurface = new OutputSurface(outputWidthRotated, outputHeightRotated, true);
decoder = MediaCodec.createDecoderByType(mime); decoder = MediaCodec.createDecoderByType(mime);
if (Build.VERSION.SDK_INT >= 31) { final boolean isHdr = MediaCodecCompat.isHdrVideo(mediaFormat);
final String VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY = "vendor.dolby.codec.transfer.value"; if (Build.VERSION.SDK_INT >= 31 && isHdr) {
MediaCodec.ParameterDescriptor descriptor = decoder.getParameterDescriptor(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY); mediaFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
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);
}
} }
decoder.configure(mediaFormat, outputSurface.getSurface(), null, 0); decoder.configure(mediaFormat, outputSurface.getSurface(), null, 0);
decoder.start(); 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; long duration = 0;
@@ -217,4 +224,5 @@ final class VideoThumbnailsExtractor {
} }
Log.i(TAG, "doExtract finished"); Log.i(TAG, "doExtract finished");
} }
} }

View File

@@ -150,11 +150,6 @@ final class VideoTrackConverter {
outputHeightRotated = outputHeight; 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); final MediaFormat outputVideoFormat = MediaFormat.createVideoFormat(videoCodec, outputWidthRotated, outputHeightRotated);
// Set some properties. Failing to specify some of these can cause the MediaCodec // 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 { void setMuxer(final @NonNull Muxer muxer) throws IOException {
mMuxer = muxer; mMuxer = muxer;
if (mEncoderOutputVideoFormat != null) { if (mEncoderOutputVideoFormat != null) {
@@ -562,20 +481,27 @@ final class VideoTrackConverter {
final Pair<MediaCodec, MediaFormat> decoderPair = MediaCodecCompat.findDecoder(inputFormat); final Pair<MediaCodec, MediaFormat> decoderPair = MediaCodecCompat.findDecoder(inputFormat);
final MediaCodec decoder = decoderPair.getFirst(); 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 // For HDR video, request SDR tone-mapping from the decoder. Only do this for HDR content
// is HLG and in-app tone mapping has to be used instead // (PQ or HLG transfer), as some hardware decoders (e.g. Qualcomm HEVC) crash when this is
if (Build.VERSION.SDK_INT >= 31) { // set on non-HDR video.
MediaCodec.ParameterDescriptor descriptor = decoder.getParameterDescriptor(VENDOR_DOLBY_CODEC_TRANSFER_PARAMKEY); final boolean isHdr = MediaCodecCompat.isHdrVideo(decoderPair.getSecond());
if (descriptor != null) { if (Build.VERSION.SDK_INT >= 31 && isHdr) {
Bundle transferBundle = new Bundle(); decoderPair.getSecond().setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
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);
}
} }
decoder.configure(decoderPair.getSecond(), surface, null, 0); decoder.configure(decoderPair.getSecond(), surface, null, 0);
decoder.start(); 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; return decoder;
} }
@@ -626,16 +552,5 @@ final class VideoTrackConverter {
return MediaConverter.getMimeTypeFor(format).startsWith("video/"); 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 { } else {
null 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
}
}
} }