话说音视频播放现在真是不搞不行啦,不光我们要从市面上 N 多的播放器中找出好用的,熟悉其 API ,甚至还需要我们能利用开源解码器来编写我们自己的视频播放器,模块,这点在越来越不一样,差异化的播放器中可以看得见,很多 app 不光播放器 UI 不一样,甚至很多逻辑都不同
所以大家努力吧,先不求能搞定 FFMG 编解码等底层技术,至少我们现在得能利用 ijkPlayer 编写自己的视频播放器出来,才能彻底搞得定 app 端的播放需求,要是打算自己上直播推流 sdk ,那么学的就更多了,都是得按年来计算了,音视频水太神了,至少要求我们得搞定 UI 部分才行,在视频这块用别人开源的播放器是走不远的,因为需求差异性太大
录制音视频 AudioRecord/MediaRecord
视频的基础知识点推荐看下面:
- Android视频开发进阶(part1-关于视频的那些术语)
- Android视频开发进阶(part2-MP4文件的解析)
- Android视频开发进阶(part3-Android的Media API)
- Android视频开发进阶(part4-自适应视频播放技术(Adaptive Streaming))
- Android视频开发进阶(part5-ExoPlayer分析1,ExoPlayer的handler)
- Android视频开发进阶(part6-安卓的DRM,视频版权保护)
视频流媒体协议有哪些
- HTTP
-> 最传统的视频流媒体,不支持实时流媒体的播放 - RTMP
-> Adobe 公司用于 flash 播放的 - RTSP
-> android 原生支持此种流媒体协议 - FLV
- HLS
-> apple 公司开发,把视频分成 3-10 秒的小段,下发 m3u8 文件来标记文件顺序,使用 HTTP、HTTPS 传输 - MMS
-> 微软公司开发的
官方原生播放器 MediaPlayer
MediaPlayer 是 Androd 多媒体框架中的一个重要组件,通过该类,我们可以解码和播放音视频,但是 MediaPlayer 本身只支持 音频 播放,需要传入专门的视频承载 view 才能播放视频
MediaPlayer 可以支持三种不同的媒体来源:
- 本地资源
- 内部URI,比如你可以通过ContentResolver来获取
- 外部URL(流)
MediaPlayer支持两种流媒体协议,HTTP 和 RTSP,这两种协议最大的不同是,RTSP 协议支持实时流媒体的播放,而 HTTP 协议不支持
原生 MediaPalyer 支持的协议和封装格式实在太有限了,如果我们想播放那些它不支持的视频,这时候就需要第三方播放器了,很多第三方播放器的底层实现都是基于 FFmpeg
MediaPlayer 对于视频格式支持的也是非常少,值能支持: MP4,AVI,3DP 这3个早期的手机品是格式
开源的音视频解码器在 API 上都参考了 MediaPlayer ,所以学习 MediaPlayer API 就是我们的第一步
- 创建 MediaPlayer 对象,可以直接和本地资源绑定
MediaPlayer mp = new MediaPlayer();
MediaPlayer mp = MediaPlayer.create(this, R.raw.test); //无需再调用setDataSource
create(Context context, Uri uri, SurfaceHolder holder)
- 设置播放资源
MediaPlayer.create(this, R.raw.test); // raw下的资源
mp.setDataSource("/sdcard/test.mp3"); // 本地文件路径
mp.setDataSource("http://www.xxx.com/music/test.mp3");// 网络URL文件
- 播放资源
// uri 资源
Uri myUri = ....; /**initialize Uri here*/
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
// 网络文件
String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepareAsync()
mediaPlayer.setOnPreparedListener({
// 加载完毕再开始播放
mediaPlayer.start()
})
- MediaPlayer 主要 API,需要熟知
getCurrentPosition( ):得到当前的播放位置
getDuration() :得到文件的时间
getVideoHeight() :得到视频高度
getVideoWidth() :得到视频宽度
isLooping():是否循环播放
isPlaying():是否正在播放
pause():暂停
prepare():准备(同步)
prepareAsync():准备(异步)
release():释放MediaPlayer对象
reset():重置MediaPlayer对象
seekTo(int msec):指定播放的位置(以毫秒为单位的时间)
setAudioStreamType(int streamtype):指定流媒体的类型
setDisplay(SurfaceHolder sh):设置用SurfaceHolder来显示多媒体
setLooping(boolean looping):设置是否循环播放
setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener):网络流媒体的缓冲监听
setOnCompletionListener(MediaPlayer.OnCompletionListener listener):网络流媒体播放结束监听
setOnErrorListener(MediaPlayer.OnErrorListener listener):设置错误信息监听
setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener):视频尺寸监听
setScreenOnWhilePlaying(boolean screenOn):设置是否使用SurfaceHolder显示
setVolume(float leftVolume, float rightVolume):设置音量
start():开始播放
stop():停止播放
我们需要熟知下面这张 MediaPalyer 解码器生命周期图,所有的开源项目都是按这个思路来做的
状态1:Idel(空闲)状态
当 mediaplayer创建或者执行reset()方法后处于这个状态。状态2:Initialized(已初始化)状态
当调用mediaplayer的setDataResource()方法给mediaplayer设置播放的数据源后,mediaplayer会处于该状态。状态3:Prepared(准备就续)状态
设置完数据源后,调用mediaplayer的prepare()方法,让mediaplayer准备播放。值得一提的是,这里除了prepare()方法,还有prepareAsnyc()方法,此方法是异步方法,一般用于网络视频的缓冲。当缓冲完毕后,就会触发准备完毕的事件。我们要做的就是监听该事件(OnPreparedListener),当缓冲完成时,执行相应的操作。在此状态上,我们可以调用seekTo()方法定位视频,此方法不改变mediaplayer的状态;亦可调用stop()放弃视频播放,使mediaplayer处于Stopped状态。一般我们会在此状态上调用start()方法开始播放视频。状态4:Started(开始)状态
当处于Prepared状态、Paused状态和PlayebackCompeleted状态时,调用Started()方法即可进入该状态。在该状态中,mediaplayer开始播放视频,可以通过seekTo()方法和start()方法改变视频播放的进度,当Looping为真且播放完毕后,它会重新开始播放(即循环播放);否则播放完毕后,会触发事件并调用OnCompletionaListener.OnCompletion()方法,进行特定操作,并进入PlaybackCompleted状态。在此状态中,亦可调用pause()方法或者stop()方法让视频暂停或停止,此时mediaplayer分别处于Stopped和Paused状态。状态5:Stopped(停止)状态
当 mediaplayer处于Prepared、Started、Paused、PlaybackCompleted状态时,调用stop()方法即可进入本状态。应特别注意的是,在本状态中,若想重新开始播放,不能直接调用start()方法,必须调用prepare()方法或prepareAsync()方法重新让mediaplayer处于Prepared状态方可调用start()方法播放视频。状态6:Paused(暂停)状态
当mediaplayer处于Started状态是,调用pause()方法即可进入本状态。在本状态里,可直接调用start()方法使,mediaplayer回到Started状态,亦可调用stop()方法停止视频播放,让播放器处于停止态。状态7:PlaybackCompleted(播放完成)状态
当mediaplayer播放完成且Looping为假时即可进入本状态。在本状态可调用start()方法使mediaplayer回到Started状态(注意此时是从头开始播放);亦可调用stop()方法使mediaplayer处于停止态,结束播放。状态8:Error(错误)状态
当mediaplayer出现错误时处于此状态。
SurfaceView , TextureView
SurfaceView , TextureView 都是 android 中用于承载视频帧显示的 view ,熟悉这2个 view 的大家都知道,这 2个 view 都是异步的,都是再非 UI 线程中计算的,拥有独立 surface 显存的,这是因为视频播放任务太重,UI 线程 hold 不住
SurfaceView
大概原理就是在现有View的位置上创建一个新的 Window,内容的显示和渲染都在新的 Window 中。这使得 SurfaceView 的绘制和刷新可以在单独的线程中进行,从而大大提高效率。但是呢,由于 SurfaceView 的内容没有显示在View中而是显示在新建的 Window中, 使得 SurfaceView 的显示不受 View 的属性控制,不能进行平移,缩放等变换,也不能放在其它 RecyclerView 或 ScrollView中,一些 View 中的特性也无法使用。TextureView
TextureView 是在4.0(API level 14)引入的,与 SurfaceView 相比,它不会创建新的窗口来显示内容。它是将内容流直接投放到View中,并且可以和其它普通View一样进行移动,旋转,缩放,动画等变化。TextureView必须在硬件加速的窗口中使用。TextureView被创建后不能直接使用,必须要在它被它添加到ViewGroup后,待SurfaceTexture准备就绪才能起作用(看TextureView的源码,TextureView是在绘制的时候创建的内部SurfaceTexture。SurfaceTexture的准备就绪、大小变化、销毁、更新等状态变化时都会回调相对应的方法。当TextureView内部创建好SurfaceTexture后,在监听器的onSurfaceTextureAvailable方法中,用SurfaceTexture来关联MediaPlayer,作为播放视频的图像数据来源。SurfaceTexture作为数据通道,把从数据源(MediaPlayer)中获取到的图像帧数据转为GL外部纹理,交给TextureVeiw作为View heirachy中的一个硬件加速层来显示,从而实现视频播放功能。
视频播放方式一:VideoView + MediaController
这是官方原生的实现,VideoView 继承 SurfaceView ,内部封装了 SurfaceView 的所有操作,MediaController 这样来显示视频控制相关的 view 部分
我个人是非常喜欢官方的 API 设计的,代码功能,层次分离,起名都非常规范,让人一看就懂,用起来也是很爽,奈何就是支持的流媒体格式太少,我们只能去使用开源组件
VideoView + MediaController 的简单使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.play);
videoview = (VideoView) findViewById(R.id.video);
mMediaController = new MediaController(this);
videoview.setMediaController(mMediaController);
mMediaController.setAnchorView(videoview)
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
loadView(url.getText().toString());
}
});
}
public void loadView(String path) {
Uri uri = Uri.parse(path);
videoview.setVideoURI(uri);
videoview.start;
videoview.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// mp.setLooping(true);
mp.start();// 播放
Toast.makeText(MainActivity.this, "开始播放!", Toast.LENGTH_LONG).show();
}
});
videoview.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Toast.makeText(MainActivity.this, "播放完毕", Toast.LENGTH_SHORT).show();
}
});
}
我们简单看下 MediaController 是如何管理 视频控制 view 的
MediaController 继承 FrameLayout
public class MediaController extends FrameLayout
在 makeControllerView 中创建 控制布局 view ,并初始化 view
protected View makeControllerView() {
LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null);
initControllerView(mRoot);
return mRoot;
}
media_controller 样式
在 setAnchorView 关联视频播放器 VideoView 时把 控制 view 添加到自己身上
public void setAnchorView(View view) {
if (mAnchor != null) {
mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener);
}
mAnchor = view;
if (mAnchor != null) {
mAnchor.addOnLayoutChangeListener(mLayoutChangeListener);
}
FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
removeAllViews();
View v = makeControllerView();
addView(v, frameParams);
}
视频播放方式二:开源解码器 + TextureView
我们注定是不会使用原生 VideoView 去远程视频的,没办法主流的流媒体格式 VideoView 都不支持,我们只能去使用 开源的视频解码器 + TextureView 去自己实现了
这就是我们后面需要自己的搞定的