Skip to content

Commit

Permalink
Add support for trim, related to issue deepmedia#37
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
mudar committed Dec 20, 2019
1 parent 57ea278 commit fb9a53d
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,13 @@ public void transcode(@NonNull TranscoderOptions options) throws InterruptedExce
// Now step for transcoders that are not completed.
audioCompleted = isCompleted(TrackType.AUDIO);
videoCompleted = isCompleted(TrackType.VIDEO);
if (!audioCompleted) {
if (!audioCompleted && !videoCompleted) {
final TrackTranscoder videoTranscoder = getCurrentTrackTranscoder(TrackType.VIDEO, options);
final TrackTranscoder audioTranscoder = getCurrentTrackTranscoder(TrackType.AUDIO, options);
stepped |= videoTranscoder.transcode(forceVideoEos) | audioTranscoder.transcode(forceAudioEos);
} else if (!audioCompleted) {
stepped |= getCurrentTrackTranscoder(TrackType.AUDIO, options).transcode(forceAudioEos);
}
if (!videoCompleted) {
} else if (!videoCompleted) {
stepped |= getCurrentTrackTranscoder(TrackType.VIDEO, options).transcode(forceVideoEos);
}
if (++loopCount % PROGRESS_INTERVAL_STEPS == 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/**
* A DataSource implementation that uses Android's Media APIs.
*/
public abstract class DefaultDataSource implements DataSource {
public abstract class DefaultDataSource extends MediaExtractorDataSource {

private final static String TAG = DefaultDataSource.class.getSimpleName();
private final static Logger LOG = new Logger(TAG);
Expand Down Expand Up @@ -214,4 +214,10 @@ public void rewind() {
mMetadata = new MediaMetadataRetriever();
mMetadataApplied = false;
}

@Override
protected MediaExtractor requireExtractor() {
ensureExtractor();
return mExtractor;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.otaliastudios.transcoder.source;

import android.media.MediaExtractor;

/**
* DataSource that allows access to its MediaExtractor.
*/
abstract class MediaExtractorDataSource implements DataSource {
abstract protected MediaExtractor requireExtractor();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.otaliastudios.transcoder.source;


import android.media.MediaExtractor;
import android.media.MediaFormat;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.otaliastudios.transcoder.engine.TrackType;
import com.otaliastudios.transcoder.internal.Logger;

import org.jetbrains.annotations.Contract;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
* A {@link DataSource} wrapper that trims source at both ends.
*/
public class TrimDataSource implements DataSource {
private static final String TAG = "TrimDataSource";
private static final Logger LOG = new Logger(TAG);
private static final int UNKNOWN = -1;

@NonNull
private MediaExtractorDataSource source;
private long trimStartUs;
private long trimDurationUs;
private boolean isVideoTrackReady = false;
private boolean hasSelectedVideoTrack = false;

public TrimDataSource(@NonNull MediaExtractorDataSource source, long trimStartMillis, long trimEndMillis) {
this.source = source;
this.trimStartUs = MILLISECONDS.toMicros(trimStartMillis);
final long trimEndUs = MILLISECONDS.toMicros(trimEndMillis);
this.trimDurationUs = computeTrimDuration(source.getDurationUs(), trimStartUs, trimEndUs);
}

@Contract(pure = true)
private static long computeTrimDuration(long duration, long trimStart, long trimEnd) {
if (duration == UNKNOWN) {
return UNKNOWN;
} else {
final long result = duration - trimStart - trimEnd;
return result >= 0 ? result : UNKNOWN;
}
}

@Override
public int getOrientation() {
return source.getOrientation();
}

@Nullable
@Override
public double[] getLocation() {
return source.getLocation();
}

@Override
public long getDurationUs() {
return trimDurationUs;
}

@Nullable
@Override
public MediaFormat getTrackFormat(@NonNull TrackType type) {
final MediaFormat trackFormat = source.getTrackFormat(type);
if (trackFormat != null) {
trackFormat.setLong(MediaFormat.KEY_DURATION, trimDurationUs);
}
return trackFormat;
}

@Override
public void selectTrack(@NonNull TrackType type) {
if (trimStartUs > 0) {
switch (type) {
case AUDIO:
if (hasTrack(TrackType.VIDEO) && !hasSelectedVideoTrack) {
selectAndSeekVideoTrack();
}
source.selectTrack(TrackType.AUDIO);
break;
case VIDEO:
if (!hasSelectedVideoTrack) {
selectAndSeekVideoTrack();
}
break;
}
} else {
source.selectTrack(type);
}
}

private boolean hasTrack(@NonNull TrackType type) {
return source.getTrackFormat(type) != null;
}

private void selectAndSeekVideoTrack() {
source.selectTrack(TrackType.VIDEO);
source.requireExtractor().seekTo(trimStartUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
hasSelectedVideoTrack = true;
}

/**
* Check if trim operation was completed successfully for selected track.
* We apply the seek operation for the video track only, so all audio frames are skipped
* until MediaExtractor reaches the first video key frame.
*/
private boolean isTrackReady(@NonNull TrackType type) {
if (isVideoTrackReady) {
return true;
}
final MediaExtractor extractor = source.requireExtractor();
if (type == TrackType.VIDEO) {
final boolean isKeyFrame = (extractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0;
if (isKeyFrame) {
final long originalTrimStartUs = trimStartUs;
trimStartUs = extractor.getSampleTime();
trimDurationUs += originalTrimStartUs - trimStartUs;
LOG.v("First video key frame is at " + trimStartUs + ", actual duration will be " + trimDurationUs);
isVideoTrackReady = true;
return true;
}
}
extractor.advance();
return false;
}

@Override
public boolean canReadTrack(@NonNull TrackType type) {
boolean canRead = source.canReadTrack(type);

if (canRead) {
return isTrackReady(type);
} else {
return false;
}
}

@Override
public void readTrack(@NonNull Chunk chunk) {
source.readTrack(chunk);
chunk.timestampUs -= trimStartUs;
}

@Override
public long getReadUs() {
return source.getReadUs();
}

@Override
public boolean isDrained() {
return source.isDrained();
}

@Override
public void releaseTrack(@NonNull TrackType type) {
switch (type) {
case AUDIO:
hasSelectedVideoTrack = false;
break;
case VIDEO:
isVideoTrackReady = false;
break;
}
source.releaseTrack(type);
}

@Override
public void rewind() {
hasSelectedVideoTrack = false;
isVideoTrackReady = false;
source.rewind();
}
}

0 comments on commit fb9a53d

Please sign in to comment.