202 lines
8.2 KiB
Java
202 lines
8.2 KiB
Java
package org.thoughtcrime.securesms.video;
|
|
|
|
import android.content.Context;
|
|
import android.media.MediaDataSource;
|
|
import android.media.MediaMetadataRetriever;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.RequiresApi;
|
|
|
|
import com.google.android.exoplayer2.util.MimeTypes;
|
|
|
|
import org.thoughtcrime.securesms.logging.Log;
|
|
import org.thoughtcrime.securesms.mms.MediaStream;
|
|
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
|
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
|
import org.thoughtcrime.securesms.video.videoconverter.BadVideoException;
|
|
import org.thoughtcrime.securesms.video.videoconverter.MediaConverter;
|
|
|
|
import java.io.Closeable;
|
|
import java.io.FileDescriptor;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.text.NumberFormat;
|
|
import java.util.Locale;
|
|
|
|
@RequiresApi(26)
|
|
public final class InMemoryTranscoder implements Closeable {
|
|
|
|
private static final String TAG = Log.tag(InMemoryTranscoder.class);
|
|
|
|
private static final int MAXIMUM_TARGET_VIDEO_BITRATE = 2_000_000;
|
|
private static final int LOW_RES_TARGET_VIDEO_BITRATE = 1_750_000;
|
|
private static final int MINIMUM_TARGET_VIDEO_BITRATE = 500_000;
|
|
private static final int AUDIO_BITRATE = 192_000;
|
|
private static final int OUTPUT_FORMAT = 720;
|
|
private static final int LOW_RES_OUTPUT_FORMAT = 480;
|
|
|
|
private final Context context;
|
|
private final MediaDataSource dataSource;
|
|
private final long upperSizeLimit;
|
|
private final long inSize;
|
|
private final long duration;
|
|
private final int inputBitRate;
|
|
private final int targetVideoBitRate;
|
|
private final long memoryFileEstimate;
|
|
private final boolean transcodeRequired;
|
|
private final long fileSizeEstimate;
|
|
private final int outputFormat;
|
|
|
|
private @Nullable MemoryFileDescriptor memoryFile;
|
|
|
|
/**
|
|
* @param upperSizeLimit A upper size to transcode to. The actual output size can be up to 10% smaller.
|
|
*/
|
|
public InMemoryTranscoder(@NonNull Context context, @NonNull MediaDataSource dataSource, long upperSizeLimit) throws IOException {
|
|
this.context = context;
|
|
this.dataSource = dataSource;
|
|
|
|
final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
|
|
mediaMetadataRetriever.setDataSource(dataSource);
|
|
|
|
long upperSizeLimitWithMargin = (long) (upperSizeLimit / 1.1);
|
|
|
|
this.inSize = dataSource.getSize();
|
|
this.duration = getDuration(mediaMetadataRetriever);
|
|
this.inputBitRate = bitRate(inSize, duration);
|
|
this.targetVideoBitRate = getTargetVideoBitRate(upperSizeLimitWithMargin, duration);
|
|
this.upperSizeLimit = upperSizeLimit;
|
|
|
|
this.transcodeRequired = inputBitRate >= targetVideoBitRate * 1.2 || inSize > upperSizeLimit || containsLocation(mediaMetadataRetriever);
|
|
if (!transcodeRequired) {
|
|
Log.i(TAG, "Video is within 20% of target bitrate, below the size limit and contained no location metadata.");
|
|
}
|
|
|
|
this.fileSizeEstimate = (targetVideoBitRate + AUDIO_BITRATE) * duration / 8000;
|
|
this.memoryFileEstimate = (long) (fileSizeEstimate * 1.1);
|
|
this.outputFormat = targetVideoBitRate < LOW_RES_TARGET_VIDEO_BITRATE
|
|
? LOW_RES_OUTPUT_FORMAT
|
|
: OUTPUT_FORMAT;
|
|
}
|
|
|
|
public @NonNull MediaStream transcode(@NonNull Progress progress) throws IOException, UndeliverableMessageException, BadVideoException {
|
|
if (memoryFile != null) throw new AssertionError("Not expecting to reuse transcoder");
|
|
|
|
float durationSec = duration / 1000f;
|
|
|
|
NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);
|
|
|
|
Log.i(TAG, String.format(Locale.US,
|
|
"Transcoding:\n" +
|
|
"Target bitrate : %s + %s = %s\n" +
|
|
"Target format : %dp\n" +
|
|
"Video duration : %.1fs\n" +
|
|
"Size limit : %s kB\n" +
|
|
"Estimate : %s kB\n" +
|
|
"Input size : %s kB\n" +
|
|
"Input bitrate : %s bps",
|
|
numberFormat.format(targetVideoBitRate),
|
|
numberFormat.format(AUDIO_BITRATE),
|
|
numberFormat.format(targetVideoBitRate + AUDIO_BITRATE),
|
|
outputFormat,
|
|
durationSec,
|
|
numberFormat.format(upperSizeLimit / 1024),
|
|
numberFormat.format(fileSizeEstimate / 1024),
|
|
numberFormat.format(inSize / 1024),
|
|
numberFormat.format(inputBitRate)));
|
|
|
|
if (fileSizeEstimate > upperSizeLimit) {
|
|
throw new UndeliverableMessageException("Size constraints could not be met!");
|
|
}
|
|
|
|
memoryFile = MemoryFileDescriptor.newMemoryFileDescriptor(context,
|
|
"TRANSCODE",
|
|
memoryFileEstimate);
|
|
final long startTime = System.currentTimeMillis();
|
|
|
|
final FileDescriptor memoryFileFileDescriptor = memoryFile.getFileDescriptor();
|
|
|
|
final MediaConverter converter = new MediaConverter();
|
|
|
|
converter.setInput(dataSource);
|
|
converter.setOutput(memoryFileFileDescriptor);
|
|
converter.setVideoResolution(outputFormat);
|
|
converter.setVideoBitrate(targetVideoBitRate);
|
|
converter.setAudioBitrate(AUDIO_BITRATE);
|
|
|
|
converter.setListener(percent -> {
|
|
progress.onProgress(percent);
|
|
return false;
|
|
});
|
|
|
|
converter.convert();
|
|
|
|
// output details of the transcoding
|
|
long outSize = memoryFile.size();
|
|
float encodeDurationSec = (System.currentTimeMillis() - startTime) / 1000f;
|
|
|
|
Log.i(TAG, String.format(Locale.US,
|
|
"Transcoding complete:\n" +
|
|
"Transcode time : %.1fs (%.1fx)\n" +
|
|
"Output size : %s kB\n" +
|
|
" of Original : %.1f%%\n" +
|
|
" of Estimate : %.1f%%\n" +
|
|
" of Memory : %.1f%%\n" +
|
|
"Output bitrate : %s bps",
|
|
encodeDurationSec,
|
|
durationSec / encodeDurationSec,
|
|
numberFormat.format(outSize / 1024),
|
|
(outSize * 100d) / inSize,
|
|
(outSize * 100d) / fileSizeEstimate,
|
|
(outSize * 100d) / memoryFileEstimate,
|
|
numberFormat.format(bitRate(outSize, duration))));
|
|
|
|
if (outSize > upperSizeLimit) {
|
|
throw new UndeliverableMessageException("Size constraints could not be met!");
|
|
}
|
|
|
|
memoryFile.seek(0);
|
|
|
|
return new MediaStream(new FileInputStream(memoryFileFileDescriptor), MimeTypes.VIDEO_MP4, 0, 0);
|
|
}
|
|
|
|
public boolean isTranscodeRequired() {
|
|
return transcodeRequired;
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
if (memoryFile != null) {
|
|
memoryFile.close();
|
|
}
|
|
}
|
|
|
|
private static int bitRate(long bytes, long duration) {
|
|
return (int) (bytes * 8 / (duration / 1000f));
|
|
}
|
|
|
|
private static int getTargetVideoBitRate(long sizeGuideBytes, long duration) {
|
|
sizeGuideBytes -= (duration / 1000d) * AUDIO_BITRATE / 8;
|
|
|
|
double targetAttachmentSizeBits = sizeGuideBytes * 8L;
|
|
|
|
double bitRateToFixTarget = targetAttachmentSizeBits / (duration / 1000d);
|
|
return Math.max(MINIMUM_TARGET_VIDEO_BITRATE, Math.min(MAXIMUM_TARGET_VIDEO_BITRATE, (int) bitRateToFixTarget));
|
|
}
|
|
|
|
private static long getDuration(MediaMetadataRetriever mediaMetadataRetriever) {
|
|
return Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
|
|
}
|
|
|
|
private static boolean containsLocation(MediaMetadataRetriever mediaMetadataRetriever) {
|
|
String locationString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION);
|
|
return locationString != null;
|
|
}
|
|
|
|
public interface Progress {
|
|
|
|
void onProgress(int percent);
|
|
}
|
|
}
|