summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk3
-rw-r--r--gallerycommon/src/com/android/gallery3d/common/ApiHelper.java2
-rw-r--r--proguard.flags3
-rw-r--r--src/com/android/gallery3d/app/PhotoPage.java2
-rw-r--r--src/com/android/gallery3d/app/ShortenExample.java156
-rw-r--r--src/com/android/gallery3d/app/TrimVideo.java178
6 files changed, 337 insertions, 7 deletions
diff --git a/Android.mk b/Android.mk
index 570b46f..5029a90 100644
--- a/Android.mk
+++ b/Android.mk
@@ -5,7 +5,8 @@ include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13
-LOCAL_STATIC_JAVA_LIBRARIES += com.android.gallery3d.common2
+LOCAL_STATIC_JAVA_LIBRARIES += com.android.gallery3d.common2
+LOCAL_STATIC_JAVA_LIBRARIES += mp4parser
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_SRC_FILES += $(call all-java-files-under, src_pd)
diff --git a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
index fe5e795..802f4ec 100644
--- a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
+++ b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
@@ -161,7 +161,7 @@ public class ApiHelper {
Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH;
public static final boolean HAS_MEDIA_MUXER =
- Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1;
+ Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
public static int getIntFieldIfExists(Class<?> klass, String fieldName,
Class<?> obj, int defaultVal) {
diff --git a/proguard.flags b/proguard.flags
index 15e9743..098a540 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -43,3 +43,6 @@
-keep interface com.actionbarsherlock.** { *; }
-keepattributes *Annotation*
+# Required for mp4parser
+-keep public class * extends com.coremedia.iso.boxes.AbstractBox
+
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index ec45cc1..c5c77e1 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -792,6 +792,8 @@ public class PhotoPage extends ActivityState implements
case R.id.action_trim: {
Intent intent = new Intent(mActivity, TrimVideo.class);
intent.setData(manager.getContentUri(path));
+ // We need the file path to wrap this into a RandomAccessFile.
+ intent.putExtra(KEY_MEDIA_ITEM_PATH, current.getFilePath());
mActivity.startActivityForResult(intent, REQUEST_TRIM);
return true;
}
diff --git a/src/com/android/gallery3d/app/ShortenExample.java b/src/com/android/gallery3d/app/ShortenExample.java
new file mode 100644
index 0000000..0ac78d9
--- /dev/null
+++ b/src/com/android/gallery3d/app/ShortenExample.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Modified example based on mp4parser google code open source project.
+// http://code.google.com/p/mp4parser/source/browse/trunk/examples/src/main/java/com/googlecode/mp4parser/ShortenExample.java
+
+package com.android.gallery3d.app;
+
+import com.coremedia.iso.IsoFile;
+import com.coremedia.iso.boxes.TimeToSampleBox;
+import com.googlecode.mp4parser.authoring.Movie;
+import com.googlecode.mp4parser.authoring.Track;
+import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
+import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
+import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Shortens/Crops a track
+ */
+public class ShortenExample {
+
+ public static void main(String[] args, File src, File dst, int startMs, int endMs) throws IOException {
+ RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
+ Movie movie = MovieCreator.build(randomAccessFile.getChannel());
+
+ // remove all tracks we will create new tracks from the old
+ List<Track> tracks = movie.getTracks();
+ movie.setTracks(new LinkedList<Track>());
+
+ double startTime = startMs/1000;
+ double endTime = endMs/1000;
+
+ boolean timeCorrected = false;
+
+ // Here we try to find a track that has sync samples. Since we can only start decoding
+ // at such a sample we SHOULD make sure that the start of the new fragment is exactly
+ // such a frame
+ for (Track track : tracks) {
+ if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
+ if (timeCorrected) {
+ // This exception here could be a false positive in case we have multiple tracks
+ // with sync samples at exactly the same positions. E.g. a single movie containing
+ // multiple qualities of the same video (Microsoft Smooth Streaming file)
+
+ throw new RuntimeException("The startTime has already been corrected by another track with SyncSample. Not Supported.");
+ }
+ startTime = correctTimeToSyncSample(track, startTime, false);
+ endTime = correctTimeToSyncSample(track, endTime, true);
+ timeCorrected = true;
+ }
+ }
+
+ for (Track track : tracks) {
+ long currentSample = 0;
+ double currentTime = 0;
+ long startSample = -1;
+ long endSample = -1;
+
+ for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
+ TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
+ for (int j = 0; j < entry.getCount(); j++) {
+ // entry.getDelta() is the amount of time the current sample covers.
+
+ if (currentTime <= startTime) {
+ // current sample is still before the new starttime
+ startSample = currentSample;
+ }
+ if (currentTime <= endTime) {
+ // current sample is after the new start time and still before the new endtime
+ endSample = currentSample;
+ } else {
+ // current sample is after the end of the cropped video
+ break;
+ }
+ currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale();
+ currentSample++;
+ }
+ }
+ movie.addTrack(new CroppedTrack(track, startSample, endSample));
+ }
+ IsoFile out = new DefaultMp4Builder().build(movie);
+
+ if (!dst.exists()) {
+ dst.createNewFile();
+ }
+
+ FileOutputStream fos = new FileOutputStream(dst);
+ FileChannel fc = fos.getChannel();
+ out.getBox(fc); // This one build up the memory.
+
+ fc.close();
+ fos.close();
+ randomAccessFile.close();
+ }
+
+ protected static long getDuration(Track track) {
+ long duration = 0;
+ for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
+ duration += entry.getCount() * entry.getDelta();
+ }
+ return duration;
+ }
+
+ private static double correctTimeToSyncSample(Track track, double cutHere, boolean next) {
+ double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
+ long currentSample = 0;
+ double currentTime = 0;
+ for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
+ TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
+ for (int j = 0; j < entry.getCount(); j++) {
+ if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
+ // samples always start with 1 but we start with zero therefore +1
+ timeOfSyncSamples[Arrays.binarySearch(track.getSyncSamples(), currentSample + 1)] = currentTime;
+ }
+ currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale();
+ currentSample++;
+ }
+ }
+ double previous = 0;
+ for (double timeOfSyncSample : timeOfSyncSamples) {
+ if (timeOfSyncSample > cutHere) {
+ if (next) {
+ return timeOfSyncSample;
+ } else {
+ return previous;
+ }
+ }
+ previous = timeOfSyncSample;
+ }
+ return timeOfSyncSamples[timeOfSyncSamples.length - 1];
+ }
+
+
+}
diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java
index 4fb2557..f7ff43e 100644
--- a/src/com/android/gallery3d/app/TrimVideo.java
+++ b/src/com/android/gallery3d/app/TrimVideo.java
@@ -19,12 +19,18 @@ package com.android.gallery3d.app;
import android.app.ActionBar;
import android.app.Activity;
import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.database.Cursor;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Environment;
import android.os.Handler;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -34,7 +40,12 @@ import android.widget.Toast;
import android.widget.VideoView;
import com.android.gallery3d.R;
+import com.android.gallery3d.util.BucketNames;
+import java.io.File;
+import java.io.IOException;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
public class TrimVideo extends Activity implements
MediaPlayer.OnErrorListener,
@@ -58,6 +69,15 @@ public class TrimVideo extends Activity implements
public static final String KEY_VIDEO_POSITION = "video_pos";
private boolean mHasPaused = false;
+ private String mSrcVideoPath = null;
+ private String mSaveFileName = null;
+ private static final String TIME_STAMP_NAME = "'TRIM'_yyyyMMdd_HHmmss";
+ private File mSrcFile = null;
+ private File mDstFile = null;
+ private File mSaveDirectory = null;
+ // For showing the result.
+ private String saveFolderName = null;
+
@Override
public void onCreate(Bundle savedInstanceState) {
mContext = getApplicationContext();
@@ -68,14 +88,14 @@ public class TrimVideo extends Activity implements
Intent intent = getIntent();
mUri = intent.getData();
-
+ mSrcVideoPath = intent.getStringExtra(PhotoPage.KEY_MEDIA_ITEM_PATH);
setContentView(R.layout.trim_view);
View rootView = findViewById(R.id.trim_view_root);
mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
mController = new TrimControllerOverlay(mContext);
- ((ViewGroup)rootView).addView(mController.getView());
+ ((ViewGroup) rootView).addView(mController.getView());
mController.setListener(this);
mController.setCanReplay(true);
@@ -107,6 +127,15 @@ public class TrimVideo extends Activity implements
}
@Override
+ public void onStop() {
+ if (mProgress != null) {
+ mProgress.dismiss();
+ mProgress = null;
+ }
+ super.onStop();
+ }
+
+ @Override
public void onDestroy() {
mVideoView.stopPlayback();
super.onDestroy();
@@ -185,6 +214,42 @@ public class TrimVideo extends Activity implements
return true;
};
+ // Copy from SaveCopyTask.java in terms of how to handle the destination
+ // path and filename : querySource() and getSaveDirectory().
+ private interface ContentResolverQueryCallback {
+ void onCursorResult(Cursor cursor);
+ }
+
+ private void querySource(String[] projection, ContentResolverQueryCallback callback) {
+ ContentResolver contentResolver = getContentResolver();
+ Cursor cursor = null;
+ try {
+ cursor = contentResolver.query(mUri, projection, null, null, null);
+ if ((cursor != null) && cursor.moveToNext()) {
+ callback.onCursorResult(cursor);
+ }
+ } catch (Exception e) {
+ // Ignore error for lacking the data column from the source.
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private File getSaveDirectory() {
+ final File[] dir = new File[1];
+ querySource(new String[] {
+ VideoColumns.DATA }, new ContentResolverQueryCallback() {
+
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ dir[0] = new File(cursor.getString(0)).getParentFile();
+ }
+ });
+ return dir[0];
+ }
+
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
@@ -192,14 +257,117 @@ public class TrimVideo extends Activity implements
finish();
return true;
} else if (id == R.id.action_trim_video) {
- // TODO: Add the new MediaMuxer API to support the trimming.
- Toast.makeText(getApplicationContext(),
- "Trimming will be implemented soon!", Toast.LENGTH_SHORT).show();
+ trimVideo();
return true;
}
return false;
}
+ private void trimVideo() {
+ // Use the default save directory if the source directory cannot be
+ // saved.
+ mSaveDirectory = getSaveDirectory();
+ if ((mSaveDirectory == null) || !mSaveDirectory.canWrite()) {
+ mSaveDirectory = new File(Environment.getExternalStorageDirectory(),
+ BucketNames.DOWNLOAD);
+ saveFolderName = getString(R.string.folder_download);
+ } else {
+ saveFolderName = mSaveDirectory.getName();
+ }
+ mSaveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(
+ new Date(System.currentTimeMillis()));
+
+ mDstFile = new File(mSaveDirectory, mSaveFileName + ".mp4");
+ mSrcFile = new File(mSrcVideoPath);
+
+ showProgressDialog();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ ShortenExample.main(null, mSrcFile, mDstFile, mTrimStartTime, mTrimEndTime);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // After trimming is done, trigger the UI changed.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // TODO: change trimming into a service to avoid
+ // this progressDialog and add notification properly.
+ if (mProgress != null) {
+ mProgress.dismiss();
+ // Update the database for adding a new video file.
+ insertContent(mDstFile);
+ Toast.makeText(getApplicationContext(),
+ "Saved into " + saveFolderName, Toast.LENGTH_SHORT)
+ .show();
+ mProgress = null;
+ }
+ }
+ });
+ }
+ }).start();
+ }
+
+ private void showProgressDialog() {
+ // create a background thread to trim the video.
+ // and show the progress.
+ mProgress = new ProgressDialog(this);
+ mProgress.setTitle("Trimming");
+ mProgress.setMessage("please wait");
+ // TODO: make this cancelable.
+ mProgress.setCancelable(false);
+ mProgress.setCanceledOnTouchOutside(false);
+ mProgress.show();
+ }
+
+ /**
+ * Insert the content (saved file) with proper video properties.
+ */
+ private Uri insertContent(File file) {
+ long now = System.currentTimeMillis() / 1000;
+
+ final ContentValues values = new ContentValues(12);
+ values.put(Video.Media.TITLE, mSaveFileName);
+ values.put(Video.Media.DISPLAY_NAME, file.getName());
+ values.put(Video.Media.MIME_TYPE, "video/mp4");
+ values.put(Video.Media.DATE_TAKEN, now);
+ values.put(Video.Media.DATE_MODIFIED, now);
+ values.put(Video.Media.DATE_ADDED, now);
+ values.put(Video.Media.DATA, file.getAbsolutePath());
+ values.put(Video.Media.SIZE, file.length());
+ // Copy the data taken and location info from src.
+ String[] projection = new String[] {
+ VideoColumns.DATE_TAKEN,
+ VideoColumns.LATITUDE,
+ VideoColumns.LONGITUDE,
+ VideoColumns.RESOLUTION,
+ };
+
+ // Copy some info from the source file.
+ querySource(projection, new ContentResolverQueryCallback() {
+
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ values.put(Video.Media.DATE_TAKEN, cursor.getLong(0));
+ double latitude = cursor.getDouble(1);
+ double longitude = cursor.getDouble(2);
+ // TODO: Change || to && after the default location issue is
+ // fixed.
+ if ((latitude != 0f) || (longitude != 0f)) {
+ values.put(Video.Media.LATITUDE, latitude);
+ values.put(Video.Media.LONGITUDE, longitude);
+ }
+ values.put(Video.Media.RESOLUTION, cursor.getString(3));
+
+ }
+ });
+
+ return getContentResolver().insert(Video.Media.EXTERNAL_CONTENT_URI, values);
+ }
+
@Override
public void onPlayPause() {
if (mVideoView.isPlaying()) {