mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-17 23:43:34 +01:00
Fix more HDR transcoding errors.
This commit is contained in:
committed by
Cody Henthorne
parent
32dc36d937
commit
caf2e555dd
@@ -18,9 +18,12 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.thoughtcrime.securesms.video.StreamingTranscoder
|
import org.thoughtcrime.securesms.video.StreamingTranscoder
|
||||||
import org.thoughtcrime.securesms.video.TranscodingPreset
|
import org.thoughtcrime.securesms.video.TranscodingPreset
|
||||||
|
import org.thoughtcrime.securesms.video.videoconverter.exceptions.EncodingException
|
||||||
|
import org.thoughtcrime.securesms.video.videoconverter.exceptions.HdrDecoderUnavailableException
|
||||||
import org.thoughtcrime.securesms.video.videoconverter.mediadatasource.InputStreamMediaDataSource
|
import org.thoughtcrime.securesms.video.videoconverter.mediadatasource.InputStreamMediaDataSource
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileNotFoundException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,17 +90,37 @@ class VideoTranscodeInstrumentationTest {
|
|||||||
|
|
||||||
val failures = mutableListOf<String>()
|
val failures = mutableListOf<String>()
|
||||||
|
|
||||||
|
val hdrWarnings = mutableListOf<String>()
|
||||||
|
|
||||||
for (videoFileName in videoFiles) {
|
for (videoFileName in videoFiles) {
|
||||||
Log.i(TAG, "Transcoding '$videoFileName' with preset ${preset.name}...")
|
Log.i(TAG, "Transcoding '$videoFileName' with preset ${preset.name}...")
|
||||||
try {
|
try {
|
||||||
transcodeVideo(videoFileName, preset)
|
transcodeVideo(videoFileName, preset)
|
||||||
Log.i(TAG, "Successfully transcoded '$videoFileName' with preset ${preset.name}")
|
Log.i(TAG, "Successfully transcoded '$videoFileName' with preset ${preset.name}")
|
||||||
|
} catch (e: HdrDecoderUnavailableException) {
|
||||||
|
Log.w(TAG, "No decoder available for HDR video '$videoFileName' with preset ${preset.name} (device limitation)", e)
|
||||||
|
hdrWarnings.add("$videoFileName [${preset.name}]: ${e::class.simpleName}: ${e.message}")
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Log.w(TAG, "Skipping '$videoFileName' with preset ${preset.name}: encoder not available on this device", e)
|
||||||
|
} catch (e: EncodingException) {
|
||||||
|
val isHdrDeviceLimitation = e.isHdrInput && (!e.toneMapApplied || isMediaCodecRuntimeFailure(e))
|
||||||
|
if (isHdrDeviceLimitation) {
|
||||||
|
Log.w(TAG, "HDR video '$videoFileName' failed with preset ${preset.name} (device limitation, toneMap=${e.toneMapApplied}, decoder=${e.decoderName}, encoder=${e.encoderName})", e)
|
||||||
|
hdrWarnings.add("$videoFileName [${preset.name}]: ${e::class.simpleName}: ${e.message}")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to transcode '$videoFileName' with preset ${preset.name} (hdr=${e.isHdrInput}, toneMap=${e.toneMapApplied}, decoder=${e.decoderName}, encoder=${e.encoderName})", e)
|
||||||
|
failures.add("$videoFileName: ${e::class.simpleName}: ${e.message}")
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to transcode '$videoFileName' with preset ${preset.name}", e)
|
Log.e(TAG, "Failed to transcode '$videoFileName' with preset ${preset.name}", e)
|
||||||
failures.add("$videoFileName: ${e::class.simpleName}: ${e.message}")
|
failures.add("$videoFileName: ${e::class.simpleName}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hdrWarnings.isNotEmpty()) {
|
||||||
|
Log.w(TAG, "${hdrWarnings.size} HDR video(s) could not be transcoded (device limitation, not a bug):\n${hdrWarnings.joinToString("\n")}")
|
||||||
|
}
|
||||||
|
|
||||||
if (failures.isNotEmpty()) {
|
if (failures.isNotEmpty()) {
|
||||||
Assert.fail(
|
Assert.fail(
|
||||||
"${failures.size}/${videoFiles.size} video(s) failed transcoding with ${preset.name}:\n" +
|
"${failures.size}/${videoFiles.size} video(s) failed transcoding with ${preset.name}:\n" +
|
||||||
@@ -106,6 +129,24 @@ class VideoTranscodeInstrumentationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the root cause of an exception is an [IllegalStateException] from
|
||||||
|
* [android.media.MediaCodec] native methods. This indicates the hardware codec crashed
|
||||||
|
* during processing, which for HDR content is a device limitation (not a Signal bug).
|
||||||
|
*/
|
||||||
|
private fun isMediaCodecRuntimeFailure(e: Exception): Boolean {
|
||||||
|
var cause: Throwable? = e.cause
|
||||||
|
while (cause != null) {
|
||||||
|
if (cause is IllegalStateException &&
|
||||||
|
cause.stackTrace.any { it.className == "android.media.MediaCodec" }
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
cause = cause.cause
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun transcodeVideo(videoFileName: String, preset: TranscodingPreset) {
|
private fun transcodeVideo(videoFileName: String, preset: TranscodingPreset) {
|
||||||
val inputFile = createTempFile("input-", "-$videoFileName")
|
val inputFile = createTempFile("input-", "-$videoFileName")
|
||||||
val outputFile = createTempFile("output-${preset.name}-", "-$videoFileName")
|
val outputFile = createTempFile("output-${preset.name}-", "-$videoFileName")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package org.thoughtcrime.securesms.video.videoconverter;
|
|||||||
|
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaCodecList;
|
import android.media.MediaCodecList;
|
||||||
|
import android.media.MediaExtractor;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -33,6 +34,7 @@ import org.thoughtcrime.securesms.video.interfaces.MediaInput;
|
|||||||
import org.thoughtcrime.securesms.video.interfaces.Muxer;
|
import org.thoughtcrime.securesms.video.interfaces.Muxer;
|
||||||
import org.thoughtcrime.securesms.video.videoconverter.exceptions.EncodingException;
|
import org.thoughtcrime.securesms.video.videoconverter.exceptions.EncodingException;
|
||||||
import org.thoughtcrime.securesms.video.videoconverter.muxer.StreamingMuxer;
|
import org.thoughtcrime.securesms.video.videoconverter.muxer.StreamingMuxer;
|
||||||
|
import org.thoughtcrime.securesms.video.videoconverter.utils.MediaCodecCompat;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
@@ -226,7 +228,33 @@ public final class MediaConverter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
throw new EncodingException("Transcode failed", exception);
|
EncodingException encodingException = new EncodingException("Transcode failed", exception);
|
||||||
|
if (videoTrackConverter != null) {
|
||||||
|
encodingException.isHdrInput = videoTrackConverter.isHdrInput();
|
||||||
|
encodingException.toneMapApplied = videoTrackConverter.isToneMapApplied();
|
||||||
|
encodingException.decoderName = videoTrackConverter.getDecoderName();
|
||||||
|
encodingException.encoderName = videoTrackConverter.getEncoderName();
|
||||||
|
} else {
|
||||||
|
// VideoTrackConverter failed during creation. Try to determine HDR
|
||||||
|
// status independently so the caller can classify the failure.
|
||||||
|
try {
|
||||||
|
final MediaExtractor extractor = mInput.createExtractor();
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < extractor.getTrackCount(); i++) {
|
||||||
|
final MediaFormat format = extractor.getTrackFormat(i);
|
||||||
|
if (getMimeTypeFor(format).startsWith("video/")) {
|
||||||
|
encodingException.isHdrInput = MediaCodecCompat.isHdrVideo(format);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
extractor.release();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Could not determine HDR status for failed transcode", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw encodingException;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mdatContentLength;
|
return mdatContentLength;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.annotation.Nullable;
|
|||||||
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.interfaces.Muxer;
|
import org.thoughtcrime.securesms.video.interfaces.Muxer;
|
||||||
|
import org.thoughtcrime.securesms.video.videoconverter.exceptions.HdrDecoderUnavailableException;
|
||||||
import org.thoughtcrime.securesms.video.videoconverter.utils.Extensions;
|
import org.thoughtcrime.securesms.video.videoconverter.utils.Extensions;
|
||||||
import org.thoughtcrime.securesms.video.videoconverter.utils.MediaCodecCompat;
|
import org.thoughtcrime.securesms.video.videoconverter.utils.MediaCodecCompat;
|
||||||
import org.thoughtcrime.securesms.video.videoconverter.utils.Preconditions;
|
import org.thoughtcrime.securesms.video.videoconverter.utils.Preconditions;
|
||||||
@@ -38,6 +39,11 @@ final class VideoTrackConverter {
|
|||||||
|
|
||||||
private static final float FRAME_RATE_TOLERANCE = 0.05f; // tolerance for transcoding VFR -> CFR
|
private static final float FRAME_RATE_TOLERANCE = 0.05f; // tolerance for transcoding VFR -> CFR
|
||||||
|
|
||||||
|
private boolean mIsHdrInput;
|
||||||
|
private boolean mToneMapApplied;
|
||||||
|
private String mDecoderName;
|
||||||
|
private String mEncoderName;
|
||||||
|
|
||||||
private final long mTimeFrom;
|
private final long mTimeFrom;
|
||||||
private final long mTimeTo;
|
private final long mTimeTo;
|
||||||
|
|
||||||
@@ -421,6 +427,11 @@ final class VideoTrackConverter {
|
|||||||
Preconditions.checkState("decoded frame count should be less than extracted frame count", mVideoDecodedFrameCount <= mVideoExtractedFrameCount);
|
Preconditions.checkState("decoded frame count should be less than extracted frame count", mVideoDecodedFrameCount <= mVideoExtractedFrameCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isHdrInput() { return mIsHdrInput; }
|
||||||
|
boolean isToneMapApplied() { return mToneMapApplied; }
|
||||||
|
String getDecoderName() { return mDecoderName; }
|
||||||
|
String getEncoderName() { return mEncoderName; }
|
||||||
|
|
||||||
private static String createFragmentShader(
|
private static String createFragmentShader(
|
||||||
final int srcWidth,
|
final int srcWidth,
|
||||||
final int srcHeight,
|
final int srcHeight,
|
||||||
@@ -473,31 +484,53 @@ final class VideoTrackConverter {
|
|||||||
MediaCodec createVideoDecoder(
|
MediaCodec createVideoDecoder(
|
||||||
final @NonNull MediaFormat inputFormat,
|
final @NonNull MediaFormat inputFormat,
|
||||||
final @NonNull Surface surface) throws IOException {
|
final @NonNull Surface surface) throws IOException {
|
||||||
final boolean isHdr = MediaCodecCompat.isHdrVideo(inputFormat);
|
final boolean isHdr = MediaCodecCompat.isHdrVideo(inputFormat);
|
||||||
|
final boolean requestToneMapping = Build.VERSION.SDK_INT >= 31 && isHdr;
|
||||||
final List<Pair<String, MediaFormat>> candidates = MediaCodecCompat.findDecoderCandidates(inputFormat);
|
final List<Pair<String, MediaFormat>> candidates = MediaCodecCompat.findDecoderCandidates(inputFormat);
|
||||||
|
|
||||||
|
mIsHdrInput = isHdr;
|
||||||
Exception lastException = null;
|
Exception lastException = null;
|
||||||
|
|
||||||
for (int i = 0; i < candidates.size(); i++) {
|
for (int i = 0; i < candidates.size(); i++) {
|
||||||
final Pair<String, MediaFormat> candidate = candidates.get(i);
|
final Pair<String, MediaFormat> candidate = candidates.get(i);
|
||||||
final String codecName = candidate.getFirst();
|
final String codecName = candidate.getFirst();
|
||||||
final MediaFormat decoderFormat = candidate.getSecond();
|
final MediaFormat baseFormat = candidate.getSecond();
|
||||||
MediaCodec decoder = null;
|
MediaCodec decoder = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
decoder = MediaCodec.createByCodecName(codecName);
|
decoder = MediaCodec.createByCodecName(codecName);
|
||||||
|
|
||||||
// For HDR video, request SDR tone-mapping from the decoder (API 31+).
|
// For HDR video on API 31+, try requesting SDR tone-mapping.
|
||||||
if (Build.VERSION.SDK_INT >= 31 && isHdr) {
|
// Some codecs reject this key, so we catch the error and retry without it.
|
||||||
decoderFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
|
if (requestToneMapping) {
|
||||||
|
try {
|
||||||
|
final MediaFormat toneMapFormat = new MediaFormat(baseFormat);
|
||||||
|
toneMapFormat.setInteger(MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
|
||||||
|
decoder.configure(toneMapFormat, surface, null, 0);
|
||||||
|
decoder.start();
|
||||||
|
|
||||||
|
mToneMapApplied = isToneMapEffective(decoder, codecName);
|
||||||
|
mDecoderName = codecName;
|
||||||
|
if (i > 0) {
|
||||||
|
Log.w(TAG, "Video decoder: succeeded with fallback codec " + codecName + " (attempt " + (i + 1) + " of " + candidates.size() + ")");
|
||||||
|
}
|
||||||
|
return decoder;
|
||||||
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
|
Log.w(TAG, "Video decoder: codec " + codecName + " rejected tone-mapping request, retrying without (attempt " + (i + 1) + " of " + candidates.size() + ")", e);
|
||||||
|
decoder.release();
|
||||||
|
decoder = MediaCodec.createByCodecName(codecName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
decoder.configure(decoderFormat, surface, null, 0);
|
|
||||||
|
decoder.configure(baseFormat, surface, null, 0);
|
||||||
decoder.start();
|
decoder.start();
|
||||||
|
|
||||||
if (i > 0) {
|
mDecoderName = codecName;
|
||||||
Log.w(TAG, "Video decoder: succeeded with fallback codec " + codecName + " (attempt " + (i + 1) + " of " + candidates.size() + ")");
|
if (i > 0 || requestToneMapping) {
|
||||||
|
Log.w(TAG, "Video decoder: succeeded with codec " + codecName + (requestToneMapping ? " (no tone-mapping)" : "") + " (attempt " + (i + 1) + " of " + candidates.size() + ")");
|
||||||
}
|
}
|
||||||
return decoder;
|
return decoder;
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
Log.w(TAG, "Video decoder: codec " + codecName + " failed (attempt " + (i + 1) + " of " + candidates.size() + ")", e);
|
Log.w(TAG, "Video decoder: codec " + codecName + " failed (attempt " + (i + 1) + " of " + candidates.size() + ")", e);
|
||||||
lastException = e;
|
lastException = e;
|
||||||
if (decoder != null) {
|
if (decoder != null) {
|
||||||
@@ -509,6 +542,9 @@ final class VideoTrackConverter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mIsHdrInput) {
|
||||||
|
throw new HdrDecoderUnavailableException("All video decoder codecs failed for HDR video", lastException);
|
||||||
|
}
|
||||||
throw new IOException("All video decoder codecs failed", lastException);
|
throw new IOException("All video decoder codecs failed", lastException);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,7 +553,7 @@ final class VideoTrackConverter {
|
|||||||
* {@link MediaCodec#createInputSurface()} (between configure and start) and then
|
* {@link MediaCodec#createInputSurface()} (between configure and start) and then
|
||||||
* {@link MediaCodec#start()} after the decoder has been created.
|
* {@link MediaCodec#start()} after the decoder has been created.
|
||||||
*/
|
*/
|
||||||
private static @NonNull
|
private @NonNull
|
||||||
MediaCodec createVideoEncoder(
|
MediaCodec createVideoEncoder(
|
||||||
final @NonNull List<MediaCodecInfo> codecCandidates,
|
final @NonNull List<MediaCodecInfo> codecCandidates,
|
||||||
final @NonNull MediaFormat format) throws IOException {
|
final @NonNull MediaFormat format) throws IOException {
|
||||||
@@ -530,11 +566,12 @@ final class VideoTrackConverter {
|
|||||||
try {
|
try {
|
||||||
encoder = MediaCodec.createByCodecName(codecInfo.getName());
|
encoder = MediaCodec.createByCodecName(codecInfo.getName());
|
||||||
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
|
mEncoderName = codecInfo.getName();
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
Log.w(TAG, "Video encoder: succeeded with fallback codec " + codecInfo.getName() + " (attempt " + (i + 1) + " of " + codecCandidates.size() + ")");
|
Log.w(TAG, "Video encoder: succeeded with fallback codec " + codecInfo.getName() + " (attempt " + (i + 1) + " of " + codecCandidates.size() + ")");
|
||||||
}
|
}
|
||||||
return encoder;
|
return encoder;
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
Log.w(TAG, "Video encoder: codec " + codecInfo.getName() + " failed (attempt " + (i + 1) + " of " + codecCandidates.size() + ")", e);
|
Log.w(TAG, "Video encoder: codec " + codecInfo.getName() + " failed (attempt " + (i + 1) + " of " + codecCandidates.size() + ")", e);
|
||||||
lastException = e;
|
lastException = e;
|
||||||
if (encoder != null) {
|
if (encoder != null) {
|
||||||
@@ -563,5 +600,37 @@ final class VideoTrackConverter {
|
|||||||
return MediaConverter.getMimeTypeFor(format).startsWith("video/");
|
return MediaConverter.getMimeTypeFor(format).startsWith("video/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether HDR-to-SDR tone-mapping is effective after the decoder has been configured
|
||||||
|
* and started with {@link MediaFormat#KEY_COLOR_TRANSFER_REQUEST}. Some codecs (especially
|
||||||
|
* software decoders and some hardware decoders) accept the tone-mapping key without error
|
||||||
|
* but don't actually perform the conversion.
|
||||||
|
*/
|
||||||
|
private static boolean isToneMapEffective(final @NonNull MediaCodec decoder, final @NonNull String codecName) {
|
||||||
|
// Software codecs never perform HDR→SDR tone-mapping.
|
||||||
|
String lower = codecName.toLowerCase(java.util.Locale.ROOT);
|
||||||
|
if (lower.startsWith("omx.google.") || lower.startsWith("c2.android.")) {
|
||||||
|
Log.w(TAG, "Video decoder: software codec " + codecName + " cannot perform HDR tone-mapping");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For hardware codecs, verify the output format. If the output transfer function
|
||||||
|
// is still HDR (ST2084 or HLG), the decoder accepted the request but isn't honoring it.
|
||||||
|
try {
|
||||||
|
MediaFormat outputFormat = decoder.getOutputFormat();
|
||||||
|
if (outputFormat.containsKey(MediaFormat.KEY_COLOR_TRANSFER)) {
|
||||||
|
int transfer = outputFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER);
|
||||||
|
if (transfer == MediaFormat.COLOR_TRANSFER_ST2084 || transfer == MediaFormat.COLOR_TRANSFER_HLG) {
|
||||||
|
Log.w(TAG, "Video decoder: codec " + codecName + " accepted tone-mapping but output transfer is " + transfer + " (still HDR)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Video decoder: could not verify tone-mapping for codec " + codecName, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
package org.thoughtcrime.securesms.video.videoconverter.exceptions
|
package org.thoughtcrime.securesms.video.videoconverter.exceptions
|
||||||
|
|
||||||
class EncodingException : Exception {
|
class EncodingException : Exception {
|
||||||
|
/** Whether the input video was HDR content. */
|
||||||
|
@JvmField var isHdrInput: Boolean = false
|
||||||
|
/** Whether HDR-to-SDR tone-mapping was successfully applied to the decoder. */
|
||||||
|
@JvmField var toneMapApplied: Boolean = false
|
||||||
|
/** The name of the video decoder codec that was selected, or null if decoder creation failed. */
|
||||||
|
@JvmField var decoderName: String? = null
|
||||||
|
/** The name of the video encoder codec that was selected, or null if encoder creation failed. */
|
||||||
|
@JvmField var encoderName: String? = null
|
||||||
|
|
||||||
constructor(message: String?) : super(message)
|
constructor(message: String?) : super(message)
|
||||||
constructor(message: String?, inner: Exception?) : super(message, inner)
|
constructor(message: String?, inner: Exception?) : super(message, inner)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package org.thoughtcrime.securesms.video.videoconverter.exceptions
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when no decoder on the device can properly decode HDR video content.
|
||||||
|
* This is typically a device limitation, not a bug.
|
||||||
|
*/
|
||||||
|
class HdrDecoderUnavailableException(message: String, cause: Throwable?) : IOException(message, cause)
|
||||||
@@ -266,6 +266,26 @@ object MediaCodecCompat {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On older APIs, the extractor may not populate KEY_COLOR_TRANSFER or HDR metadata keys.
|
||||||
|
// Check the HEVC profile as a fallback: Main 10 and its HDR variants indicate 10-bit
|
||||||
|
// content that typically requires HDR-capable decoders.
|
||||||
|
val mime = try { format.getString(MediaFormat.KEY_MIME) } catch (_: Exception) { null }
|
||||||
|
if (mime.equals("video/hevc", ignoreCase = true)) {
|
||||||
|
try {
|
||||||
|
val profile = format.getInteger(MediaFormat.KEY_PROFILE)
|
||||||
|
if (profile == CodecProfileLevel.HEVCProfileMain10 ||
|
||||||
|
profile == CodecProfileLevel.HEVCProfileMain10HDR10 ||
|
||||||
|
profile == CodecProfileLevel.HEVCProfileMain10HDR10Plus
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (_: NullPointerException) {
|
||||||
|
// key doesn't exist
|
||||||
|
} catch (_: ClassCastException) {
|
||||||
|
// key exists but wrong type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user