因為自己的手機沒廣播裝置無法直接接收無線電波來播放,所以一直很想寫網路廣播軟體程式,直到去年秋天找了一下如何在 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,其處理流程:
- 在 Java 端呼叫 NDK 進行 mms decoding
- 在 NDK 中,使用 ffmepg library 對 mms 來源進行 audio decoding
- 在 NDK 中,請 Java 層初始化 AudioTrack 物件,並記得 mAudioTrack.play()
- 在 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 上
- 實作播放系列,例如暫停、開始、停掉等功能