Android多媒体开发系列文章
一、什么是多媒体
多媒体(duō méi tǐ) 的英文单词是Multimedia,它由media和multi两部分组成。一般理解为多种媒体的综合
多媒体是计算机和视频技术的结合,实际上它是两个媒体;声音和图像,或者用现在的术语:音响和电视。
多媒体(Multimedia),在计算机系统中,组合两种或两种以上媒体的一种人机交互式信息交流和传播媒体
使用的媒体包括文字、图片、照片、声音 (包含音乐、语音旁白、特殊音效)、动画和影片,以及程式所提供的互动功能
多媒体是超媒体(Hypermedia)系统中的一个子集,而超媒体系统是使用超链接 (Hyperlink)构成的全球信息系统
全球信息系统是因特网上使用 TCP/IP 协议和 UDP/IP 协议
二、音乐播放器
Android 官方提供了MediaPlayer 核心类,用于播放音乐,其状态流程如下图所示。MediaPlayer 必须严格按照状态图操作,否则就会出现错误,这些错误都是底层抛出,严格按照状态图操作的话一般就不会出问题。
MediaPlayer,原生的API,可以播放音视频,但是支持的格式比较少,实际开发中用的比较少,但是还是很有必要学习,熟悉API,因为Vitamio框架的API大部分跟原生的API是一样的
1、MediaPlayer使用流程图
2、MediaPlayer核心方法
方法 | 说明 |
---|---|
create() | 播放本地res/raw/目录下的资源 |
reset() | 重置为初始状态 |
setAudioStreamType() | 设置音乐格式,例如:AudioManager.STREAM_MUSIC |
setDataSource() | 设置音频源,本地网络资源均可 |
prepare() | 播放前的准备工作 |
prepareAsync() | 异步进行准备工作,播放网络音频的时候使用 |
start() | 开始或恢复播放 |
pause() | 暂停播放 |
stop() | 停止播放 |
release() | 释放资源 |
getDuration() | 获取音乐最大长度(毫秒单位) |
getCurrentPosition() | 获取当前的播放进度 |
seekTo() | 拖拽进度 |
setDisplay() | 设置输出画面 |
setOnPreparedListener() | 设置准备监听 |
为了演示MediaPlayer 的使用,我们需要提前准备一个mp3 文件放到sdcard 中
需求:制作一个播放器,能够播放/暂停/停止音乐文件,并且添加一个SeekBar(可以拖拽的ProgressBar),当音乐播放时SeekBar 也会不断的跟新当前的进度,当用户拖动SeekBar 时可以更改播放的进度
布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="play"
android:text="播放"/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="pause"
android:text="暂停"/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="stop"
android:text="停止"/>
</LinearLayout>
<SeekBar
android:id="@+id/sb"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
代码实现
public class MainActivity extends Activity implements OnSeekBarChangeListener {
private SeekBar sb;
private MediaPlayer player;
private int duration;
// 播放器的几个状态
private static final int PLAYING = 1;// 播放状态
private static final int PAUSING = 2;// 暂停状态
private static final int STOPPING = 3;// 停止状态
private volatile int CURRENT = 0;// 当前状态
private Timer timer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sb = (SeekBar) findViewById(R.id.sb);
//设置拖动监听
sb.setOnSeekBarChangeListener(this);
}
//播放
public void play(View view) {
if (player != null) {
if (CURRENT == PLAYING) {
Toast.makeText(this, "音乐已经在播放了", Toast.LENGTH_SHORT).show();
return;
} else if (CURRENT == PAUSING) {
player.start();
CURRENT = PLAYING;
return;
}
}
try {
//创建一个播放器对象
player = new MediaPlayer();
//给播放器设置音乐路径
player.setDataSource("/mnt/sdcard/test.mp3");
//设置音乐格式
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
//准备
player.prepare();
//获取音乐最大长度(毫秒单位)
duration = player.getDuration();
//给SeekBar 设置最大值
sb.setMax(duration);
//音乐开始播放
player.start();
//设置当前的状态为播放
CURRENT = PLAYING;
if (timer == null) {
//创建定时器
timer = new Timer();
}
/**
* 参数1:匿名内部类,相当于Runnable 类
* 参数2:第一次延时多长时间(毫秒)后执行,0 则代表立即执行
* 参数3:每隔多长时间(毫秒)执行一次
*/
timer.schedule(new TimerTask() {
@Override
public void run() {//该方法每1 秒被调用一次
if (CURRENT == PLAYING) {
runOnUiThread(new Runnable() {
@Override
public void run() {
//双重判断,尽可能避免线程问题,因为该段代码时在主线程中的,
//第一次判断是在子线程中进行的
if (player != null && CURRENT == PLAYING) {
//获取当前的播放进度
int currentPosition = player.getCurrentPosition();
//设置给SeekBar
sb.setProgress(currentPosition);
}
}
});
}
}
}, 0, 1000);
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "音乐播放失败" + e, 0).show();
}
}
/**
* 暂停
*/
public void pause(View view) {
if (player != null && CURRENT == PLAYING) {
player.pause();
CURRENT = PAUSING;
}
}
/**
* 停止
*/
public void stop(View view) {
if (player != null) {
if (CURRENT == PLAYING || CURRENT == PAUSING) {
CURRENT = STOPPING;
//取消定时器
timer.cancel();
timer = null;
player.stop();
player.reset();
player.release();
player = null;
sb.setProgress(0);
}
}
}
/*
* 拖动过程中回调多次
*/
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (player == null) {
sb.setProgress(0);
} else {
player.seekTo(progress);
}
}
/*
* 开始拖动前回调一次
*/
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (player == null) {
Toast.makeText(this, "音乐播放器还未开始", Toast.LENGTH_SHORT).show();
}
}
/*
* 结束拖动后回调一次
*/
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
@Override
protected void onDestroy() {
super.onDestroy();
stop(null);
}
}
3、播放本地res/raw/目录下的资源
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you
4、播放本地URI资源
Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
5、播放网络资源
String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long! (for buffering, etc)
mediaPlayer.start();
6、异步准备
String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
}
});
7、在后台Service异步执行播放任务
public class MyService extends Service implements MediaPlayer.OnPreparedListener {
MediaPlayer mMediaPlayer;
private static final String ACTION_PLAY = "com.example.action.PLAY";
MediaPlayer mMediaPlayer = null;
public int onStartCommand(Intent intent, int flags, int startId) {
...
if (intent.getAction().equals(ACTION_PLAY)) {
mMediaPlayer = ... // initialize it here
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.prepareAsync(); // prepare async to not block main thread
}
}
/** Called when MediaPlayer is ready */
public void onPrepared(MediaPlayer player) {
player.start();
}
}
public void initMediaPlayer() {
// ...initialize the MediaPlayer here...
mMediaPlayer.setOnErrorListener(this);
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// ... react appropriately ...
// The MediaPlayer has moved to the Error state, must be reset!
}
8、在手机睡眠时使用唤醒锁
mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
//wifi锁
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");
wifiLock.acquire();
//当暂停,不再需要网络时释放锁
wifiLock.release();
9、在前台服务运行播放任务
String songName;
// assign the song name to songName
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
new Intent(getApplicationContext(), MainActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification();
notification.tickerText = text;
notification.icon = R.drawable.play0;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",
"Playing: " + songName, pi);
startForeground(NOTIFICATION_ID, notification);
10、处理音频焦点
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// could not get audio focus.
}
requestAudioFocus() 方法的第一个参数是一个 AudioManager.OnAudioFocusChangeListener,当任何时候音频焦点发生变化的时候,会回调 OnAudioFocusChangeListener的onAudioFocusChange()方法
class MyService extends Service
implements AudioManager.OnAudioFocusChangeListener {
// ....
public void onAudioFocusChange(int focusChange) {
// Do something based on focus change...
}
}
11、播放完毕时手动释放资源
public class MyService extends Service {
MediaPlayer mMediaPlayer;
// ...
@Override
public void onDestroy() {
if (mMediaPlayer != null) mMediaPlayer.release();
}
}
三、Mp3文件简介
ID3
一般是位于一个mp3文件的开头或末尾的若干字节内,附加了关于该mp3的歌手,标题,专辑名称,年代,风格等信息,该信息就被称为ID3信息,ID3信息分为两个版本,v1和v2版。 其中:v1版的ID3在mp3文件的末尾128字节,以TAG三个字符开头,后面跟上歌曲信息。 v2版一般位于mp3的开头,可以存储歌词,该专辑的图片等大容量的信息。
V1与V2
- ID3V1记录在MP3文件的末尾,长度固定
- ID3V2就记录在MP3文件的首部。 ID3V2一共有4个版本,但流行的播放软件一般只支持第3版,既ID3v2.3。
- 对ID3V2的操作比ID3V1要慢。而且ID3V2结构比ID3V1的结构要复杂得多,但比ID3V1全面且可以伸缩和扩展。
四、视频播放器
1、SurfaceView
SurfaceView提供了一个绘画的界面,你可以控制该界面的格式和大小,SurfaceView 负责在屏幕正确位置安置一个界面。SurfaceView的其中一个目的,是为了在子线程渲染屏幕,但需要注意几点
- 所有SurfaceView 和SurfaceHolder.Callback 的方法,都必须在主线程调用
- 必须保证绘制线程,必须在SurfaceView 有效的情况下才能使用,也就是在SurfaceHolder.Callback.surfaceCreated() 和SurfaceHolder.Callback.surfaceDestroyed()之间调用
SurfaceView是View的子类,等同于TextView、ImageView等一系列控件。核心功能可以通过子线程进行界面的绘制,绘制需要注意的内容:
所有SurfaceView和SurfaceHolder.Callback的方法都应该在UI线程里调用,一般来说就是应用程序主线程。渲染线程所要访问的各种变量应该作同步处理。
由于surface可能被销毁,它只在SurfaceHolder.Callback.surfaceCreated()和 SurfaceHolder.Callback.surfaceDestroyed()之间有效,所以要确保渲染线程访问的是合法有效的surface
- 双缓冲技术:内存中有两个画布,A画布显示至屏幕,B画布在内存中绘制下一帧画面,绘制完毕后B显示至屏幕,A在内存中继续绘制下一帧画面
- SurfaceView 是双缓冲机制,一个用来缓冲数据,另一个用来展现数据,可以提高数据展示的速度,单缓冲需要先加载数据再去展示数据,但是双缓冲在展示第一个页面数据的时候已经把第二个页面的数据加载好了
- 对画面的实时更新要求较高,重量级组件,可见时才创建
- SurfaceView一旦不可见,就会被销毁,一旦可见,就会被创建,销毁时停止播放,再次创建时再开始播放
- 播放视频也是用MediaPlayer,不过跟音频不同,要设置显示在哪个SurfaceView
2、使用MediaPlayer+SurfaceView 播放视频
在该节中,视频播放依然使用MediaPlayer 类,为了方便演示,我们直接使用本文中创建的工程,只需在布局文件添加SurfaceView 控件即可
布局文件
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/sv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
实现代码
public class MainActivity extends Activity {
private MediaPlayer player;
static int currentPosition;
private SurfaceView sv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sv = (SurfaceView) findViewById(R.id.sv);
//拿到surfaceview的控制器
final SurfaceHolder sh = sv.getHolder();
// Thread t = new Thread(){
// @Override
// public void run() {
// try {
// sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// runOnUiThread(new Runnable() {
// @Override
// public void run() {
// MediaPlayer player = new MediaPlayer();
// player.reset();
// try {
// player.setDataSource("sdcard/2.3gp");
// player.setDisplay(sh);
// player.prepare();
// player.start();
// } catch (Exception e) {
// e.printStackTrace();
// }
//
// }
// });
//
// }
// };
// t.start();
//SurfaceView是重量级组件,可见时才会创建
//给SurfaceHolder设置CallBack,类似于侦听,可以知道SurfaceView的状态
sh.addCallback(new Callback() {
//surfaceView销毁时调用
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//每次surfaceview销毁时,同时停止播放视频
if(player != null){
currentPosition = player.getCurrentPosition();
player.stop();
player.release();
player = null;
}
}
//surfaceView创建时调用
@Override
public void surfaceCreated(SurfaceHolder holder) {
//每次surfaceView创建时才去播放视频
if(player == null){
player = new MediaPlayer();
player.reset();
try {
player.setDataSource("sdcard/2.3gp");
player.setDisplay(sh);
player.prepare();
player.start();
player.seekTo(currentPosition);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//surfaceView结构改变时调用
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
});
}
}
使用VideoView 播放视频
VideoView 跟MediaPlayer 相比播放视频步骤要简单的多,因为VideoView 原生提供了播放,暂停、快进、快退、进度条等方法。使用起来要方便的很多
1、设置布局文件,布局文件比较简单,因此这里只给你VideoView 标签
<VideoView
android:id="@+id/vv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
2、设置VideoView 的播放文件路径和媒体控制器,调用start 方法即可播放媒体文件
//实例化VideoView 对象
vv = (VideoView) findViewById(R.id.vv);
//从界面获取播放路径
et_path = (EditText) findViewById(R.id.et_path);
//给VideoView 设置视频路径
vv.setVideoPath(et_path.getText().toString());
//设置VideoView 控制器,我们当前类实现了MediaPlayerControl 接口
vv.setMediaController(new MediaController(this));
//开始播放
vv.start();
//设置当前播放器窗口设置为焦点
vv.requestFocus();
3、覆写MediaPlayerControl 接口中的抽象方法
@Override
public void start() {
}
@Override
public void pause() {
}
@Override
public int getDuration() {
return 0;
}
@Override
public int getCurrentPosition() {
return 0;
}
@Override
public void seekTo(int pos) {
}
@Override
public boolean isPlaying() {
return false;
}
@Override
public int getBufferPercentage() {
return 0;
}
@Override
public boolean canPause() {
return false;
}
@Override
public boolean canSeekBackward() {
return false;
}
@Override
public boolean canSeekForward() {
return false;
}
@Override
public int getAudioSessionId() {
return 0;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
return super.onKeyMultiple(keyCode, repeatCount, event);
}
注意:上面的方法都是回调方法,我们可以在这些方法里面实现我们的业务逻辑。只有当我们给VideoView设置setMediaController 后控制器才会出现
收音机
- 播放协议: MMS
- MMS(Microsoft Media Server protocol)是一种串流媒体传送协议 ,android并不支持这种流媒体协议
引入Vitamo框架进行播放
- 核心类:io.vov.vitamio.MediaPlayer
- 操作:同系统的MediaPlayer,代码编写与播放网络音乐相近
视频处理
电影文件有很多基本的组成部分。首先,文件本身被称为容器Container,容器的类型决定了信息被存放在文件中的位置。AVI和Quicktime就是容器的例子。接着,你有一组流,例如,你经常有的是一个音频流和一个视频流。(一个流只是一种想像出来的词语,用来表示一连串的通过时间来串连的数据元素)。在流中的数据元素被称为帧Frame。每个流是由不同的编码器来编码生成的。编解码器描述了实际的数据是如何被编码Coded和解码DECoded的,因此它的名字叫做CODEC。接着从流中被读出来的叫做包Packets。包是一段数据,它包含了一段可以被解码成方便我们最后在应用程序中操作的原始帧的数据。
七个模块分别为:读文件模块,解复用模块 ,视频解码模块,音频解码音频,颜色空间转换模块,视频显示模块,音频播放模块
粗略的分为五类,分别是 Source filer, Demux flter, Decoder filter, Color Space converter filter,Render filter,各类 filter的功能与作用简述如下
Source filter
Source filter 源过滤器的作用是为下级 demux filter 以包的形式源源不断的提供数据流。在通常情况下,我们有多种方式可以获得数据流,一种是从本地文件中读取,一种是从网上获取,Sourcefilter 另外一个作用就是屏蔽读本地文件和获取网络数据的差别,在下一级的 demux filter 看来,本地文件和网络数据是一样的。
Demux filter
解复用过滤器的作用是识别文件类型,媒体类型,分离出各媒体原始数据流,打上时钟信息后送给下级 decoder filter。为识别出不同的文件类型和媒体类型,常规的做法是读取一部分数据,然后遍历解复用过滤器支持的文件格式和媒体数据格式,做匹配来确定是哪种文件类型,哪种媒体类型;有些媒体类型的原始数据外面还有其他的信息,比如时间,包大小,是否完整包等等。demux filter 解析数据包后取出原始数据,有些类型的媒体不管是否是完整包都立即送往下级 decoder filter,有些类型的媒体要送完整数据包,此时可能有一些数据包拼接的动作;当然时钟信息的计算也是 demux filter 的工作内容,这个时钟用于各媒体之间的同步。在本例中,AVI Splitter 是 Demux filter。
Decoder filter
解码过滤器的作用就是解码数据包,并且把同步时钟信息传递下去。对视频媒体而言,通常是解码成 YUV 数据,然后利用显卡硬件直接支持 YUV 格式数据 Overlay 快速显示的特性让显卡极速显示。YUV格式是一个统称,常见的有 YV12,YUY2,UYVY 等等。有些非常古老的显卡和嵌入式系统不支持 YUV 数据显示,那就要转换成 RGB 格式的数据,每一帧的每一个像素点都要转换,分别计算 RGB 分量,并且因为转换是浮点运算,虽然有定点算法,还是要耗掉相当一部分 CPU,总体上效率底下;对音频媒体而言,通常是解码成 PCM 数据,然后送给声卡直接输出。在本例中,AVI Decompress 和 ACM Warper 是 decoder filter。
Color space converter filter
颜色空间转换过滤器的作用是把视频解码器解码出来的数据转换成当前显示系统支持的颜色格式。通常视频解码器解码出来的是 YUV 数据,PC 系统是直接支持 YUV 格式的,也支持 RGB 格式,有些嵌入式系统只支持 RGB 格式的。在本例中,视频解码器解码出来的是 RGB8 格式的数据,Color space converter filter 把 RGB8 转换成 RGB32 显示。
Render filter
渲染过滤器的作用就是在适当的时间渲染相应的媒体,对视频媒体就是直接显示图像,对音频就是播放声音。视音频同步的策略方法有好几种,其中最简单的一种就是默认视频和音频基准时间相同,这时音频可以不打时钟信息,通过计算音频的采样频率,量化 bit 数,声道数等基本参数就知道音频 PCM 的数据速率,按照这个速率往前播放即可;视频必须要使用同步时钟信息来决定什么时候显示。DirectShow 采用一个有序链表 ,把接收到的数据包放进有序链表中,启动一个定时器,每次定时器时间到就扫描链表,比较时钟信息,或者显示相应的帧,或者什么也不做,每次接收到新的数据帧,首先判断时钟信息,如果是历史数据帧就丢弃,如果是将来显示数据帧就进有序链表,如果当前时间帧就直接显示。如此这样,保持视频和音频在人体感觉误差范围内相对的动态同步。在本例中 VideoRender 和 Default DirectSound Device 是 Render filter,同时也是 Sink filter
JetPlayer
JetPlayer jetPlayer = JetPlayer.getJetPlayer();
jetPlayer.loadJetFile("/sdcard/level1.jet");
byte segmentId = 0;
// queue segment 5, repeat once, use General MIDI, transpose by -1 octave
jetPlayer.queueJetSegment(5, -1, 1, -1, 0, segmentId++);
// queue segment 2
jetPlayer.queueJetSegment(2, -1, 0, 0, 0, segmentId++);
jetPlayer.play();
TextureView
TextureView、Surfaceview、SurfaceTexture、GLSurfaceView