Skip to content

Commit

Permalink
Use Litr transcoder engine.
Browse files Browse the repository at this point in the history
  • Loading branch information
dragermrb committed Feb 25, 2022
1 parent dbc1aa8 commit 4ff510a
Show file tree
Hide file tree
Showing 13 changed files with 557 additions and 19 deletions.
3 changes: 3 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<VideoTrackFormat> 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<TrackTransformationInfo> trackTransformationInfos) {
videoTransformationListener.onCompleted(id, trackTransformationInfos);

mediaTransformer.release();
}

@Override
public void onCancelled(@NonNull String id, @Nullable List<TrackTransformationInfo> trackTransformationInfos) {
videoTransformationListener.onCancelled(id, trackTransformationInfos);

mediaTransformer.release();
}

@Override
public void onError(@NonNull String id, @Nullable Throwable cause, @Nullable List<TrackTransformationInfo> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 4ff510a

Please sign in to comment.