Restrict video send duration.

This commit is contained in:
Alan Evans 2020-11-17 17:13:46 -04:00 committed by Alex Hart
parent 95468c85a8
commit 6e7858e00f
9 changed files with 84 additions and 41 deletions

View file

@ -26,7 +26,6 @@ import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.camera.view.SignalCameraView;
@ -47,8 +46,6 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.video.VideoUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@ -232,7 +229,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
camera.setCaptureMode(SignalCameraView.CaptureMode.MIXED);
int maxDuration = VideoUtil.getMaxVideoDurationInSeconds(requireContext(), viewModel.getMediaConstraints());
int maxDuration = VideoUtil.getMaxVideoRecordDurationInSeconds(requireContext(), viewModel.getMediaConstraints());
Log.d(TAG, "Max duration: " + maxDuration + " sec");
captureButton.setVideoCaptureListener(new CameraXVideoCaptureHelper(
@ -269,7 +266,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
"API: " + Build.VERSION.SDK_INT + ", " +
"MFD: " + MemoryFileDescriptor.supported() + ", " +
"Camera: " + CameraXUtil.getLowestSupportedHardwareLevel(requireContext()) + ", " +
"MaxDuration: " + VideoUtil.getMaxVideoDurationInSeconds(requireContext(), viewModel.getMediaConstraints()) + " sec");
"MaxDuration: " + VideoUtil.getMaxVideoRecordDurationInSeconds(requireContext(), viewModel.getMediaConstraints()) + " sec");
}
viewModel.onCameraControlsInitialized();
@ -280,7 +277,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
requireArguments().getBoolean(IS_VIDEO_ENABLED, true) &&
MediaConstraints.isVideoTranscodeAvailable() &&
CameraXUtil.isMixedModeSupported(context) &&
VideoUtil.getMaxVideoDurationInSeconds(context, viewModel.getMediaConstraints()) > 0;
VideoUtil.getMaxVideoRecordDurationInSeconds(context, viewModel.getMediaConstraints()) > 0;
}
private void displayVideoRecordingTooltipIfNecessary(CameraButtonView captureButton) {

View file

@ -21,9 +21,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.util.Pair;
import androidx.core.util.Supplier;
import androidx.fragment.app.Fragment;
@ -149,7 +147,6 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
private TextView charactersLeft;
private RecyclerView mediaRail;
private MediaRailAdapter mediaRailAdapter;
private AlertDialog progressDialog;
private int visibleHeight;
@ -549,7 +546,11 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
MediaSendFragment fragment = getMediaSendFragment();
if (fragment != null) {
fragment.pausePlayback();
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this);
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()).observe(this, result -> {
dialog.dismiss();
finish();
});
} else {
@ -570,7 +571,14 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
sendButton.setEnabled(false);
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList(), composeText.getMentions()).observe(this, this::setActivityResultAndFinish);
fragment.pausePlayback();
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this);
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList(), composeText.getMentions())
.observe(this, result -> {
dialog.dismiss();
setActivityResultAndFinish(result);
});
}
private static Map<Media, MediaTransform> buildModelsToTransform(@NonNull MediaSendFragment fragment) {
@ -771,15 +779,6 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
.setOnDismissListener(() -> TextSecurePreferences.setHasSeenViewOnceTooltip(this, true))
.show(TooltipPopup.POSITION_ABOVE);
break;
case SHOW_RENDER_PROGRESS:
progressDialog = SimpleProgressDialog.show(new ContextThemeWrapper(MediaSendActivity.this, R.style.TextSecure_MediaSendProgressDialog));
break;
case HIDE_RENDER_PROGRESS:
if (progressDialog != null) {
progressDialog.dismiss();
progressDialog = null;
}
break;
}
});
}

View file

@ -1,16 +1,17 @@
package org.thoughtcrime.securesms.mediasend;
import androidx.lifecycle.ViewModelProviders;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.viewpager.widget.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.viewpager.widget.ViewPager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ControllableViewPager;
import org.thoughtcrime.securesms.util.Util;
@ -147,6 +148,10 @@ public class MediaSendFragment extends Fragment {
});
}
void pausePlayback() {
fragmentPagerAdapter.pausePlayback();
}
private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
@Override
public void onPageSelected(int position) {

View file

@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.mediasend;
import android.net.Uri;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import android.view.View;
import android.view.ViewGroup;
import com.annimon.stream.Stream;
@ -122,6 +123,14 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
return fragments.containsKey(position) ? fragments.get(position).getPlaybackControls() : null;
}
void pausePlayback() {
for (MediaSendPageFragment fragment : fragments.values()) {
if (fragment instanceof MediaSendVideoFragment) {
((MediaSendVideoFragment)fragment).pausePlayback();
}
}
}
void notifyHidden() {
Stream.of(fragments.values()).forEach(MediaSendPageFragment::notifyHidden);
}

View file

@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.scribbles.VideoEditorHud;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Throttler;
import org.thoughtcrime.securesms.video.VideoPlayer;
@ -203,6 +202,10 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
@Override
public void notifyHidden() {
pausePlayback();
}
public void pausePlayback() {
if (player != null) {
player.pause();
if (hud != null) {

View file

@ -460,15 +460,12 @@ class MediaSendViewModel extends ViewModel {
}
MutableLiveData<MediaSendActivityResult> result = new MutableLiveData<>();
Runnable dialogRunnable = () -> event.postValue(Event.SHOW_RENDER_PROGRESS);
String trimmedBody = isViewOnce() ? "" : body.toString().trim();
List<Media> initialMedia = getSelectedMediaOrDefault();
List<Mention> trimmedMentions = isViewOnce() ? Collections.emptyList() : mentions;
Preconditions.checkState(initialMedia.size() > 0, "No media to send!");
Util.runOnMainDelayed(dialogRunnable, 250);
MediaRepository.transformMedia(application, initialMedia, modelsToTransform, (oldToNew) -> {
List<Media> updatedMedia = new ArrayList<>(oldToNew.values());
@ -499,7 +496,6 @@ class MediaSendViewModel extends ViewModel {
uploadRepository.deleteAbandonedAttachments();
}
Util.cancelRunnableOnMain(dialogRunnable);
result.postValue(MediaSendActivityResult.forPreUpload(uploadResults, splitBody, transport, isViewOnce(), trimmedMentions));
});
});
@ -681,7 +677,7 @@ class MediaSendViewModel extends ViewModel {
}
enum Event {
VIEW_ONCE_TOOLTIP, SHOW_RENDER_PROGRESS, HIDE_RENDER_PROGRESS
VIEW_ONCE_TOOLTIP
}
enum Page {

View file

@ -14,11 +14,13 @@ import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.video.VideoUtil;
import org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* The HUD (heads-up display) that contains all of the tools for editing video.
@ -69,6 +71,8 @@ public final class VideoEditorHud extends LinearLayout {
return;
}
videoTimeLine.setTimeLimit(VideoUtil.getMaxVideoUploadDurationInSeconds(), TimeUnit.SECONDS);
videoTimeLine.setInput(DecryptableUriMediaInput.createForUri(getContext(), uri));
videoTimeLine.setOnRangeChangeListener(new VideoThumbnailsRangeSelectorView.OnRangeChangeListener() {

View file

@ -17,9 +17,12 @@ public final class VideoUtil {
public static final int AUDIO_BIT_RATE = 192_000;
public static final int VIDEO_FRAME_RATE = 30;
public static final int VIDEO_BIT_RATE = 2_000_000;
public static final int VIDEO_LONG_WIDTH = 1280;
public static final int VIDEO_SHORT_WIDTH = 720;
public static final int VIDEO_MAX_LENGTH_S = 30;
static final int VIDEO_SHORT_WIDTH = 720;
private static final int VIDEO_LONG_WIDTH = 1280;
private static final int VIDEO_MAX_RECORD_LENGTH_S = 30;
private static final int VIDEO_MAX_UPLOAD_LENGTH_S = 120;
private static final int TOTAL_BYTES_PER_SECOND = (VIDEO_BIT_RATE / 8) + (AUDIO_BIT_RATE / 8);
@ -38,11 +41,15 @@ public final class VideoUtil {
: new Size(VIDEO_LONG_WIDTH, VIDEO_SHORT_WIDTH);
}
public static int getMaxVideoDurationInSeconds(@NonNull Context context, @NonNull MediaConstraints mediaConstraints) {
public static int getMaxVideoRecordDurationInSeconds(@NonNull Context context, @NonNull MediaConstraints mediaConstraints) {
int allowedSize = mediaConstraints.getCompressedVideoMaxSize(context);
int duration = (int) Math.floor((float) allowedSize / TOTAL_BYTES_PER_SECOND);
return Math.min(duration, VIDEO_MAX_LENGTH_S);
return Math.min(duration, VIDEO_MAX_RECORD_LENGTH_S);
}
public static int getMaxVideoUploadDurationInSeconds() {
return VIDEO_MAX_UPLOAD_LENGTH_S;
}
@RequiresApi(21)

View file

@ -11,12 +11,14 @@ import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
@ -24,6 +26,8 @@ import java.util.concurrent.TimeUnit;
@RequiresApi(api = 23)
public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView {
private static final String TAG = Log.tag(VideoThumbnailsRangeSelectorView.class);
private static final long MINIMUM_SELECTABLE_RANGE = TimeUnit.MILLISECONDS.toMicros(500);
private static final int ANIMATION_DURATION_MS = 100;
@ -63,6 +67,7 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
@ColorInt private int thumbHintBackgroundColor;
private long dragStartTimeMs;
private long dragEndTimeMs;
private long maximumSelectableRangeMicros;
public VideoThumbnailsRangeSelectorView(final Context context) {
super(context);
@ -137,6 +142,13 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
}
}
if (setMinValue(getMinValue())) {
Log.d(TAG, "Clamped video duration to " + getMaxValue());
if (onRangeChangeListener != null) {
onRangeChangeListener.onRangeDragEnd(getMinValue(), getMaxValue(), getDuration(), Thumb.MAX);
}
}
invalidate();
}
@ -310,11 +322,18 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
final long duration = getDuration();
final long minDiff = Math.max(MINIMUM_SELECTABLE_RANGE, pixelToDuration(thumbSizePixels * 2.5f));
final long maxDiff = maximumSelectableRangeMicros <= MINIMUM_SELECTABLE_RANGE ? 0 : Math.max(maximumSelectableRangeMicros, pixelToDuration(thumbSizePixels * 2.5f));
if (thumb == Thumb.MIN) {
newMin = clamp(newMin, 0, currentMax - minDiff);
if (maxDiff > 0) {
newMax = clamp(newMax, newMin + minDiff, Math.min(newMin + maxDiff, duration));
}
} else {
newMax = clamp(newMax, currentMin + minDiff, duration);
if (maxDiff > 0) {
newMin = clamp(newMin, Math.max(0, newMax - maxDiff), newMax - minDiff);
}
}
if (newMin != currentMin || newMax != currentMax) {
@ -425,6 +444,10 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
}
}
public void setTimeLimit(int t, @NonNull TimeUnit timeUnit) {
maximumSelectableRangeMicros = timeUnit.toMicros(t);
}
public enum Thumb {
MIN,
MAX,