Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

android : trim mp4 video

Tags:

android

video

I am trying to create a demo which takes a mp4 file as input and trims it to 10 seconds from 00:01 to 00:10

I am using mp4parser for achieving my task.

compile 'com.googlecode.mp4parser:isoparser:1.1.18'

I have following code in my activity:

MainActivity.java:

import android.Manifest;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.MediaController;
import android.widget.VideoView;

import com.coremedia.iso.boxes.CompositionTimeToSample;
import com.coremedia.iso.boxes.Container;
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.AppendTrack;
import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.videoView)
    VideoView mVideoView;

    Uri uri;

    String TAG = getClass().getSimpleName();

    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.getCompositionTimeEntries().size(); i++) {
            CompositionTimeToSample.Entry entry = track.getCompositionTimeEntries().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.getCount() / (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];
    }

    /**
     * Get a file path from a Uri. This will get the the path for Storage Access
     * Framework Documents, as well as the _data field for the MediaStore and
     * other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri     The Uri to query.
     * @author paulburke
     */
    public static String getPath(final Context context, final Uri uri) {

        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }

                // TODO handle non-primary volumes
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[]{
                        split[1]
                };

                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.
     *
     * @param context       The context.
     * @param uri           The Uri to query.
     * @param selection     (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     */
    public static String getDataColumn(Context context, Uri uri, String selection,
                                       String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                final int column_index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(column_index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.bind(this);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (isStoragePermissionGranted()) {
                    Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show();
                    new TrimAsyncTask(MainActivity.this).execute();
                }
            }
        });
    }

    public boolean isStoragePermissionGranted() {
        if (Build.VERSION.SDK_INT >= 23) {
            if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    == PackageManager.PERMISSION_GRANTED) {
                Log.v(TAG, "Permission is granted");
                return true;
            } else {

                Log.v(TAG, "Permission is revoked");
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
                return false;
            }
        } else { //permission is automatically granted on sdk<23 upon installation
            Log.v(TAG, "Permission is granted");
            return true;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Log.v(TAG, "Permission: " + permissions[0] + "was " + grantResults[0]);
            //resume tasks needing this permission
            new TrimAsyncTask(this).execute();
        }
    }

    public void Mp4CutProcessBegin(Context context) {
        try {
            File sdCard = Environment.getExternalStorageDirectory();
            Movie movie = MovieCreator.build(getPath(context, uri));

            List<Track> tracks = movie.getTracks();
            movie.setTracks(new LinkedList<Track>());
            // remove all tracks we will create new tracks from the old

            double startTime1 = 1000000;  // What Time Should i set here and in which unit of time.
            double endTime1 = 10000000;   // What Time Should i set here and in which unit of time.

            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
            Log.i(TAG, "Tracks: " + String.valueOf(tracks.size()));
            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.");
                    }
                    startTime1 = correctTimeToSyncSample(track, startTime1, false);
                    endTime1 = correctTimeToSyncSample(track, endTime1, true);
                    timeCorrected = true;
                }
            }

            for (Track track : tracks) {
                long currentSample = 0;
                double currentTime = 0;
                double lastTime = -1;
                long startSample1 = -1;
                long endSample1 = -1;

                for (int i = 0; i < track.getSampleDurations().length; i++) {
                    long delta = track.getSampleDurations()[i];

                    if (currentTime > lastTime && currentTime <= startTime1) {
                        // current sample is still before the new starttime
                        startSample1 = currentSample;
                    }
                    if (currentTime > lastTime && currentTime <= endTime1) {
                        // current sample is after the new start time and still before the new endtime
                        endSample1 = currentSample;
                    }
                    lastTime = currentTime;
                    currentTime += (double) delta / (double) track.getTrackMetaData().getTimescale();
                    currentSample++;
                }
                movie.addTrack(new AppendTrack(new CroppedTrack(track, startSample1, endSample1)));
            }
            long start1 = System.currentTimeMillis();
            Container out = new DefaultMp4Builder().build(movie);
            long start2 = System.currentTimeMillis();
            File output = new File(sdCard + "/output.mp4");
            if (output.exists()) {
                output.delete();
            }
            FileOutputStream fos = new FileOutputStream(sdCard + "/output.mp4"); //String.format("output-%f-%f.mp4", startTime1, endTime1)
            FileChannel fc = fos.getChannel();
            out.writeContainer(fc);

            fc.close();
            fos.close();
            long start3 = System.currentTimeMillis();
            System.err.println("Building IsoFile took : " + (start2 - start1) + "ms");
            System.err.println("Writing IsoFile took  : " + (start3 - start2) + "ms");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @OnClick(R.id.buttonLoad)
    public void load() {
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.setType("video/*");
        startActivityForResult(Intent.createChooser(i, "Pick a photo"), 1);
    }

    public void loadVideo(Uri uri) {
        mVideoView.setMediaController(new MediaController(this));
        mVideoView.setVideoURI(uri);
        mVideoView.requestFocus();
    }

    @OnClick(R.id.buttonPlay)
    public void startVideo() {
        mVideoView.start();
    }

    @OnClick(R.id.buttonStop)
    public void stopVideo() {
        mVideoView.stopPlayback();
        loadVideo(uri);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == Activity.RESULT_OK) {
            uri = data.getData();
            loadVideo(uri);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    public class TrimAsyncTask extends AsyncTask<Void, Void, Void> {

        Context mContext;

        public TrimAsyncTask(Context context) {
            mContext = context;
        }

        @Override
        protected Void doInBackground(Void... params) {
            Mp4CutProcessBegin(mContext);
            return null;
        }
    }
}

Log that is generated from my Log.x():

03-18 10:27:48.717 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.717 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.719 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.719 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.719 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.719 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.719 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.719 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.719 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.719 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.735 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of tkhd
03-18 10:27:48.736 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.736 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of tkhd
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stco
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stsc
03-18 10:27:48.737 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stsz
03-18 10:27:48.749 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of hdlr
03-18 10:27:48.750 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stts
03-18 10:27:48.750 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stss
03-18 10:27:48.758 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of mdhd
03-18 10:27:48.761 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of elst
03-18 10:27:48.762 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of mvhd
03-18 10:27:48.769 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stco
03-18 10:27:48.769 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stsc
03-18 10:27:48.769 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stsz
03-18 10:27:48.778 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of hdlr
03-18 10:27:48.778 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of stts
03-18 10:27:48.779 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of mdhd
03-18 10:27:48.780 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: AbstractBox:parsing details of elst
03-18 10:27:48.781 26598-26598/com.letsnurture.ln_202.videotrimdemo I/MainActivity: Tracks: 2
03-18 10:27:48.784 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:Creating movie Movie{ track_1 (vide) track_2 (soun) }
03-18 10:27:48.786 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:Calculating chunk offsets for track_1
03-18 10:27:48.786 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:done with stbl for track_1
03-18 10:27:48.786 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:done with trak for track_1
03-18 10:27:48.787 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:done with stbl for track_2
03-18 10:27:48.787 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:done with trak for track_2
03-18 10:27:48.789 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:About to create mdat
03-18 10:27:48.789 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:mdat crated
03-18 10:27:48.790 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.790 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:48.793 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:potentially expensive size() call
03-18 10:27:48.793 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: LazyList:blowup running
03-18 10:27:49.067 26598-26598/com.letsnurture.ln_202.videotrimdemo D/isoparser: DefaultMp4Builder:About to write 0
03-18 10:27:49.068 26598-26598/com.letsnurture.ln_202.videotrimdemo W/System.err: Building IsoFile took : 12ms
03-18 10:27:49.068 26598-26598/com.letsnurture.ln_202.videotrimdemo W/System.err: Writing IsoFile took  : 272ms

Problem: my output.mp4 file is resulted in 4KB and on playing it nothing is being played. Just black screen and player stops as soon as it starts.

Am I giving wrong start time and end time ?

Please help me with this. Thanks.

like image 396
Chintan Soni Avatar asked Jan 06 '23 10:01

Chintan Soni


1 Answers

Okay, it looks like you taken this or this code as base (actually, there are too many non-working examples in mp4parser repository). Unfortunately, this code is quite obsolete because mp4parser had some breaking changes; so, you should use older version of mp4parser (I guess 0.9.x) or fix the code - this thread will be helpful.

I wrote a little class which cuts a mp4 file (using mp4parser 1.1.18):

import com.coremedia.iso.boxes.Container;
import com.coremedia.iso.boxes.MovieHeaderBox;
import com.googlecode.mp4parser.FileDataSourceImpl;
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 com.googlecode.mp4parser.util.Matrix;
import com.googlecode.mp4parser.util.Path;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * @author Nikolai Doronin {@literal <[email protected]>}.
 * @since 3/23/16.
 */
public class Mp4Cutter2 {

    public static void startTrim(File src, File dst, int startMs, int endMs) throws IOException {
        FileDataSourceImpl file = new FileDataSourceImpl(src);
        Movie movie = MovieCreator.build(file);
        // 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.getSampleDurations().length; i++) {
                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) track.getSampleDurations()[i] / (double) track.getTrackMetaData().getTimescale();
                currentSample++;
            }
            movie.addTrack(new CroppedTrack(track, startSample, endSample));
        }

        Container out = new DefaultMp4Builder().build(movie);
        MovieHeaderBox mvhd = Path.getPath(out, "moov/mvhd");
        mvhd.setMatrix(Matrix.ROTATE_180);
        if (!dst.exists()) {
            dst.createNewFile();
        }
        FileOutputStream fos = new FileOutputStream(dst);
        WritableByteChannel fc = fos.getChannel();
        try {
            out.writeContainer(fc);
        } finally {
            fc.close();
            fos.close();
            file.close();
        }

        file.close();
    }


    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.getSampleDurations().length; i++) {
            long delta = track.getSampleDurations()[i];

            if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
                timeOfSyncSamples[Arrays.binarySearch(track.getSyncSamples(), currentSample + 1)] = currentTime;
            }
            currentTime += (double) delta / (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];
    }
}

Usage:


final File source = new File("/home/user/downloads/1.mp4");
final File output = new File("/home/user/downloads/1.mp4" + System.currentTimeMillis() + ".mp4");
final int start = 24*1000;
final int end = 37*1000;
Mp4Cutter2.startTrim(source, output, start, end);

Test video: original, clipped.

I didn't look at your code closely, but I suppose that the problem is in correctTimeToSyncSample method.

Limitation

I should pay attention that it may be impossible to cut video exactly from 1s to 10s using mp4parser. In the test video, correctTimeToSyncSample changed startTime and endTime from 24,37 to 21.12, 43.44. You can skip this method and always use given start-end values, but you should be ready to some glitches in the output video, because a content of specific frame in h264 and similar codecs may depend on a content of previos frame/frames. Please have a look: original video, clipped video.

If you really need control start and stop values, you should think about ffmpeg.

like image 89
Mikalai Daronin Avatar answered Feb 06 '23 20:02

Mikalai Daronin