kopia lustrzana https://github.com/ryukoposting/Signal-Android
Display video file output size and duration during clipping.
Prevent video upscale, i.e. use input bit rate if lower than our normal target rates. Do not time limit videos that are under the send file size. Increase time limit to 10 minutes to match our lowest acceptable bitrate.fork-5.53.8
rodzic
abb1ca2afe
commit
89f2c25d73
|
@ -256,7 +256,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
} else if (!Util.isEmpty(media)) {
|
||||
viewModel.onSelectedMediaChanged(this, media);
|
||||
|
||||
Fragment fragment = MediaSendFragment.newInstance(Locale.getDefault());
|
||||
Fragment fragment = MediaSendFragment.newInstance();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||
.commit();
|
||||
|
@ -306,7 +306,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
sendButton.setTransport(transport);
|
||||
sendButton.disableTransport(transport.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
|
||||
|
||||
countButton.setOnClickListener(v -> navigateToMediaSend(Locale.getDefault()));
|
||||
countButton.setOnClickListener(v -> navigateToMediaSend());
|
||||
|
||||
composeText.append(viewModel.getBody());
|
||||
|
||||
|
@ -368,7 +368,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
@Override
|
||||
public void onMediaSelected(@NonNull Media media) {
|
||||
viewModel.onSingleMediaSelected(this, media);
|
||||
navigateToMediaSend(Locale.getDefault());
|
||||
navigateToMediaSend();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -458,7 +458,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
||||
|
||||
viewModel.onMediaCaptured(media);
|
||||
navigateToMediaSend(Locale.getDefault());
|
||||
navigateToMediaSend();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -469,7 +469,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
|
||||
@Override
|
||||
public void onCameraCountButtonClicked() {
|
||||
navigateToMediaSend(Locale.getDefault());
|
||||
navigateToMediaSend();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -841,8 +841,8 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
|
||||
}
|
||||
|
||||
private void navigateToMediaSend(@NonNull Locale locale) {
|
||||
MediaSendFragment fragment = MediaSendFragment.newInstance(locale);
|
||||
private void navigateToMediaSend() {
|
||||
MediaSendFragment fragment = MediaSendFragment.newInstance();
|
||||
String backstackTag = null;
|
||||
|
||||
if (getSupportFragmentManager().findFragmentByTag(TAG_SEND) != null) {
|
||||
|
|
|
@ -14,10 +14,10 @@ import androidx.viewpager.widget.ViewPager;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ControllableViewPager;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -25,20 +25,14 @@ import java.util.Map;
|
|||
*/
|
||||
public class MediaSendFragment extends Fragment {
|
||||
|
||||
private static final String TAG = MediaSendFragment.class.getSimpleName();
|
||||
|
||||
private static final String KEY_LOCALE = "locale";
|
||||
|
||||
private ViewGroup playbackControlsContainer;
|
||||
private ControllableViewPager fragmentPager;
|
||||
private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
|
||||
|
||||
private MediaSendViewModel viewModel;
|
||||
|
||||
|
||||
public static MediaSendFragment newInstance(@NonNull Locale locale) {
|
||||
public static MediaSendFragment newInstance() {
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(KEY_LOCALE, locale);
|
||||
|
||||
MediaSendFragment fragment = new MediaSendFragment();
|
||||
fragment.setArguments(args);
|
||||
|
@ -61,8 +55,7 @@ public class MediaSendFragment extends Fragment {
|
|||
fragmentPager = view.findViewById(R.id.mediasend_pager);
|
||||
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container);
|
||||
|
||||
|
||||
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager());
|
||||
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager(), viewModel.isSms() ? MediaConstraints.getMmsMediaConstraints(-1) : MediaConstraints.getPushMediaConstraints());
|
||||
fragmentPager.setAdapter(fragmentPagerAdapter);
|
||||
|
||||
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.net.http.LoggingEventHandler;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
|
@ -12,6 +13,10 @@ import androidx.fragment.app.FragmentStatePagerAdapter;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
|
@ -25,12 +30,14 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
|
|||
private final List<Media> media;
|
||||
private final Map<Integer, MediaSendPageFragment> fragments;
|
||||
private final Map<Uri, Object> savedState;
|
||||
private final MediaConstraints mediaConstraints;
|
||||
|
||||
MediaSendFragmentPagerAdapter(@NonNull FragmentManager fm) {
|
||||
MediaSendFragmentPagerAdapter(@NonNull FragmentManager fm, @NonNull MediaConstraints mediaConstraints) {
|
||||
super(fm);
|
||||
this.media = new ArrayList<>();
|
||||
this.fragments = new HashMap<>();
|
||||
this.savedState = new HashMap<>();
|
||||
this.mediaConstraints = mediaConstraints;
|
||||
this.media = new ArrayList<>();
|
||||
this.fragments = new HashMap<>();
|
||||
this.savedState = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -42,7 +49,9 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
|
|||
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
|
||||
return ImageEditorFragment.newInstance(mediaItem.getUri());
|
||||
} else if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
return MediaSendVideoFragment.newInstance(mediaItem.getUri());
|
||||
return MediaSendVideoFragment.newInstance(mediaItem.getUri(),
|
||||
mediaConstraints.getCompressedVideoMaxSize(ApplicationDependencies.getApplication()),
|
||||
mediaConstraints.getVideoMaxSize(ApplicationDependencies.getApplication()));
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Can only render images and videos. Found mimetype: '" + mediaItem.getMimeType() + "'");
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
|
|||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.scribbles.VideoEditorHud;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
import org.thoughtcrime.securesms.video.VideoBitRateCalculator;
|
||||
import org.thoughtcrime.securesms.video.VideoPlayer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -28,7 +29,9 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
|
|||
|
||||
private static final String TAG = Log.tag(MediaSendVideoFragment.class);
|
||||
|
||||
private static final String KEY_URI = "uri";
|
||||
private static final String KEY_URI = "uri";
|
||||
private static final String KEY_MAX_OUTPUT = "max_output_size";
|
||||
private static final String KEY_MAX_SEND = "max_send_size";
|
||||
|
||||
private final Throttler videoScanThrottle = new Throttler(150);
|
||||
private final Handler handler = new Handler();
|
||||
|
@ -40,9 +43,11 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
|
|||
@Nullable private VideoEditorHud hud;
|
||||
private Runnable updatePosition;
|
||||
|
||||
public static MediaSendVideoFragment newInstance(@NonNull Uri uri) {
|
||||
public static MediaSendVideoFragment newInstance(@NonNull Uri uri, long maxCompressedVideoSize, long maxAttachmentSize) {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(KEY_URI, uri);
|
||||
args.putLong(KEY_MAX_OUTPUT, maxCompressedVideoSize);
|
||||
args.putLong(KEY_MAX_SEND, maxAttachmentSize);
|
||||
|
||||
MediaSendVideoFragment fragment = new MediaSendVideoFragment();
|
||||
fragment.setArguments(args);
|
||||
|
@ -71,7 +76,9 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
|
|||
player = view.findViewById(R.id.video_player);
|
||||
|
||||
uri = requireArguments().getParcelable(KEY_URI);
|
||||
VideoSlide slide = new VideoSlide(requireContext(), uri, 0);
|
||||
long maxOutput = requireArguments().getLong(KEY_MAX_OUTPUT);
|
||||
long maxSend = requireArguments().getLong(KEY_MAX_SEND);
|
||||
VideoSlide slide = new VideoSlide(requireContext(), uri, 0);
|
||||
|
||||
player.setWindow(requireActivity().getWindow());
|
||||
player.setVideoSource(slide, true);
|
||||
|
@ -84,7 +91,7 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
|
|||
player.clip(data.startTimeUs, data.endTimeUs, true);
|
||||
}
|
||||
try {
|
||||
hud.setVideoSource(slide);
|
||||
hud.setVideoSource(slide, new VideoBitRateCalculator(maxOutput), maxSend);
|
||||
hud.setVisibility(View.VISIBLE);
|
||||
startPositionUpdates();
|
||||
} catch (IOException e) {
|
||||
|
|
|
@ -672,6 +672,10 @@ class MediaSendViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
boolean isSms() {
|
||||
return transport.isSms();
|
||||
}
|
||||
|
||||
enum Error {
|
||||
ITEM_TOO_LARGE, TOO_MANY_ITEMS, NO_ITEMS, ONLY_ITEM_TOO_LARGE
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.scribbles;
|
|||
|
||||
import android.animation.Animator;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
|
@ -16,6 +18,8 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.video.VideoBitRateCalculator;
|
||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||
import org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView;
|
||||
|
||||
|
@ -64,17 +68,23 @@ public final class VideoEditorHud extends LinearLayout {
|
|||
}
|
||||
|
||||
@RequiresApi(api = 23)
|
||||
public void setVideoSource(VideoSlide slide) throws IOException {
|
||||
public void setVideoSource(@NonNull VideoSlide slide, @NonNull VideoBitRateCalculator videoBitRateCalculator, long maxSendSize)
|
||||
throws IOException
|
||||
{
|
||||
Uri uri = slide.getUri();
|
||||
|
||||
if (uri == null || !slide.hasVideo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoTimeLine.setTimeLimit(VideoUtil.getMaxVideoUploadDurationInSeconds(), TimeUnit.SECONDS);
|
||||
|
||||
videoTimeLine.setInput(DecryptableUriMediaInput.createForUri(getContext(), uri));
|
||||
|
||||
long size = tryGetUriSize(getContext(), uri, Long.MAX_VALUE);
|
||||
|
||||
if (size > maxSendSize) {
|
||||
videoTimeLine.setTimeLimit(VideoUtil.getMaxVideoUploadDurationInSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
videoTimeLine.setOnRangeChangeListener(new VideoThumbnailsRangeSelectorView.OnRangeChangeListener() {
|
||||
|
||||
@Override
|
||||
|
@ -92,18 +102,26 @@ public final class VideoEditorHud extends LinearLayout {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onRangeDrag(long minValue, long maxValue, long duration, VideoThumbnailsRangeSelectorView.Thumb thumb) {
|
||||
public void onRangeDrag(long minValueUs, long maxValueUs, long durationUs, VideoThumbnailsRangeSelectorView.Thumb thumb) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onEditVideoDuration(duration, minValue, maxValue, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, false);
|
||||
eventListener.onEditVideoDuration(durationUs, minValueUs, maxValueUs, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRangeDragEnd(long minValue, long maxValue, long duration, VideoThumbnailsRangeSelectorView.Thumb thumb) {
|
||||
public void onRangeDragEnd(long minValueUs, long maxValueUs, long durationUs, VideoThumbnailsRangeSelectorView.Thumb thumb) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onEditVideoDuration(duration, minValue, maxValue, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, true);
|
||||
eventListener.onEditVideoDuration(durationUs, minValueUs, maxValueUs, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoThumbnailsRangeSelectorView.Quality getQuality(long clipDurationUs, long totalDurationUs) {
|
||||
int inputBitRate = VideoBitRateCalculator.bitRate(size, TimeUnit.MICROSECONDS.toMillis(totalDurationUs));
|
||||
|
||||
VideoBitRateCalculator.Quality targetQuality = videoBitRateCalculator.getTargetQuality(TimeUnit.MICROSECONDS.toMillis(clipDurationUs), inputBitRate);
|
||||
return new VideoThumbnailsRangeSelectorView.Quality(targetQuality.getFileSizeEstimate(), (int) (100 * targetQuality.getQuality()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -164,4 +182,29 @@ public final class VideoEditorHud extends LinearLayout {
|
|||
|
||||
void onSeek(long position, boolean dragComplete);
|
||||
}
|
||||
|
||||
private long tryGetUriSize(@NonNull Context context, @NonNull Uri uri, long defaultValue) {
|
||||
try {
|
||||
return getSize(context, uri);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static long getSize(@NonNull Context context, @NonNull Uri uri) throws IOException {
|
||||
long size = 0;
|
||||
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.SIZE) >= 0) {
|
||||
size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
|
||||
}
|
||||
}
|
||||
|
||||
if (size <= 0) {
|
||||
size = MediaUtil.getMediaSize(context, uri);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
/**
|
||||
* Used for the pretty formatting of bytes for user display.
|
||||
*/
|
||||
public enum MemoryUnitFormat {
|
||||
BYTES(" B"),
|
||||
KILO_BYTES(" kB"),
|
||||
MEGA_BYTES(" MB"),
|
||||
GIGA_BYTES(" GB"),
|
||||
TERA_BYTES(" TB");
|
||||
|
||||
private static final DecimalFormat ONE_DP = new DecimalFormat("#,##0.0");
|
||||
private static final DecimalFormat OPTIONAL_ONE_DP = new DecimalFormat("#,##0.#");
|
||||
|
||||
private final String unitString;
|
||||
|
||||
MemoryUnitFormat(String unitString) {
|
||||
this.unitString = unitString;
|
||||
}
|
||||
|
||||
public double fromBytes(long bytes) {
|
||||
return bytes / Math.pow(1000, ordinal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string suitable to present to the user from the specified {@param bytes}.
|
||||
* It will pick a suitable unit of measure to display depending on the size of the bytes.
|
||||
* It will not select a unit of measure lower than the specified {@param minimumUnit}.
|
||||
*
|
||||
* @param forceOneDp If true, will include 1 decimal place, even if 0. If false, will only show 1 dp when it's non-zero.
|
||||
*/
|
||||
public static String formatBytes(long bytes, @NonNull MemoryUnitFormat minimumUnit, boolean forceOneDp) {
|
||||
if (bytes <= 0) bytes = 0;
|
||||
|
||||
int ordinal = bytes != 0 ? (int) (Math.log10(bytes) / 3) : 0;
|
||||
|
||||
if (ordinal >= MemoryUnitFormat.values().length) {
|
||||
ordinal = MemoryUnitFormat.values().length - 1;
|
||||
}
|
||||
|
||||
MemoryUnitFormat unit = MemoryUnitFormat.values()[ordinal];
|
||||
|
||||
if (unit.ordinal() < minimumUnit.ordinal()) {
|
||||
unit = minimumUnit;
|
||||
}
|
||||
|
||||
return (forceOneDp ? ONE_DP : OPTIONAL_ONE_DP).format(unit.fromBytes(bytes)) + unit.unitString;
|
||||
}
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
return formatBytes(bytes, BYTES, false);
|
||||
}
|
||||
}
|
|
@ -62,7 +62,6 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
@ -625,12 +624,7 @@ public class Util {
|
|||
}
|
||||
|
||||
public static String getPrettyFileSize(long sizeBytes) {
|
||||
if (sizeBytes <= 0) return "0";
|
||||
|
||||
String[] units = new String[]{"B", "kB", "MB", "GB", "TB"};
|
||||
int digitGroups = (int) (Math.log10(sizeBytes) / 3);
|
||||
|
||||
return new DecimalFormat("#,##0.#").format(sizeBytes/Math.pow(1000, digitGroups)) + " " + units[digitGroups];
|
||||
return MemoryUnitFormat.formatBytes(sizeBytes);
|
||||
}
|
||||
|
||||
public static void sleep(long millis) {
|
||||
|
|
|
@ -11,11 +11,11 @@ import androidx.annotation.RequiresApi;
|
|||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.media.MediaInput;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
||||
import org.thoughtcrime.securesms.video.videoconverter.EncodingException;
|
||||
import org.thoughtcrime.securesms.video.videoconverter.MediaConverter;
|
||||
import org.thoughtcrime.securesms.media.MediaInput;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileDescriptor;
|
||||
|
@ -29,25 +29,17 @@ public final class InMemoryTranscoder implements Closeable {
|
|||
|
||||
private static final String TAG = Log.tag(InMemoryTranscoder.class);
|
||||
|
||||
private static final int MAXIMUM_TARGET_VIDEO_BITRATE = VideoUtil.VIDEO_BIT_RATE;
|
||||
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 = VideoUtil.AUDIO_BIT_RATE;
|
||||
private static final int OUTPUT_FORMAT = VideoUtil.VIDEO_SHORT_WIDTH;
|
||||
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 final @Nullable Options options;
|
||||
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 VideoBitRateCalculator.Quality targetQuality;
|
||||
private final long memoryFileEstimate;
|
||||
private final boolean transcodeRequired;
|
||||
private final long fileSizeEstimate;
|
||||
private final @Nullable Options options;
|
||||
|
||||
private @Nullable MemoryFileDescriptor memoryFile;
|
||||
|
||||
|
@ -67,24 +59,19 @@ public final class InMemoryTranscoder implements Closeable {
|
|||
throw new VideoSourceException("Unable to read datasource", e);
|
||||
}
|
||||
|
||||
long upperSizeLimitWithMargin = (long) (upperSizeLimit / 1.1);
|
||||
this.inSize = dataSource.getSize();
|
||||
this.duration = getDuration(mediaMetadataRetriever);
|
||||
this.inputBitRate = VideoBitRateCalculator.bitRate(inSize, duration);
|
||||
this.targetQuality = new VideoBitRateCalculator(upperSizeLimit).getTargetQuality(duration, inputBitRate);
|
||||
this.upperSizeLimit = upperSizeLimit;
|
||||
|
||||
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) || options != null;
|
||||
this.transcodeRequired = inputBitRate >= targetQuality.getTargetTotalBitRate() * 1.2 || inSize > upperSizeLimit || containsLocation(mediaMetadataRetriever) || options != null;
|
||||
if (!transcodeRequired) {
|
||||
Log.i(TAG, "Video is within 20% of target bitrate, below the size limit, contained no location metadata or custom options.");
|
||||
}
|
||||
|
||||
this.fileSizeEstimate = (targetVideoBitRate + AUDIO_BITRATE) * duration / 8000;
|
||||
this.fileSizeEstimate = targetQuality.getFileSizeEstimate();
|
||||
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,
|
||||
|
@ -106,10 +93,10 @@ public final class InMemoryTranscoder implements Closeable {
|
|||
"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,
|
||||
numberFormat.format(targetQuality.getTargetVideoBitRate()),
|
||||
numberFormat.format(targetQuality.getTargetAudioBitRate()),
|
||||
numberFormat.format(targetQuality.getTargetTotalBitRate()),
|
||||
targetQuality.getOutputResolution(),
|
||||
durationSec,
|
||||
numberFormat.format(upperSizeLimit / 1024),
|
||||
numberFormat.format(fileSizeEstimate / 1024),
|
||||
|
@ -131,9 +118,9 @@ public final class InMemoryTranscoder implements Closeable {
|
|||
|
||||
converter.setInput(new MediaInput.MediaDataSourceMediaInput(dataSource));
|
||||
converter.setOutput(memoryFileFileDescriptor);
|
||||
converter.setVideoResolution(outputFormat);
|
||||
converter.setVideoBitrate(targetVideoBitRate);
|
||||
converter.setAudioBitrate(AUDIO_BITRATE);
|
||||
converter.setVideoResolution(targetQuality.getOutputResolution());
|
||||
converter.setVideoBitrate(targetQuality.getTargetVideoBitRate());
|
||||
converter.setAudioBitrate(targetQuality.getTargetAudioBitRate());
|
||||
|
||||
if (options != null) {
|
||||
if (options.endTimeUs > 0) {
|
||||
|
@ -169,7 +156,7 @@ public final class InMemoryTranscoder implements Closeable {
|
|||
(outSize * 100d) / inSize,
|
||||
(outSize * 100d) / fileSizeEstimate,
|
||||
(outSize * 100d) / memoryFileEstimate,
|
||||
numberFormat.format(bitRate(outSize, duration))));
|
||||
numberFormat.format(VideoBitRateCalculator.bitRate(outSize, duration))));
|
||||
|
||||
if (outSize > upperSizeLimit) {
|
||||
throw new VideoSizeException("Size constraints could not be met!");
|
||||
|
@ -191,19 +178,6 @@ public final class InMemoryTranscoder implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
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) throws VideoSourceException {
|
||||
String durationString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
||||
if (durationString == null) {
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package org.thoughtcrime.securesms.video;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
/**
|
||||
* Calculates a target quality output for a video to fit within a specified size.
|
||||
*/
|
||||
public final class VideoBitRateCalculator {
|
||||
|
||||
private static final int MAXIMUM_TARGET_VIDEO_BITRATE = VideoUtil.VIDEO_BIT_RATE;
|
||||
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 = VideoUtil.AUDIO_BIT_RATE;
|
||||
private static final int OUTPUT_FORMAT = VideoUtil.VIDEO_SHORT_WIDTH;
|
||||
private static final int LOW_RES_OUTPUT_FORMAT = 480;
|
||||
|
||||
private final long upperFileSizeLimitWithMargin;
|
||||
|
||||
public VideoBitRateCalculator(long upperFileSizeLimit) {
|
||||
upperFileSizeLimitWithMargin = (long) (upperFileSizeLimit / 1.1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the output quality of a video of the given {@param duration}.
|
||||
*/
|
||||
public Quality getTargetQuality(long duration, int inputTotalBitRate) {
|
||||
int maxVideoBitRate = Math.min(MAXIMUM_TARGET_VIDEO_BITRATE, inputTotalBitRate - AUDIO_BITRATE);
|
||||
int minVideoBitRate = Math.min(MINIMUM_TARGET_VIDEO_BITRATE, maxVideoBitRate);
|
||||
|
||||
int targetVideoBitRate = Math.max(minVideoBitRate, Math.min(getTargetVideoBitRate(upperFileSizeLimitWithMargin, duration), maxVideoBitRate));
|
||||
int bitRateRange = maxVideoBitRate - minVideoBitRate;
|
||||
double quality = bitRateRange == 0 ? 1 : (targetVideoBitRate - minVideoBitRate) / (double) bitRateRange;
|
||||
|
||||
return new Quality(targetVideoBitRate, AUDIO_BITRATE, quality, duration);
|
||||
}
|
||||
|
||||
private int getTargetVideoBitRate(long sizeGuideBytes, long duration) {
|
||||
double durationSeconds = duration / 1000d;
|
||||
|
||||
sizeGuideBytes -= durationSeconds * AUDIO_BITRATE / 8;
|
||||
|
||||
double targetAttachmentSizeBits = sizeGuideBytes * 8L;
|
||||
|
||||
return (int) (targetAttachmentSizeBits / durationSeconds);
|
||||
}
|
||||
|
||||
public static int bitRate(long bytes, long durationMs) {
|
||||
return (int) (bytes * 8 / (durationMs / 1000f));
|
||||
}
|
||||
|
||||
public static class Quality {
|
||||
private final int targetVideoBitRate;
|
||||
private final int targetAudioBitRate;
|
||||
private final double quality;
|
||||
private final long duration;
|
||||
|
||||
private Quality(int targetVideoBitRate, int targetAudioBitRate, double quality, long duration) {
|
||||
this.targetVideoBitRate = targetVideoBitRate;
|
||||
this.targetAudioBitRate = targetAudioBitRate;
|
||||
this.quality = Math.max(0, Math.min(quality, 1));
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* [0..1]
|
||||
* <p>
|
||||
* 0 = {@link #MINIMUM_TARGET_VIDEO_BITRATE}
|
||||
* 1 = {@link #MAXIMUM_TARGET_VIDEO_BITRATE}
|
||||
*/
|
||||
public double getQuality() {
|
||||
return quality;
|
||||
}
|
||||
|
||||
public int getTargetVideoBitRate() {
|
||||
return targetVideoBitRate;
|
||||
}
|
||||
|
||||
public int getTargetAudioBitRate() {
|
||||
return targetAudioBitRate;
|
||||
}
|
||||
|
||||
public int getTargetTotalBitRate() {
|
||||
return targetVideoBitRate + targetAudioBitRate;
|
||||
}
|
||||
|
||||
public boolean useLowRes() {
|
||||
return targetVideoBitRate < LOW_RES_TARGET_VIDEO_BITRATE;
|
||||
}
|
||||
|
||||
public int getOutputResolution() {
|
||||
return useLowRes() ? LOW_RES_OUTPUT_FORMAT
|
||||
: OUTPUT_FORMAT;
|
||||
}
|
||||
|
||||
public long getFileSizeEstimate() {
|
||||
return getTargetTotalBitRate() * duration / 8000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Quality{" +
|
||||
"targetVideoBitRate=" + targetVideoBitRate +
|
||||
", targetAudioBitRate=" + targetAudioBitRate +
|
||||
", quality=" + quality +
|
||||
", duration=" + duration +
|
||||
", filesize=" + getFileSizeEstimate() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@ import androidx.annotation.RequiresApi;
|
|||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class VideoUtil {
|
||||
|
||||
public static final int AUDIO_BIT_RATE = 192_000;
|
||||
|
@ -22,7 +24,7 @@ public final class VideoUtil {
|
|||
|
||||
private static final int VIDEO_LONG_WIDTH = 1280;
|
||||
private static final int VIDEO_MAX_RECORD_LENGTH_S = 60;
|
||||
private static final int VIDEO_MAX_UPLOAD_LENGTH_S = 120;
|
||||
private static final int VIDEO_MAX_UPLOAD_LENGTH_S = (int) TimeUnit.MINUTES.toSeconds(10);
|
||||
|
||||
private static final int TOTAL_BYTES_PER_SECOND = (VIDEO_BIT_RATE / 8) + (AUDIO_BIT_RATE / 8);
|
||||
|
||||
|
|
|
@ -19,8 +19,10 @@ import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.MemoryUnitFormat;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RequiresApi(api = 23)
|
||||
|
@ -68,6 +70,8 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
private long dragStartTimeMs;
|
||||
private long dragEndTimeMs;
|
||||
private long maximumSelectableRangeMicros;
|
||||
private Quality outputQuality;
|
||||
private long qualityAvailableTimeMs;
|
||||
|
||||
public VideoThumbnailsRangeSelectorView(final Context context) {
|
||||
super(context);
|
||||
|
@ -149,6 +153,11 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
}
|
||||
}
|
||||
|
||||
if (onRangeChangeListener != null) {
|
||||
onRangeChangeListener.onRangeDragEnd(getMinValue(), getMaxValue(), getDuration(), Thumb.MIN);
|
||||
setOutputQuality(onRangeChangeListener.getQuality(getClipDuration(), getDuration()));
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
|
@ -172,6 +181,11 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
|
||||
@Override
|
||||
protected void onDraw(final Canvas canvas) {
|
||||
if (thumbHintTextSize > 0) {
|
||||
thumbTimeTextPaint.getTextBounds("0", 0, "0".length(), tempDrawRect);
|
||||
canvas.translate(0, tempDrawRect.height());
|
||||
}
|
||||
|
||||
super.onDraw(canvas);
|
||||
|
||||
canvas.translate(getPaddingLeft(), getPaddingTop());
|
||||
|
@ -237,6 +251,8 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
if (dragEndTimeMs > 0 && (lastDragThumb == Thumb.MIN || lastDragThumb == Thumb.MAX)) {
|
||||
drawTimeHint(canvas, drawableWidth, drawableHeight, lastDragThumb, true);
|
||||
}
|
||||
|
||||
drawDurationAndSizeHint(canvas, drawableWidth);
|
||||
}
|
||||
|
||||
// draw current position marker
|
||||
|
@ -292,6 +308,42 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
}
|
||||
}
|
||||
|
||||
private void drawDurationAndSizeHint(Canvas canvas, int drawableWidth) {
|
||||
if (outputQuality == null) return;
|
||||
|
||||
canvas.save();
|
||||
long microsecondValue = getMaxValue() - getMinValue();
|
||||
long seconds = TimeUnit.MICROSECONDS.toSeconds(microsecondValue);
|
||||
String durationAndSize = String.format(Locale.getDefault(), "%d:%02d • %s", seconds / 60, seconds % 60, MemoryUnitFormat.formatBytes(outputQuality.fileSize, MemoryUnitFormat.MEGA_BYTES, true));
|
||||
float topBottomPadding = thumbHintTextSize * 0.5f;
|
||||
float leftRightPadding = thumbHintTextSize * 0.75f;
|
||||
|
||||
thumbTimeTextPaint.getTextBounds(durationAndSize, 0, durationAndSize.length(), tempDrawRect);
|
||||
|
||||
timePillRect.set(tempDrawRect.left - leftRightPadding, tempDrawRect.top - topBottomPadding, tempDrawRect.right + leftRightPadding, tempDrawRect.bottom + topBottomPadding);
|
||||
|
||||
float halfPillWidth = timePillRect.width() / 2f;
|
||||
float halfPillHeight = timePillRect.height() / 2f;
|
||||
|
||||
long animationTime = Math.min(ANIMATION_DURATION_MS, System.currentTimeMillis() - qualityAvailableTimeMs);
|
||||
float animationPosition = animationTime / (float) ANIMATION_DURATION_MS;
|
||||
float scaleIn = 0.2f * animationPosition + 0.8f;
|
||||
int alpha = (int) (255 * animationPosition);
|
||||
|
||||
canvas.translate(Math.max(halfPillWidth, Math.min((right + left) / 2f, drawableWidth - halfPillWidth)), - 2 * halfPillHeight);
|
||||
canvas.scale(scaleIn, scaleIn);
|
||||
thumbTimeBackgroundPaint.setAlpha(alpha);
|
||||
thumbTimeTextPaint.setAlpha(alpha);
|
||||
canvas.translate(leftRightPadding - halfPillWidth, halfPillHeight);
|
||||
canvas.drawRoundRect(timePillRect, halfPillHeight, halfPillHeight, thumbTimeBackgroundPaint);
|
||||
canvas.drawText(durationAndSize, 0, 0, thumbTimeTextPaint);
|
||||
canvas.restore();
|
||||
|
||||
if (animationTime < ANIMATION_DURATION_MS) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public long getMinValue() {
|
||||
return minValue == null ? 0 : minValue;
|
||||
}
|
||||
|
@ -300,6 +352,10 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
return maxValue == null ? getDuration() : maxValue;
|
||||
}
|
||||
|
||||
public long getClipDuration() {
|
||||
return getMaxValue() - getMinValue();
|
||||
}
|
||||
|
||||
private boolean setMinValue(long minValue) {
|
||||
if (this.minValue == null || this.minValue != minValue) {
|
||||
return setMinMax(minValue, getMaxValue(), Thumb.MIN);
|
||||
|
@ -383,6 +439,7 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
onRangeChangeListener.onPositionDrag(dragPosition);
|
||||
} else {
|
||||
onRangeChangeListener.onRangeDrag(getMinValue(), getMaxValue(), getDuration(), dragThumb);
|
||||
setOutputQuality(onRangeChangeListener.getQuality(getClipDuration(), getDuration()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
@ -394,6 +451,7 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
onRangeChangeListener.onEndPositionDrag(dragPosition);
|
||||
} else {
|
||||
onRangeChangeListener.onRangeDragEnd(getMinValue(), getMaxValue(), getDuration(), dragThumb);
|
||||
setOutputQuality(onRangeChangeListener.getQuality(getClipDuration(), getDuration()));
|
||||
}
|
||||
lastDragThumb = dragThumb;
|
||||
dragEndTimeMs = System.currentTimeMillis();
|
||||
|
@ -410,6 +468,16 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
return true;
|
||||
}
|
||||
|
||||
private void setOutputQuality(@Nullable Quality outputQuality) {
|
||||
if (!Objects.equals(this.outputQuality, outputQuality)) {
|
||||
if (this.outputQuality == null) {
|
||||
qualityAvailableTimeMs = System.currentTimeMillis();
|
||||
}
|
||||
this.outputQuality = outputQuality;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable Thumb closestThumb(@Px float x) {
|
||||
float midPoint = (right + left) / 2f;
|
||||
Thumb possibleThumb = x < midPoint ? Thumb.MIN : Thumb.MAX;
|
||||
|
@ -463,5 +531,34 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
|
|||
void onRangeDrag(long minValue, long maxValue, long duration, Thumb thumb);
|
||||
|
||||
void onRangeDragEnd(long minValue, long maxValue, long duration, Thumb thumb);
|
||||
|
||||
@Nullable Quality getQuality(long clipDurationUs, long totalDurationUs);
|
||||
}
|
||||
|
||||
public static final class Quality {
|
||||
private final long fileSize;
|
||||
private final int qualityRange;
|
||||
|
||||
public Quality(long fileSize, int qualityRange) {
|
||||
this.fileSize = fileSize;
|
||||
this.qualityRange = qualityRange;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if (!(o instanceof Quality)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Quality quality = (Quality) o;
|
||||
|
||||
return fileSize == quality.fileSize &&
|
||||
qualityRange == quality.qualityRange;
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
int result = (int) (fileSize ^ (fileSize >>> 32));
|
||||
result = 31 * result + qualityRange;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
<org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView
|
||||
android:id="@+id/video_timeline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_margin="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
app:cursorColor="#fff"
|
||||
app:cursorWidth="2dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
Ładowanie…
Reference in New Issue