android進階4step2:Android音視訊處理——音訊管理
音訊
MediaPlayer
MediaPlayer可以播放音訊和視訊 ,它用於控制Android下播放檔案或流的類。Android的多媒體框架支援各種常見的多媒體型別,這樣在程式中可以很容易地整合音訊、視訊或者圖片。Android下對於音訊或者視訊的支援均需要使用到MediaPlayer類
MediaPlayer生命週期和呼叫的方法
橢圓代表MediaPlayer物件可能駐留的狀態。弧線表示驅動MediaPlayer在各個狀態之間遷移的播放控制操作。這裡有兩種型別的弧線。由一個箭頭
開始的弧代表同步的方法呼叫,而以雙箭頭開頭的代表的弧線代表非同步方法呼叫。
MediaPlayer是基於狀態的,只有在特定狀態才能執行特定的方法。所以認清MediaPlayer生命週期十分重要的。
1、當MediaPlayer通過new方式進行初始化或MediaPlayer呼叫了reset()方法後,它就處於Idle狀態。當呼叫了release()方法後,它就處於End狀態。這兩種狀態之間是MediaPlayer物件的生命週期。
1.1、在一個新構建的MediaPlayer物件和一個呼叫了reset()方法的MediaPlayer物件之間有一個微小的但是十分重要的差別。
在處於Idle狀態
當一個MediaPlayer物件剛被構建的時候,內部的播放引擎和物件的狀態都沒有改變,在這個時候呼叫以上的那些方法,框架將無法回撥客戶端程式註冊的OnErrorListener.onError()方法;但若這個MediaPlayer物件呼叫了reset()方法之後,再呼叫以上的那些方法,內部的播放引擎就會回撥客戶端程式註冊的OnErrorListener.onError()方法了,並將錯誤的狀態傳入。
1.2、一旦一個MediaPlayer物件不再被使用,應立即呼叫release()方法來釋放在內部的播放引擎中與這個MediaPlayer物件關聯的資源。資源可能包括如硬體加速元件的單態元件,若沒有呼叫release()方法可能會導致之後的MediaPlayer物件例項無法使用這種單態硬體資源,從而退回到軟體實現或執行失敗。一旦MediaPlayer物件進入了End狀態,它不能再被使用,也沒有辦法再遷移到其它狀態。(不可逆,需要重新構建)
1.3、使用new操作符建立的MediaPlayer物件處於Idle狀態,而那些通過過載的create()便利方法建立的MediaPlayer物件卻不是處於Idle狀態。事實上,如果成功呼叫了過載的create()方法,那麼這些物件已經是Prepare狀態了。
2、在 一般情況下,由於種種原因一些播放控制操作可能會失敗,如不支援的音訊/視訊格式,缺少隔行掃描的音訊/視訊,解析度太高,流超時等等。因此,錯誤報告和恢復在這種情況下是非常重要的。有時,由於程式設計錯誤,在處於無效狀態的情況下呼叫了一個播放控制操作可能發生。在所有這些錯誤條件下,內部的播放引擎會呼叫一個由客戶端程式設計師提供的OnErrorListener.onError()方法。客戶端程式設計師可以通過呼叫 MediaPlayer.setOnErrorListener(android.media.MediaPlayer.OnErrorListener)方法來註冊OnErrorListener。
2.1、一旦發生錯誤,MediaPlayer物件會進入到Error狀態。
2.2、為了重用一個處於Error狀態的MediaPlayer物件,可以呼叫reset()方法來把這個物件恢復成Idle狀態。
2.3、註冊一個OnErrorListener來獲知內部播放引擎發生的錯誤是好的程式設計習慣。
2.4、在不合法的狀態下呼叫一些方法,如prepare(),prepareAsync()和setDataSource()方法會丟擲IllegalStateException異常。
3、調 用setDataSource(FileDescriptor)方法,或setDataSource(String)方法,或 setDataSource(Context,Uri)方法,或setDataSource(FileDescriptor,long,long)方法會使處於Idle狀態的物件遷移到Initialized狀態。
3.1、若當此MediaPlayer處於其它的狀態下,呼叫setDataSource()方法,會丟擲IllegalStateException異常。
3.2、好的程式設計習慣是不要疏忽了呼叫setDataSource()方法的時候可能會丟擲的IllegalArgumentException異常和IOException異常。
4、在開始播放之前,MediaPlayer物件必須要進入Prepared狀態。
4.1、有兩種方法(同步和非同步)可以使MediaPlayer物件進入Prepared狀態:要麼呼叫prepare()方法(同步),此方法返回就表示該MediaPlayer物件已經進入了Prepared狀態;要麼呼叫prepareAsync()方法(非同步),此方法會使此MediaPlayer物件進入Preparing狀態並返回,而內部的播放引擎會繼續未完成的準備工作。當同步版本返回時或非同步版本的準備工作完全完成時就會呼叫客戶端程式設計師提供的OnPreparedListener.onPrepared()監聽方法。可以呼叫MediaPlayer.setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener)方法來註冊OnPreparedListener。
4.2、Preparing是一箇中間狀態,在此狀態下呼叫任何具備影響的方法的結果都是未知的!
4.3、在不合適的狀態下呼叫prepare()和prepareAsync()方法會丟擲IllegalStateException異常。當MediaPlayer物件處於Prepared狀態的時候,可以調整音訊/視訊的屬性,如音量,播放時是否一直亮屏,迴圈播放等。
5、要開始播放,必須呼叫start()方法。當此方法成功返回時,MediaPlayer的物件處於Started狀態。isPlaying()方法可以被呼叫來測試某個MediaPlayer物件是否在Started狀態。
5.1、當處於Started狀態時,內部播放引擎會呼叫客戶端程式設計師提供的OnBufferingUpdateListener.onBufferingUpdate()回撥方法,此回撥方法允許應用程式追蹤流播放的緩衝的狀態。
5.2、對一個已經處於Started 狀態的MediaPlayer物件呼叫start()方法沒有影響。
6、播放可以被暫停,停止,以及調整當前播放位置。當呼叫pause()方法並返回時,會使MediaPlayer物件進入Paused狀態。注意 Started與Paused狀態的相互轉換在內部的播放引擎中是非同步的。所以可能需要一點時間在isPlaying()方法中更新狀態,若在播放流內 容,這段時間可能會有幾秒鐘。
6.1、呼叫start()方法會讓一個處於Paused狀態的MediaPlayer物件從之前暫停的地方恢復播放。當呼叫start()方法返回的時候,MediaPlayer物件的狀態會又變成Started狀態。
6.2、對一個已經處於Paused狀態的MediaPlayer物件pause()方法沒有影響。
7、呼叫stop()方法會停止播放,並且還會讓一個處於Started,Paused,Prepared或PlaybackCompleted狀態的MediaPlayer進入Stopped狀態。
7.1、對一個已經處於Stopped狀態的MediaPlayer物件stop()方法沒有影響。
8、呼叫seekTo()方法可以調整播放的位置。
8.1、seekTo(int)方法是非同步執行的,所以它可以馬上返回,但是實際的定位播放操作可能需要一段時間才能完成,尤其是播放流形式的音訊/視訊。當實際的定位播放操作完成之後,內部的播放引擎會呼叫客戶端程式設計師提供的OnSeekComplete.onSeekComplete()回撥方法。可以通過setOnSeekCompleteListener(OnSeekCompleteListener)方法註冊。
8.2、注意,seekTo(int)方法也可以在其它狀態下呼叫,比如Prepared,Paused和PlaybackCompleted狀態。此外,目前的播放位置,實際可以呼叫getCurrentPosition()方法得到,它可以幫助如音樂播放器的應用程式不斷更新播放進度
9、當播放到流的末尾,播放就完成了。
9.1、如果呼叫了setLooping(boolean)方法開啟了迴圈模式,那麼這個MediaPlayer物件會重新進入Started狀態。
9.2、若沒有開啟迴圈模式,那麼內部的播放引擎會呼叫客戶端程式設計師提供的OnCompletion.onCompletion()回撥方法。可以通過呼叫MediaPlayer.setOnCompletionListener(OnCompletionListener)方法來設定。內部的播放引擎一旦呼叫了OnCompletion.onCompletion()回撥方法,說明這個MediaPlayer物件進入了PlaybackCompleted狀態。
9.3、當處於PlaybackCompleted狀態的時候,可以再呼叫start()方法來讓這個MediaPlayer物件再進入Started狀態。
一般使用過程:
要想利用MediaPlayer實現音訊的播放,首先要對MediaPlayer進行初始化工作,得到MediaPlayer物件,在通過MediaPlayer進行相應的操作。
一般過程:初始化MediaPlayer - 載入媒體源 - 準備 - 開始播放
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource("...");
mediaPlayer.prepare();
mediaPlayer.start();
MediaPlayer支援多種不同的媒體源: 本地資源、內部的URI,比如一個你可能會從ContentResolver獲取的uri、外部URL(流)。
1、raw檔案中媒體源:假如res/raw檔案中包含一個sound_music.mp3檔案。
MediaPlayer mediaPlayer = MediaPlayer.create(this, R.raw.sound_music);
2、assets檔案中媒體源:假如在assets中包含一個sound_music.mp3檔案。
try {
AssetFileDescriptor fd = getAssets().openFd("sound_music.mp3");
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
} catch (IOException e) {
e.printStackTrace();
}
3、SD卡中媒體源:假如在SD卡中包含一個sound_music.mp3檔案。 需要許可權
try {
MediaPlayer mediaPlayer = new MediaPlayer();
String path = "/sdcard/sound_music.mp3";
mediaPlayer.setDataSource(path);
} catch (IOException e) {
e.printStackTrace();
}
4、網路資源:假如有一個網路資源http://ibooker.cc/ibooker/musics/sound_music.mp3。
MediaPlayer mediaPlayer = new MediaPlayer();
// 方式一
// Uri uri = Uri.parse("http://ibooker.cc/ibooker/musics/sound_music.mp3");
// mediaPlayer.setDataSource(this, uri);
// 方式二
mediaPlayer.setDataSource("http://ibooker.cc/ibooker/musics/sound_music.mp3");
簡單案例:實現音樂後臺播放 使用Service 因為Service不隨Activity的影響
MySerivce.java
public class MyService extends Service {
public MyService() {
}
MediaPlayer player;
@Override
public void onCreate() {
super.onCreate();
player = new MediaPlayer();
//重置,使MediaPlayer重回idel狀態
//player.reset();
Log.e("TAG","呼叫了OnCreate方法");
try {
//2.設定播放源,Initialized狀態
//設定SDCard下面、網路中的音樂
player.setDataSource(Environment.getExternalStorageDirectory() + "/Victoria.mp3");
// player.setDataSource("http://192.168.10.148:8080/music/Victoria.mp3");
//3.進入準備狀態
player.prepare();
//4.播放音樂
player.start();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY; //服務的啟動必須有明確的呼叫startService才能生效
//如果不返回這個模式,那麼在Service執行ondestroy方法之後還有執行oncreate方法導致
// 音樂還會播放
//使用這個之後,只有在activity startService之後才能播放
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onDestroy() {
super.onDestroy();
player.stop();
player.release();
}
}
public class MainActivity extends AppCompatActivity {
//MediaPlayer player;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//啟動服務
startService(new Intent(this,MyService.class));
}
AudioManager
AudioManager(音訊管理器),提供了音量控制與鈴聲模式相關操作。
常用方法:
- adjustStreamVolume 漸進式 (一點點增加或減少音量)
- etStreamVolume 直接設定(音量)
這裡介紹第一種模式
在MainActivty中新增音量的控制方法 增大減少、靜音和非靜音
public void voiceControll(View v){
//1.獲取聲音管理器
AudioManager manager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);
//2.操作
switch (v.getId()){
case R.id.up://增大音量
//引數1:聲音型別
// AudioManager.STREAM_ALARM 警報
// AudioManager.STREAM_MUSIC 媒體
// AudioManager.STREAM_NOTIFICATION 通知
// AudioManager.STREAM_RING 鈴聲
// AudioManager.STREAM_VOICE_CALL 通話
// AudioManager.STREAM_SYSTEM 系統
//引數2:調整方向,增加/減少
// AudioManager.ADJUST_RAISE
// AudioManager.ADJUST_LOWER
// AudioManager.ADJUST_SAME
//引數3:FLAG_PLAY_SOUND 播放聲音 FLAG_SHOW_UI 出現音量條 0什麼也沒有
manager.adjustStreamVolume(AudioManager.STREAM_MUSIC ,AudioManager.ADJUST_RAISE,AudioManager.FLAG_PLAY_SOUND);
break;
case R.id.down://減小音量
manager.adjustStreamVolume(AudioManager.STREAM_MUSIC ,AudioManager.ADJUST_LOWER,AudioManager.FLAG_SHOW_UI);
break;
case R.id.mute://靜音
//API:>=23 手機版本>6.0
//IlleagalArguementException:Bad deriction -100
//manager.adjustStreamVolume(AudioManager.STREAM_MUSIC ,AudioManager.ADJUST_MUTE,AudioManager.FLAG_SHOW_UI);
//API<23
manager.setStreamMute(AudioManager.STREAM_MUSIC,true);
break;
case R.id.unmute://還原
//manager.adjustStreamVolume(AudioManager.STREAM_MUSIC ,AudioManager.ADJUST_UNMUTE,AudioManager.FLAG_SHOW_UI);
manager.setStreamMute(AudioManager.STREAM_MUSIC,false);
break;
}
}
專案案例:
實現簡單的音樂播放器
實現:暫停、停止、下一首、上一首歌
未解決:關閉app後音樂也關閉了 奇怪!
第一部分:佈局檔案 activity_music.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.demo.android4step2.MusicActivity"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="▶"
android:id="@+id/playpause"
android:onClick="controll"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="■"
android:id="@+id/stop"
android:onClick="controll"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="◁"
android:id="@+id/last"
android:onClick="controll"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="▷"
android:id="@+id/next"
android:onClick="controll"/>
</LinearLayout>
<ProgressBar
android:id="@+id/music_pro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:layout_margin="5dp"/>
<ListView
android:id="@+id/music_view"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
</LinearLayout>
記得加讀取記憶體的許可權
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
AndroidMainfest.xml Application 標籤中 新增Service
<service
android:name=".MusicService"
android:enabled="true"
android:exported="true"></service>
MusicService.java
public class MusicService extends Service {
MediaPlayer player;
private static int index;
public MusicService() {
}
@Override
public void onCreate() {
super.onCreate();
player = new MediaPlayer();
//進度條變化
new Thread() {
@Override
public void run() {
super.run();
while (true) {
try {
sleep(200);
//每隔200毫秒更新進度條
if (player != null) {
MusicActivity.musicPro.setProgress(player.getCurrentPosition());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
/**
* 多次點選“播放音樂”按鈕,
* “onCreate()”方法只會在初始時呼叫一次
* “onStartCommand(Intent intent, int flags, int startId)”方法會在每次點選時都被呼叫
* 點選“停止音樂”按鈕,“onDestroy()”方法會被呼叫
* 當中,每次回撥onStartCommand()方法時,引數“startId”的值都是遞增的startId用於唯一標識每次對Service發起的處理請求
* 如果服務同時處理多個 onStartCommand() 請求,則不應在處理完一個啟動請求之後立即銷燬服務
* 因為此時可能已經收到了新的啟動請求,在第一個請求結束時停止服務會導致第二個請求被終止。
* 為了避免這一問題,可以使用 stopSelf(int) 確保服務停止請求始終基於最新一次的啟動請求。
* 也就是說,如果呼叫 stopSelf(int) 方法的引數值與onStartCommand()接受到的最新的startId值不相符的話
* stopSelf()方法就會失效,從而避免終止尚未處理的請求
*
* @param intent
* @param flags
* @param startId
* @return
*/
int isStarting = -1;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
/**
* 接收activity傳來的值
*/
index = intent.getIntExtra("index", 0);
String tag = intent.getStringExtra("tag");
if (tag.equals("play")) {
play(index);
isStarting = 1;
} else if (tag.equals("pause")) {
player.pause();
isStarting = 1;
} else if (tag.equals("start")) {
//如果是第一次開啟,點選播放 預設播放第一首歌
if (isStarting == -1) {
play(0);
} else {
player.start();
}
}
return START_NOT_STICKY; //服務的啟動必須有明確的呼叫startService才能生效
}
/**
* 根據傳進來的index 播放響應的歌曲
*
* @param index
*/
private void play(final int index) {
String path = MusicActivity.pathList.get(index);
try {
if (player.isPlaying()) {
//如果音樂正在播放,將音樂停掉
player.stop();
}
//重置 是Medie進入idel狀態
player.reset();
player.setDataSource(path);
player.prepare();
player.start();
player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
//一首歌曲播放完 自動播放下一首
int i = index + 1;
play(i);
}
});
//設定進度條最大值
MusicActivity.musicPro.setMax(player.getDuration());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
//釋放資源
@Override
public void onDestroy() {
super.onDestroy();
player.stop();
player.release();
isStarting = -1;
player = null;
}
}
MusicActivity.java
public class MusicActivity extends AppCompatActivity {
private ListView musicView;
//存放所有音樂曲目的名字
private ArrayList<String> nameList = new ArrayList<>();
//存放歌曲的路徑 讓Service 訪問
public static ArrayList<String> pathList = new ArrayList<>();
private ArrayAdapter adapter;
//當前播放的曲目
private int index = 0;
private Button playpause;
//進度條
public static ProgressBar musicPro;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music);
musicView = findViewById(R.id.music_view);
playpause = findViewById(R.id.playpause);
musicPro = findViewById(R.id.music_pro);
getMusic(this);
//建立adapter
//上下文,item佈局檔案,資料來源
adapter = new ArrayAdapter(this, android.R.layout.simple_expandable_list_item_1, nameList);
musicView.setAdapter(adapter);
musicView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
/**
* 點選列表將索引傳到service中 activity——>service傳值
*/
Intent intent = new Intent(MusicActivity.this, MusicService.class);
intent.putExtra("index", position);
//操作標識
intent.putExtra("tag", "play");
startService(intent);
//儲存當前播放歌曲的索引
index = position;
playpause.setText("||");
}
});
}
/**
* 獲取本地檔案的音樂檔案
*/
public void getMusic(Context context) {
//查詢媒體資料庫
ContentResolver resolver = context.getContentResolver();
/**
* Uri:這個Uri代表要查詢的資料庫名稱加上表的名稱。
這個Uri一般都直接從MediaStore裡取得,例如我要取所有歌的資訊,
就必須利用MediaStore.Audio.Media. EXTERNAL _CONTENT_URI這個Uri。
*
*/
Cursor cursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
//遍歷媒體資料庫
if (cursor.moveToFirst()) {
int i = 0;
while (!cursor.isAfterLast()) {
//歌曲編號
int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID));
//歌曲標題
String title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
//歌曲檔案的路徑MediaStore.Audio.Media.DATA
String url = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
//歌曲的歌手名MediaStore.Audio.Media.ARTIST
String artist = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
//歌曲檔案的大小MediaStore.Audio.Media.SIZE
long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE));
if (size > 1024 * 800) { //是否大於800K
if (title.equals("<unknown>") || title.equals("")) {
title = "未知";
}
if ("<unknown>".equals(artist) || "".equals(artist)) {
artist = "未知";
}
nameList.add(++i + "-" + title);
pathList.add(url);
}
cursor.moveToNext();
}
}
}
/**
* 音樂播放控制邏輯介面
*
* @param v
*/
public void controll(View v) {
switch (v.getId()) {
case R.id.playpause:
Intent it1 = new Intent(MusicActivity.this, MusicService.class);
if (playpause.getText().equals("▶")) {
//播放
it1.putExtra("tag", "start");
playpause.setText("||");
} else {
//暫停
it1.putExtra("tag", "pause");
playpause.setText("▶");
}
startService(it1);
break;
case R.id.stop:
Intent it2 = new Intent(MusicActivity.this, MusicService.class);
//停止service 執行 Service 的onDestroy方法
stopService(it2);
playpause.setText("▶");
break;
case R.id.last:
if (index == 0) {//如果已經到了第一首,那麼再次點選上一首到末尾
index = pathList.size() - 1;
} else {
index--;
}
Intent it3 = new Intent(MusicActivity.this, MusicService.class);
it3.putExtra("index", index);
it3.putExtra("tag", "play");
startService(it3);
playpause.setText("||");
break;
case R.id.next:
if (index == pathList.size() - 1) {
index = 0;
} else {
index++;
}
Intent it4 = new Intent(MusicActivity.this, MusicService.class);
it4.putExtra("index", index);
it4.putExtra("tag", "play");
startService(it4);
playpause.setText("||");
break;
}
}
}