From ba5926ec3e2e7432d67f4f5e309cf418022f9fde Mon Sep 17 00:00:00 2001 From: Marco Nelissen Date: Wed, 28 May 2014 08:57:21 -0700 Subject: [PATCH] NDK media codec sample Change-Id: Id7e70cfccdace8bce253bdac4434c42f52ff72f3 --- .../samples/native-codec/AndroidManifest.xml | 24 ++ .../android-L/samples/native-codec/README.txt | 2 + .../samples/native-codec/default.properties | 4 + .../samples/native-codec/jni/Android.mk | 30 ++ .../samples/native-codec/jni/Application.mk | 2 + .../samples/native-codec/jni/looper.cpp | 152 ++++++++ .../samples/native-codec/jni/looper.h | 41 ++ .../native-codec/jni/native-codec-jni.cpp | 328 ++++++++++++++++ .../native-codec/res/drawable/icon.png | Bin 0 -> 2574 bytes .../samples/native-codec/res/layout/main.xml | 84 +++++ .../native-codec/res/values/strings.xml | 22 ++ .../example/nativecodec/MyGLSurfaceView.java | 354 ++++++++++++++++++ .../com/example/nativecodec/NativeCodec.java | 319 ++++++++++++++++ .../samples/native-codec/testfile.mp4 | Bin 0 -> 930185 bytes 14 files changed, 1362 insertions(+) create mode 100644 ndk/platforms/android-L/samples/native-codec/AndroidManifest.xml create mode 100644 ndk/platforms/android-L/samples/native-codec/README.txt create mode 100644 ndk/platforms/android-L/samples/native-codec/default.properties create mode 100644 ndk/platforms/android-L/samples/native-codec/jni/Android.mk create mode 100644 ndk/platforms/android-L/samples/native-codec/jni/Application.mk create mode 100644 ndk/platforms/android-L/samples/native-codec/jni/looper.cpp create mode 100644 ndk/platforms/android-L/samples/native-codec/jni/looper.h create mode 100644 ndk/platforms/android-L/samples/native-codec/jni/native-codec-jni.cpp create mode 100644 ndk/platforms/android-L/samples/native-codec/res/drawable/icon.png create mode 100644 ndk/platforms/android-L/samples/native-codec/res/layout/main.xml create mode 100644 ndk/platforms/android-L/samples/native-codec/res/values/strings.xml create mode 100644 ndk/platforms/android-L/samples/native-codec/src/com/example/nativecodec/MyGLSurfaceView.java create mode 100644 ndk/platforms/android-L/samples/native-codec/src/com/example/nativecodec/NativeCodec.java create mode 100644 ndk/platforms/android-L/samples/native-codec/testfile.mp4 diff --git a/ndk/platforms/android-L/samples/native-codec/AndroidManifest.xml b/ndk/platforms/android-L/samples/native-codec/AndroidManifest.xml new file mode 100644 index 000000000..addf1f0e6 --- /dev/null +++ b/ndk/platforms/android-L/samples/native-codec/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ndk/platforms/android-L/samples/native-codec/README.txt b/ndk/platforms/android-L/samples/native-codec/README.txt new file mode 100644 index 000000000..446941a7e --- /dev/null +++ b/ndk/platforms/android-L/samples/native-codec/README.txt @@ -0,0 +1,2 @@ +This sample app requires a video file to be placed in /sdcard/testfile.mp4 +For demonstration purposes we have supplied such a file. diff --git a/ndk/platforms/android-L/samples/native-codec/default.properties b/ndk/platforms/android-L/samples/native-codec/default.properties new file mode 100644 index 000000000..92f9bf030 --- /dev/null +++ b/ndk/platforms/android-L/samples/native-codec/default.properties @@ -0,0 +1,4 @@ +# Indicates whether an apk should be generated for each density. +split.density=false +# Project target. +target=android-L diff --git a/ndk/platforms/android-L/samples/native-codec/jni/Android.mk b/ndk/platforms/android-L/samples/native-codec/jni/Android.mk new file mode 100644 index 000000000..34300c4be --- /dev/null +++ b/ndk/platforms/android-L/samples/native-codec/jni/Android.mk @@ -0,0 +1,30 @@ +# Copyright (C) 2014 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. +# +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := native-codec-jni +LOCAL_SRC_FILES := native-codec-jni.cpp looper.cpp +# for native multimedia +LOCAL_LDLIBS += -lOpenMAXAL -lmediandk +# for logging +LOCAL_LDLIBS += -llog +# for native windows +LOCAL_LDLIBS += -landroid + +LOCAL_CFLAGS += -UNDEBUG + +include $(BUILD_SHARED_LIBRARY) diff --git a/ndk/platforms/android-L/samples/native-codec/jni/Application.mk b/ndk/platforms/android-L/samples/native-codec/jni/Application.mk new file mode 100644 index 000000000..5b3fb7259 --- /dev/null +++ b/ndk/platforms/android-L/samples/native-codec/jni/Application.mk @@ -0,0 +1,2 @@ +APP_PLATFORM := android-L +APP_ABI := all diff --git a/ndk/platforms/android-L/samples/native-codec/jni/looper.cpp b/ndk/platforms/android-L/samples/native-codec/jni/looper.cpp new file mode 100644 index 000000000..98112f4d8 --- /dev/null +++ b/ndk/platforms/android-L/samples/native-codec/jni/looper.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2014 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. + */ + +#include "looper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// for __android_log_print(ANDROID_LOG_INFO, "YourApp", "formatted message"); +#include +#define TAG "NativeCodec-looper" +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__) + + +struct loopermessage; +typedef struct loopermessage loopermessage; + +struct loopermessage { + int what; + void *obj; + loopermessage *next; + bool quit; +}; + + + +void* looper::trampoline(void* p) { + ((looper*)p)->loop(); + return NULL; +} + +looper::looper() { + sem_init(&headdataavailable, 0, 0); + sem_init(&headwriteprotect, 0, 1); + pthread_attr_t attr; + pthread_attr_init(&attr); + + pthread_create(&worker, &attr, trampoline, this); + running = true; +} + + +looper::~looper() { + if (running) { + LOGV("Looper deleted while still running. Some messages will not be processed"); + quit(); + } +} + +void looper::post(int what, void *data, bool flush) { + loopermessage *msg = new loopermessage(); + msg->what = what; + msg->obj = data; + msg->next = NULL; + msg->quit = false; + addmsg(msg, flush); +} + +void looper::addmsg(loopermessage *msg, bool flush) { + sem_wait(&headwriteprotect); + loopermessage *h = head; + + if (flush) { + while(h) { + loopermessage *next = h->next; + delete h; + h = next; + } + h = NULL; + } + if (h) { + while (h->next) { + h = h->next; + } + h->next = msg; + } else { + head = msg; + } + LOGV("post msg %d", msg->what); + sem_post(&headwriteprotect); + sem_post(&headdataavailable); +} + +void looper::loop() { + while(true) { + // wait for available message + sem_wait(&headdataavailable); + + // get next available message + sem_wait(&headwriteprotect); + loopermessage *msg = head; + if (msg == NULL) { + LOGV("no msg"); + sem_post(&headwriteprotect); + continue; + } + head = msg->next; + sem_post(&headwriteprotect); + + if (msg->quit) { + LOGV("quitting"); + delete msg; + return; + } + LOGV("processing msg %d", msg->what); + handle(msg->what, msg->obj); + delete msg; + } +} + +void looper::quit() { + LOGV("quit"); + loopermessage *msg = new loopermessage(); + msg->what = 0; + msg->obj = NULL; + msg->next = NULL; + msg->quit = true; + addmsg(msg, false); + void *retval; + pthread_join(worker, &retval); + sem_destroy(&headdataavailable); + sem_destroy(&headwriteprotect); + running = false; +} + +void looper::handle(int what, void* obj) { + LOGV("dropping msg %d %p", what, obj); +} + diff --git a/ndk/platforms/android-L/samples/native-codec/jni/looper.h b/ndk/platforms/android-L/samples/native-codec/jni/looper.h new file mode 100644 index 000000000..531a7cbcf --- /dev/null +++ b/ndk/platforms/android-L/samples/native-codec/jni/looper.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 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. + */ + +#include +#include + +struct loopermessage; + +class looper { + public: + looper(); + ~looper(); + + void post(int what, void *data, bool flush = false); + void quit(); + + virtual void handle(int what, void *data); + + private: + void addmsg(loopermessage *msg, bool flush); + static void* trampoline(void* p); + void loop(); + loopermessage *head; + pthread_t worker; + sem_t headwriteprotect; + sem_t headdataavailable; + bool running; +}; diff --git a/ndk/platforms/android-L/samples/native-codec/jni/native-codec-jni.cpp b/ndk/platforms/android-L/samples/native-codec/jni/native-codec-jni.cpp new file mode 100644 index 000000000..6053abe2f --- /dev/null +++ b/ndk/platforms/android-L/samples/native-codec/jni/native-codec-jni.cpp @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2014 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. + */ + +/* This is a JNI example where we use native methods to play video + * using the native AMedia* APIs. + * See the corresponding Java source file located at: + * + * src/com/example/nativecodec/NativeMedia.java + * + * In this example we use assert() for "impossible" error conditions, + * and explicit handling and recovery for more likely error conditions. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "looper.h" +#include "media/NdkMediaCodec.h" +#include "media/NdkMediaExtractor.h" + +// for __android_log_print(ANDROID_LOG_INFO, "YourApp", "formatted message"); +#include +#define TAG "NativeCodec" +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__) + +// for native window JNI +#include + +typedef struct { + int fd; + ANativeWindow* window; + AMediaExtractor* ex; + AMediaCodec *codec; + int64_t renderstart; + bool sawInputEOS; + bool sawOutputEOS; + bool isPlaying; + bool renderonce; +} workerdata; + +workerdata data = {-1, NULL, NULL, NULL, 0, false, false, false, false}; + +enum { + kMsgCodecBuffer, + kMsgPause, + kMsgResume, + kMsgPauseAck, + kMsgDecodeDone, + kMsgSeek, +}; + + + +class mylooper: public looper { + virtual void handle(int what, void* obj); +}; + +static mylooper *mlooper = NULL; + +int64_t systemnanotime() { + timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return now.tv_sec * 1000000000LL + now.tv_nsec; +} + +void doCodecWork(workerdata *d) { + + ssize_t bufidx = -1; + if (!d->sawInputEOS) { + bufidx = AMediaCodec_dequeueInputBuffer(d->codec, 2000); + LOGV("input buffer %zd", bufidx); + if (bufidx >= 0) { + size_t bufsize; + uint8_t *buf = AMediaCodec_getInputBuffer(d->codec, bufidx, &bufsize); + ssize_t sampleSize = AMediaExtractor_readSampleData(d->ex, buf, bufsize); + if (sampleSize < 0) { + sampleSize = 0; + d->sawInputEOS = true; + LOGV("EOS"); + } + int64_t presentationTimeUs = AMediaExtractor_getSampleTime(d->ex); + + AMediaCodec_queueInputBuffer(d->codec, bufidx, 0, sampleSize, presentationTimeUs, + d->sawInputEOS ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM : 0); + AMediaExtractor_advance(d->ex); + } + } + + if (!d->sawOutputEOS) { + AMediaCodecBufferInfo info; + ssize_t status = AMediaCodec_dequeueOutputBuffer(d->codec, &info, 0); + if (status >= 0) { + if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) { + LOGV("output EOS"); + d->sawOutputEOS = true; + } + int64_t presentationNano = info.presentationTimeUs * 1000; + if (d->renderstart < 0) { + d->renderstart = systemnanotime() - presentationNano; + } + int64_t delay = (d->renderstart + presentationNano) - systemnanotime(); + if (delay > 0) { + usleep(delay / 1000); + } + AMediaCodec_releaseOutputBuffer(d->codec, status, info.size != 0); + if (d->renderonce) { + d->renderonce = false; + return; + } + } else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) { + LOGV("output buffers changed"); + } else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) { + AMediaFormat *format = NULL; + format = AMediaCodec_getOutputFormat(d->codec); + LOGV("format changed to: %s", AMediaFormat_toString(format)); + AMediaFormat_delete(format); + } else if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) { + LOGV("no output buffer right now"); + } else { + LOGV("unexpected info code: %zd", status); + } + } + + if (!d->sawInputEOS || !d->sawOutputEOS) { + mlooper->post(kMsgCodecBuffer, d); + } +} + +void mylooper::handle(int what, void* obj) { + switch (what) { + case kMsgCodecBuffer: + doCodecWork((workerdata*)obj); + break; + + case kMsgDecodeDone: + { + workerdata *d = (workerdata*)obj; + AMediaCodec_stop(d->codec); + AMediaCodec_delete(d->codec); + AMediaExtractor_delete(d->ex); + d->sawInputEOS = true; + d->sawOutputEOS = true; + } + break; + + case kMsgSeek: + { + workerdata *d = (workerdata*)obj; + AMediaExtractor_seekTo(d->ex, 0, AMEDIAEXTRACTOR_SEEK_NEXT_SYNC); + AMediaCodec_flush(d->codec); + d->renderstart = -1; + d->sawInputEOS = false; + d->sawOutputEOS = false; + if (!d->isPlaying) { + d->renderonce = true; + post(kMsgCodecBuffer, d); + } + LOGV("seeked"); + } + break; + + case kMsgPause: + { + workerdata *d = (workerdata*)obj; + if (d->isPlaying) { + // flush all outstanding codecbuffer messages with a no-op message + d->isPlaying = false; + post(kMsgPauseAck, NULL, true); + } + } + break; + + case kMsgResume: + { + workerdata *d = (workerdata*)obj; + if (!d->isPlaying) { + d->renderstart = -1; + d->isPlaying = true; + post(kMsgCodecBuffer, d); + } + } + break; + } +} + + + + +extern "C" { + +jboolean Java_com_example_nativecodec_NativeCodec_createStreamingMediaPlayer(JNIEnv* env, + jclass clazz, jstring filename) +{ + LOGV("@@@ create"); + + // convert Java string to UTF-8 + const char *utf8 = env->GetStringUTFChars(filename, NULL); + LOGV("opening %s", utf8); + int fd = open(utf8, O_RDONLY); + env->ReleaseStringUTFChars(filename, utf8); + if (fd < 0) { + LOGV("failed: %d (%s)", fd, strerror(errno)); + return JNI_FALSE; + } + + data.fd = fd; + + workerdata *d = &data; + + AMediaExtractor *ex = AMediaExtractor_new(); + media_status_t err = AMediaExtractor_setDataSourceFd(ex, d->fd, 0 , LONG_MAX); + close(d->fd); + if (err != AMEDIA_OK) { + LOGV("setDataSource error: %d", err); + return JNI_FALSE; + } + + int numtracks = AMediaExtractor_getTrackCount(ex); + + AMediaCodec *codec = NULL; + + LOGV("input has %d tracks", numtracks); + for (int i = 0; i < numtracks; i++) { + AMediaFormat *format = AMediaExtractor_getTrackFormat(ex, i); + const char *s = AMediaFormat_toString(format); + LOGV("track %d format: %s", i, s); + const char *mime; + if (!AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime)) { + LOGV("no mime type"); + return JNI_FALSE; + } else if (!strncmp(mime, "video/", 6)) { + // Omitting most error handling for clarity. + // Production code should check for errors. + AMediaExtractor_selectTrack(ex, i); + codec = AMediaCodec_createDecoderByType(mime); + AMediaCodec_configure(codec, format, d->window, NULL, 0); + d->ex = ex; + d->codec = codec; + d->renderstart = -1; + d->sawInputEOS = false; + d->sawOutputEOS = false; + d->isPlaying = false; + d->renderonce = true; + AMediaCodec_start(codec); + } + AMediaFormat_delete(format); + } + + mlooper = new mylooper(); + mlooper->post(kMsgCodecBuffer, d); + + return JNI_TRUE; +} + +// set the playing state for the streaming media player +void Java_com_example_nativecodec_NativeCodec_setPlayingStreamingMediaPlayer(JNIEnv* env, + jclass clazz, jboolean isPlaying) +{ + LOGV("@@@ playpause: %d", isPlaying); + if (mlooper) { + if (isPlaying) { + mlooper->post(kMsgResume, &data); + } else { + mlooper->post(kMsgPause, &data); + } + } +} + + +// shut down the native media system +void Java_com_example_nativecodec_NativeCodec_shutdown(JNIEnv* env, jclass clazz) +{ + LOGV("@@@ shutdown"); + if (mlooper) { + mlooper->post(kMsgDecodeDone, &data, true /* flush */); + mlooper->quit(); + delete mlooper; + mlooper = NULL; + } + if (data.window) { + ANativeWindow_release(data.window); + data.window = NULL; + } +} + + +// set the surface +void Java_com_example_nativecodec_NativeCodec_setSurface(JNIEnv *env, jclass clazz, jobject surface) +{ + // obtain a native window from a Java surface + if (data.window) { + ANativeWindow_release(data.window); + data.window = NULL; + } + data.window = ANativeWindow_fromSurface(env, surface); + LOGV("@@@ setsurface %p", data.window); +} + + +// rewind the streaming media player +void Java_com_example_nativecodec_NativeCodec_rewindStreamingMediaPlayer(JNIEnv *env, jclass clazz) +{ + LOGV("@@@ rewind"); + mlooper->post(kMsgSeek, &data); +} + +} diff --git a/ndk/platforms/android-L/samples/native-codec/res/drawable/icon.png b/ndk/platforms/android-L/samples/native-codec/res/drawable/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a07c69fa5a0f4da5d5efe96eea12a543154dbab6 GIT binary patch literal 2574 zcmV+p3i0)cP)Q`Og{P|8RRXpj5bgrSmEzSMfBn+{{vpNxw?;5UX;iv9sYxy_`IQHs$i<61a_iv^L>h8s-`D(`e@|IgS*Fj zNGM876Gf;3D8*1UX9a%v>yJKD*QkCwW2AirU(L{qNA)JghmGItc;(H<$!ABY&gBy1vJIEUj-b8%el*o|VkG)LqNx#TG>Jvj^jIte!!+RY z)T4j$7+PoF1AkRBf}R#^T=-q|PaK1$c<4UH)Hpq3$4WA|xtr!ZQLC=*vNE>O6E9kp+5X0eKB$6>C(lPwI@3#oY zhS_%x7e|j!$yG?ECXmh~EH~^OeuK}+sWoJse3Z3?ha3n`MM9KvA?uqpEnBg4Q46)7 zM$p%a$@l;+O}vfvx%XjH`}a{(-HHth9!JaUwV0*VqGR48^gWNYN<&~7x)y$e!X>e` zZ5!6KZoxbKuV9XUDI%#M1~IVh?pNSdeb~6@$y`v|yk=XK+fHxnDqnUK4&=QRNyIVf zYbDM*cI>~qIy*a7=z7uqkw@agd(<=y-Q7L!ty_23SGdXmahO<;N=wB+j;lNm%=OHC zy zU|>La6h%92y4IPufI$9>Xu!@y`TaNgtg&41@PwMwBdmSm7)xAWDLoqjZ==P2#*k7! z3o1)cVSI3KP_!?d8G^Lg0FtLXC~JYdxi|c%h~lXEixY=%VSFF@!*3&&9>(Rb|iK54Cx5;s~PY5iaV1het%w`dgQFBAJ;aFK zImQC}(|QaCFYUm1JVfzSc)ebv=)ObI)0jwJb``}Zj9J0n0Xgn*Zc(rFM9$xh_makZbm-at_v5^SW zM1y1SW@%+FuIy*WR)i3A2N_q;(YO`O!A|Ts^%z}9ZepCj3ytlw#x%N_fNrKKtPh`< z|1{UqF`4LxHaCQ79+E=uUXCOZ35jAMRz%R%0(P!0FMv=sk>Nr8%+OzY^c-M9@+fz=G`qa@v4sF5u-2289-#$**LWnyNNDwDf1( zkUiMnw|y$tn>pQP=Vn!#|17L^5AGrjtBkN$D@v)Z7LXc5EFhLB4<;7Wehh)CMqX|W zqsiZaO^benJ_hwa&V0ub$-_HUk**?g6fm9|!@kguU6*zhK)$qn-<3*kFrYPIaqR=V zUaUvk>@F_89b@tHs8R!*QKY;INJ<2_U+K6Ca3e9Gsl2{qY0%a7J?uICWgHuLfj+MB z=GkAN1&ifT#2u}B+2S#~$5jA(Qn^;H%CCmIae4AE-Dsng|Hl*Ov!z72k3ZnJs{pp| z+pW`DDueC#mEWOf=ucJ!dTL}hzOeiS-i?m2E;`EKz4<&Lu~NnW?peqVU^@<+T3KKu z{yrI%Qy-Z%HEvLUz}n^~m?7x`xuCtNR#L2En!T>dQtIKdS#V-Hzt3RtwTeYtmQ&dR z6qXZvac*oc@BUYEH%@Ylv_1&tSjkbzzU6*h1(3^C`;1z;g_SmOtclS?KWk2VYE zM*oS<=C483XckW?GN|1jfh3Ro(h + + + + + + + +