Skip to content

Commit e37040a

Browse files
authored
Fix various bugs (#18)
* Create TrackDataSink * Add TranscoderOptions(DataSink) instead of output path * Fix MultiDataSink and TrackDataSink writing * Add mimeType option to both strategies * Fix bugs * Do not release source for one track if it's still used for another * Fix rotation bug * Remove TrackDataSink
1 parent 87f3fd4 commit e37040a

File tree

13 files changed

+185
-73
lines changed

13 files changed

+185
-73
lines changed

demo/src/main/java/com/otaliastudios/transcoder/demo/TranscoderActivity.java

+41-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import android.annotation.SuppressLint;
44
import android.content.ClipData;
55
import android.content.Intent;
6+
import android.media.MediaMuxer;
67
import android.net.Uri;
78
import android.os.Bundle;
89
import android.os.SystemClock;
@@ -14,14 +15,19 @@
1415
import com.otaliastudios.transcoder.Transcoder;
1516
import com.otaliastudios.transcoder.TranscoderListener;
1617
import com.otaliastudios.transcoder.TranscoderOptions;
18+
import com.otaliastudios.transcoder.engine.TrackStatus;
1719
import com.otaliastudios.transcoder.engine.TrackType;
1820
import com.otaliastudios.transcoder.internal.Logger;
21+
import com.otaliastudios.transcoder.sink.DataSink;
22+
import com.otaliastudios.transcoder.sink.DefaultDataSink;
1923
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
2024
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy;
25+
import com.otaliastudios.transcoder.strategy.RemoveTrackStrategy;
2126
import com.otaliastudios.transcoder.strategy.TrackStrategy;
2227
import com.otaliastudios.transcoder.strategy.size.AspectRatioResizer;
2328
import com.otaliastudios.transcoder.strategy.size.FractionResizer;
2429
import com.otaliastudios.transcoder.strategy.size.PassThroughResizer;
30+
import com.otaliastudios.transcoder.validator.DefaultValidator;
2531

2632
import java.io.File;
2733
import java.io.IOException;
@@ -58,6 +64,7 @@ public class TranscoderActivity extends AppCompatActivity implements
5864
private TextView mAudioReplaceView;
5965

6066
private boolean mIsTranscoding;
67+
private boolean mIsAudioOnly;
6168
private Future<Void> mTranscodeFuture;
6269
private Uri mTranscodeInputUri1;
6370
private Uri mTranscodeInputUri2;
@@ -111,10 +118,13 @@ protected void onCreate(Bundle savedInstanceState) {
111118
mAudioReplaceGroup.setOnCheckedChangeListener((group, checkedId) -> {
112119
mAudioReplacementUri = null;
113120
mAudioReplaceView.setText("No replacement selected.");
114-
if (checkedId == R.id.replace_yes && !mIsTranscoding) {
115-
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT)
116-
.setType("audio/*"), REQUEST_CODE_PICK_AUDIO);
121+
if (checkedId == R.id.replace_yes) {
122+
if (!mIsTranscoding) {
123+
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT)
124+
.setType("audio/*"), REQUEST_CODE_PICK_AUDIO);
125+
}
117126
}
127+
onCheckedChanged(group, checkedId);
118128
});
119129
}
120130

@@ -136,10 +146,20 @@ private void syncParameters() {
136146
case R.id.sampleRate_48: sampleRate = 48000; break;
137147
default: sampleRate = DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT;
138148
}
139-
mTranscodeAudioStrategy = DefaultAudioStrategy.builder()
140-
.channels(channels)
141-
.sampleRate(sampleRate)
142-
.build();
149+
boolean removeAudio;
150+
switch (mAudioReplaceGroup.getCheckedRadioButtonId()) {
151+
case R.id.replace_remove: removeAudio = true; break;
152+
case R.id.replace_yes: removeAudio = false; break;
153+
default: removeAudio = false;
154+
}
155+
if (removeAudio) {
156+
mTranscodeAudioStrategy = new RemoveTrackStrategy();
157+
} else {
158+
mTranscodeAudioStrategy = DefaultAudioStrategy.builder()
159+
.channels(channels)
160+
.sampleRate(sampleRate)
161+
.build();
162+
}
143163

144164
int frames;
145165
switch (mVideoFramesGroup.getCheckedRadioButtonId()) {
@@ -234,7 +254,8 @@ private void transcode() {
234254
// Launch the transcoding operation.
235255
mTranscodeStartTime = SystemClock.uptimeMillis();
236256
setIsTranscoding(true);
237-
TranscoderOptions.Builder builder = Transcoder.into(mTranscodeOutputFile.getAbsolutePath());
257+
DataSink sink = new DefaultDataSink(mTranscodeOutputFile.getAbsolutePath());
258+
TranscoderOptions.Builder builder = Transcoder.into(sink);
238259
if (mAudioReplacementUri == null) {
239260
if (mTranscodeInputUri1 != null) builder.addDataSource(this, mTranscodeInputUri1);
240261
if (mTranscodeInputUri2 != null) builder.addDataSource(this, mTranscodeInputUri2);
@@ -249,6 +270,13 @@ private void transcode() {
249270
.setAudioTrackStrategy(mTranscodeAudioStrategy)
250271
.setVideoTrackStrategy(mTranscodeVideoStrategy)
251272
.setVideoRotation(rotation)
273+
.setValidator(new DefaultValidator() {
274+
@Override
275+
public boolean validate(@NonNull TrackStatus videoStatus, @NonNull TrackStatus audioStatus) {
276+
mIsAudioOnly = !videoStatus.isTranscoding();
277+
return super.validate(videoStatus, audioStatus);
278+
}
279+
})
252280
.setSpeed(speed)
253281
.transcode();
254282
}
@@ -268,19 +296,16 @@ public void onTranscodeCompleted(int successCode) {
268296
if (successCode == Transcoder.SUCCESS_TRANSCODED) {
269297
LOG.w("Transcoding took " + (SystemClock.uptimeMillis() - mTranscodeStartTime) + "ms");
270298
onTranscodeFinished(true, "Transcoded file placed on " + mTranscodeOutputFile);
299+
File file = mTranscodeOutputFile;
300+
String type = mIsAudioOnly ? "audio/mp4" : "video/mp4";
271301
Uri uri = FileProvider.getUriForFile(TranscoderActivity.this,
272-
FILE_PROVIDER_AUTHORITY,
273-
mTranscodeOutputFile);
302+
FILE_PROVIDER_AUTHORITY, file);
274303
startActivity(new Intent(Intent.ACTION_VIEW)
275-
.setDataAndType(uri, "video/mp4")
304+
.setDataAndType(uri, type)
276305
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION));
277306
} else if (successCode == Transcoder.SUCCESS_NOT_NEEDED) {
278-
// TODO: Not sure this works
279307
LOG.i("Transcoding was not needed.");
280-
onTranscodeFinished(true, "Transcoding not needed, source file not touched.");
281-
startActivity(new Intent(Intent.ACTION_VIEW)
282-
.setDataAndType(mTranscodeInputUri1, "video/mp4")
283-
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION));
308+
onTranscodeFinished(true, "Transcoding not needed, source file untouched.");
284309
}
285310
}
286311

demo/src/main/res/layout/activity_transcoder.xml

+8-1
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,13 @@
312312
android:paddingRight="8dp"
313313
android:layout_width="wrap_content"
314314
android:layout_height="wrap_content" />
315+
<com.google.android.material.radiobutton.MaterialRadioButton
316+
android:id="@+id/replace_remove"
317+
android:text="Remove"
318+
android:paddingLeft="8dp"
319+
android:paddingRight="8dp"
320+
android:layout_width="wrap_content"
321+
android:layout_height="wrap_content" />
315322
</RadioGroup>
316323
<TextView
317324
android:id="@+id/replace_info"
@@ -329,7 +336,7 @@
329336
android:padding="16dp"
330337
android:layout_width="match_parent"
331338
android:layout_height="wrap_content"
332-
android:text="Note: our API offers many more options than these!\nYou can select more than one video, in which case they will be concatenated together." />
339+
android:text="Note: our API offers many more options than these!\n\nNote: When clicking below, you can select more than one video. If you do, videos will be concatenated together." />
333340

334341
<!-- SPACE AND BUTTONS -->
335342
<Space

lib/src/main/java/com/otaliastudios/transcoder/Transcoder.java

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import android.os.Handler;
1919

2020
import com.otaliastudios.transcoder.engine.Engine;
21+
import com.otaliastudios.transcoder.sink.DataSink;
2122
import com.otaliastudios.transcoder.source.DataSource;
2223
import com.otaliastudios.transcoder.internal.Logger;
2324
import com.otaliastudios.transcoder.validator.Validator;
@@ -98,6 +99,18 @@ public static TranscoderOptions.Builder into(@NonNull String outPath) {
9899
return new TranscoderOptions.Builder(outPath);
99100
}
100101

102+
/**
103+
* Starts building transcoder options.
104+
* Requires a non null sink.
105+
*
106+
* @param dataSink the output sink
107+
* @return an options builder
108+
*/
109+
@NonNull
110+
public static TranscoderOptions.Builder into(@NonNull DataSink dataSink) {
111+
return new TranscoderOptions.Builder(dataSink);
112+
}
113+
101114
/**
102115
* Transcodes video file asynchronously.
103116
*

lib/src/main/java/com/otaliastudios/transcoder/TranscoderOptions.java

+13-10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import com.otaliastudios.transcoder.engine.TrackType;
99
import com.otaliastudios.transcoder.resample.AudioResampler;
1010
import com.otaliastudios.transcoder.resample.DefaultAudioResampler;
11+
import com.otaliastudios.transcoder.sink.DataSink;
12+
import com.otaliastudios.transcoder.sink.DefaultDataSink;
1113
import com.otaliastudios.transcoder.source.DataSource;
1214
import com.otaliastudios.transcoder.source.FileDescriptorDataSource;
1315
import com.otaliastudios.transcoder.source.FilePathDataSource;
@@ -38,7 +40,7 @@ public class TranscoderOptions {
3840

3941
private TranscoderOptions() {}
4042

41-
private String outPath;
43+
private DataSink dataSink;
4244
private List<DataSource> videoDataSources;
4345
private List<DataSource> audioDataSources;
4446
private TrackStrategy audioTrackStrategy;
@@ -53,8 +55,8 @@ private TranscoderOptions() {}
5355
Handler listenerHandler;
5456

5557
@NonNull
56-
public String getOutputPath() {
57-
return outPath;
58+
public DataSink getDataSink() {
59+
return dataSink;
5860
}
5961

6062
@NonNull
@@ -102,7 +104,7 @@ public AudioResampler getAudioResampler() {
102104
}
103105

104106
public static class Builder {
105-
private String outPath;
107+
private DataSink dataSink;
106108
private final List<DataSource> audioDataSources = new ArrayList<>();
107109
private final List<DataSource> videoDataSources = new ArrayList<>();
108110
private TranscoderListener listener;
@@ -116,7 +118,11 @@ public static class Builder {
116118
private AudioResampler audioResampler;
117119

118120
Builder(@NonNull String outPath) {
119-
this.outPath = outPath;
121+
this.dataSink = new DefaultDataSink(outPath);
122+
}
123+
124+
Builder(@NonNull DataSink dataSink) {
125+
this.dataSink = dataSink;
120126
}
121127

122128
@NonNull
@@ -169,7 +175,7 @@ public Builder addDataSource(@NonNull Context context, @NonNull Uri uri) {
169175
}
170176

171177
@NonNull
172-
@SuppressWarnings("unused")
178+
@SuppressWarnings({"unused", "UnusedReturnValue"})
173179
public Builder addDataSource(@NonNull TrackType type, @NonNull Context context, @NonNull Uri uri) {
174180
return addDataSource(type, new UriDataSource(context, uri));
175181
}
@@ -319,9 +325,6 @@ public TranscoderOptions build() {
319325
if (audioDataSources.isEmpty() && videoDataSources.isEmpty()) {
320326
throw new IllegalStateException("we need at least one data source");
321327
}
322-
if (outPath == null) {
323-
throw new IllegalStateException("out path can't be null");
324-
}
325328
if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
326329
throw new IllegalArgumentException("Accepted values for rotation are 0, 90, 180, 270");
327330
}
@@ -352,7 +355,7 @@ public TranscoderOptions build() {
352355
options.listener = listener;
353356
options.audioDataSources = audioDataSources;
354357
options.videoDataSources = videoDataSources;
355-
options.outPath = outPath;
358+
options.dataSink = dataSink;
356359
options.listenerHandler = listenerHandler;
357360
options.audioTrackStrategy = audioTrackStrategy;
358361
options.videoTrackStrategy = videoTrackStrategy;

lib/src/main/java/com/otaliastudios/transcoder/engine/Engine.java

+7-6
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.otaliastudios.transcoder.internal.ValidatorException;
2323
import com.otaliastudios.transcoder.sink.DataSink;
2424
import com.otaliastudios.transcoder.sink.InvalidOutputFormatException;
25-
import com.otaliastudios.transcoder.sink.DefaultDataSink;
2625
import com.otaliastudios.transcoder.source.DataSource;
2726
import com.otaliastudios.transcoder.strategy.TrackStrategy;
2827
import com.otaliastudios.transcoder.time.TimeInterpolator;
@@ -132,9 +131,9 @@ private void computeTrackStatus(@NonNull TrackType type,
132131
}
133132

134133
private boolean isCompleted(@NonNull TrackType type) {
134+
if (mDataSources.require(type).isEmpty()) return true;
135135
int current = mCurrentStep.require(type);
136-
return !mDataSources.require(type).isEmpty()
137-
&& current == mDataSources.require(type).size() - 1
136+
return current == mDataSources.require(type).size() - 1
138137
&& current == mTranscoders.require(type).size() - 1
139138
&& mTranscoders.require(type).get(current).isFinished();
140139
}
@@ -196,7 +195,7 @@ private void closeCurrentStep(@NonNull TrackType type) {
196195
TrackTranscoder transcoder = mTranscoders.require(type).get(current);
197196
DataSource dataSource = mDataSources.require(type).get(current);
198197
transcoder.release();
199-
dataSource.release();
198+
dataSource.releaseTrack(type);
200199
mCurrentStep.set(type, current + 1);
201200
}
202201

@@ -266,7 +265,7 @@ private long getTrackDurationUs(@NonNull TrackType type) {
266265

267266
private long getTotalDurationUs() {
268267
boolean hasVideo = hasVideoSources() && mStatuses.requireVideo().isTranscoding();
269-
boolean hasAudio = hasAudioSources() && mStatuses.requireVideo().isTranscoding();
268+
boolean hasAudio = hasAudioSources() && mStatuses.requireAudio().isTranscoding();
270269
long video = hasVideo ? getTrackDurationUs(TrackType.VIDEO) : Long.MAX_VALUE;
271270
long audio = hasAudio ? getTrackDurationUs(TrackType.AUDIO) : Long.MAX_VALUE;
272271
return Math.min(video, audio);
@@ -302,7 +301,7 @@ private double getTrackProgress(@NonNull TrackType type) {
302301
* @throws InterruptedException when cancel to transcode
303302
*/
304303
public void transcode(@NonNull TranscoderOptions options) throws InterruptedException {
305-
mDataSink = new DefaultDataSink(options.getOutputPath());
304+
mDataSink = options.getDataSink();
306305
mDataSources.setVideo(options.getVideoDataSources());
307306
mDataSources.setAudio(options.getAudioDataSources());
308307

@@ -347,6 +346,8 @@ public void transcode(@NonNull TranscoderOptions options) throws InterruptedExce
347346
boolean forceAudioEos = false, forceVideoEos = false;
348347
double audioProgress = 0, videoProgress = 0;
349348
while (!(audioCompleted && videoCompleted)) {
349+
LOG.v("new step: " + loopCount);
350+
350351
if (Thread.interrupted()) {
351352
throw new InterruptedException();
352353
}

lib/src/main/java/com/otaliastudios/transcoder/sink/DefaultDataSink.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public DefaultDataSink(@NonNull String outputFilePath) {
6565
this(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
6666
}
6767

68+
@SuppressWarnings("WeakerAccess")
6869
public DefaultDataSink(@NonNull String outputFilePath, int format) {
6970
try {
7071
mMuxer = new MediaMuxer(outputFilePath, format);
@@ -91,15 +92,13 @@ public void setTrackStatus(@NonNull TrackType type, @NonNull TrackStatus status)
9192
}
9293

9394
@Override
94-
public void setTrackFormat(@NonNull TrackType type,
95-
@NonNull MediaFormat format) {
95+
public void setTrackFormat(@NonNull TrackType type, @NonNull MediaFormat format) {
9696
boolean shouldValidate = mStatus.require(type) == TrackStatus.COMPRESSING;
9797
if (shouldValidate) {
9898
mMuxerChecks.checkOutputFormat(type, format);
9999
}
100100
mLastFormat.set(type, format);
101101
startIfNeeded();
102-
103102
}
104103

105104
private void startIfNeeded() {

lib/src/main/java/com/otaliastudios/transcoder/sink/MultiDataSink.java

+4
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,12 @@ public void setTrackFormat(@NonNull TrackType type, @NonNull MediaFormat format)
5050

5151
@Override
5252
public void writeTrack(@NonNull TrackType type, @NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) {
53+
int position = byteBuffer.position();
54+
int limit = byteBuffer.limit();
5355
for (DataSink sink : sinks) {
5456
sink.writeTrack(type, byteBuffer, bufferInfo);
57+
byteBuffer.position(position);
58+
byteBuffer.limit(limit);
5559
}
5660
}
5761

lib/src/main/java/com/otaliastudios/transcoder/source/DataSource.java

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
package com.otaliastudios.transcoder.source;
22

3-
import android.media.MediaExtractor;
43
import android.media.MediaFormat;
5-
import android.media.MediaMetadataRetriever;
6-
import android.util.Pair;
74

85
import androidx.annotation.NonNull;
96
import androidx.annotation.Nullable;
107

11-
import com.otaliastudios.transcoder.engine.TrackStatus;
128
import com.otaliastudios.transcoder.engine.TrackType;
139

14-
import java.io.IOException;
1510
import java.nio.ByteBuffer;
1611

1712
/**
@@ -94,9 +89,10 @@ public interface DataSource {
9489
boolean isDrained();
9590

9691
/**
97-
* Called to release resources.
92+
* Called to release resources for a given track.
93+
* @param type track type
9894
*/
99-
void release();
95+
void releaseTrack(@NonNull TrackType type);
10096

10197
/**
10298
* Represents a chunk of data.

0 commit comments

Comments
 (0)