diff --git a/android/build.gradle b/android/build.gradle index ce0af33..401e0ec 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,7 +52,10 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':capacitor-android') implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation 'com.otaliastudios:transcoder:0.10.3' + implementation 'com.linkedin.android.litr:litr:1.4.18' + testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/MediaFormatUtils.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/MediaFormatUtils.java new file mode 100644 index 0000000..9ba78fc --- /dev/null +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/MediaFormatUtils.java @@ -0,0 +1,25 @@ +package com.whiteguru.capacitor.plugin.videoeditor; + +import android.media.MediaFormat; + +import androidx.annotation.NonNull; + +public class MediaFormatUtils { + public static int getInt(@NonNull MediaFormat mediaFormat, @NonNull String key) { + return getInt(mediaFormat, key, -1); + } + + public static int getInt(@NonNull MediaFormat mediaFormat, @NonNull String key, int defaultValue) { + if (mediaFormat.containsKey(key)) { + return mediaFormat.getInteger(key); + } + return defaultValue; + } + + public static long getLong(@NonNull MediaFormat mediaFormat, @NonNull String key) { + if (mediaFormat.containsKey(key)) { + return mediaFormat.getLong(key); + } + return -1; + } +} diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/TranscodeSettings.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/TranscodeSettings.java index 7936c88..5560b72 100644 --- a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/TranscodeSettings.java +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/TranscodeSettings.java @@ -1,16 +1,15 @@ package com.whiteguru.capacitor.plugin.videoeditor; public class TranscodeSettings { + private int height = 0; private int width = 0; private boolean keepAspectRatio = true; - private String codec = ""; - public TranscodeSettings() { } - public TranscodeSettings(int height, int width, boolean keepAspectRatio, String codec) { + public TranscodeSettings(int height, int width, boolean keepAspectRatio) { if (height < 0) { throw new IllegalArgumentException("Parameter height cannot be negative"); } @@ -22,7 +21,6 @@ public TranscodeSettings(int height, int width, boolean keepAspectRatio, String this.height = height; this.width = width; this.keepAspectRatio = keepAspectRatio; - this.codec = codec; } public int getHeight() { @@ -56,12 +54,4 @@ public boolean isKeepAspectRatio() { public void setKeepAspectRatio(boolean keepAspectRatio) { this.keepAspectRatio = keepAspectRatio; } - - public String getCodec() { - return codec; - } - - public void setCodec(String codec) { - this.codec = codec; - } } diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/TrimSettings.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/TrimSettings.java index 22d04f8..3b101b9 100644 --- a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/TrimSettings.java +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/TrimSettings.java @@ -20,6 +20,10 @@ public TrimSettings(long startsAt, long endsAt) { this.endsAt = endsAt; } + /** + * Get startsAt in miliSeconds + * @return startsAt in miliSeconds + */ public long getStartsAt() { return startsAt; } @@ -32,6 +36,10 @@ public void setStartsAt(long startsAt) { this.startsAt = startsAt; } + /** + * Get endsAt in miliSeconds + * @return endsAt in miliSeconds + */ public long getEndsAt() { return endsAt; } diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorLitr.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorLitr.java new file mode 100644 index 0000000..4a25040 --- /dev/null +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorLitr.java @@ -0,0 +1,214 @@ +package com.whiteguru.capacitor.plugin.videoeditor; + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.getcapacitor.Logger; +import com.linkedin.android.litr.MediaTransformer; +import com.linkedin.android.litr.TransformationListener; +import com.linkedin.android.litr.TransformationOptions; +import com.linkedin.android.litr.analytics.TrackTransformationInfo; +import com.linkedin.android.litr.io.MediaRange; +import com.whiteguru.capacitor.plugin.videoeditor.dto.SourceMedia; +import com.whiteguru.capacitor.plugin.videoeditor.dto.VideoSize; +import com.whiteguru.capacitor.plugin.videoeditor.dto.VideoTrackFormat; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.UUID; + +public class VideoEditorLitr { + static final int DEFAULT_VIDEO_FRAMERATE = 30; + static final int DEFAULT_VIDEO_KEY_FRAME_INTERVAL = 5; + static final int DEFAULT_AUDIO_BITRATE = 128000; + static final int DEFAULT_AUDIO_CHANNEL_COUNT = 2; + static final int DEFAULT_AUDIO_SAMPLE_RATE = 44100; + static final String DEFAULT_AUDIO_MIME = "audio/mp4a-latm"; + + // For AVC this should be a reasonable default. + // https://stackoverflow.com/a/5220554/4288782 + public static long estimateVideoBitRate(int width, int height, int frameRate) { + return (long) (0.07F * 2 * width * height * frameRate); + } + + public void edit(Context context, File srcFile, File outFile, TrimSettings trimSettings, TranscodeSettings transcodeSettings, TransformationListener videoTransformationListener) throws IOException { + MediaTransformer mediaTransformer = new MediaTransformer(context); + + String requestId = UUID.randomUUID().toString(); + Uri sourceVideoUri = Uri.fromFile(srcFile); + String targetVideoFilePath = outFile.getPath(); + SourceMedia sourceMedia = new SourceMedia(context, sourceVideoUri); + + // Resolution + List videoTracks = sourceMedia.getVideoTracks(); + if (videoTracks.size() == 0) { + throw new IOException("Video track not found"); + } + VideoSize targetVideoSize = calculateTargetVideoSize(videoTracks.get(0), transcodeSettings); + + Logger.debug("Source video size: " + (new VideoSize(videoTracks.get(0).width, videoTracks.get(0).height))); + Logger.debug("Target video size: " + targetVideoSize); + + // Trim + long startsAtUs = trimSettings.getStartsAt() * 1000; + long endsAtUs = trimSettings.getEndsAt() == 0 ? Long.MAX_VALUE : trimSettings.getEndsAt() * 1000; + + TransformationOptions transformationOptions = new TransformationOptions.Builder() + .setGranularity(MediaTransformer.GRANULARITY_DEFAULT) + .setSourceMediaRange(new MediaRange(startsAtUs, endsAtUs)) + .build(); + + // Video codec config + MediaFormat targetVideoFormat = new MediaFormat(); + targetVideoFormat.setString(MediaFormat.KEY_MIME, "video/avc"); + targetVideoFormat.setInteger(MediaFormat.KEY_WIDTH, targetVideoSize.width); + targetVideoFormat.setInteger(MediaFormat.KEY_HEIGHT, targetVideoSize.height); + targetVideoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, DEFAULT_VIDEO_FRAMERATE); + targetVideoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_VIDEO_KEY_FRAME_INTERVAL); + targetVideoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + targetVideoFormat.setInteger(MediaFormat.KEY_BIT_RATE, (int) estimateVideoBitRate( + targetVideoSize.width, + targetVideoSize.height, + DEFAULT_VIDEO_FRAMERATE + )); + + // Audio codec config + MediaFormat targetAudioFormat = new MediaFormat(); + targetAudioFormat.setString(MediaFormat.KEY_MIME, DEFAULT_AUDIO_MIME); + targetAudioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, DEFAULT_AUDIO_CHANNEL_COUNT); + targetAudioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, DEFAULT_AUDIO_SAMPLE_RATE); + targetAudioFormat.setInteger(MediaFormat.KEY_BIT_RATE, DEFAULT_AUDIO_BITRATE); + + TransformationListener listener = new TransformationListener() { + @Override + public void onStarted(@NonNull String id) { + videoTransformationListener.onStarted(id); + } + + @Override + public void onProgress(@NonNull String id, float progress) { + videoTransformationListener.onProgress(id, progress); + } + + @Override + public void onCompleted(@NonNull String id, @Nullable List trackTransformationInfos) { + videoTransformationListener.onCompleted(id, trackTransformationInfos); + + mediaTransformer.release(); + } + + @Override + public void onCancelled(@NonNull String id, @Nullable List trackTransformationInfos) { + videoTransformationListener.onCancelled(id, trackTransformationInfos); + + mediaTransformer.release(); + } + + @Override + public void onError(@NonNull String id, @Nullable Throwable cause, @Nullable List trackTransformationInfos) { + videoTransformationListener.onError(id, cause, trackTransformationInfos); + + mediaTransformer.release(); + } + }; + + mediaTransformer.transform( + requestId, + sourceVideoUri, + targetVideoFilePath, + targetVideoFormat, + targetAudioFormat, + listener, + transformationOptions + ); + } + + public void thumbnail(Context context, Uri srcUri, File outFile, int at, int width, int height) throws IOException { + + int quality = 80; + + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(context, srcUri); + + Bitmap bitmap = mmr.getFrameAtTime((long) at * 1000 * 1000); + + if (width > 0 || height > 0) { + int videoWidth = bitmap.getWidth(); + int videoHeight = bitmap.getHeight(); + double aspectRatio = (double) videoWidth / (double) videoHeight; + + int scaleWidth = Double.valueOf(height * aspectRatio).intValue(); + int scaleHeight = Double.valueOf(scaleWidth / aspectRatio).intValue(); + + final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, scaleWidth, scaleHeight, false); + bitmap.recycle(); + bitmap = resizedBitmap; + } + + OutputStream outStream = new FileOutputStream(outFile); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outStream); + } + + private VideoSize calculateTargetVideoSize(VideoTrackFormat videoTrackFormat, TranscodeSettings transcodeSettings) { + if (transcodeSettings.isKeepAspectRatio()) { + int mostSize = transcodeSettings.getWidth() == 0 && transcodeSettings.getHeight() == 0 + ? 1280 + : Math.max(transcodeSettings.getWidth(), transcodeSettings.getHeight()); + + return calculateVideoSizeAtMost(videoTrackFormat, mostSize); + } else { + if (transcodeSettings.getWidth() > 0 && transcodeSettings.getHeight() > 0) { + return new VideoSize( + transcodeSettings.getWidth(), + transcodeSettings.getHeight() + ); + } else { + return calculateVideoSizeAtMost(videoTrackFormat, 720); + } + } + } + + private VideoSize calculateVideoSizeAtMost(VideoTrackFormat videoTrackFormat, int mostSize) { + int sourceMajor = Math.max(videoTrackFormat.width, videoTrackFormat.height); + + if (sourceMajor <= mostSize) { + // No resize needed + return new VideoSize(videoTrackFormat.width, videoTrackFormat.height); + } + + int outWidth; + int outHeight; + if (videoTrackFormat.width >= videoTrackFormat.height) { + // Landscape + float inputRatio = (float) videoTrackFormat.height / videoTrackFormat.width; + + outWidth = mostSize; + outHeight = (int) ((float) mostSize * inputRatio); + } else { + // Portrait + float inputRatio = (float) videoTrackFormat.width / videoTrackFormat.height; + + outHeight = mostSize; + outWidth = (int) ((float) mostSize * inputRatio); + } + + if (outWidth % 2 != 0) { + outWidth--; + } + if (outHeight % 2 != 0) { + outHeight--; + } + + return new VideoSize(outWidth, outHeight); + } +} diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditor.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorOtaliastudios.java similarity index 96% rename from android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditor.java rename to android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorOtaliastudios.java index 205d1bb..d13bb48 100644 --- a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditor.java +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorOtaliastudios.java @@ -19,7 +19,7 @@ import java.io.IOException; import java.io.OutputStream; -public class VideoEditor { +public class VideoEditorOtaliastudios { public void edit(File srcFile, File outFile, TrimSettings trimSettings, TranscodeSettings transcodeSettings, TranscoderListener listenerListener) throws IOException { DataSource source = new FilePathDataSource(srcFile.getAbsolutePath()); source.initialize(); @@ -29,8 +29,8 @@ public void edit(File srcFile, File outFile, TrimSettings trimSettings, Transcod throw new IllegalArgumentException("Input video with 0 duration"); } - long startsAtUs = trimSettings.getStartsAt() * 1000 * 1000; - long endsAtUs = trimSettings.getEndsAt() == 0 ? durationUs : Math.min(durationUs, trimSettings.getEndsAt() * 1000 * 1000); + long startsAtUs = trimSettings.getStartsAt() * 1000; + long endsAtUs = trimSettings.getEndsAt() == 0 ? durationUs : Math.min(durationUs, trimSettings.getEndsAt() * 1000); DataSource clip = new ClipDataSource( source, diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorPlugin.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorPlugin.java index bf85602..cc8a117 100644 --- a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorPlugin.java +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/VideoEditorPlugin.java @@ -4,6 +4,9 @@ import android.content.Context; import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.getcapacitor.JSObject; import com.getcapacitor.Logger; import com.getcapacitor.PermissionState; @@ -13,13 +16,18 @@ import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.Permission; import com.getcapacitor.annotation.PermissionCallback; + import com.otaliastudios.transcoder.TranscoderListener; +import com.linkedin.android.litr.analytics.TrackTransformationInfo; +import com.linkedin.android.litr.TransformationListener; + import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.List; @CapacitorPlugin( name = "VideoEditor", @@ -40,6 +48,97 @@ public class VideoEditorPlugin extends Plugin { @PluginMethod public void edit(PluginCall call) { + editLitr(call); + } + + public void editLitr(PluginCall call) { + String path = call.getString("path"); + JSObject trim = call.getObject("trim", new JSObject()); + JSObject transcode = call.getObject("transcode", new JSObject()); + + if (path == null) { + call.reject("Input file path is required"); + return; + } + + if (checkStoragePermissions(call)) { + Uri inputUri = Uri.parse(path); + File inputFile = new File(inputUri.getPath()); + + if (!inputFile.canRead()) { + call.reject("Cannot read input file: " + inputFile.getAbsolutePath()); + return; + } + + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date()); + String fileName = "VID_" + timeStamp + "_"; + File storageDir = getContext().getCacheDir(); + + execute( + () -> { + try { + File outputFile = File.createTempFile(fileName, ".mp4", storageDir); + + VideoEditorLitr implementation = new VideoEditorLitr(); + + TrimSettings trimSettings = new TrimSettings(trim.getInteger("startsAt", 0), trim.getInteger("endsAt", 0)); + + TranscodeSettings transcodeSettings = new TranscodeSettings( + transcode.getInteger("height", 0), + transcode.getInteger("width", 0), + transcode.getBoolean("keepAspectRatio", true) + ); + + TransformationListener videoTransformationListener = new TransformationListener() { + @Override + public void onStarted(@NonNull String id) { + Logger.debug("Transcode started"); + } + + @Override + public void onProgress(@NonNull String id, float progress) { + Logger.debug("Transcode running " + progress); + + JSObject ret = new JSObject(); + ret.put("progress", progress); + + notifyListeners("transcodeProgress", ret); + } + + @Override + public void onCompleted(@NonNull String id, @Nullable List trackTransformationInfos) { + Logger.debug("Transcode completed"); + + JSObject ret = new JSObject(); + ret.put("file", createMediaFile(outputFile)); + call.resolve(ret); + } + + @Override + public void onCancelled(@NonNull String id, @Nullable List trackTransformationInfos) { + Logger.debug("Transcode cancelled"); + + call.reject("Transcode canceled"); + } + + @Override + public void onError(@NonNull String id, @Nullable Throwable cause, @Nullable List trackTransformationInfos) { + Logger.debug("Transcode error: " + (cause != null ? cause.getMessage() : "")); + + call.reject("Transcode failed: " + (cause != null ? cause.getMessage() : "")); + } + }; + + implementation.edit(getContext(), inputFile, outputFile, trimSettings, transcodeSettings, videoTransformationListener); + } catch (IOException e) { + call.reject(e.getMessage()); + } + } + ); + } + } + + public void editOtaliastudio(PluginCall call) { String path = call.getString("path"); JSObject trim = call.getObject("trim", new JSObject()); JSObject transcode = call.getObject("transcode", new JSObject()); @@ -67,15 +166,14 @@ public void edit(PluginCall call) { try { File outputFile = File.createTempFile(fileName, ".mp4", storageDir); - VideoEditor implementation = new VideoEditor(); + VideoEditorOtaliastudios implementation = new VideoEditorOtaliastudios(); TrimSettings trimSettings = new TrimSettings(trim.getInteger("startsAt", 0), trim.getInteger("endsAt", 0)); TranscodeSettings transcodeSettings = new TranscodeSettings( transcode.getInteger("height", 0), transcode.getInteger("width", 0), - transcode.getBoolean("keepAspectRatio", true), - "" + transcode.getBoolean("keepAspectRatio", true) ); TranscoderListener listenerListener = new TranscoderListener() { @@ -138,7 +236,7 @@ public void thumbnail(PluginCall call) { try { outputFile = File.createTempFile(fileName, ".jpg", storageDir); - VideoEditor implementation = new VideoEditor(); + VideoEditorLitr implementation = new VideoEditorLitr(); implementation.thumbnail(this.getContext(), inputUri, outputFile, at, width, height); } catch (IOException e) { diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/AudioTrackFormat.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/AudioTrackFormat.java new file mode 100644 index 0000000..624223b --- /dev/null +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/AudioTrackFormat.java @@ -0,0 +1,23 @@ +package com.whiteguru.capacitor.plugin.videoeditor.dto; + +import androidx.annotation.NonNull; + +public class AudioTrackFormat extends MediaTrackFormat { + + public int channelCount; + public int samplingRate; + public int bitrate; + public long duration; + + public AudioTrackFormat(int index, @NonNull String mimeType) { + super(index, mimeType); + } + + public AudioTrackFormat(@NonNull AudioTrackFormat audioTrackFormat) { + super(audioTrackFormat); + this.channelCount = audioTrackFormat.channelCount; + this.samplingRate = audioTrackFormat.samplingRate; + this.bitrate = audioTrackFormat.bitrate; + this.duration = audioTrackFormat.duration; + } +} diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/GenericTrackFormat.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/GenericTrackFormat.java new file mode 100644 index 0000000..9a71d2d --- /dev/null +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/GenericTrackFormat.java @@ -0,0 +1,10 @@ +package com.whiteguru.capacitor.plugin.videoeditor.dto; + +import androidx.annotation.NonNull; + +public class GenericTrackFormat extends MediaTrackFormat { + + public GenericTrackFormat(int index, @NonNull String mimeType) { + super(index, mimeType); + } +} diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/MediaTrackFormat.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/MediaTrackFormat.java new file mode 100644 index 0000000..c269bb7 --- /dev/null +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/MediaTrackFormat.java @@ -0,0 +1,19 @@ +package com.whiteguru.capacitor.plugin.videoeditor.dto; + +import androidx.annotation.NonNull; + +public class MediaTrackFormat { + + public int index; + public String mimeType; + + MediaTrackFormat(int index, @NonNull String mimeType) { + this.index = index; + this.mimeType = mimeType; + } + + MediaTrackFormat(@NonNull MediaTrackFormat mediaTrackFormat) { + this.index = mediaTrackFormat.index; + this.mimeType = mediaTrackFormat.mimeType; + } +} diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/SourceMedia.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/SourceMedia.java new file mode 100644 index 0000000..97d464b --- /dev/null +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/SourceMedia.java @@ -0,0 +1,101 @@ +package com.whiteguru.capacitor.plugin.videoeditor.dto; + +import static com.whiteguru.capacitor.plugin.videoeditor.MediaFormatUtils.getInt; +import static com.whiteguru.capacitor.plugin.videoeditor.MediaFormatUtils.getLong; + +import android.content.Context; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import com.linkedin.android.litr.utils.MediaFormatUtils; +import com.linkedin.android.litr.utils.TranscoderUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SourceMedia { + + public Uri uri; + public long size; + public float durationMs; // in miliSeconds + + public List tracks = new ArrayList<>(); + + public SourceMedia(Context context, @NonNull Uri uri) throws IOException { + this.loadUri(context, uri); + } + + @NonNull + public void loadUri(Context context, @NonNull Uri uri) throws IOException { + this.uri = uri; + this.size = TranscoderUtils.getSize(context, uri); + this.durationMs = getMediaDuration(context, uri) / 1000f; + + try { + MediaExtractor mediaExtractor = new MediaExtractor(); + mediaExtractor.setDataSource(context, uri, null); + this.tracks = new ArrayList<>(mediaExtractor.getTrackCount()); + + for (int track = 0; track < mediaExtractor.getTrackCount(); track++) { + MediaFormat mediaFormat = mediaExtractor.getTrackFormat(track); + String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME); + if (mimeType == null) { + continue; + } + + if (mimeType.startsWith("video")) { + VideoTrackFormat videoTrack = new VideoTrackFormat(track, mimeType); + videoTrack.width = getInt(mediaFormat, MediaFormat.KEY_WIDTH); + videoTrack.height = getInt(mediaFormat, MediaFormat.KEY_HEIGHT); + videoTrack.duration = getLong(mediaFormat, MediaFormat.KEY_DURATION); + videoTrack.frameRate = MediaFormatUtils.getFrameRate(mediaFormat, -1).intValue(); + videoTrack.keyFrameInterval = MediaFormatUtils.getIFrameInterval(mediaFormat, -1).intValue(); + videoTrack.rotation = getInt(mediaFormat, MediaFormat.KEY_ROTATION, 0); + videoTrack.bitrate = getInt(mediaFormat, MediaFormat.KEY_BIT_RATE); + this.tracks.add(videoTrack); + } else if (mimeType.startsWith("audio")) { + AudioTrackFormat audioTrack = new AudioTrackFormat(track, mimeType); + audioTrack.channelCount = getInt(mediaFormat, MediaFormat.KEY_CHANNEL_COUNT); + audioTrack.samplingRate = getInt(mediaFormat, MediaFormat.KEY_SAMPLE_RATE); + audioTrack.duration = getLong(mediaFormat, MediaFormat.KEY_DURATION); + audioTrack.bitrate = getInt(mediaFormat, MediaFormat.KEY_BIT_RATE); + this.tracks.add(audioTrack); + } else { + this.tracks.add(new GenericTrackFormat(track, mimeType)); + } + } + } catch (IOException ex) { + throw new IOException("Failed to extract sourceMedia"); + } + } + + public List getVideoTracks(){ + List videoTracks = new ArrayList<>(); + + for (MediaTrackFormat track : tracks) { + if (track instanceof VideoTrackFormat) { + videoTracks.add((VideoTrackFormat) track); + } + } + + return videoTracks; + } + + /** + * Returns media duration in microSeconds + * @param context Activity context + * @param uri Media uri + * @return media duration in microSeconds + */ + private long getMediaDuration(Context context, @NonNull Uri uri) { + MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(context, uri); + String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + return Long.parseLong(durationStr) * 1000; + } +} diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/VideoSize.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/VideoSize.java new file mode 100644 index 0000000..8364b1c --- /dev/null +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/VideoSize.java @@ -0,0 +1,19 @@ +package com.whiteguru.capacitor.plugin.videoeditor.dto; + +public class VideoSize { + public int width; + public int height; + + public VideoSize(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public String toString() { + return "VideoSize{" + + "width=" + width + + ", height=" + height + + '}'; + } +} diff --git a/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/VideoTrackFormat.java b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/VideoTrackFormat.java new file mode 100644 index 0000000..fc24d16 --- /dev/null +++ b/android/src/main/java/com/whiteguru/capacitor/plugin/videoeditor/dto/VideoTrackFormat.java @@ -0,0 +1,28 @@ +package com.whiteguru.capacitor.plugin.videoeditor.dto; + +import androidx.annotation.NonNull; + +public class VideoTrackFormat extends MediaTrackFormat { + public int width; + public int height; + public int bitrate; + public int frameRate; + public int keyFrameInterval; + public long duration; + public int rotation; + + public VideoTrackFormat(int index, @NonNull String mimeType) { + super(index, mimeType); + } + + public VideoTrackFormat(@NonNull VideoTrackFormat videoTrackFormat) { + super(videoTrackFormat); + this.width = videoTrackFormat.width; + this.height = videoTrackFormat.height; + this.bitrate = videoTrackFormat.bitrate; + this.frameRate = videoTrackFormat.frameRate; + this.keyFrameInterval = videoTrackFormat.keyFrameInterval; + this.duration = videoTrackFormat.duration; + this.rotation = videoTrackFormat.rotation; + } +}