在上一篇讲到了Android中关于音频的一些入门级的知识点,接下来就看看Android中关于视频的使用。需要说明的,对于android来说,其音频和视频在底层使用的是同一个东西,在上一篇关于音频的使用过程中,我们使用到的框架有两个,MediaPlayer和ExoPlayer,还不了解这两个的可以去看看我上一篇文章Android之媒体探究(一)—— 音频。而在这篇视频里面,我们同样使用的也是这两个。废话不多说,先看下效果。因为录制的视频有点长,所以录制为两段连着看。
正文
其实实现视频播放的方式有很多种,这里我只是简单的列举了几种供大家作为入门的参考,实际在项目里面实现视频播放要复杂的多,因为要考虑很多很多的东西,比如声道、屏幕比例大小、清晰度等等,但是万变不离其宗,所有复杂的东西都是从一个一个小点开始的,所以这篇文章不会讲很深的东西而是带大家入个门。
实现视频播放的方法
(一)使用VideoView + MediaController
(二)使用MediaPlayer + SurfaceView
(三)使用MediaPlayer + SurfaceView + MediaController
(四)使用ExoPlayer
视频播放原理
系统会首先确定视频的格式,然后得到视频的编码..然后对编码进行解码,得到一帧一帧的图像,最后在画布上进行迅速更新,显然需要在独立的线程中完成。
VideoView
VideoView是系统提供的一个用于最快速接入视频播放的View,VideoView是继承自SurfaceView的,其实真正用于播放视频的是MediaPlayer+SurfaceView,VideoView只是将这两者封装起来,便于开发者使用。
SurfaceView
我们知道,在android里面,系统提供了View进行绘画处理,基本上大多数时候我们使用的所有的控件都是继承自ViewGroup或者View,而又因为ViewGroup又是继承自View,所以对于大多数的控件来说,其都是继承于View。那么,在android里面有没有不是继承于View的呢?其实是有的,就是我们今天的主角——SurfaceView。我们知道View是通过刷新来重绘视图,系统通过发出VSSYNC信号来进行屏幕的重绘,刷新的时间间隔是16ms,如果我们可以在16ms以内将绘制工作完成,则没有任何问题,如果我们绘制过程逻辑很复杂,并且我们的界面更新还非常频繁,这时候就会造成界面的卡顿,影响用户体验,为此Android提供了SurfaceView来解决这一问题
View和SurfaceView的区别
1 . View适用于主动更新的情况,而SurfaceView则适用于被动更新的情况,比如频繁刷新界面。
2 . View在主线程中对页面进行刷新,而SurfaceView则开启一个子线程来对页面进行刷新。
3 . View在绘图时没有实现双缓冲机制,SurfaceView在底层机制中就实现了双缓冲机制。
双缓冲技术
双缓冲技术是游戏开发中的一个重要的技术。当一个动画争先显示时,程序又在改变它,前面还没有显示完,程序又请求重新绘制,这样屏幕就会不停地闪烁。而双缓冲技术是把要处理的图片在内存中处理好之后,再将其显示在屏幕上。双缓冲主要是为了解决 反复局部刷屏带来的闪烁。把要画的东西先画到一个内存区域里,然后整体的一次性画出来。
MediaController
一个包含媒体播放器(MediaPlayer)控件的视图。包含了一些典型的按钮,像"播放(Play)/暂停(Pause)", "倒带(Rewind)", "快进(Fast Forward)"与进度滑动器(progress slider)。它管理媒体播放器(MediaController)的状态以保持控件的同步。
通过编程来实例化使用这个类。这个媒体控制器将创建一个具有默认设置的控件,并把它们放到一个窗口里漂浮在你的应用程序上。具体来说,这些控件会漂浮在通过setAnchorView()指定的视图上。如果这个窗口空闲3秒那么它将消失,直到用户触摸这个视图的时候重现。
好,介绍完上面的几个重要概念,下面就直接进入编码阶段了。
(一)使用VideoView + MediaController
XML文件
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MediaControllerVideoViewActivity">
<RelativeLayout
android:id="@+id/video_view_container"
android:layout_width="match_parent"
android:layout_height="205dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
实现
class MediaControllerVideoViewActivity : AppCompatActivity() {
private var video_view: VideoView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media_controller_video_view)
}
override fun onResume() {
super.onResume()
mediaControllerVideoView()
}
/**
* 通过MediaController控制界面中的快进、快退、暂停、播放等
* 控制器是由系统提供的,无法进行自定义
*/
private fun mediaControllerVideoView() {
if (video_view_container != null) {
video_view_container.removeAllViews()
}
video_view = VideoView(applicationContext)
video_view_container.addView(video_view)
val path = "https://video-qn.51miz.com/preview/video/00/00/12/30/V-123077-95B77BE6.mp4"
video_view!!.setVideoPath(path)
val mediaController = MediaController(this)
video_view!!.setMediaController(mediaController)
video_view!!.start()
video_view!!.requestFocus()
}
/**
* Activity关闭的时候释放资源
*/
override fun onDestroy() {
super.onDestroy()
video_view!!.stopPlayback()
video_view_container.removeAllViews()
video_view = null
}
}
(二)使用MediaPlayer + SurfaceView
XML文件
<RelativeLayout 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"
tools:context=".MediaVideoActivity">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="200dp"/>
<LinearLayout
android:id="@+id/media_type"
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_alignParentBottom="true"
android:background="@color/dialog_title_text_color"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/last_song"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/last_song" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/media_play"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/media_song"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/media_pause_day" />
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/next_song"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/next_song" />
</RelativeLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/seek_bar_layout"
android:layout_width="wrap_content"
android:layout_above="@+id/media_type"
android:layout_height="50dp">
<TextView
android:id="@+id/media_book_start_time"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:paddingLeft="15dp"
android:textColor="@color/colorAccent"
android:textSize="12sp" />
<TextView
android:id="@+id/media_book_total_time"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:paddingRight="15dp"
android:textColor="@color/colorAccent"
android:textSize="12sp" />
<SeekBar
android:id="@+id/seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/media_book_total_time"
android:layout_toRightOf="@+id/media_book_start_time"
android:background="@null"
android:maxHeight="1dp"
android:progress="0"
android:progressDrawable="@drawable/seek_bar_bg"
android:splitTrack="false"
android:thumb="@drawable/seekbar_thumb" />
</RelativeLayout>
</RelativeLayout>
实现
class MediaVideoActivity : AppCompatActivity(), SurfaceHolder.Callback, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, MediaPlayer.OnInfoListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnSeekCompleteListener, MediaPlayer.OnVideoSizeChangedListener, SeekBar.OnSeekBarChangeListener, MediaPlayer.OnBufferingUpdateListener {
private var mMediaPlayer: MediaPlayer? = null
companion object {
const val UPDATE_TIME = 0x0001
}
private val mHandler: Handler = object : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
UPDATE_TIME -> {
updateTime()
sendEmptyMessageDelayed(UPDATE_TIME, 500)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media_video)
}
override fun onResume() {
super.onResume()
mediaPlayerSurfaceView()
}
/**
* MediaPlayer+SurfaceView+自定义控制器
* 所有的界面控制器由自己定义
*/
private fun mediaPlayerSurfaceView() {
val path = "https://video-qn.51miz.com/preview/video/00/00/12/37/V-123783-D16F674D.mp4"
// 设置SurfaceView
surfaceView.setZOrderOnTop(false)
surfaceView.holder.addCallback(this)
// 设置MediaPlayer
mMediaPlayer = MediaPlayer()
mMediaPlayer?.setOnCompletionListener(this)
mMediaPlayer?.setOnErrorListener(this)
mMediaPlayer?.setOnInfoListener(this)
mMediaPlayer?.setOnPreparedListener(this)
mMediaPlayer?.setOnSeekCompleteListener(this)
mMediaPlayer?.setOnVideoSizeChangedListener(this)
mMediaPlayer?.setOnBufferingUpdateListener(this)
try {
//使用手机本地视频
mMediaPlayer?.setDataSource(path)
} catch (e: Exception) {
e.printStackTrace()
}
seek_bar.setOnSeekBarChangeListener(this)
media_play.setOnClickListener {
play()
}
}
/**
* Surface被创建
*/
override fun surfaceCreated(holder: SurfaceHolder) {
mMediaPlayer?.setDisplay(holder)
mMediaPlayer?.prepareAsync()
}
/**
* Surface发生改变
*/
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
/**
* Surface销毁
*/
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
/**
* MediaPlayer装载完毕
*/
override fun onPrepared(mp: MediaPlayer?) {
media_book_start_time.text = TimeUtil.getTime(mp!!.currentPosition / 1000)
media_book_total_time.text = TimeUtil.getTime(mp.duration / 1000)
seek_bar.max = mp.duration
seek_bar.progress = mp.currentPosition
}
/**
* 播放结束
*/
override fun onCompletion(mp: MediaPlayer?) {
mHandler.removeMessages(UPDATE_TIME)
seek_bar.progress = 0
media_book_start_time.text = TimeUtil.getTime(0)
media_song.setImageResource(R.drawable.media_pause_day)
}
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
return false
}
override fun onInfo(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
return false
}
override fun onSeekComplete(mp: MediaPlayer?) {
}
override fun onVideoSizeChanged(mp: MediaPlayer?, width: Int, height: Int) {
}
private fun play() {
if (mMediaPlayer == null) {
return
}
if (mMediaPlayer!!.isPlaying) {
mMediaPlayer!!.pause()
mHandler.removeMessages(UPDATE_TIME)
media_song.setImageResource(R.drawable.media_pause_day)
} else {
mMediaPlayer!!.start()
mHandler.sendEmptyMessageDelayed(UPDATE_TIME, 500)
media_song.setImageResource(R.drawable.media_play_day)
}
}
/**
* 更新播放时间
*/
private fun updateTime() {
media_book_start_time.text = TimeUtil.getTime(mMediaPlayer!!.currentPosition / 1000)
seek_bar.progress = mMediaPlayer!!.currentPosition
}
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (mMediaPlayer != null && fromUser) {
mMediaPlayer!!.seekTo(progress)
media_book_start_time.text = TimeUtil.getTime(mMediaPlayer!!.currentPosition / 1000)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
if (!mMediaPlayer!!.isPlaying) {
play()
}
}
/**
* 获取缓冲的信息
*/
override fun onBufferingUpdate(mp: MediaPlayer?, percent: Int) {
Log.i("aaaaaaaaaaaaaaaaaaaaa", "当前缓冲$percent")
}
override fun onDestroy() {
super.onDestroy()
mHandler.removeMessages(UPDATE_TIME)
mMediaPlayer!!.stop()
mMediaPlayer!!.release()
mMediaPlayer = null
}
}
(三)使用MediaPlayer + SurfaceView + MediaController
XML文件
<RelativeLayout 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:id="@+id/root_all"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".MediaPlayerControllerSurfaceViewActivity">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="200dp" />
</RelativeLayout>
实现
class MediaPlayerControllerSurfaceViewActivity : AppCompatActivity(), SurfaceHolder.Callback, MediaPlayer.OnBufferingUpdateListener, MediaController.MediaPlayerControl {
private var mMediaPlayer: MediaPlayer? = null
private var mMediaController: MediaController? = null
private var bufferPercentage = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media_player_controller_surface_view)
init()
}
private fun init() {
mMediaPlayer = MediaPlayer()
mMediaController = MediaController(this)
mMediaController?.setAnchorView(root_all)
surfaceView.setZOrderOnTop(false)
surfaceView.holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
surfaceView.holder.addCallback(this)
}
override fun onResume() {
super.onResume()
try {
val path = "https://video-qn.51miz.com/preview/video/00/00/12/37/V-123783-D16F674D.mp4"
mMediaPlayer?.setDataSource(path)
mMediaPlayer?.setOnBufferingUpdateListener(this)
mMediaController?.setMediaPlayer(this)
mMediaController?.isEnabled = true
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onPause() {
super.onPause()
if (mMediaPlayer!!.isPlaying) {
mMediaPlayer!!.stop()
}
}
override fun onDestroy() {
super.onDestroy()
if (mMediaPlayer != null) {
mMediaPlayer!!.release()
mMediaPlayer = null
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
mMediaController?.show()
return super.onTouchEvent(event)
}
override fun surfaceCreated(holder: SurfaceHolder) {
mMediaPlayer?.setDisplay(holder)
mMediaPlayer?.prepareAsync()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
override fun onBufferingUpdate(mp: MediaPlayer?, percent: Int) {
bufferPercentage = percent
}
override fun start() {
if (mMediaPlayer != null) {
mMediaPlayer?.start()
}
}
override fun pause() {
if (mMediaPlayer != null) {
mMediaPlayer?.pause()
}
}
override fun getDuration() = mMediaPlayer!!.duration
override fun getCurrentPosition() = mMediaPlayer!!.currentPosition
override fun seekTo(pos: Int) {
mMediaPlayer!!.seekTo(pos)
}
override fun isPlaying(): Boolean {
if (mMediaPlayer!!.isPlaying) {
return true
}
return false
}
override fun getBufferPercentage(): Int {
return bufferPercentage
}
override fun canPause(): Boolean {
return true
}
override fun canSeekBackward(): Boolean {
return true
}
override fun canSeekForward(): Boolean {
return true
}
override fun getAudioSessionId(): Int {
return 0
}
}
(四)使用ExoPlayer
XML文件
<RelativeLayout 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"
tools:context=".ExoPlayerVideoActivity">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/exo_play_show"
android:layout_width="match_parent"
android:layout_height="205dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="startExoPlayer"
android:text="播放" />
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="pauseExoPlayer"
android:text="暂停" />
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="stopExoPlayer"
android:text="结束" />
</LinearLayout>
</RelativeLayout>
实现
class ExoPlayerVideoActivity : AppCompatActivity(), Player.EventListener {
private var mSimpleExoPlayer: ExoPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_exo_player_video)
initExoPlayer()
}
private fun initExoPlayer() {
// 创建一个播放器
mSimpleExoPlayer = ExoPlayerFactory.newSimpleInstance(this)
// 将播放器附着在一个View上
exo_play_show.player = mSimpleExoPlayer
// 准备播放器资源
val dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, application.packageName))
val videoSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse("https://video-qn.51miz.com/preview/video/00/00/12/37/V-123783-D16F674D.mp4"))
mSimpleExoPlayer!!.prepare(videoSource)
}
fun startExoPlayer(view: View) {
mSimpleExoPlayer!!.playWhenReady = true
}
fun pauseExoPlayer(view: View) {
mSimpleExoPlayer!!.playWhenReady = false
}
fun stopExoPlayer(view: View) {
mSimpleExoPlayer!!.stop()
}
override fun onDestroy() {
super.onDestroy()
mSimpleExoPlayer!!.stop()
mSimpleExoPlayer!!.release()
mSimpleExoPlayer = null
}
}
最后别忘记在清单文件里面添加权限
<uses-permission android:name="android.permission.TYPE_APPLICATION_OVERLAY" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNET" />
网友评论