理解 Audio 音频系统六 之 Audio音乐播放器编写 及 MediaPlayer 实例化代码分析



六、Audio音乐播放器编写

6.1 音乐播放器APK编写

6.1.1 APK 界面 xml 文件代码

  1. activity_main.xml 文件内容
<!-- @\MusicDeamon\app\src\main\res\layout\activity_main.xml -->

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="55dp"
        android:layout_weight="1">

        <TextView
            android:id="@+id/textView3"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="Text Reserve ..." />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="298dp"
        android:layout_weight="7"
        android:orientation="vertical">

        <ListView
            android:id="@+id/list_view"
            android:layout_width="match_parent"
            android:layout_height="271dp"
            android:layout_weight="1"
            tools:layout_editor_absoluteX="16dp"
            tools:layout_editor_absoluteY="46dp" />

        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="match_parent"
            android:layout_height="56dp" />

        <TextView
            android:id="@+id/MusicName"
            android:layout_width="match_parent"
            android:layout_height="44dp"
            android:text="Text Reserve ..." />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="67dp"
            android:layout_gravity="top"
            android:layout_weight="10">
            <Button
                android:id="@+id/btn_pre"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="btn_pre_click"
                android:text="上一曲" />
            <Button
                android:id="@+id/btn_start"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="btn_start_click"
                android:text="开始/暂停" />
            <Button
                android:id="@+id/btn_next"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="btn_next_click"
                android:text="下一曲" />
            <Button
                android:id="@+id/btn_stop"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="btn_stop_click"
                android:text="停止" />
        </LinearLayout>
        
    </LinearLayout>
</LinearLayout>
  1. item.xml 文件内容
<!-- @ \MusicDeamon\app\src\main\res\layout\item.xml -->

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/mName"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="aaaa"
        android:textSize="20sp"
        />
</LinearLayout>

6.1.2 APK Java 文件代码

// @ \MusicDeamon\app\src\main\java\com\jaimex\musicdeamon\MainActivity.java
package com.jaimex.musicdeamon;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.SimpleAdapter;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class MainActivity extends AppCompatActivity {

    private final static String TAG="MusicDaemon";
    private TextView MusicNameTextView;
    private SeekBar seekBar;
    private ListView listView;

    private List<Map<String, String>> data;
    private int current;
    private MediaPlayer player;
    private Handler handler = new Handler();
    private Button btn_next;
    private Button btn_pre;
    private Button btn_start;
    private Button btn_stop;

    private boolean isPause;
    private boolean isStartTrackingTouch;
    private Object file;

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

        // 找到控件
        MusicNameTextView = (TextView) findViewById(R.id.MusicName);
        seekBar = (SeekBar) findViewById(R.id.seekBar);
        listView = (ListView) findViewById(R.id.list_view);
        btn_next = (Button) findViewById(R.id.btn_next);
        btn_pre = (Button) findViewById(R.id.btn_pre);
        btn_start = (Button) findViewById(R.id.btn_start);
        btn_stop = (Button) findViewById(R.id.btn_stop);

		//1. 创建一个音乐播放器
        player = new MediaPlayer(); 
        //2. 显示音乐播放列表
        generateListView(); 
		//3. 进度条监听器
        seekBar.setOnSeekBarChangeListener(new MySeekBarListener()); 
		//4. 实例化MyPlayerListener, 监听播放器是否播放完毕,如果播放完毕,则自动播放下一首
        player.setOnCompletionListener(new MyPlayerListener()); 

        Log.i(TAG, "onCreate: end");
    }

    ///////////////////////////////////////////////////////////
    /* 前一首按钮 */
    public void btn_pre_click(View view) {
        Log.i(TAG, "btn_pre_click: ");
        previous();
    }

    /* 下一首按钮 */
    public void btn_next_click(View view) {
        Log.i(TAG, "btn_next_click: ");
        next();
    }

    /* 暂停/播放按钮 */
    public void btn_start_click(View view) {
        Log.i(TAG, "btn_start_click: ");
        //默认从第一首歌曲开始播放
        if (!player.isPlaying() && !isPause) {
            play();
            return;
        }
        Button button = (Button) view;
        //暂停/播放按钮
        if ("||".equals(button.getText())) {
            pause();
            button.setText("▶");
        } else {
            resume();
            button.setText("||");
        }
    }

    /* 停止播放 */
    public void btn_stop_click(View view) {
        Log.i(TAG, "btn_stop_click: ");
        if (player != null && player.isPlaying()) {
            player.stop();
            player.reset();
        }
    }

    ///////////////////////////////////////////////////////////
    /* 播放器监听器*/
    private final class MyPlayerListener implements MediaPlayer.OnCompletionListener {
        //歌曲播放完后自动播放下一首歌曲
        public void onCompletion(MediaPlayer mp) {
            Log.i(TAG, "onCompletion: ");
            next();
        }
    }

    /* 播放前一首歌 */
    private void previous() {
        current = current - 1 < 0 ? data.size() - 1 : current - 1;
        Log.i(TAG, "previous: " + "current=" + current);
        play();
    }

    /* 播放下一首歌 */
    private void next() {
        current = (current + 1) % data.size();
        Log.i(TAG, "next: " + "current=" + current);
        play();

    }

    /*
     * 进度条监听器
     */
    private final class MySeekBarListener implements SeekBar.OnSeekBarChangeListener {
        //移动触发
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            Log.i(TAG, "onProgressChanged: ");
        }
        //起始触发
        public void onStartTrackingTouch(SeekBar seekBar) {
            isStartTrackingTouch = true;
            Log.i(TAG, "onStartTrackingTouch: " + isStartTrackingTouch);
        }
        //结束触发
        public void onStopTrackingTouch(SeekBar seekBar) {
            player.seekTo(seekBar.getProgress());
            isStartTrackingTouch = false;
            Log.i(TAG, "onStartTrackingTouch: " + isStartTrackingTouch);
        }
    }

    /*
     * 显示音乐播放列表
     */
    private void generateListView() {
        List<File> list = new ArrayList<File>();
        //获取sdcard中的所有歌曲
        findAll(Environment.getExternalStorageDirectory(), list);
        //for(File f : list)
        //    Log.i(TAG, "generateListView: " + "  list name=" + f.getName());
        
        //播放列表进行排序,字符顺序
        Collections.sort(list);
        data = new ArrayList<Map<String, String>>();
        for (File file : list) {
            Map<String, String> map = new HashMap<String, String>();
            map.put("name", file.getName());
            map.put("path", file.getAbsolutePath());
            Log.i(TAG, "generateListView: "+" name="+file.getName() + " path="+file.getAbsolutePath());
            data.add(map);
        }
        SimpleAdapter adapter = new SimpleAdapter(this, data, R.layout.item, new String[] { "path" }, new int[] { R.id.mName });
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new MyItemListener());
    }
    private final class MyItemListener implements AdapterView.OnItemClickListener {
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            current = position;
            Log.i(TAG, "onItemClick: "+ "current=" + current);
            play();
        }
    }
    private void play() {
        Log.i(TAG, "play: "+"+++");
        try {
            //重播
            player.reset();
            //获取歌曲路径
            player.setDataSource(data.get(current).get("path"));
            //缓冲
            player.prepare();
            //开始播放
            player.start();
            //显示歌名
            MusicNameTextView.setText("当前播放歌曲: " + data.get(current).get("name"));
            //设置进度条长度
            seekBar.setMax(player.getDuration());
            //播放按钮样式
            btn_start.setText("||");
            //发送一个Runnable, handler收到之后就会执行run()方法
            handler.post(new Runnable() {
                public void run() {
                    // 更新进度条状态
                    if (!isStartTrackingTouch)
                        seekBar.setProgress(player.getCurrentPosition());
                    // 1秒之后再次发送
                    handler.postDelayed(this, 1000);
                }
            });
        } catch (Exception e) {
            Log.i(TAG, "play: "+" error !!!");
            e.printStackTrace();
        }
        Log.i(TAG, "play: "+"---");
    }

    /**
     * 查找文件路径中所有mp3文件
     * @param file 要找的目录
     * @param list 用来装文件的List
     */
    private void findAll(File file, List<File> list) {
        //Log.i(TAG, "findAll: " + " file path=" + file.getAbsolutePath() );
        File[] subFiles = file.listFiles();

        if (subFiles != null){
            for (File subFile : subFiles) {
                //Log.i(TAG, "findAll: "+ " file name =" + subFile.getName());
                if (subFile.isFile() && subFile.getName().endsWith(".mp3")) {
                    list.add(subFile);
                    Log.i(TAG, "findAll: "+ " file name =" + subFile.getName());
                }else if (subFile.isDirectory())//如果是目录
                    findAll(subFile, list); //递归
            }
        }
    }
    /* 开始操作 */
    private void resume() {
        if (isPause) {
            player.start();
            isPause = false;
        }
        Log.i(TAG, "resume: "+isPause);
    }
    
    /* 暂停操作 */
    private void pause() {
        if (player != null && player.isPlaying()) {
            player.pause();
            isPause = true;
            Log.i(TAG, "pause: "+isPause);
        }
    }
    /* 收到广播时暂停 */
    private final class PhoneListener extends BroadcastReceiver {
        public void onReceive(Context context, Intent intent) {
            Log.i(TAG, "onReceive: ");
            pause();
        }
    }
    /* 恢复播放 */
    protected void onResume() {
        super.onResume();
        Log.i(TAG, "onResume: ");
        //resume();
    }
}

6.1.3 APK 实测结果

在安卓模拟器上实测,正常,界面比较简漏
在这里插入图片描述


6.2 MediaPlay 播放流程代码分析

简化前面 apk java 的代码,可以知道要使用 MediaPlay 播放一首歌的话,
主要代码如下:

开始播放流程 :
player = new MediaPlayer(); //1. 创建一个音乐播放器
//设置歌曲播放路径
player.setDataSource(data.get(current).get("path"));
//缓冲
player.prepare();
//开始播放
player.start();

接下来,我们依次分析下前面的代码流程

6.2.1 MediaPlayer 实例化代码分析

player = new MediaPlayer();

在MediaPlayer 代码中,首先会加 libmedia_jni.so 库文件,接着调用 native_init() 初始化 Native 层JNI 方法。

native_init 主要流程如下:

  1. 在Native 层中找到Java 中MediaPlayer Class 的对象,通过该对象可找到中的 Java 方法
  2. 获得 MediaPlayer.java 中 postEventFromNative() 方法供回调使用
  3. 获得 MediaPlayer.java 中 mNativeSurfaceTexture 变量
@ \frameworks\base\media\java\android\media\MediaPlayer.java
static {
	System.loadLibrary("media_jni");
	native_init();
}
--------------------------------------------------------------------------------
   
native_init方法定义如下
@ \frameworks\base\media\jni\android_media_MediaPlayer.cpp
{"native_init", "()V",  (void *)android_media_MediaPlayer_native_init},
------->

// This function gets some field IDs, which in turn causes class initialization.
// It is called from a static block in MediaPlayer, which won't run until the
// first time an instance of this class is used.
static void android_media_MediaPlayer_native_init(JNIEnv *env)
{
    jclass clazz;
    // 1. 在Native 层中找到Java 中MediaPlayer Class 的对象,通过该对象可找到中的 Java 方法
    clazz = env->FindClass("android/media/MediaPlayer");
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
	// 2. 获得 MediaPlayer.java 中 postEventFromNative() 方法供回调使用
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative","(Ljava/lang/Object;IIILjava/lang/Object;)V");
    // 3. 获得 MediaPlayer.java 中 mNativeSurfaceTexture 变量
    fields.surface_texture = env->GetFieldID(clazz, "mNativeSurfaceTexture", "J");
    env->DeleteLocalRef(clazz);  // 清空 clazz 局部变量
    
    clazz = env->FindClass("android/net/ProxyInfo");
    fields.proxyConfigGetHost = env->GetMethodID(clazz, "getHost", "()Ljava/lang/String;");
    fields.proxyConfigGetPort = env->GetMethodID(clazz, "getPort", "()I");
    fields.proxyConfigGetExclusionList = env->GetMethodID(clazz, "getExclusionListAsString", "()Ljava/lang/String;");
    env->DeleteLocalRef(clazz);

    gBufferingParamsFields.init(env);

    // Modular DRM
    FIND_CLASS(clazz, "android/media/MediaDrm$MediaDrmStateException");
    if (clazz) {
        GET_METHOD_ID(gStateExceptionFields.init, clazz, "<init>", "(ILjava/lang/String;)V");
        gStateExceptionFields.classId = static_cast<jclass>(env->NewGlobalRef(clazz));

        env->DeleteLocalRef(clazz);
    } else {
        ALOGE("JNI android_media_MediaPlayer_native_init couldn't get clazz android/media/MediaDrm$MediaDrmStateException");
    }
    gPlaybackParamsFields.init(env);
    gSyncParamsFields.init(env);
    gVolumeShaperFields.init(env);
}

在构造函数中:

@ \frameworks\base\media\java\android\media\MediaPlayer.java
public class MediaPlayer extends PlayerBase implements SubtitleController.Listener, VolumeAutomation
{
	public MediaPlayer() {
        super(new AudioAttributes.Builder().build(), AudioPlaybackConfiguration.PLAYER_TYPE_JAM_MEDIAPLAYER);

        Looper looper;
        // 如果调用MediaPlayer构造方法的线程有自己的looper,就用子线程的looper,否则使用主线程的looper
        if ((looper = Looper.myLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else if ((looper = Looper.getMainLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else {
            mEventHandler = null;
        }

        mTimeProvider = new TimeProvider(this);
        mOpenSubtitleSources = new Vector<InputStream>();

        /* Native setup requires a weak reference to our object.
         * It's easier to create it here than in C++.
         */
        native_setup(new WeakReference<MediaPlayer>(this));
        baseRegisterPlayer();
    }

}
--------------------------------------------------------------------------------

native_setup 方法定义如下
@ \frameworks\base\media\jni\android_media_MediaPlayer.cpp
{"native_setup",  "(Ljava/lang/Object;)V", (void *)android_media_MediaPlayer_native_setup},
---------->
static void android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    ALOGV("native_setup");
    sp<MediaPlayer> mp = new MediaPlayer();   //创建native的MediaPlayer对象

	//为native的对象设置监听
    // create new listener and give it to MediaPlayer  
    sp<JNIMediaPlayerListener> listener = new JNIMediaPlayerListener(env, thiz, weak_this); 
    mp->setListener(listener); 

	//将native对象的保存到Java对象的成员mNativeContext中
    // Stow our new C++ MediaPlayer in an opaque field in the Java object.
    setMediaPlayer(env, thiz, mp);
}

--------------------------------------------------------------------------------
@ \frameworks\base\media\java\android\media\PlayerBase.java
protected void baseRegisterPlayer() {
        int newPiid = AudioPlaybackConfiguration.PLAYER_PIID_INVALID;
        IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE);
        mAppOps = IAppOpsService.Stub.asInterface(b);
        // initialize mHasAppOpsPlayAudio
        updateAppOpsPlayAudio();
        // register a callback to monitor whether the OP_PLAY_AUDIO is still allowed
        mAppOpsCallback = new IAppOpsCallbackWrapper(this);
        try {
            mAppOps.startWatchingMode(AppOpsManager.OP_PLAY_AUDIO,
                    ActivityThread.currentPackageName(), mAppOpsCallback);
        } catch (RemoteException e) {
            mHasAppOpsPlayAudio = false;
        }
        try {
            newPiid = getService().trackPlayer( new PlayerIdCard(mImplType, mAttributes, new IPlayerWrapper(this)));
        } catch (RemoteException e) {
            Log.e(TAG, "Error talking to audio service, player will not be tracked", e);
        }
        mPlayerIId = newPiid;
    }


6.2.2 setDataSource() 代码分析

@ \frameworks\base\media\java\android\media\MediaPlayer.java

public void setDataSource(String path) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
	setDataSource(path, null, null);
}

private void setDataSource(String path, Map<String, String> headers, List<HttpCookie> cookies)
            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException
{
	String[] keys = null;
	String[] values = null;
	if (headers != null) {
		keys = new String[headers.size()];
		values = new String[headers.size()];
		for (Map.Entry<String, String> entry: headers.entrySet()) {
			keys[i] = entry.getKey();
			values[i] = entry.getValue();
			++i;
		}
	}
	setDataSource(path, keys, values, cookies);
}

private void setDataSource(String path, String[] keys, String[] values,List<HttpCookie> cookies)
		throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
	final Uri uri = Uri.parse(path);
	final String scheme = uri.getScheme();
	if ("file".equals(scheme)) {
		path = uri.getPath();
	} else if (scheme != null) {
		// handle non-file sources
		nativeSetDataSource( MediaHTTPService.createHttpServiceBinderIfNecessary(path, cookies),
                path,keys,values);
		return;
	}
	final File file = new File(path);
	if (file.exists()) {
		FileInputStream is = new FileInputStream(file);
		FileDescriptor fd = is.getFD();
    	setDataSource(fd);
  		is.close();
	} else {
		throw new IOException("setDataSource failed.");
	}
}

private native void nativeSetDataSource(
        IBinder httpServiceBinder, String path, String[] keys, String[] values)
        throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;

6.2.3 prepare() 代码分析


6.2.4 start() 开始播放音乐代码分析

参考:
https://blog.csdn.net/super_dc/article/category/2240809
https://blog.csdn.net/zhuyong006/article/details/86080498

发布了329 篇原创文章 · 获赞 66 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/Ciellee/article/details/103011699