Skip to content

Feature/muted audio fixes #51 #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import com.otaliastudios.transcoder.source.DataSource;
import com.otaliastudios.transcoder.source.FileDescriptorDataSource;
import com.otaliastudios.transcoder.source.FilePathDataSource;
import com.otaliastudios.transcoder.source.TrimDataSource;
import com.otaliastudios.transcoder.source.BlankAudioDataSource;
import com.otaliastudios.transcoder.source.UriDataSource;
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategies;
Expand Down Expand Up @@ -318,6 +318,43 @@ public Builder setAudioResampler(@NonNull AudioResampler audioResampler) {
return this;
}

/**
* Generates muted audio data sources if needed
* @return The list of audio data sources including the muted sources
*/
private List<DataSource> buildAudioDataSources()
{
// Check if we have a mix of empty and non-empty data sources
// This would cause an error in Engine::computeTrackStatus
boolean hasMissingAudioDataSources = false;
boolean hasAudioDataSources = false;
boolean hasValidAudioDataSources = true;
for (DataSource dataSource : audioDataSources) {
if (dataSource.getTrackFormat(TrackType.AUDIO) == null) {
hasMissingAudioDataSources = true;
} else {
hasAudioDataSources = true;
}
if (hasAudioDataSources && hasMissingAudioDataSources) {
hasValidAudioDataSources = false;
break;
}
}
if (hasValidAudioDataSources) {
return audioDataSources;
}
// Fix the audioDataSources by replacing the empty data source by muted data source
List<DataSource> result = new ArrayList<>();
for (DataSource dataSource : audioDataSources) {
if (dataSource.getTrackFormat(TrackType.AUDIO) != null) {
result.add(dataSource);
} else {
result.add(new BlankAudioDataSource(dataSource.getDurationUs()));
}
}
return result;
}

@NonNull
public TranscoderOptions build() {
if (listener == null) {
Expand Down Expand Up @@ -354,7 +391,7 @@ public TranscoderOptions build() {
}
TranscoderOptions options = new TranscoderOptions();
options.listener = listener;
options.audioDataSources = audioDataSources;
options.audioDataSources = buildAudioDataSources();
options.videoDataSources = videoDataSources;
options.dataSink = dataSink;
options.listenerHandler = listenerHandler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class MediaFormatConstants {
// Audio formats
// from MediaFormat of API level >= 21
public static final String MIMETYPE_AUDIO_AAC = "audio/mp4a-latm";
public static final String MIMETYPE_AUDIO_RAW = "audio/raw";

private MediaFormatConstants() { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.otaliastudios.transcoder.source;

import android.media.MediaFormat;

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

import com.otaliastudios.transcoder.engine.TrackType;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import static com.otaliastudios.transcoder.internal.MediaFormatConstants.MIMETYPE_AUDIO_RAW;

/**
* A {@link DataSource} that provides a silent audio track of the a specific duration.
* This class can be used to concatenate a DataSources that has a video track only with another
* that has both video and audio track.
*/
public class BlankAudioDataSource implements DataSource {
private final static String TAG = BlankAudioDataSource.class.getSimpleName();

private static final int CHANNEL_COUNT = 2;
private static final int SAMPLE_RATE = 44100;
private static final int BITS_PER_SAMPLE = 16;
private static final int BIT_RATE = CHANNEL_COUNT * SAMPLE_RATE * BITS_PER_SAMPLE;
private static final double SAMPLES_PER_PERIOD = 2048;
private static final double PERIOD_TIME_SECONDS = SAMPLES_PER_PERIOD / SAMPLE_RATE;
private static final long PERIOD_TIME_US = (long) (1000000 * PERIOD_TIME_SECONDS);
private static final int PERIOD_SIZE = (int) (PERIOD_TIME_SECONDS * BIT_RATE / 8);

private final long durationUs;
private final ByteBuffer byteBuffer;
private final MediaFormat audioFormat;

private long currentTimestampUs = 0L;

public BlankAudioDataSource(long durationUs) {
this.durationUs = durationUs;
this.byteBuffer = ByteBuffer.allocateDirect(PERIOD_SIZE).order(ByteOrder.nativeOrder());
this.audioFormat = new MediaFormat();
audioFormat.setString(MediaFormat.KEY_MIME, MIMETYPE_AUDIO_RAW);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNEL_COUNT);
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, PERIOD_SIZE);
audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
}

@Override
public int getOrientation() {
return 0;
}

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

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

@Nullable
@Override
public MediaFormat getTrackFormat(@NonNull TrackType type) {
return (type == TrackType.AUDIO) ? audioFormat : null;
}

@Override
public void selectTrack(@NonNull TrackType type) {
// Nothing to do
}

@Override
public long seekTo(long desiredTimestampUs) {
currentTimestampUs = desiredTimestampUs;
return desiredTimestampUs;
}

@Override
public boolean canReadTrack(@NonNull TrackType type) {
return type == TrackType.AUDIO;
}

@Override
public void readTrack(@NonNull Chunk chunk) {
byteBuffer.clear();
chunk.buffer = byteBuffer;
chunk.isKeyFrame = true;
chunk.timestampUs = currentTimestampUs;
chunk.bytes = PERIOD_SIZE;

currentTimestampUs += PERIOD_TIME_US;
}

@Override
public long getReadUs() {
return currentTimestampUs;
}

@Override
public boolean isDrained() {
return currentTimestampUs >= getDurationUs();
}

@Override
public void releaseTrack(@NonNull TrackType type) {
// Nothing to do
}

@Override
public void rewind() {
currentTimestampUs = 0;
}
}