学习学习之鸿蒙&Android

Android之媒体探究(二)—— 视频

2021-08-23  本文已影响0人  乌托邦式的爱情

在上一篇讲到了Android中关于音频的一些入门级的知识点,接下来就看看Android中关于视频的使用。需要说明的,对于android来说,其音频和视频在底层使用的是同一个东西,在上一篇关于音频的使用过程中,我们使用到的框架有两个,MediaPlayer和ExoPlayer,还不了解这两个的可以去看看我上一篇文章Android之媒体探究(一)—— 音频。而在这篇视频里面,我们同样使用的也是这两个。废话不多说,先看下效果。因为录制的视频有点长,所以录制为两段连着看。

视频播放1.gif 视频播放2.gif

正文

其实实现视频播放的方式有很多种,这里我只是简单的列举了几种供大家作为入门的参考,实际在项目里面实现视频播放要复杂的多,因为要考虑很多很多的东西,比如声道、屏幕比例大小、清晰度等等,但是万变不离其宗,所有复杂的东西都是从一个一个小点开始的,所以这篇文章不会讲很深的东西而是带大家入个门。

实现视频播放的方法

(一)使用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" />
上一篇下一篇

猜你喜欢

热点阅读