2013年2月17日 星期日

Android 開發筆記 - 網路廣播程式及實作原理 (使用FFmpeg播放mms)


因為自己的手機沒廣播裝置無法直接接收無線電波來播放,所以一直很想寫網路廣播軟體程式,直到去年秋天找了一下如何在 Android 播放 mms 串流後,發現本身 Android 還是不太支援,就開始嘗試編 ffmpeg 處理 mms 播放,而編出 ffmpeg 後又停擺一陣子,實在是隔行如隔山 :P 所幸春節還有一點時間,卡在心中好一陣子的議題,終於趁著春節最後一天把該弄懂得都弄的差不多了,順手記一下,等下一個空閒時刻再包成一個完整一點的 android app 吧!


參考資料:



簡言之,如果想寫隻程式播放網路廣播(mms)的流程,首先要確認 Android 本身是否已支援播放 mms protocol:


try {
    MediaPlayer mMediaPlayer = MediaPlayer.create(this, Uri.parse("http://mms.example.com/radio/"));
    mMediaPlayer.prepare();
    mMediaPlayer.start();
} catch (Exception e) {
    e.printStackTrace();
}


若要測試指定的 mms 位置是否正確,可以先用 ffplay (安裝ffmpeg) 測試,(記得要用 mmsh:// ):


$ ffplay mmsh://mms.example.com/radio/


最後,則是寫程式用 ffmepg 相關程式碼來寫隻簡單的 Android app,其處理流程:



  1. 在 Java 端呼叫 NDK 進行 mms decoding

  2. 在 NDK 中,使用 ffmepg library 對 mms 來源進行 audio decoding

  3. 在 NDK 中,請 Java 層初始化 AudioTrack 物件,並記得 mAudioTrack.play()

  4. 在 NDK 中,定期將 decoding 資料餵給 Java 層 AudioTrack


記得要給 <uses-permission android:name="android.permission.INTERNET"/> 權限!測試的實機也要開網路,不然會得到 I/O Error 的訊息。


MainActivity.java:


public class MainActivity extends Activity {


   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      playRadioNDKInit();
      playRadioNDKRun("mmsh://xxx.xxx.xxx.xxx/");

      /*
      try {
         MediaPlayer mMediaPlayer = MediaPlayer.create(this, Uri.parse("http://xxx.xxx.xxx.xxx/"));
         mMediaPlayer.prepare();
         mMediaPlayer.start();
      } catch (Exception e) {
         e.printStackTrace();
      }
      // */
   }


   AudioTrack playRadio_AudioTrack = null;
   int playRadio_MinBufferSize = 0;
   int playRadio_BytesWritten = 0;
   List<byte []> playRadio_AudioBuffer = new ArrayList<byte []>();

   void playRadio_Step1_Init(int AVCodecContext_sampleRate, int AVCodecContext_channels) {
      android.util.Log.e("MainActivity", "playRadio_Step1_Init:"+AVCodecContext_sampleRate+","+AVCodecContext_channels);
      AVCodecContext_channels = (AVCodecContext_channels == 1) ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
      playRadio_MinBufferSize = AudioTrack.getMinBufferSize(AVCodecContext_sampleRate, AVCodecContext_channels, AudioFormat.ENCODING_PCM_16BIT) * 4;
      playRadio_BytesWritten = 0;
      playRadio_AudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, AVCodecContext_sampleRate, AVCodecContext_channels, AudioFormat.ENCODING_PCM_16BIT, playRadio_MinBufferSize, AudioTrack.MODE_STREAM);
   }

   int playRadio_Step2_fillBuffer(byte[] audio_frame_buffer) {
      if(audio_frame_buffer.length > 0 ) {
         playRadio_AudioBuffer.add(playRadio_AudioBuffer.size(), audio_frame_buffer);
         playRadio_BytesWritten += audio_frame_buffer.length;
         if(playRadio_BytesWritten > playRadio_MinBufferSize)
            return 0;
      }
      return 1;
   }

   void playRadio_Step3_audioTrackWrite(byte[] audio_frame_buffer) {
      if( playRadio_AudioTrack != null) {
         if( playRadio_AudioBuffer.size() > 0 ) {
            for(int i=0; i<playRadio_AudioBuffer.size(); ++i) {
               byte []audoFrameBuffer = playRadio_AudioBuffer.get(i);
               playRadio_AudioTrack.write(audoFrameBuffer, 0, audoFrameBuffer.length);
            }
            playRadio_AudioBuffer.clear();
            playRadio_BytesWritten = 0;
         }
         if( audio_frame_buffer.length > 0 )
            playRadio_AudioTrack.write(audio_frame_buffer, 0, audio_frame_buffer.length);
         playRadio_AudioTrack.play();
      }
   }
   public native void playRadioNDKInit();
   public native void playRadioNDKRun(String mmsh_uri);
   static {
      System.loadLibrary("ffmpeg");
      System.loadLibrary("ffmpeg-jni");
   }
}


jni/ffmpeg-ini.c:


#include <string.h>
#include <jni.h>
#include <android/log.h>
#include <stdio.h>


#include "libavformat/avformat.h"


#define LOG_TAG "FFmpegJNI"
#define LOGI(...) {__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__);}
#define LOGE(...) {__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__);}


jstring Java_com_example_internetradioplayer_MainActivity_stringFromJNI( JNIEnv* env, jobject thiz )
{
   return (*env)->NewStringUTF(env, "Hello from My JNI !");
}


static jmethodID playRadio_Step1_Init;
static jmethodID playRadio_Step2_fillBuffer;
static jmethodID playRadio_Step3_audioTrackWrite;


void Java_com_example_internetradioplayer_MainActivity_playRadioNDKInit( JNIEnv* env, jobject thiz )
{
   jclass cls = (*env)->GetObjectClass(env, thiz);


   playRadio_Step1_Init = (*env)->GetMethodID(env, cls, "playRadio_Step1_Init", "(II)V");
   if(!playRadio_Step1_Init)
      LOGE("playRadio_Step1_Init not found");
   playRadio_Step2_fillBuffer = (*env)->GetMethodID(env, cls, "playRadio_Step2_fillBuffer", "([B)I");
   if(!playRadio_Step2_fillBuffer)
      LOGE("playRadio_Step2_fillBuffer not found");
   playRadio_Step3_audioTrackWrite = (*env)->GetMethodID(env, cls, "playRadio_Step3_audioTrackWrite", "([B)V");
   if(!playRadio_Step3_audioTrackWrite)
      LOGE("playRadio_Step3_audioTrackWrite not found");
}


void Java_com_example_internetradioplayer_MainActivity_playRadioNDKRun( JNIEnv* env, jobject thiz, jstring mms_uri )
{
   int stream_index = -1, i = 0, ret = 0;
   AVCodec *codec;
   AVFormatContext *pFormatCtx = avformat_alloc_context();
   avcodec_register_all();
   av_register_all();
   avformat_network_init();


   // jstring to char *
   const int out_buffer_max_line = 1024;
   char mms_location[out_buffer_max_line+1];
   jbyteArray java_bytes_array= (jbyteArray)(*env)->CallObjectMethod(env, mms_uri, (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/String"), "getBytes", "(Ljava/lang/String;)[B"), (*env)->NewStringUTF(env, "utf-8"));
   jsize java_bytes_array_length = (*env)->GetArrayLength(env, java_bytes_array);
   jbyte* java_bytes = (*env)->GetByteArrayElements(env, java_bytes_array, JNI_FALSE);
   if(java_bytes_array_length > 0 && java_bytes_array_length < out_buffer_max_line ) {
      memcpy(mms_location, java_bytes, java_bytes_array_length);
      mms_location[java_bytes_array_length] = '\0';
   } else {
      mms_location[0] = '\0';
   }


   if ( ( ret = avformat_open_input(&pFormatCtx, mms_location, NULL, NULL) ) != 0) {
      LOGE("avformat_open_input() failed: (%d, %s)", ret, av_err2str(ret));
      return;
   }


   if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
      LOGE("Unable to locate stream information");
      return;
   }


   // Find the first audio stream
   for (i = 0; i < pFormatCtx->nb_streams; i++) {
      if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
         stream_index = i;
         break;
      }
   }


   if (stream_index == -1) {
      LOGE("stream_index == -1");
      return;
   }


   if( (codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id) ) == NULL ) {
      LOGE("avcodec_find_decoder() failed to find audio decoder");
      return;
   }


   if (avcodec_open2(pFormatCtx->streams[stream_index]->codec, codec, NULL) < 0) {
      LOGE("avcodec_open2() failed");
      return;
   }


   (*env)->CallVoidMethod(env, thiz, playRadio_Step1_Init, pFormatCtx->streams[stream_index]->codec->sample_rate, pFormatCtx->streams[stream_index]->codec->channels);


   AVPacket packet;
   memset(&packet, 0, sizeof(packet));
   av_init_packet(&packet);


   AVFrame *decoded_frame = avcodec_alloc_frame();
   int got_frame;
   jbyteArray retArray = NULL;


   int flag_continue = 1;
   while(flag_continue) {
      ret = av_read_frame(pFormatCtx, &packet);
      if( ret < 0 ) {
         if (ret == AVERROR_EOF || url_feof(pFormatCtx->pb)) {
            LOGE("buffering, break", pFormatCtx->nb_streams);
            break;
         }
      }
      if( packet.stream_index != stream_index )
         continue;
      if( ( ret = avcodec_decode_audio4(pFormatCtx->streams[stream_index]->codec, decoded_frame, &got_frame, &packet) ) <= 0 && got_frame ) {
         LOGE("buffering, no frame: %d, %d", ret, got_frame);
      } else {
         int out_size = av_samples_get_buffer_size(NULL, pFormatCtx->streams[stream_index]->codec->channels, decoded_frame->nb_samples, pFormatCtx->streams[stream_index]->codec->sample_fmt, 1);
         LOGE("buffering, avcodec_decode_audio4 %d , %d, %d, %d", packet.stream_index, stream_index, ret, out_size);


         if(!retArray)
            retArray = (*env)->NewByteArray(env, out_size);
         else if((*env)->GetArrayLength(env, retArray) != out_size) {
            (*env)->DeleteLocalRef(env, retArray);
            retArray = (*env)->NewByteArray(env, out_size);
         }
         void *temp = (*env)->GetPrimitiveArrayCritical(env, (jarray)retArray, 0);
         memcpy(temp, decoded_frame->data[0], out_size);
         (*env)->ReleasePrimitiveArrayCritical(env, retArray, temp, 0);
         flag_continue = (*env)->CallIntMethod(env, thiz, playRadio_Step2_fillBuffer, retArray);
      }
      av_free_packet(&packet);
   }


   while (av_read_frame(pFormatCtx, &packet) >= 0){
      if( ( ret = avcodec_decode_audio4(pFormatCtx->streams[stream_index]->codec, decoded_frame, &got_frame, &packet) ) <= 0 && got_frame ) {
         LOGE("no frame");
         break;
      } else {
         int out_size = av_samples_get_buffer_size(NULL, pFormatCtx->streams[stream_index]->codec->channels, decoded_frame->nb_samples, pFormatCtx->streams[stream_index]->codec->sample_fmt, 1);


         if(!retArray)
            retArray = (*env)->NewByteArray(env, out_size);
         else if((*env)->GetArrayLength(env, retArray) != out_size) {
            (*env)->DeleteLocalRef(env, retArray);
            retArray = (*env)->NewByteArray(env, out_size);
         }
         void *temp = (*env)->GetPrimitiveArrayCritical(env, (jarray)retArray, 0);
         memcpy(temp, decoded_frame->data[0], out_size);
         (*env)->ReleasePrimitiveArrayCritical(env, retArray, temp, 0);
         (*env)->CallVoidMethod(env, thiz, playRadio_Step3_audioTrackWrite, retArray);
      }
   }
   if(retArray)
      (*env)->DeleteLocalRef(env, retArray);
}


jni/Android.mk:


LOCAL_PATH := $(call my-dir)


include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg-prebuilt
LOCAL_SRC_FILES := ffmpeg/armv7/libffmpeg.so
LOCAL_EXPORT_C_INCLUDES := ffmpeg/armv7/include
LOCAL_EXPORT_LDLIBS := ffmpeg/armv7/libffmpeg.so
LOCAL_PRELINK_MODULE := true
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg-jni
LOCAL_SRC_FILES := ffmpeg-jni.c
LOCAL_C_INCLUDES := $(LOCAL_PATH)/ffmpeg/armv7/include
LOCAL_LDLIBS := $(LOCAL_PATH)/ffmpeg/armv7/libffmpeg.so -lm -llog
LOCAL_SHARED_LIBRARY := ffmpeg-prebuilt
include $(BUILD_SHARED_LIBRARY)


以上程式在 Nexus S/Android 4.1.2 實機測試可以播出聲音,且在 Android emulator 4.1 & ARM (armeabi-v7a) 亦可以播出聲音,代表 ffmpeg 的使用還算正常,但此篇僅單純記錄如何 call ffmpeg functions,若要真的弄出可以跑在各種平台的程式且正確使用,還須需要:



  • 調整編譯 ffmpeg 以及 jni/Android.mk 的編譯方式以支援各種 CPU 平台

  • 實作 threading、event callback 等細節,此例仍是卡在 main thread 上

  • 實作播放系列,例如暫停、開始、停掉等功能


3 則留言:

  1. 請問您後續有沒有將 thread 的部份做出來, 我試著用 pthread 加上 thread, 但是跑個 2 秒就掛了

    回覆刪除
  2. Hi~ 後來我一直很忙,到現在都還沒補完 XD
    只能祝福你把它完成吧!

    回覆刪除
  3. Hi, I'm a beginner in android, I followed your tutorial, but I did not lead to solutions. You can share your solution (part of ffmpeg generation later), or link to your solution. Thank you

    回覆刪除