OSDN Git Service

The trimming solution with the mp4parser library.
authorTeng-Hui Zhu <ztenghui@google.com>
Sun, 23 Sep 2012 22:02:56 +0000 (15:02 -0700)
committerAndroid (Google) Code Review <android-gerrit@google.com>
Mon, 24 Sep 2012 20:36:40 +0000 (13:36 -0700)
bug:7093055

Change-Id: I598a81d80c9c5107696f3af7761207e3ec88f3ff

Android.mk
gallerycommon/src/com/android/gallery3d/common/ApiHelper.java
proguard.flags
src/com/android/gallery3d/app/PhotoPage.java
src/com/android/gallery3d/app/ShortenExample.java [new file with mode: 0644]
src/com/android/gallery3d/app/TrimVideo.java

index 570b46f..5029a90 100644 (file)
@@ -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)
index fe5e795..802f4ec 100644 (file)
@@ -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) {
index 15e9743..098a540 100644 (file)
@@ -43,3 +43,6 @@
 -keep interface com.actionbarsherlock.** { *; }
 -keepattributes *Annotation*
 
+# Required for mp4parser
+-keep public class * extends com.coremedia.iso.boxes.AbstractBox
+
index ec45cc1..c5c77e1 100644 (file)
@@ -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 (file)
index 0000000..0ac78d9
--- /dev/null
@@ -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];
+    }
+
+
+}
index 4fb2557..f7ff43e 100644 (file)
@@ -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()) {