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

View File

@@ -21,9 +21,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.util.Pair; import androidx.core.util.Pair;
import androidx.core.util.Supplier; import androidx.core.util.Supplier;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@@ -149,7 +147,6 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
private TextView charactersLeft; private TextView charactersLeft;
private RecyclerView mediaRail; private RecyclerView mediaRail;
private MediaRailAdapter mediaRailAdapter; private MediaRailAdapter mediaRailAdapter;
private AlertDialog progressDialog;
private int visibleHeight; private int visibleHeight;
@@ -549,7 +546,11 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
MediaSendFragment fragment = getMediaSendFragment(); MediaSendFragment fragment = getMediaSendFragment();
if (fragment != null) { if (fragment != null) {
fragment.pausePlayback();
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this);
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()).observe(this, result -> { viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()).observe(this, result -> {
dialog.dismiss();
finish(); finish();
}); });
} else { } else {
@@ -570,7 +571,14 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
sendButton.setEnabled(false); 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) { 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)) .setOnDismissListener(() -> TextSecurePreferences.setHasSeenViewOnceTooltip(this, true))
.show(TooltipPopup.POSITION_ABOVE); .show(TooltipPopup.POSITION_ABOVE);
break; 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; package org.thoughtcrime.securesms.mediasend;
import androidx.lifecycle.ViewModelProviders;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; 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.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; 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.R;
import org.thoughtcrime.securesms.components.ControllableViewPager; import org.thoughtcrime.securesms.components.ControllableViewPager;
import org.thoughtcrime.securesms.util.Util; 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 { private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
@Override @Override
public void onPageSelected(int position) { public void onPageSelected(int position) {

View File

@@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import android.net.Uri; import android.net.Uri;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.fragment.app.FragmentStatePagerAdapter;
import android.view.View;
import android.view.ViewGroup;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
@@ -122,6 +123,14 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
return fragments.containsKey(position) ? fragments.get(position).getPlaybackControls() : null; 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() { void notifyHidden() {
Stream.of(fragments.values()).forEach(MediaSendPageFragment::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.MediaConstraints;
import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.scribbles.VideoEditorHud; import org.thoughtcrime.securesms.scribbles.VideoEditorHud;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Throttler; import org.thoughtcrime.securesms.util.Throttler;
import org.thoughtcrime.securesms.video.VideoPlayer; import org.thoughtcrime.securesms.video.VideoPlayer;
@@ -203,6 +202,10 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
@Override @Override
public void notifyHidden() { public void notifyHidden() {
pausePlayback();
}
public void pausePlayback() {
if (player != null) { if (player != null) {
player.pause(); player.pause();
if (hud != null) { if (hud != null) {

View File

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

View File

@@ -14,11 +14,13 @@ import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput; 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 org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.TimeUnit;
/** /**
* The HUD (heads-up display) that contains all of the tools for editing video. * 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; return;
} }
videoTimeLine.setTimeLimit(VideoUtil.getMaxVideoUploadDurationInSeconds(), TimeUnit.SECONDS);
videoTimeLine.setInput(DecryptableUriMediaInput.createForUri(getContext(), uri)); videoTimeLine.setInput(DecryptableUriMediaInput.createForUri(getContext(), uri));
videoTimeLine.setOnRangeChangeListener(new VideoThumbnailsRangeSelectorView.OnRangeChangeListener() { videoTimeLine.setOnRangeChangeListener(new VideoThumbnailsRangeSelectorView.OnRangeChangeListener() {

View File

@@ -14,12 +14,15 @@ import org.thoughtcrime.securesms.util.MediaUtil;
public final class VideoUtil { public final class VideoUtil {
public static final int AUDIO_BIT_RATE = 192_000; public static final int AUDIO_BIT_RATE = 192_000;
public static final int VIDEO_FRAME_RATE = 30; public static final int VIDEO_FRAME_RATE = 30;
public static final int VIDEO_BIT_RATE = 2_000_000; 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; static final int VIDEO_SHORT_WIDTH = 720;
public static final int VIDEO_MAX_LENGTH_S = 30;
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); 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); : 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 allowedSize = mediaConstraints.getCompressedVideoMaxSize(context);
int duration = (int) Math.floor((float) allowedSize / TOTAL_BYTES_PER_SECOND); 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) @RequiresApi(21)

View File

@@ -11,12 +11,14 @@ import android.util.AttributeSet;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.Px; import androidx.annotation.Px;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -24,6 +26,8 @@ import java.util.concurrent.TimeUnit;
@RequiresApi(api = 23) @RequiresApi(api = 23)
public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView { 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 long MINIMUM_SELECTABLE_RANGE = TimeUnit.MILLISECONDS.toMicros(500);
private static final int ANIMATION_DURATION_MS = 100; private static final int ANIMATION_DURATION_MS = 100;
@@ -63,6 +67,7 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
@ColorInt private int thumbHintBackgroundColor; @ColorInt private int thumbHintBackgroundColor;
private long dragStartTimeMs; private long dragStartTimeMs;
private long dragEndTimeMs; private long dragEndTimeMs;
private long maximumSelectableRangeMicros;
public VideoThumbnailsRangeSelectorView(final Context context) { public VideoThumbnailsRangeSelectorView(final Context context) {
super(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(); invalidate();
} }
@@ -310,11 +322,18 @@ public final class VideoThumbnailsRangeSelectorView extends VideoThumbnailsView
final long duration = getDuration(); final long duration = getDuration();
final long minDiff = Math.max(MINIMUM_SELECTABLE_RANGE, pixelToDuration(thumbSizePixels * 2.5f)); 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) { if (thumb == Thumb.MIN) {
newMin = clamp(newMin, 0, currentMax - minDiff); newMin = clamp(newMin, 0, currentMax - minDiff);
if (maxDiff > 0) {
newMax = clamp(newMax, newMin + minDiff, Math.min(newMin + maxDiff, duration));
}
} else { } else {
newMax = clamp(newMax, currentMin + minDiff, duration); newMax = clamp(newMax, currentMin + minDiff, duration);
if (maxDiff > 0) {
newMin = clamp(newMin, Math.max(0, newMax - maxDiff), newMax - minDiff);
}
} }
if (newMin != currentMin || newMax != currentMax) { 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 { public enum Thumb {
MIN, MIN,
MAX, MAX,