diff options
| -rw-r--r-- | api/current.txt | 14 | ||||
| -rw-r--r-- | media/java/android/media/MediaMuxer.java | 288 | ||||
| -rw-r--r-- | media/jni/Android.mk | 1 | ||||
| -rw-r--r-- | media/jni/android_media_MediaMuxer.cpp | 233 | ||||
| -rw-r--r-- | media/jni/android_media_MediaPlayer.cpp | 6 |
5 files changed, 542 insertions, 0 deletions
diff --git a/api/current.txt b/api/current.txt index 385d9ac37957..b3dde6b8ffe6 100644 --- a/api/current.txt +++ b/api/current.txt @@ -11584,6 +11584,20 @@ package android.media { field public static final int OPTION_PREVIOUS_SYNC = 0; // 0x0 } + public final class MediaMuxer { + ctor public MediaMuxer(java.lang.String, int) throws java.io.IOException; + method public int addTrack(android.media.MediaFormat); + method public void release(); + method public void start(); + method public void stop(); + method public void writeSampleData(int, java.nio.ByteBuffer, android.media.MediaCodec.BufferInfo); + field public static final int SAMPLE_FLAG_SYNC = 1; // 0x1 + } + + public static final class MediaMuxer.OutputFormat { + field public static final int MUXER_OUTPUT_MPEG_4 = 0; // 0x0 + } + public class MediaPlayer { ctor public MediaPlayer(); method public void addTimedTextSource(java.lang.String, java.lang.String) throws java.io.IOException, java.lang.IllegalArgumentException, java.lang.IllegalStateException; diff --git a/media/java/android/media/MediaMuxer.java b/media/java/android/media/MediaMuxer.java new file mode 100644 index 000000000000..dfd0e9476354 --- /dev/null +++ b/media/java/android/media/MediaMuxer.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2013 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 android.media; + +import android.media.MediaCodec.BufferInfo; + +import dalvik.system.CloseGuard; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * MediaMuxer facilitates muxing elementary streams. Currently only supports an + * mp4 file as the output and at most one audio and/or one video elementary + * stream. + * <p> + * It is generally used like this: + * + * <pre> + * MediaMuxer muxer = new MediaMuxer(...); + * MediaFormat audioFormat = new MediaFormat(...); + * MediaFormat videoFormat = new MediaFormat(...); + * int audioTrackIndex = muxer.addTrack(audioFormat); + * int videoTrackIndex = muxer.addTrack(videoFormat); + * ByteBuffer inputBuffer = ByteBuffer.allocate(...); + * muxer.start(); + * while(inputBuffer has new data) { + * if (new data is audio sample) { + * muxer.writeSampleData(audioTrackIndex, inputBuffer, ...); + * } else if (new data is video sample) { + * muxer.writeSampleData(videoTrackIndex, inputBuffer, ...); + * } + * } + * muxer.stop(); + * muxer.release(); + * </pre> + */ + +final public class MediaMuxer { + + private int mNativeContext; + + static { + System.loadLibrary("media_jni"); + } + + /** + * Defines the output format. These constants are used with constructor. + */ + public static final class OutputFormat { + /* Do not change these values without updating their counterparts + * in include/media/stagefright/MediaMuxer.h! + */ + private OutputFormat() {} + /** MPEG4 media file format*/ + public static final int MUXER_OUTPUT_MPEG_4 = 0; + }; + + /** + * The sample is a sync sample, which does not require other video samples + * to decode. This flag is used in {@link #writeSampleData} to indicate + * which sample is a sync sample. + */ + /* Keep this flag in sync with its equivalent in + * include/media/stagefright/MediaMuxer.h. + */ + public static final int SAMPLE_FLAG_SYNC = 1; + + // All the native functions are listed here. + private static native int nativeSetup(FileDescriptor fd, int format); + private static native void nativeRelease(int nativeObject); + private static native void nativeStart(int nativeObject); + private static native void nativeStop(int nativeObject); + private static native int nativeAddTrack(int nativeObject, String[] keys, + Object[] values); + private static native void nativeWriteSampleData(int nativeObject, + int trackIndex, ByteBuffer byteBuf, + int offset, int size, long presentationTimeUs, int flags); + + // Muxer internal states. + private static final int MUXER_STATE_UNINITIALIZED = -1; + private static final int MUXER_STATE_INITIALIZED = 0; + private static final int MUXER_STATE_STARTED = 1; + private static final int MUXER_STATE_STOPPED = 2; + + private int mState = MUXER_STATE_UNINITIALIZED; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + private int mLastTrackIndex = -1; + + private int mNativeObject; + + /** + * Constructor + * Creates a media muxer that writes to the specified path. + * @param path The path of the output media file. + * @param format The format of the output media file. + * @see android.media.MediaMuxer.OutputFormat + * @throws IOException if failed to open the file for write + */ + public MediaMuxer(String path, int format) throws IOException { + if (path == null) { + throw new IllegalArgumentException("path must not be null"); + } + if (format != OutputFormat.MUXER_OUTPUT_MPEG_4) { + throw new IllegalArgumentException("format is invalid"); + } + FileOutputStream fos = null; + try { + File file = new File(path); + fos = new FileOutputStream(file); + FileDescriptor fd = fos.getFD(); + mNativeObject = nativeSetup(fd, format); + mState = MUXER_STATE_INITIALIZED; + mCloseGuard.open("release"); + } finally { + if (fos != null) { + fos.close(); + } + } + } + + /** + * Starts the muxer. + * <p>Make sure this is called after {@link #addTrack} and before + * {@link #writeSampleData}.</p> + */ + public void start() { + if (mNativeObject == 0) { + throw new IllegalStateException("Muxer has been released!"); + } + if (mState == MUXER_STATE_INITIALIZED) { + nativeStart(mNativeObject); + mState = MUXER_STATE_STARTED; + } else { + throw new IllegalStateException("Can't start due to wrong state."); + } + } + + /** + * Stops the muxer. + * <p>Once the muxer stops, it can not be restarted.</p> + */ + public void stop() { + if (mState == MUXER_STATE_STARTED) { + nativeStop(mNativeObject); + mState = MUXER_STATE_STOPPED; + } else { + throw new IllegalStateException("Can't stop due to wrong state."); + } + } + + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + if (mNativeObject != 0) { + nativeRelease(mNativeObject); + mNativeObject = 0; + } + } finally { + super.finalize(); + } + } + + /** + * Adds a track with the specified format. + * @param format The media format for the track. + * @return The track index for this newly added track, and it should be used + * in the {@link #writeSampleData}. + */ + public int addTrack(MediaFormat format) { + if (format == null) { + throw new IllegalArgumentException("format must not be null."); + } + if (mState != MUXER_STATE_INITIALIZED) { + throw new IllegalStateException("Muxer is not initialized."); + } + if (mNativeObject == 0) { + throw new IllegalStateException("Muxer has been released!"); + } + int trackIndex = -1; + // Convert the MediaFormat into key-value pairs and send to the native. + Map<String, Object> formatMap = format.getMap(); + + String[] keys = null; + Object[] values = null; + int mapSize = formatMap.size(); + if (mapSize > 0) { + keys = new String[mapSize]; + values = new Object[mapSize]; + int i = 0; + for (Map.Entry<String, Object> entry : formatMap.entrySet()) { + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + ++i; + } + trackIndex = nativeAddTrack(mNativeObject, keys, values); + } else { + throw new IllegalArgumentException("format must not be empty."); + } + + // Track index number is expected to incremented as addTrack succeed. + // However, if format is invalid, it will get a negative trackIndex. + if (mLastTrackIndex >= trackIndex) { + throw new IllegalArgumentException("Invalid format."); + } + mLastTrackIndex = trackIndex; + return trackIndex; + } + + /** + * Writes an encoded sample into the muxer. The application needs to make + * sure that the samples are written into the right tracks. + * @param byteBuf The encoded sample. + * @param trackIndex The track index for this sample. + * @param bufferInfo The buffer information related to this sample. + */ + public void writeSampleData(int trackIndex, ByteBuffer byteBuf, + BufferInfo bufferInfo) { + if (trackIndex < 0 || trackIndex > mLastTrackIndex) { + throw new IllegalArgumentException("trackIndex is invalid"); + } + + if (byteBuf == null) { + throw new IllegalArgumentException("byteBuffer must not be null"); + } + + if (bufferInfo == null) { + throw new IllegalArgumentException("bufferInfo must not be null"); + } + if (bufferInfo.size < 0 || bufferInfo.offset < 0 + || (bufferInfo.offset + bufferInfo.size) > byteBuf.capacity() + || bufferInfo.presentationTimeUs < 0) { + throw new IllegalArgumentException("bufferInfo must specify a" + + " valid buffer offset, size and presentation time"); + } + + if (mNativeObject == 0) { + throw new IllegalStateException("Muxer has been released!"); + } + + if (mState != MUXER_STATE_STARTED) { + throw new IllegalStateException("Can't write, muxer is not started"); + } + + nativeWriteSampleData(mNativeObject, trackIndex, byteBuf, + bufferInfo.offset, bufferInfo.size, + bufferInfo.presentationTimeUs, bufferInfo.flags); + } + + /** + * Make sure you call this when you're done to free up any resources + * instead of relying on the garbage collector to do this for you at + * some point in the future. + */ + public void release() { + if (mState == MUXER_STATE_STARTED) { + throw new IllegalStateException("Can't release when muxer is started"); + } + if (mNativeObject != 0) { + nativeRelease(mNativeObject); + mNativeObject = 0; + mCloseGuard.close(); + } + mState = MUXER_STATE_UNINITIALIZED; + } +} diff --git a/media/jni/Android.mk b/media/jni/Android.mk index 6aaab2203869..ac8fb743ca76 100644 --- a/media/jni/Android.mk +++ b/media/jni/Android.mk @@ -6,6 +6,7 @@ LOCAL_SRC_FILES:= \ android_media_MediaCodec.cpp \ android_media_MediaCodecList.cpp \ android_media_MediaExtractor.cpp \ + android_media_MediaMuxer.cpp \ android_media_MediaPlayer.cpp \ android_media_MediaRecorder.cpp \ android_media_MediaScanner.cpp \ diff --git a/media/jni/android_media_MediaMuxer.cpp b/media/jni/android_media_MediaMuxer.cpp new file mode 100644 index 000000000000..30ebb00a0ad5 --- /dev/null +++ b/media/jni/android_media_MediaMuxer.cpp @@ -0,0 +1,233 @@ +/* + * Copyright 2013, 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. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "MediaMuxer-JNI" +#include <utils/Log.h> + +#include "android_media_Utils.h" +#include "android_runtime/AndroidRuntime.h" +#include "jni.h" +#include "JNIHelp.h" + +#include <media/stagefright/foundation/ABuffer.h> +#include <media/stagefright/foundation/ADebug.h> +#include <media/stagefright/foundation/AMessage.h> +#include <media/stagefright/MediaMuxer.h> + +namespace android { + +struct fields_t { + jfieldID context; + jmethodID arrayID; +}; + +static fields_t gFields; + +} + +using namespace android; + +static jint android_media_MediaMuxer_addTrack( + JNIEnv *env, jclass clazz, jint nativeObject, jobjectArray keys, + jobjectArray values) { + sp<MediaMuxer> muxer(reinterpret_cast<MediaMuxer *>(nativeObject)); + if (muxer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Muxer was not set up correctly"); + return -1; + } + + sp<AMessage> trackformat; + status_t err = ConvertKeyValueArraysToMessage(env, keys, values, + &trackformat); + if (err != OK) { + jniThrowException(env, "java/lang/IllegalArgumentException", + "ConvertKeyValueArraysToMessage got an error"); + return err; + } + + // Return negative value when errors happen in addTrack. + jint trackIndex = muxer->addTrack(trackformat); + + if (trackIndex < 0) { + jniThrowException(env, "java/lang/IllegalStateException", + "Failed to add the track to the muxer"); + return -1; + } + return trackIndex; +} + +static void android_media_MediaMuxer_writeSampleData( + JNIEnv *env, jclass clazz, jint nativeObject, jint trackIndex, + jobject byteBuf, jint offset, jint size, jlong timeUs, jint flags) { + sp<MediaMuxer> muxer(reinterpret_cast<MediaMuxer *>(nativeObject)); + if (muxer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Muxer was not set up correctly"); + return; + } + + // Try to convert the incoming byteBuffer into ABuffer + void *dst = env->GetDirectBufferAddress(byteBuf); + + jlong dstSize; + jbyteArray byteArray = NULL; + + if (dst == NULL) { + + byteArray = + (jbyteArray)env->CallObjectMethod(byteBuf, gFields.arrayID); + + if (byteArray == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", + "byteArray is null"); + return; + } + + jboolean isCopy; + dst = env->GetByteArrayElements(byteArray, &isCopy); + + dstSize = env->GetArrayLength(byteArray); + } else { + dstSize = env->GetDirectBufferCapacity(byteBuf); + } + + if (dstSize < (offset + size)) { + ALOGE("writeSampleData saw wrong dstSize %lld, size %d, offset %d", + dstSize, size, offset); + if (byteArray != NULL) { + env->ReleaseByteArrayElements(byteArray, (jbyte *)dst, 0); + } + jniThrowException(env, "java/lang/IllegalArgumentException", + "sample has a wrong size"); + return; + } + + sp<ABuffer> buffer = new ABuffer((char *)dst + offset, size); + + status_t err = muxer->writeSampleData(buffer, trackIndex, timeUs, flags); + + if (byteArray != NULL) { + env->ReleaseByteArrayElements(byteArray, (jbyte *)dst, 0); + } + + if (err != OK) { + jniThrowException(env, "java/lang/IllegalStateException", + "writeSampleData returned an error"); + } + return; +} + +// Constructor counterpart. +static jint android_media_MediaMuxer_native_setup( + JNIEnv *env, jclass clazz, jobject fileDescriptor, + jint format) { + int fd = jniGetFDFromFileDescriptor(env, fileDescriptor); + ALOGV("native_setup: fd %d", fd); + + MediaMuxer::OutputFormat fileFormat = + static_cast<MediaMuxer::OutputFormat>(format); + sp<MediaMuxer> muxer = new MediaMuxer(fd, fileFormat); + muxer->incStrong(clazz); + return int(muxer.get()); +} + +static void android_media_MediaMuxer_start(JNIEnv *env, jclass clazz, + jint nativeObject) { + sp<MediaMuxer> muxer(reinterpret_cast<MediaMuxer *>(nativeObject)); + if (muxer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Muxer was not set up correctly"); + return; + } + status_t err = muxer->start(); + + if (err != OK) { + jniThrowException(env, "java/lang/IllegalStateException", + "Failed to start the muxer"); + return; + } + +} + +static void android_media_MediaMuxer_stop(JNIEnv *env, jclass clazz, + jint nativeObject) { + sp<MediaMuxer> muxer(reinterpret_cast<MediaMuxer *>(nativeObject)); + if (muxer == NULL) { + jniThrowException(env, "java/lang/IllegalStateException", + "Muxer was not set up correctly"); + return; + } + + status_t err = muxer->stop(); + + if (err != OK) { + jniThrowException(env, "java/lang/IllegalStateException", + "Failed to stop the muxer"); + return; + } +} + +static void android_media_MediaMuxer_native_release( + JNIEnv *env, jclass clazz, jint nativeObject) { + sp<MediaMuxer> muxer(reinterpret_cast<MediaMuxer *>(nativeObject)); + if (muxer != NULL) { + muxer->decStrong(clazz); + } +} + +static JNINativeMethod gMethods[] = { + + { "nativeAddTrack", "(I[Ljava/lang/String;[Ljava/lang/Object;)I", + (void *)android_media_MediaMuxer_addTrack }, + + { "nativeStart", "(I)V", (void *)android_media_MediaMuxer_start}, + + { "nativeWriteSampleData", "(IILjava/nio/ByteBuffer;IIJI)V", + (void *)android_media_MediaMuxer_writeSampleData }, + + { "nativeStop", "(I)V", (void *)android_media_MediaMuxer_stop}, + + { "nativeSetup", "(Ljava/io/FileDescriptor;I)I", + (void *)android_media_MediaMuxer_native_setup }, + + { "nativeRelease", "(I)V", + (void *)android_media_MediaMuxer_native_release }, + +}; + +// This function only registers the native methods, and is called from +// JNI_OnLoad in android_media_MediaPlayer.cpp +int register_android_media_MediaMuxer(JNIEnv *env) { + int err = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaMuxer", gMethods, NELEM(gMethods)); + + jclass clazz = env->FindClass("android/media/MediaMuxer"); + CHECK(clazz != NULL); + + gFields.context = env->GetFieldID(clazz, "mNativeContext", "I"); + CHECK(gFields.context != NULL); + + jclass byteBufClass = env->FindClass("java/nio/ByteBuffer"); + CHECK(byteBufClass != NULL); + + gFields.arrayID = + env->GetMethodID(byteBufClass, "array", "()[B"); + CHECK(gFields.arrayID != NULL); + + return err; +} diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index 8768e96d2ef3..e9f6a1ee8859 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -883,6 +883,7 @@ extern int register_android_media_MediaCodec(JNIEnv *env); extern int register_android_media_MediaExtractor(JNIEnv *env); extern int register_android_media_MediaCodecList(JNIEnv *env); extern int register_android_media_MediaMetadataRetriever(JNIEnv *env); +extern int register_android_media_MediaMuxer(JNIEnv *env); extern int register_android_media_MediaRecorder(JNIEnv *env); extern int register_android_media_MediaScanner(JNIEnv *env); extern int register_android_media_ResampleInputStream(JNIEnv *env); @@ -963,6 +964,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) goto bail; } + if (register_android_media_MediaMuxer(env) < 0) { + ALOGE("ERROR: MediaMuxer native registration failed"); + goto bail; + } + if (register_android_media_MediaCodecList(env) < 0) { ALOGE("ERROR: MediaCodec native registration failed"); goto bail; |