Android 下音频播放<一> MediaPlayer
2014-09-25 16:57
429 查看
最近在Android游戏开发中,需要处理WAV和OGG的播放。其中背景音乐需求为一路WAV或者OGG。在Android.media Package中。选中了MediaPlayer作为背景音乐的播放。
0. 简介:
android.media.MediaPlayer class用来控制播放Audio/Video 文件和流。(不光是文件,还支持流)
1. Playback状态机:
可以将播放文件和流看作状态机管理。下图详细描述了各状态以及如何在各状态间转移。
注:单箭头表示同步调用。双箭头表示异步调用。
![](http://s5.sinaimg.cn/middle/602f8770hbc32d76254f4&690)
现在详细分析每个状态以及状态间转移。
1.1:Idle状态:
当MediaPlayer object刚被使用new 创建,或者调用reset()后,此MediaPlayer object处于Idle状态。
两种方法进入Idle状态,有个细小却重要的区别:当程序在Idle状态错误的调用了类似getCurrentPosition(),GetDuration(),getVideoight(),setAudioStreamType(), setLooping(),setVolume(),pause(),start(),stop(),seekTo(),prepare()等方法。
如果MediaPlayer object刚被new 出来,处于idle状态。用户使用OnErrorListener.onError()注册的callback函数不会被内在palyer Engine调用,且状态不会发生改变,继续为idle.
如果是reset() 后处于idle状态,则注册的callback函数被调用。且MediaPlayer object会转为Error状态。
1.2:End状态:
当MediaPlayer object调用release() 后,它处于End 状态。
如果MediaPlayer object不再使用时,请立即调用release(),这样,内部player engine可以释放掉他们所占用的资源。 这类资源,不光包括内存资源,还包括Audio,Video硬件解码设备。
当用户进入End状态后,没有任何渠道去转回其它状态。
1.3:Initialized状态:
当MediaPlayer Object为Idle状态时,调用setDataSource(FileDescriptor), or setDataSource(String), or setDataSource(Context, Uri), or setDataSource(FileDescriptor, long, long)转变其状态到Initialized状态。
如果setDataSource()在非Idle状态调用, IllegalStateException会被抛出。
所以,setDataSource()时必须捕获IllegalArgumentException and IOException异常。
1.4:Prepared状态:
MediaPlayer Object 必须首先进入Prepared状态,之后才能开始start.
有两种方式进入Prepared状态:
调用prepare() 则以同步方式进入Prepared状态。当prepare()返回时,MediaPlayer Object 已经进入Prepared状态。
调用prepareAsync()则以异步方式进入Prepared状态。
当在错误状态调用prepare() or prepareAsync() ,则抛出异常IllegalStateException。
1.5: Started状态:
要播放,则需要调用start(),当start()返回successfully时,MediaPlayer Object则进入Started状态。
isPlaying()可以测试是否位于Started 状态。
在MediaPlayer Object处于Started状态时,调用start()没有任何效果。
1.6:Paused状态:
调用pause(),当其返回时,MediaoPlayer Object进入Paused状态。从started状态到pasued状态,Play Engine有缺陷。所以导致isPlaying()...
MediaPlayer进入Paused状态后,调用start()重新开始播放(Resume).但Position是从刚才暂停处开始播放,而不是从头。且状态变化为Started。
在Paused状态,再次调用pause()没有反应 。
1.7 Stop状态:
在Started,Paused,Prepared或者PlaybackCompleted状态,调用stop(),则进入Stopped状态。
在Stopped状态,MediaPlayer Object不能再次调用start()去播放,除非调用prepare()或prepareAsync()使其进入Prepared状态。
在Stopped状态下再次调用stop.无效。
1.8 PlaybackCompleted状态:
当Video/Audio播放完毕后,进入PlaybackCompleted模式。
但如果使用setLooping(true),则播放完毕后,立刻重新播放。且状态继续为started.
如果setLooping(false).播放完毕后,MediaPlayer Object进入PlaybackCompleted模式,并调用setOnCompletionListener(OnCompletionListener)注册的callback函数。
如在 PlaybackCompleted状态下,调用start(),会进入started模式,并从头播放。
2. 回调机制:
MediaPlayer使用一些方法来设置回调函数,当发生某种特定情况时,内部palyer Engine会调用之。
使用方法,可以在稍后的例子中看到。
3. 播放指针:
可以调用 seekTo()来调整当前播放指针。
下表列出了各种方法在哪些状态下调用合法,在哪些状态下非法以及导致的后果:
例子程序如下:
package com.Android.AudioPlayer;
import android.media.MediaPlayer;
import android.util.Log;
import java.io.IOException;
import java.lang.String;
public class MusicPlayer {
static public int Object_Num = 0;
static String TAG = "MusicPlayer";
static MediaPlayer mMP;
static int seekend = 0;
//0: idle. 1: Initialized 2: Prepared 3: Started 4:Paused
5:Stoped 6:PlaybackComplete
static int mode = -1; // -1: End
private static class BufferUpdateListener implements MediaPlayer.OnBufferingUpdateListener
{
public void onBufferingUpdate(MediaPlayer mp, int percent)
{
Log.w(TAG, "updateBuffer.");
return;
}
}
private static class CompletionListener implements MediaPlayer.OnCompletionListener
{
public void onCompletion(MediaPlayer mp)
{
Log.w(TAG, "Mediaplay completion");
return ;
}
}
private static class ErrorListener implements MediaPlayer.OnErrorListener
{
public boolean onError(MediaPlayer mp, int what, int extra)
{
Log.w(TAG, "Mediaplay Error");
return false;
}
}
private static class InfoListener implements MediaPlayer.OnInfoListener
{
public boolean onInfo(MediaPlayer mp, int what, int extra)
{
Log.w(TAG, String.format("MediaPlayer Info:[%d]", what));
return false;
}
}
private static class PreparedListener implements MediaPlayer.OnPreparedListener
{
public void onPrepared(MediaPlayer mp)
{
Log.w(TAG, "Mediaplay onPrepared");
return;
}
}
private static class SeekCompleteListener implements MediaPlayer.OnSeekCompleteListener
{
public void onSeekComplete(MediaPlayer mp)
{
Log.w(TAG, "Mediaplay SeekComplete");
seekend = 1;
return;
}
}
public void LoadMusic(String MusicFile)
{
if(Object_Num > 0)
{
unLoadMusic();
}
MediaPlayer mp = new MediaPlayer();
mode = 0; //idle
// set
mp.setOnBufferingUpdateListener(new BufferUpdateListener());
mp.setOnCompletionListener(new CompletionListener());
mp.setOnErrorListener(new ErrorListener());
mp.setOnPreparedListener(new PreparedListener());
mp.setOnSeekCompleteListener(new SeekCompleteListener());
mp.setOnInfoListener(new InfoListener());
//int id = mp.getAudioSessionId();
int position = mp.getCurrentPosition();
Log.w(TAG, String.format("Create MediaPlayer. Position :[%d]", position));
mMP = mp;
try {
mp.setDataSource(MusicFile);
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
Log.w(TAG, "setDataSource:IllegalArgumentException");
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
Log.w(TAG, "setDataSource:IllegalStateException");
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
Log.w(TAG, "setDataSource:IOException");
e.printStackTrace();
}
mode = 1; //Initialized
try {
mp.prepare();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
Log.w(TAG, "prepare:IllegalStateException");
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
Log.w(TAG, "prepare:IOException");
e.printStackTrace();
}
mode = 2; //prepared
Object_Num++;
return;
}
public void unLoadMusic()
{
if(Object_Num == 1)
{
mMP.stop();
mMP.release();
mode = -1; //end
Object_Num--;
}
return;
}
public void PlayMusic(boolean looping)
{
if(Object_Num == 1)
{
//Prepared Started Paused PlaybackComplete
if(mode == 2 || mode == 4 || mode == 6 || mode == 3)
{
mMP.seekTo(0);
mMP.setLooping(looping);
mMP.start();
mode = 3;
}
}
return;
}
public void StopMusic()
{
if(Object_Num == 1)
{
// started or paused
if(mode == 3 || mode == 4)
{
mMP.pause();
mMP.seekTo(0);
mode = 4;
}
}
return;
}
public void PauseMusic()
{
if(Object_Num == 1)
{
if(mode == 3 || mode == 4)
mMP.pause();
}
return;
}
public void ResumeMusic()
{
if(Object_Num == 1)
{
if(mode == 4)
mMP.start();
}
return;
}
}
MediaPlayer所支持格式:
这个问题,看到网上有不少人回答。但个人觉得回答并不正确。
MediaPlayer底层具体实现决定了所支持格式。毕竟JAVA层不太可能直接操作硬件去播放Video/Audio. 它也是通过JNI与底层C打交道播放Audio/Video. 那么底层的实现才是决定MediaPlayer支持格式的关键。
例如:Sam使用Hi3716C来测试,发现其底层是用HiPlayer来实现,支持WAV,OGG等。
但MTK5502平台,则只支持WAV.(可能OGG没来的及加入)
0. 简介:
android.media.MediaPlayer class用来控制播放Audio/Video 文件和流。(不光是文件,还支持流)
1. Playback状态机:
可以将播放文件和流看作状态机管理。下图详细描述了各状态以及如何在各状态间转移。
注:单箭头表示同步调用。双箭头表示异步调用。
现在详细分析每个状态以及状态间转移。
1.1:Idle状态:
当MediaPlayer object刚被使用new 创建,或者调用reset()后,此MediaPlayer object处于Idle状态。
两种方法进入Idle状态,有个细小却重要的区别:当程序在Idle状态错误的调用了类似getCurrentPosition(),GetDuration(),getVideoight(),setAudioStreamType(), setLooping(),setVolume(),pause(),start(),stop(),seekTo(),prepare()等方法。
如果MediaPlayer object刚被new 出来,处于idle状态。用户使用OnErrorListener.onError()注册的callback函数不会被内在palyer Engine调用,且状态不会发生改变,继续为idle.
如果是reset() 后处于idle状态,则注册的callback函数被调用。且MediaPlayer object会转为Error状态。
1.2:End状态:
当MediaPlayer object调用release() 后,它处于End 状态。
如果MediaPlayer object不再使用时,请立即调用release(),这样,内部player engine可以释放掉他们所占用的资源。 这类资源,不光包括内存资源,还包括Audio,Video硬件解码设备。
当用户进入End状态后,没有任何渠道去转回其它状态。
1.3:Initialized状态:
当MediaPlayer Object为Idle状态时,调用setDataSource(FileDescriptor), or setDataSource(String), or setDataSource(Context, Uri), or setDataSource(FileDescriptor, long, long)转变其状态到Initialized状态。
如果setDataSource()在非Idle状态调用, IllegalStateException会被抛出。
所以,setDataSource()时必须捕获IllegalArgumentException and IOException异常。
1.4:Prepared状态:
MediaPlayer Object 必须首先进入Prepared状态,之后才能开始start.
有两种方式进入Prepared状态:
调用prepare() 则以同步方式进入Prepared状态。当prepare()返回时,MediaPlayer Object 已经进入Prepared状态。
调用prepareAsync()则以异步方式进入Prepared状态。
当在错误状态调用prepare() or prepareAsync() ,则抛出异常IllegalStateException。
1.5: Started状态:
要播放,则需要调用start(),当start()返回successfully时,MediaPlayer Object则进入Started状态。
isPlaying()可以测试是否位于Started 状态。
在MediaPlayer Object处于Started状态时,调用start()没有任何效果。
1.6:Paused状态:
调用pause(),当其返回时,MediaoPlayer Object进入Paused状态。从started状态到pasued状态,Play Engine有缺陷。所以导致isPlaying()...
MediaPlayer进入Paused状态后,调用start()重新开始播放(Resume).但Position是从刚才暂停处开始播放,而不是从头。且状态变化为Started。
在Paused状态,再次调用pause()没有反应 。
1.7 Stop状态:
在Started,Paused,Prepared或者PlaybackCompleted状态,调用stop(),则进入Stopped状态。
在Stopped状态,MediaPlayer Object不能再次调用start()去播放,除非调用prepare()或prepareAsync()使其进入Prepared状态。
在Stopped状态下再次调用stop.无效。
1.8 PlaybackCompleted状态:
当Video/Audio播放完毕后,进入PlaybackCompleted模式。
但如果使用setLooping(true),则播放完毕后,立刻重新播放。且状态继续为started.
如果setLooping(false).播放完毕后,MediaPlayer Object进入PlaybackCompleted模式,并调用setOnCompletionListener(OnCompletionListener)注册的callback函数。
如在 PlaybackCompleted状态下,调用start(),会进入started模式,并从头播放。
2. 回调机制:
MediaPlayer使用一些方法来设置回调函数,当发生某种特定情况时,内部palyer Engine会调用之。
使用方法,可以在稍后的例子中看到。
3. 播放指针:
可以调用 seekTo()来调整当前播放指针。
下表列出了各种方法在哪些状态下调用合法,在哪些状态下非法以及导致的后果:
Valid and invalid states
Method Name | Valid Sates | Invalid States | Comments |
attachAuxEffect | {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Idle, Error} | This method must be called after setDataSource. Calling it does not change the object state. |
getAudioSessionId | any | {} | This method can be called in any state and calling it does not change the object state. |
getCurrentPosition | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. |
getDuration | {Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Idle, Initialized, Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. |
getVideoHeight | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. |
getVideoWidth | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. |
isPlaying | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. |
pause | {Started, Paused} | {Idle, Initialized, Prepared, Stopped, PlaybackCompleted, Error} | Successful invoke of this method in a valid state transfers the object to thePaused state. Calling this method in an invalid state transfers the object to the Error state. |
prepare | {Initialized, Stopped} | {Idle, Prepared, Started, Paused, PlaybackCompleted, Error} | Successful invoke of this method in a valid state transfers the object to thePrepared state. Calling this method in an invalid state throws an IllegalStateException. |
prepareAsync | {Initialized, Stopped} | {Idle, Prepared, Started, Paused, PlaybackCompleted, Error} | Successful invoke of this method in a valid state transfers the object to thePreparing state. Calling this method in an invalid state throws an IllegalStateException. |
release | any | {} | After release(), the object is no longer available. |
reset | {Idle, Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} | {} | After reset(), the object is like being just created. |
seekTo | {Prepared, Started, Paused, PlaybackCompleted} | {Idle, Initialized, Stopped, Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. |
setAudioSessionId | {Idle} | {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} | This method must be called in idle state as the audio session ID must be known before calling setDataSource. Calling it does not change the object state. |
setAudioStreamType | {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} | {Error} | Successful invoke of this method does not change the state. In order for the target audio stream type to become effective, this method must be called before prepare() or prepareAsync(). |
setAuxEffectSendLevel | any | {} | Calling this method does not change the object state. |
setDataSource | {Idle} | {Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted, Error} | Successful invoke of this method in a valid state transfers the object to theInitialized state. Calling this method in an invalid state throws an IllegalStateException. |
setDisplay | any | {} | This method can be called in any state and calling it does not change the object state. |
setSurface | any | {} | This method can be called in any state and calling it does not change the object state. |
setLooping | {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} | {Error} | Successful invoke of this method in a valid state does not change the state. Calling this method in an invalid state transfers the object to the Error state. |
isLooping | any | {} | This method can be called in any state and calling it does not change the object state. |
setOnBufferingUpdateListener | any | {} | This method can be called in any state and calling it does not change the object state. |
setOnCompletionListener | any | {} | This method can be called in any state and calling it does not change the object state. |
setOnErrorListener | any | {} | This method can be called in any state and calling it does not change the object state. |
setOnPreparedListener | any | {} | This method can be called in any state and calling it does not change the object state. |
setOnSeekCompleteListener | any | {} | This method can be called in any state and calling it does not change the object state. |
setScreenOnWhilePlaying | any | {} | This method can be called in any state and calling it does not change the object state. |
setVolume | {Idle, Initialized, Stopped, Prepared, Started, Paused, PlaybackCompleted} | {Error} | Successful invoke of this method does not change the state. |
setWakeMode | any | {} | This method can be called in any state and calling it does not change the object state. |
start | {Prepared, Started, Paused, PlaybackCompleted} | {Idle, Initialized, Stopped, Error} | Successful invoke of this method in a valid state transfers the object to theStarted state. Calling this method in an invalid state transfers the object to the Error state. |
stop | {Prepared, Started, Stopped, Paused, PlaybackCompleted} | {Idle, Initialized, Error} | Successful invoke of this method in a valid state transfers the object to theStopped state. Calling this method in an invalid state transfers the object to the Error state. |
package com.Android.AudioPlayer;
import android.media.MediaPlayer;
import android.util.Log;
import java.io.IOException;
import java.lang.String;
public class MusicPlayer {
static public int Object_Num = 0;
static String TAG = "MusicPlayer";
static MediaPlayer mMP;
static int seekend = 0;
//0: idle. 1: Initialized 2: Prepared 3: Started 4:Paused
5:Stoped 6:PlaybackComplete
static int mode = -1; // -1: End
private static class BufferUpdateListener implements MediaPlayer.OnBufferingUpdateListener
{
public void onBufferingUpdate(MediaPlayer mp, int percent)
{
Log.w(TAG, "updateBuffer.");
return;
}
}
private static class CompletionListener implements MediaPlayer.OnCompletionListener
{
public void onCompletion(MediaPlayer mp)
{
Log.w(TAG, "Mediaplay completion");
return ;
}
}
private static class ErrorListener implements MediaPlayer.OnErrorListener
{
public boolean onError(MediaPlayer mp, int what, int extra)
{
Log.w(TAG, "Mediaplay Error");
return false;
}
}
private static class InfoListener implements MediaPlayer.OnInfoListener
{
public boolean onInfo(MediaPlayer mp, int what, int extra)
{
Log.w(TAG, String.format("MediaPlayer Info:[%d]", what));
return false;
}
}
private static class PreparedListener implements MediaPlayer.OnPreparedListener
{
public void onPrepared(MediaPlayer mp)
{
Log.w(TAG, "Mediaplay onPrepared");
return;
}
}
private static class SeekCompleteListener implements MediaPlayer.OnSeekCompleteListener
{
public void onSeekComplete(MediaPlayer mp)
{
Log.w(TAG, "Mediaplay SeekComplete");
seekend = 1;
return;
}
}
public void LoadMusic(String MusicFile)
{
if(Object_Num > 0)
{
unLoadMusic();
}
MediaPlayer mp = new MediaPlayer();
mode = 0; //idle
// set
mp.setOnBufferingUpdateListener(new BufferUpdateListener());
mp.setOnCompletionListener(new CompletionListener());
mp.setOnErrorListener(new ErrorListener());
mp.setOnPreparedListener(new PreparedListener());
mp.setOnSeekCompleteListener(new SeekCompleteListener());
mp.setOnInfoListener(new InfoListener());
//int id = mp.getAudioSessionId();
int position = mp.getCurrentPosition();
Log.w(TAG, String.format("Create MediaPlayer. Position :[%d]", position));
mMP = mp;
try {
mp.setDataSource(MusicFile);
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
Log.w(TAG, "setDataSource:IllegalArgumentException");
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
Log.w(TAG, "setDataSource:IllegalStateException");
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
Log.w(TAG, "setDataSource:IOException");
e.printStackTrace();
}
mode = 1; //Initialized
try {
mp.prepare();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
Log.w(TAG, "prepare:IllegalStateException");
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
Log.w(TAG, "prepare:IOException");
e.printStackTrace();
}
mode = 2; //prepared
Object_Num++;
return;
}
public void unLoadMusic()
{
if(Object_Num == 1)
{
mMP.stop();
mMP.release();
mode = -1; //end
Object_Num--;
}
return;
}
public void PlayMusic(boolean looping)
{
if(Object_Num == 1)
{
//Prepared Started Paused PlaybackComplete
if(mode == 2 || mode == 4 || mode == 6 || mode == 3)
{
mMP.seekTo(0);
mMP.setLooping(looping);
mMP.start();
mode = 3;
}
}
return;
}
public void StopMusic()
{
if(Object_Num == 1)
{
// started or paused
if(mode == 3 || mode == 4)
{
mMP.pause();
mMP.seekTo(0);
mode = 4;
}
}
return;
}
public void PauseMusic()
{
if(Object_Num == 1)
{
if(mode == 3 || mode == 4)
mMP.pause();
}
return;
}
public void ResumeMusic()
{
if(Object_Num == 1)
{
if(mode == 4)
mMP.start();
}
return;
}
}
MediaPlayer所支持格式:
这个问题,看到网上有不少人回答。但个人觉得回答并不正确。
MediaPlayer底层具体实现决定了所支持格式。毕竟JAVA层不太可能直接操作硬件去播放Video/Audio. 它也是通过JNI与底层C打交道播放Audio/Video. 那么底层的实现才是决定MediaPlayer支持格式的关键。
例如:Sam使用Hi3716C来测试,发现其底层是用HiPlayer来实现,支持WAV,OGG等。
但MTK5502平台,则只支持WAV.(可能OGG没来的及加入)
相关文章推荐
- 从零开始学android<Mediaplayer播放器组件(播放音频).四十八.>
- 【Android开发学习45】使用google语音识别引擎(Google Speech API)<一>
- android Fragment实现<一>
- Android开发工具——ADB(Android Debug Bridge) <一>概览
- 【Qt编程】基于Qt的词典开发系列<十三>音频播放
- Android 4.2 Input Event事件处理流程<一>事情派发
- Android 4.2 Input Event事件处理流程<一>---应用注册
- 深度分析:Android4.3下MMS发送到附件为音频文件(音频为系统内置音频)的彩信给自己,添加音频-发送彩信-接收彩信-下载音频附件-预览-播放(三,接收彩信<1,接收短信>)
- Android: Git/Gerrit/Repo 的使用 <一>
- 深度分析:Android4.3下MMS发送到附件为音频文件(音频为系统内置音频)的彩信给自己,添加音频-发送彩信-接收彩信-下载音频附件-预览-播放(三,接收彩信<1,接收短信>)
- cocos2d-x基础<一> Android环境配置和HelloWorld的运行
- Android UI设计之<一>使用ImageView实现ProgressBar旋转效果
- Android 新API 之 MediaCodec使用笔记 <一>
- android 数据存储<一>----android短信发送器之文件的读写(手机+SD卡)
- 【android初学日志】Conversion to Dalvik format failed: Unable to execute dex: java.nio.BufferOverf<一>
- Android中自定义对话框(Dialog) <一>
- 深度分析:Android4.3下MMS发送到附件为音频文件(音频为系统内置音频)的彩信给自己,添加音频-发送彩信-接收彩信-下载音频附件-预览-播放(二,发送彩信<2>)
- 深度分析:Android4.3下MMS发送到附件为音频文件(音频为系统内置音频)的彩信给自己,添加音频-发送彩信-接收彩信-下载音频附件-预览-播放(二,发送彩信<1>)
- 深度分析:Android4.3下MMS发送到附件为音频文件(音频为系统内置音频)的彩信给自己,添加音频-发送彩信-接收彩信-下载音频附件-预览-播放(二,发送彩信<1>)
- 深度分析:Android4.3下MMS发送到附件为音频文件(音频为系统内置音频)的彩信给自己,添加音频-发送彩信-接收彩信-下载音频附件-预览-播放(三,接收彩信<2,下载彩信>)