4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 | * use this file except in compliance with the License. You may obtain a copy of
6 | * the License at
7 | *
10 | * Unless required by applicable law or agreed To in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | * License for the specific language governing permissions and limitations under
14 | * the License.
15 | */
16 | package com.deep.videotrimmer.utils;
17 |
18 | import android.util.Log;
19 |
20 | import java.util.ArrayList;
21 | import java.util.List;
22 | import java.util.concurrent.Executor;
23 | import java.util.concurrent.ExecutorService;
24 | import java.util.concurrent.Executors;
25 | import java.util.concurrent.Future;
26 | import java.util.concurrent.ScheduledExecutorService;
27 | import java.util.concurrent.TimeUnit;
28 | import java.util.concurrent.atomic.AtomicBoolean;
29 | /**
30 | * Created by Deep Patel
31 | * (Sr. Android Developer)
32 | * on 6/4/2018
33 | */
34 | public final class BackgroundExecutor {
35 |
36 | private static final String TAG = "BackgroundExecutor";
37 |
38 | public static final Executor DEFAULT_EXECUTOR = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors());
39 | private static Executor executor = DEFAULT_EXECUTOR;
40 | private static final List TASKS = new ArrayList<>();
41 | private static final ThreadLocal CURRENT_SERIAL = new ThreadLocal<>();
42 |
43 | private BackgroundExecutor() {
44 | }
45 |
46 | /**
47 | * Execute a runnable after the given delay.
48 | *
49 | * @param runnable the task to execute
50 | * @param delay the time from now to delay execution, in milliseconds
51 | * if delay is strictly positive and the current
52 | * executor does not support scheduling (if
53 | * Executor has been called with such an
54 | * executor)
55 | * @return Future associated to the running task
56 | * @throws IllegalArgumentException if the current executor set by Executor
57 | * does not support scheduling
58 | */
59 | private static Future> directExecute(Runnable runnable, long delay) {
60 | Future> future = null;
61 | if (delay > 0) {
62 | /* no serial, but a delay: schedule the task */
63 | if (!(executor instanceof ScheduledExecutorService)) {
64 | throw new IllegalArgumentException("The executor set does not support scheduling");
65 | }
66 | ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) executor;
67 | future = scheduledExecutorService.schedule(runnable, delay, TimeUnit.MILLISECONDS);
68 | } else {
69 | if (executor instanceof ExecutorService) {
70 | ExecutorService executorService = (ExecutorService) executor;
71 | future = executorService.submit(runnable);
72 | } else {
73 | /* non-cancellable task */
74 | executor.execute(runnable);
75 | }
76 | }
77 | return future;
78 | }
79 |
80 | /**
81 | * Execute a task after (at least) its delay and after all
82 | * tasks added with the same non-null serial(if any) have
83 | * completed execution.
84 | *
85 | * @param task the task to execute
86 | * @throws IllegalArgumentException if task.delay is strictly positive and the
87 | * current executor does not support scheduling (if
88 | * Executor has been called with such an
89 | * executor)
90 | */
91 | public static synchronized void execute(Task task) {
92 | Future> future = null;
93 | if (task.serial == null || !hasSerialRunning(task.serial)) {
94 | task.executionAsked = true;
95 | future = directExecute(task, task.remainingDelay);
96 | }
97 | if ((task.id != null || task.serial != null) && !task.managed.get()) {
98 | /* keep task */
99 | task.future = future;
100 | TASKS.add(task);
101 | }
102 | }
103 |
104 | /**
105 | * Indicates whether a task with the specified serial has been
106 | * submitted to the executor.
107 | *
108 | * @param serial the serial queue
109 | * @return true if such a task has been submitted,
110 | * falseotherwise
111 | */
112 | private static boolean hasSerialRunning(String serial) {
113 | for (Task task : TASKS) {
114 | if (task.executionAsked && serial.equals(task.serial)) {
115 | return true;
116 | }
117 | }
118 | return false;
119 | }
120 |
121 | /**
122 | * Retrieve and remove the first task having the specified
123 | * serial (if any).
124 | *
125 | * @param serial the serial queue
126 | * @return task if found, null otherwise
127 | */
128 | private static Task take(String serial) {
129 | int len = TASKS.size();
130 | for (int i = 0; i < len; i++) {
131 | if (serial.equals(TASKS.get(i).serial)) {
132 | return TASKS.remove(i);
133 | }
134 | }
135 | return null;
136 | }
137 |
138 | /**
139 | * Cancel all tasks having the specified id
140 | *
141 | * @param id the cancellation identifier
142 | * @param mayInterruptIfRunning true if the thread executing this task should be
143 | * interrupted; otherwise, in-progress tasks are allowed to
144 | * complete
145 | */
146 | public static synchronized void cancelAll(String id, boolean mayInterruptIfRunning) {
147 | for (int i = TASKS.size() - 1; i >= 0; i--) {
148 | Task task = TASKS.get(i);
149 | if (id.equals(task.id)) {
150 | if (task.future != null) {
151 | task.future.cancel(mayInterruptIfRunning);
152 | if (!task.managed.getAndSet(true)) {
153 | /*
154 | * the task has been submitted to the executor, but its
155 | * execution has not started yet, so that its run()
156 | * method will never call postExecute()
157 | */
158 | task.postExecute();
159 | }
160 | } else if (task.executionAsked) {
161 | Log.w(TAG, "A task with id " + task.id + " cannot be cancelled (the executor set does not support it)");
162 | } else {
163 | /* this task has not been submitted to the executor */
164 | TASKS.remove(i);
165 | }
166 | }
167 | }
168 | }
169 |
170 | public static abstract class Task implements Runnable {
171 |
172 | private String id;
173 | private long remainingDelay;
174 | private long targetTimeMillis; /* since epoch */
175 | private String serial;
176 | private boolean executionAsked;
177 | private Future> future;
178 |
179 | /*
180 | * A task can be cancelled after it has been submitted to the executor
181 | * but before its run() method is called. In that case, run() will never
182 | * be called, hence neither will postExecute(): the tasks with the same
183 | * serial identifier (if any) will never be submitted.
184 | *
185 | * Therefore, cancelAll() *must* call postExecute() if run() is not
186 | * started.
187 | *
188 | * This flag guarantees that either cancelAll() or run() manages this
189 | * task post execution, but not both.
190 | */
191 | private AtomicBoolean managed = new AtomicBoolean();
192 |
193 | public Task(String id, long delay, String serial) {
194 | if (!"".equals(id)) {
195 | this.id = id;
196 | }
197 | if (delay > 0) {
198 | remainingDelay = delay;
199 | targetTimeMillis = System.currentTimeMillis() + delay;
200 | }
201 | if (!"".equals(serial)) {
202 | this.serial = serial;
203 | }
204 | }
205 |
206 | @Override
207 | public void run() {
208 | if (managed.getAndSet(true)) {
209 | /* cancelled and postExecute() already called */
210 | return;
211 | }
212 |
213 | try {
214 | CURRENT_SERIAL.set(serial);
215 | execute();
216 | } finally {
217 | /* handle next tasks */
218 | postExecute();
219 | }
220 | }
221 |
222 | public abstract void execute();
223 |
224 | private void postExecute() {
225 | if (id == null && serial == null) {
226 | /* nothing to do */
227 | return;
228 | }
229 | CURRENT_SERIAL.set(null);
230 | synchronized (BackgroundExecutor.class) {
231 | /* execution complete */
232 | TASKS.remove(this);
233 |
234 | if (serial != null) {
235 | Task next = take(serial);
236 | if (next != null) {
237 | if (next.remainingDelay != 0) {
238 | /* the delay may not have elapsed yet */
239 | next.remainingDelay = Math.max(0L, targetTimeMillis - System.currentTimeMillis());
240 | }
241 | /* a task having the same serial was queued, execute it */
242 | BackgroundExecutor.execute(next);
243 | }
244 | }
245 | }
246 | }
247 | }
248 | }
249 |
250 |
--------------------------------------------------------------------------------
/videotrimmer/src/main/java/com/deep/videotrimmer/utils/FileUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2016 Knowledge, education for life.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.deep.videotrimmer.utils;
25 |
26 | import android.annotation.SuppressLint;
27 | import android.content.ContentUris;
28 | import android.content.Context;
29 | import android.database.Cursor;
30 | import android.net.Uri;
31 | import android.os.Build;
32 | import android.os.Environment;
33 | import android.provider.DocumentsContract;
34 | import android.provider.MediaStore;
35 | import android.support.annotation.NonNull;
36 | /**
37 | * Created by Deep Patel
38 | * (Sr. Android Developer)
39 | * on 6/4/2018
40 | */
41 | public class FileUtils {
42 |
43 | /**
44 | * Get a file path from a Uri. This will get the the path for Storage Access
45 | * Framework Documents, as well as the _data field for the MediaStore and
46 | * other file-based ContentProviders.
47 | *
48 | * Callers should check whether the path is local before assuming it
49 | * represents a local file.
50 | *
51 | * @param context The context.
52 | * @param uri The Uri to query.
53 | * @author Deep Patel
54 | */
55 | @SuppressLint("NewApi")
56 | public static String getPath(final Context context, final Uri uri) {
57 |
58 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
59 |
60 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
61 | if (isExternalStorageDocument(uri)) {
62 | final String docId = DocumentsContract.getDocumentId(uri);
63 | final String[] split = docId.split(":");
64 | final String type = split[0];
65 |
66 | if ("primary".equalsIgnoreCase(type)) {
67 | return Environment.getExternalStorageDirectory() + "/" + split[1];
68 | }
69 |
70 | }
71 | else if (isDownloadsDocument(uri)) {
72 |
73 | final String id = DocumentsContract.getDocumentId(uri);
74 | final Uri contentUri = ContentUris.withAppendedId(
75 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
76 |
77 | return getDataColumn(context, contentUri, null, null);
78 | }
79 | else if (isMediaDocument(uri)) {
80 | final String docId = DocumentsContract.getDocumentId(uri);
81 | final String[] split = docId.split(":");
82 | final String type = split[0];
83 |
84 | Uri contentUri = null;
85 | if ("image".equals(type)) {
86 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
87 | } else if ("video".equals(type)) {
88 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
89 | } else if ("audio".equals(type)) {
90 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
91 | }
92 |
93 | final String selection = "_id=?";
94 | final String[] selectionArgs = new String[]{
95 | split[1]
96 | };
97 |
98 | return getDataColumn(context, contentUri, selection, selectionArgs);
99 | }
100 | }
101 | else if ("content".equalsIgnoreCase(uri.getScheme())) {
102 |
103 | if (isGooglePhotosUri(uri))
104 | return uri.getLastPathSegment();
105 |
106 | return getDataColumn(context, uri, null, null);
107 | }
108 | else if ("file".equalsIgnoreCase(uri.getScheme())) {
109 | return uri.getPath();
110 | }
111 |
112 | return null;
113 | }
114 |
115 | /**
116 | * Get the value of the data column for this Uri. This is useful for
117 | * MediaStore Uris, and other file-based ContentProviders.
118 | *
119 | * @param context The context.
120 | * @param uri The Uri to query.
121 | * @param selection (Optional) Filter used in the query.
122 | * @param selectionArgs (Optional) Selection arguments used in the query.
123 | * @return The value of the _data column, which is typically a file path.
124 | * @author Deep Patel
125 | */
126 | private static String getDataColumn(@NonNull Context context, Uri uri, String selection, String[] selectionArgs) {
127 |
128 | Cursor cursor = null;
129 | final String column = "_data";
130 | final String[] projection = {column};
131 |
132 | try {
133 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
134 | if (cursor != null && cursor.moveToFirst()) {
135 | final int column_index = cursor.getColumnIndexOrThrow(column);
136 | return cursor.getString(column_index);
137 | }
138 | } finally {
139 | if (cursor != null)
140 | cursor.close();
141 | }
142 | return null;
143 | }
144 |
145 | /**
146 | * @param uri The Uri to check.
147 | * @return Whether the Uri authority is Google Photos.
148 | */
149 | private static boolean isGooglePhotosUri(Uri uri) {
150 | return "com.google.android.apps.photos.content".equals(uri.getAuthority());
151 | }
152 |
153 | /**
154 | * @param uri The Uri to check.
155 | * @return Whether the Uri authority is ExternalStorageProvider.
156 | */
157 | private static boolean isExternalStorageDocument(@NonNull Uri uri) {
158 | return "com.android.externalstorage.documents".equals(uri.getAuthority());
159 | }
160 |
161 | /**
162 | * @param uri The Uri to check.
163 | * @return Whether the Uri authority is DownloadsProvider.
164 | */
165 | private static boolean isDownloadsDocument(@NonNull Uri uri) {
166 | return "com.android.providers.downloads.documents".equals(uri.getAuthority());
167 | }
168 |
169 | /**
170 | * @param uri The Uri to check.
171 | * @return Whether the Uri authority is MediaProvider.
172 | */
173 | private static boolean isMediaDocument(@NonNull Uri uri) {
174 | return "com.android.providers.media.documents".equals(uri.getAuthority());
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/videotrimmer/src/main/java/com/deep/videotrimmer/utils/TrimVideoUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2016 Knowledge, education for life.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.deep.videotrimmer.utils;
25 |
26 | import android.net.Uri;
27 | import android.support.annotation.NonNull;
28 | import android.util.Log;
29 |
30 | import com.coremedia.iso.boxes.Container;
31 | import com.deep.videotrimmer.interfaces.OnTrimVideoListener;
32 | import com.googlecode.mp4parser.FileDataSourceViaHeapImpl;
33 | import com.googlecode.mp4parser.authoring.Movie;
34 | import com.googlecode.mp4parser.authoring.Track;
35 | import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
36 | import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
37 | import com.googlecode.mp4parser.authoring.tracks.AppendTrack;
38 | import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
39 |
40 | import java.io.File;
41 | import java.io.FileOutputStream;
42 | import java.io.IOException;
43 | import java.nio.channels.FileChannel;
44 | import java.text.SimpleDateFormat;
45 | import java.util.Arrays;
46 | import java.util.Date;
47 | import java.util.LinkedList;
48 | import java.util.List;
49 | import java.util.Locale;
50 | /**
51 | * Created by Deep Patel
52 | * (Sr. Android Developer)
53 | * on 6/4/2018
54 | */
55 | public class TrimVideoUtils {
56 |
57 | private static final String TAG = TrimVideoUtils.class.getSimpleName();
58 |
59 | @SuppressWarnings("ResultOfMethodCallIgnored")
60 | public static void startTrim(@NonNull File src, @NonNull String dst, long startMs, long endMs, @NonNull OnTrimVideoListener callback) throws IOException {
61 | final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
62 | final String fileName = "MP4_" + timeStamp + ".mp4";
63 | final String filePath = dst + fileName;
64 |
65 | File file = new File(filePath);
66 | file.getParentFile().mkdirs();
67 | Log.d(TAG, "Generated file path " + filePath);
68 | genVideoUsingMp4Parser(src, file, startMs, endMs, callback);
69 | }
70 |
71 | @SuppressWarnings("ResultOfMethodCallIgnored")
72 | private static void genVideoUsingMp4Parser(@NonNull File src, @NonNull File dst, long startMs, long endMs, @NonNull OnTrimVideoListener callback) throws IOException {
73 |
74 | Movie movie = MovieCreator.build(new FileDataSourceViaHeapImpl(src.getAbsolutePath()));
75 |
76 | List