FrameworkMuxer.java
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.common.util.Util.castNonNull;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.ParcelFileDescriptor;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.MediaFormatUtil;
import androidx.media3.common.util.Util;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
/** Muxer implementation that uses a {@link MediaMuxer}. */
@RequiresApi(18)
/* package */ final class FrameworkMuxer implements Muxer {
public static final class Factory implements Muxer.Factory {
@Override
public FrameworkMuxer create(String path, String outputMimeType) throws IOException {
MediaMuxer mediaMuxer = new MediaMuxer(path, mimeTypeToMuxerOutputFormat(outputMimeType));
return new FrameworkMuxer(mediaMuxer);
}
@RequiresApi(26)
@Override
public FrameworkMuxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType)
throws IOException {
MediaMuxer mediaMuxer =
new MediaMuxer(
parcelFileDescriptor.getFileDescriptor(),
mimeTypeToMuxerOutputFormat(outputMimeType));
return new FrameworkMuxer(mediaMuxer);
}
@Override
public boolean supportsOutputMimeType(String mimeType) {
try {
mimeTypeToMuxerOutputFormat(mimeType);
} catch (IllegalStateException e) {
return false;
}
return true;
}
@Override
public boolean supportsSampleMimeType(
@Nullable String sampleMimeType, String containerMimeType) {
// MediaMuxer supported sample formats are documented in MediaMuxer.addTrack(MediaFormat).
boolean isAudio = MimeTypes.isAudio(sampleMimeType);
boolean isVideo = MimeTypes.isVideo(sampleMimeType);
if (containerMimeType.equals(MimeTypes.VIDEO_MP4)) {
if (isVideo) {
return MimeTypes.VIDEO_H263.equals(sampleMimeType)
|| MimeTypes.VIDEO_H264.equals(sampleMimeType)
|| MimeTypes.VIDEO_MP4V.equals(sampleMimeType)
|| (Util.SDK_INT >= 24 && MimeTypes.VIDEO_H265.equals(sampleMimeType));
} else if (isAudio) {
return MimeTypes.AUDIO_AAC.equals(sampleMimeType)
|| MimeTypes.AUDIO_AMR_NB.equals(sampleMimeType)
|| MimeTypes.AUDIO_AMR_WB.equals(sampleMimeType);
}
} else if (containerMimeType.equals(MimeTypes.VIDEO_WEBM) && SDK_INT >= 21) {
if (isVideo) {
return MimeTypes.VIDEO_VP8.equals(sampleMimeType)
|| (Util.SDK_INT >= 24 && MimeTypes.VIDEO_VP9.equals(sampleMimeType));
} else if (isAudio) {
return MimeTypes.AUDIO_VORBIS.equals(sampleMimeType);
}
}
return false;
}
}
private final MediaMuxer mediaMuxer;
private final MediaCodec.BufferInfo bufferInfo;
private boolean isStarted;
private FrameworkMuxer(MediaMuxer mediaMuxer) {
this.mediaMuxer = mediaMuxer;
bufferInfo = new MediaCodec.BufferInfo();
}
@Override
public int addTrack(Format format) {
String sampleMimeType = checkNotNull(format.sampleMimeType);
MediaFormat mediaFormat;
if (MimeTypes.isAudio(sampleMimeType)) {
mediaFormat =
MediaFormat.createAudioFormat(
castNonNull(sampleMimeType), format.sampleRate, format.channelCount);
} else {
mediaFormat =
MediaFormat.createVideoFormat(castNonNull(sampleMimeType), format.width, format.height);
mediaMuxer.setOrientationHint(format.rotationDegrees);
}
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
return mediaMuxer.addTrack(mediaFormat);
}
@Override
public void writeSampleData(
int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) {
if (!isStarted) {
isStarted = true;
mediaMuxer.start();
}
int offset = data.position();
int size = data.limit() - offset;
int flags = isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0;
bufferInfo.set(offset, size, presentationTimeUs, flags);
mediaMuxer.writeSampleData(trackIndex, data, bufferInfo);
}
@Override
public void release(boolean forCancellation) {
if (!isStarted) {
mediaMuxer.release();
return;
}
isStarted = false;
try {
mediaMuxer.stop();
} catch (IllegalStateException e) {
if (SDK_INT < 30) {
// Set the muxer state to stopped even if mediaMuxer.stop() failed so that
// mediaMuxer.release() doesn't attempt to stop the muxer and therefore doesn't throw the
// same exception without releasing its resources. This is already implemented in MediaMuxer
// from API level 30.
try {
Field muxerStoppedStateField = MediaMuxer.class.getDeclaredField("MUXER_STATE_STOPPED");
muxerStoppedStateField.setAccessible(true);
int muxerStoppedState = castNonNull((Integer) muxerStoppedStateField.get(mediaMuxer));
Field muxerStateField = MediaMuxer.class.getDeclaredField("mState");
muxerStateField.setAccessible(true);
muxerStateField.set(mediaMuxer, muxerStoppedState);
} catch (Exception reflectionException) {
// Do nothing.
}
}
// It doesn't matter that stopping the muxer throws if the transformation is being cancelled.
if (!forCancellation) {
throw e;
}
} finally {
mediaMuxer.release();
}
}
/**
* Converts a {@link MimeTypes MIME type} into a {@link MediaMuxer.OutputFormat MediaMuxer output
* format}.
*
* @param mimeType The {@link MimeTypes MIME type} to convert.
* @return The corresponding {@link MediaMuxer.OutputFormat MediaMuxer output format}.
* @throws IllegalArgumentException If the {@link MimeTypes MIME type} is not supported as output
* format.
*/
private static int mimeTypeToMuxerOutputFormat(String mimeType) {
if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
return MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
} else if (SDK_INT >= 21 && mimeType.equals(MimeTypes.VIDEO_WEBM)) {
return MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM;
} else {
throw new IllegalArgumentException("Unsupported output MIME type: " + mimeType);
}
}
}