Skip to content

Commit fb9a53d

Browse files
committed
Add support for trim, related to issue deepmedia#37
- New component TrimDataSource, wrapping DataSource to be trimmed. - MediaExtractorDataSource is an abstract class to limit visibility of MediaExtractor to package - Updates to Engine to replace selectAudio/transcode/selectVideo/transcode sequence by selectAudio/selectVideo/transcode/transcode
1 parent 57ea278 commit fb9a53d

File tree

4 files changed

+200
-4
lines changed

4 files changed

+200
-4
lines changed

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,13 @@ public void transcode(@NonNull TranscoderOptions options) throws InterruptedExce
364364
// Now step for transcoders that are not completed.
365365
audioCompleted = isCompleted(TrackType.AUDIO);
366366
videoCompleted = isCompleted(TrackType.VIDEO);
367-
if (!audioCompleted) {
367+
if (!audioCompleted && !videoCompleted) {
368+
final TrackTranscoder videoTranscoder = getCurrentTrackTranscoder(TrackType.VIDEO, options);
369+
final TrackTranscoder audioTranscoder = getCurrentTrackTranscoder(TrackType.AUDIO, options);
370+
stepped |= videoTranscoder.transcode(forceVideoEos) | audioTranscoder.transcode(forceAudioEos);
371+
} else if (!audioCompleted) {
368372
stepped |= getCurrentTrackTranscoder(TrackType.AUDIO, options).transcode(forceAudioEos);
369-
}
370-
if (!videoCompleted) {
373+
} else if (!videoCompleted) {
371374
stepped |= getCurrentTrackTranscoder(TrackType.VIDEO, options).transcode(forceVideoEos);
372375
}
373376
if (++loopCount % PROGRESS_INTERVAL_STEPS == 0) {

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/**
1919
* A DataSource implementation that uses Android's Media APIs.
2020
*/
21-
public abstract class DefaultDataSource implements DataSource {
21+
public abstract class DefaultDataSource extends MediaExtractorDataSource {
2222

2323
private final static String TAG = DefaultDataSource.class.getSimpleName();
2424
private final static Logger LOG = new Logger(TAG);
@@ -214,4 +214,10 @@ public void rewind() {
214214
mMetadata = new MediaMetadataRetriever();
215215
mMetadataApplied = false;
216216
}
217+
218+
@Override
219+
protected MediaExtractor requireExtractor() {
220+
ensureExtractor();
221+
return mExtractor;
222+
}
217223
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.otaliastudios.transcoder.source;
2+
3+
import android.media.MediaExtractor;
4+
5+
/**
6+
* DataSource that allows access to its MediaExtractor.
7+
*/
8+
abstract class MediaExtractorDataSource implements DataSource {
9+
abstract protected MediaExtractor requireExtractor();
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package com.otaliastudios.transcoder.source;
2+
3+
4+
import android.media.MediaExtractor;
5+
import android.media.MediaFormat;
6+
7+
import androidx.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
10+
import com.otaliastudios.transcoder.engine.TrackType;
11+
import com.otaliastudios.transcoder.internal.Logger;
12+
13+
import org.jetbrains.annotations.Contract;
14+
15+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
16+
17+
/**
18+
* A {@link DataSource} wrapper that trims source at both ends.
19+
*/
20+
public class TrimDataSource implements DataSource {
21+
private static final String TAG = "TrimDataSource";
22+
private static final Logger LOG = new Logger(TAG);
23+
private static final int UNKNOWN = -1;
24+
25+
@NonNull
26+
private MediaExtractorDataSource source;
27+
private long trimStartUs;
28+
private long trimDurationUs;
29+
private boolean isVideoTrackReady = false;
30+
private boolean hasSelectedVideoTrack = false;
31+
32+
public TrimDataSource(@NonNull MediaExtractorDataSource source, long trimStartMillis, long trimEndMillis) {
33+
this.source = source;
34+
this.trimStartUs = MILLISECONDS.toMicros(trimStartMillis);
35+
final long trimEndUs = MILLISECONDS.toMicros(trimEndMillis);
36+
this.trimDurationUs = computeTrimDuration(source.getDurationUs(), trimStartUs, trimEndUs);
37+
}
38+
39+
@Contract(pure = true)
40+
private static long computeTrimDuration(long duration, long trimStart, long trimEnd) {
41+
if (duration == UNKNOWN) {
42+
return UNKNOWN;
43+
} else {
44+
final long result = duration - trimStart - trimEnd;
45+
return result >= 0 ? result : UNKNOWN;
46+
}
47+
}
48+
49+
@Override
50+
public int getOrientation() {
51+
return source.getOrientation();
52+
}
53+
54+
@Nullable
55+
@Override
56+
public double[] getLocation() {
57+
return source.getLocation();
58+
}
59+
60+
@Override
61+
public long getDurationUs() {
62+
return trimDurationUs;
63+
}
64+
65+
@Nullable
66+
@Override
67+
public MediaFormat getTrackFormat(@NonNull TrackType type) {
68+
final MediaFormat trackFormat = source.getTrackFormat(type);
69+
if (trackFormat != null) {
70+
trackFormat.setLong(MediaFormat.KEY_DURATION, trimDurationUs);
71+
}
72+
return trackFormat;
73+
}
74+
75+
@Override
76+
public void selectTrack(@NonNull TrackType type) {
77+
if (trimStartUs > 0) {
78+
switch (type) {
79+
case AUDIO:
80+
if (hasTrack(TrackType.VIDEO) && !hasSelectedVideoTrack) {
81+
selectAndSeekVideoTrack();
82+
}
83+
source.selectTrack(TrackType.AUDIO);
84+
break;
85+
case VIDEO:
86+
if (!hasSelectedVideoTrack) {
87+
selectAndSeekVideoTrack();
88+
}
89+
break;
90+
}
91+
} else {
92+
source.selectTrack(type);
93+
}
94+
}
95+
96+
private boolean hasTrack(@NonNull TrackType type) {
97+
return source.getTrackFormat(type) != null;
98+
}
99+
100+
private void selectAndSeekVideoTrack() {
101+
source.selectTrack(TrackType.VIDEO);
102+
source.requireExtractor().seekTo(trimStartUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
103+
hasSelectedVideoTrack = true;
104+
}
105+
106+
/**
107+
* Check if trim operation was completed successfully for selected track.
108+
* We apply the seek operation for the video track only, so all audio frames are skipped
109+
* until MediaExtractor reaches the first video key frame.
110+
*/
111+
private boolean isTrackReady(@NonNull TrackType type) {
112+
if (isVideoTrackReady) {
113+
return true;
114+
}
115+
final MediaExtractor extractor = source.requireExtractor();
116+
if (type == TrackType.VIDEO) {
117+
final boolean isKeyFrame = (extractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0;
118+
if (isKeyFrame) {
119+
final long originalTrimStartUs = trimStartUs;
120+
trimStartUs = extractor.getSampleTime();
121+
trimDurationUs += originalTrimStartUs - trimStartUs;
122+
LOG.v("First video key frame is at " + trimStartUs + ", actual duration will be " + trimDurationUs);
123+
isVideoTrackReady = true;
124+
return true;
125+
}
126+
}
127+
extractor.advance();
128+
return false;
129+
}
130+
131+
@Override
132+
public boolean canReadTrack(@NonNull TrackType type) {
133+
boolean canRead = source.canReadTrack(type);
134+
135+
if (canRead) {
136+
return isTrackReady(type);
137+
} else {
138+
return false;
139+
}
140+
}
141+
142+
@Override
143+
public void readTrack(@NonNull Chunk chunk) {
144+
source.readTrack(chunk);
145+
chunk.timestampUs -= trimStartUs;
146+
}
147+
148+
@Override
149+
public long getReadUs() {
150+
return source.getReadUs();
151+
}
152+
153+
@Override
154+
public boolean isDrained() {
155+
return source.isDrained();
156+
}
157+
158+
@Override
159+
public void releaseTrack(@NonNull TrackType type) {
160+
switch (type) {
161+
case AUDIO:
162+
hasSelectedVideoTrack = false;
163+
break;
164+
case VIDEO:
165+
isVideoTrackReady = false;
166+
break;
167+
}
168+
source.releaseTrack(type);
169+
}
170+
171+
@Override
172+
public void rewind() {
173+
hasSelectedVideoTrack = false;
174+
isVideoTrackReady = false;
175+
source.rewind();
176+
}
177+
}

0 commit comments

Comments
 (0)