精华内容
下载资源
问答
  • Android音视频开发

    2021-02-01 14:38:09
    Android 音视频开发学习思路 Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。只能通过一点点的学习和积累把这块的知识串联积累起来。 初级入门篇: Android 音视频...

    Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。只能通过一点点的学习和积累把这块的知识串联积累起来。

    初级入门篇:

    初级入门篇主要是接触Android多媒体展示相关的API,通过单独的列举和使用这些API,对Android音视频处理有一个基本的轮廓,虽然知识点相对来说是比较散的,但是点成线,线称面,基本的基础掌握了,通过学习Android音视频核心的API将音视频的流程串联起来,这样对于音视频的了解和控制就不仅仅局限于最外层的API了,而是能够通过相对底层的方式来加深对Android 音视频开发的认知。 

    中级进阶篇:

    OpenGL ES 学习记录

    学习 Android 平台 OpenGL ES API,了解 OpenGL 开发的基本流程,使用 OpenGL 绘制基本图形,并了解相关的API的简单使用

    动手实践,积累实战经验:

    个人学习成果展示:

    OpenSL ES 学习记录

    学习 Android 平台 OpenSL ES API,了解 OpenSL 开发的基本流程,使用OpenSL播放PCM数据,并了解相关API的简单使用

    高级探究篇:

    • 深入研究音视频相关的网络协议,如 rtmp,hls,以及封包格式,如:flv,mp4
    • 深入学习一些音视频领域的开源项目,如 webrtc,ffmpeg,ijkplayer,librtmp 等等
    • 将 ffmpeg 库移植到 Android 平台,结合上面积累的经验,编写一款简易的音视频播放器
    • 将 x264 库移植到 Android 平台,结合上面积累的经验,完成视频数据 H264 软编功能
    • 将 librtmp 库移植到 Android 平台,结合上面积累的经验,完成 Android RTMP 推流功能 

    FFmpeg 学习记录

    FFmpeg 结构体学习

    音视频开发开源库

    GPUImageIjkPlayerlibrestreamingRTMPDumpSoundTouch

    学习展望

      完成上面的学习后,可以尝试做一款音视频相关的APP,这个APP尽可能多的用上你学习的知识,看看能做到什么程度。如果你能很好的做出来,并认真的把上面列举的所有的点都完成和整理了,相信你在Android音视频领域会越走越好。

    推荐的学习资料:

        1. 《雷霄骅的专栏》:http://blog.csdn.net/leixiaohua1020

        2. 《Android音频开发》:http://ticktick.blog.51cto.com/823160/d-15

        3. 《FFMPEG Tips》:http://ticktick.blog.51cto.com/823160/d-17

        4. 《Learn OpenGL 中文》:https://learnopengl-cn.github.io/

        5. 《Android Graphic 架构》:https://source.android.com/devices/graphics/

        6. 《Jhuster的专栏》:http://blog.51cto.com/ticktick

        7. 《ywl5320的专栏》:https://blog.csdn.net/ywl5320

    展开全文
  • 一、Android音视频硬解码篇: 1,音视频基础知识 2,音视频硬解码流程 3,音视频播放:音视频同步 4,音视频解封和封装:生成一个MP4 二、使用OpenGL渲染视频画面篇 1,初步了解OpenGL ES 2,使用OpenGL渲染...

    https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3472732648,1327081532&fm=26&gp=0.jpg

     

    目录


    一、Android音视频硬解码篇:

    二、使用OpenGL渲染视频画面篇

    三、Android FFmpeg音视频解码篇

    • 1,FFmpeg so库编译
    • 2,Android 引入FFmpeg
    • 3,Android FFmpeg视频解码播放
    • 4,Android FFmpeg+OpenSL ES音频解码播放
    • 5,Android FFmpeg+OpenGL ES播放视频
    • 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
    • 7,Android FFmpeg视频编码

    本文你可以了解到

    上一篇文章,主要讲了Android MediaCodec实现音视频硬解码的流程,搭建了基础解码框架。本文将讲解具体的音视频渲染,包括MediaCodec初始化、Surface初始化,AudioTrack初始化、音视频数据流分离提取等,以及非常重要的音视频同步。

    在上一篇文章定义的解码流程框架基类中,预留了几个虚函数,留给子类初始化自己的东西,本篇,就来看看如何实现。

    一、音视频数据流分离提取器

    上篇文章,多次提到音视频数据分离提取器,在实现音视频解码器子类之前,先把这个实现了。

    封装Android原生提取器

    之前提过,Android原生自带有一个MediaExtractor,用于音视频数据分离和提取,接来下基于这个,做一个支持音视频提取的工具类MMExtractor:

      class MMExtractor(path: String?) {
    
          /**音视频分离器*/
          private var mExtractor: MediaExtractor? = null
        
          /**音频通道索引*/
          private var mAudioTrack = -1
        
          /**视频通道索引*/
          private var mVideoTrack = -1
        
          /**当前帧时间戳*/
          private var mCurSampleTime: Long = 0
        
          /**开始解码时间点*/
          private var mStartPos: Long = 0
    
          init {
              //【1,初始化】
              mExtractor = MediaExtractor()
              mExtractor?.setDataSource(path)
          }
    
          /**
           * 获取视频格式参数
           */
          fun getVideoFormat(): MediaFormat? {
              //【2.1,获取视频多媒体格式】
              for (i in 0 until mExtractor!!.trackCount) {
                  val mediaFormat = mExtractor!!.getTrackFormat(i)
                  val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
                  if (mime.startsWith("video/")) {
                      mVideoTrack = i
                      break
                  }
              }
              return if (mVideoTrack >= 0)
                  mExtractor!!.getTrackFormat(mVideoTrack)
              else null
          }
    
          /**
           * 获取音频格式参数
           */
          fun getAudioFormat(): MediaFormat? {
              //【2.2,获取音频频多媒体格式】
              for (i in 0 until mExtractor!!.trackCount) {
                  val mediaFormat = mExtractor!!.getTrackFormat(i)
                  val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
                  if (mime.startsWith("audio/")) {
                      mAudioTrack = i
                      break
                  }
              }
              return if (mAudioTrack >= 0) {
                  mExtractor!!.getTrackFormat(mAudioTrack)
              } else null
          }
    
          /**
           * 读取视频数据
           */
          fun readBuffer(byteBuffer: ByteBuffer): Int {
              //【3,提取数据】
              byteBuffer.clear()
              selectSourceTrack()
              var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)
              if (readSampleCount < 0) {
                  return -1
              }
              mCurSampleTime = mExtractor!!.sampleTime
              mExtractor!!.advance()
              return readSampleCount
          }
    
          /**
           * 选择通道
           */
          private fun selectSourceTrack() {
              if (mVideoTrack >= 0) {
                  mExtractor!!.selectTrack(mVideoTrack)
              } else if (mAudioTrack >= 0) {
                  mExtractor!!.selectTrack(mAudioTrack)
              }
          }
    
           /**
           * Seek到指定位置,并返回实际帧的时间戳
           */
          fun seek(pos: Long): Long {
              mExtractor!!.seekTo(pos, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
              return mExtractor!!.sampleTime
          }
    
          /**
           * 停止读取数据
           */
          fun stop() {
              //【4,释放提取器】
              mExtractor?.release()
              mExtractor = null
          }
    
          fun getVideoTrack(): Int {
              return mVideoTrack
          }
    
          fun getAudioTrack(): Int {
              return mAudioTrack
          }
    
          fun setStartPos(pos: Long) {
              mStartPos = pos
          }
    
          /**
           * 获取当前帧时间
           */
           fun getCurrentTimestamp(): Long {
              return mCurSampleTime
          }
      }  

    比较简单,直接把代码贴出来了。

    关键部分有5个,做一下简单讲解:

    • 【1,初始化】

    很简单,两句代码:新建,然后设置音视频文件路径

      mExtractor = MediaExtractor()
      mExtractor?.setDataSource(path)
    • 【2.1/2.2,获取音视频多媒体格式】

    音频和视频是一样的:
    1)遍历视频文件中所有的通道,一般是音频和视频两个通道;
    2) 然后获取对应通道的编码格式,判断是否包含"video/"或者"audio/"开头的编码格式;
    3)最后通过获取的索引,返回对应的音视频多媒体格式信息。

    • 【3,提取数据】

    重点看看如何提取数据:
    1)readBuffer(byteBuffer: ByteBuffer)中的参数就是解码器传进来的,用于存放待解码数据的缓冲区。
    2)selectSourceTrack()方法中,根据当前选择的通道(同时只选择一个音/视频通道),调用mExtractor!!.selectTrack(mAudioTrack)将通道切换正确。
    3)然后读取数据:

      var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)

    此时,将返回读取到的音视频数据流的大小,小于0表示数据已经读完。
    4)进入下一帧:先记录当前帧的时间戳,然后调用advance进入下一帧,这时读取指针将自动移动到下一帧开头。

      //记录当前帧的时间戳
      mCurSampleTime = mExtractor!!.sampleTime
      //进入下一帧
      mExtractor!!.advance()
    • 【4,释放提取器】

    客户端退出解码的时候,需要调用stop是否提取器相关资源。

    说明:seek(pos: Long)方法,主要用于跳播,快速将数据定位到指定的播放位置,但是,于视频中,除了I帧以外,PB帧都需要依赖其他的帧进行解码,所以,通常只能seek到I帧,但是I帧通常和指定的播放位置有一定误差,因此需要指定seek靠近哪个关键帧,有以下三种类型:
    SEEK_TO_PREVIOUS_SYNC:跳播位置的上一个关键帧
    SEEK_TO_NEXT_SYNC:跳播位置的下一个关键帧
    SEEK_TO_CLOSEST_SYNC:距离跳播位置的最近的关键帧


    到这里你就可以明白,为什么我们平时在看视频时,拖动进度条释放以后,视频通常会在你释放的位置往前一点


    封装音频和视频提取器

    上面封装的工具中,可以支持音频和视频的数据提取,下面我们将利用这个工具,用于分别提取音频和视频的数据。


    先回顾一下,上篇文章定义的提取器模型:

      interface IExtractor {
    
          fun getFormat(): MediaFormat?
    
          /**
          * 读取音视频数据
            */
          fun readBuffer(byteBuffer: ByteBuffer): Int
    
          /**
           * 获取当前帧时间
           */
          fun getCurrentTimestamp(): Long
    
          /**
           * Seek到指定位置,并返回实际帧的时间戳
           */
          fun seek(pos: Long): Long
    
          fun setStartPos(pos: Long)
    
          /**
           * 停止读取数据
           */
          fun stop()
      }

    有了上面封装的工具,一切就变得很简单了,做一个代理转接就行了。

    • 视频提取器
      class VideoExtractor(path: String): IExtractor {
    
          private val mMediaExtractor = MMExtractor(path)
    
          override fun getFormat(): MediaFormat? {
              return mMediaExtractor.getVideoFormat()
          }
    
          override fun readBuffer(byteBuffer: ByteBuffer): Int {
              return mMediaExtractor.readBuffer(byteBuffer)
          }
    
          override fun getCurrentTimestamp(): Long {
              return mMediaExtractor.getCurrentTimestamp()
          }
    
          override fun seek(pos: Long): Long {
              return mMediaExtractor.seek(pos)
          }
    
          override fun setStartPos(pos: Long) {
              return mMediaExtractor.setStartPos(pos)
          }
    
          override fun stop() {
              mMediaExtractor.stop()
          }
      }
    • 音频提取器
      class AudioExtractor(path: String): IExtractor {
    
          private val mMediaExtractor = MMExtractor(path)
    
          override fun getFormat(): MediaFormat? {
              return mMediaExtractor.getAudioFormat()
          }
    
          override fun readBuffer(byteBuffer: ByteBuffer): Int {
              return mMediaExtractor.readBuffer(byteBuffer)
          }
    
          override fun getCurrentTimestamp(): Long {
              return mMediaExtractor.getCurrentTimestamp()
          }
      
          override fun seek(pos: Long): Long {
              return mMediaExtractor.seek(pos)
          }
    
          override fun setStartPos(pos: Long) {
              return mMediaExtractor.setStartPos(pos)
          }
    
          override fun stop() {
              mMediaExtractor.stop()
          }
      }


    二、视频播放


    我们先来定义一个视频解码器子类,继承BaseDecoder

       class VideoDecoder(path: String,
                         sfv: SurfaceView?,
                         surface: Surface?): BaseDecoder(path) {
          private val TAG = "VideoDecoder"
        
          private val mSurfaceView = sfv
          private var mSurface = surface
        
          override fun check(): Boolean {
              if (mSurfaceView == null && mSurface == null) {
                  Log.w(TAG, "SurfaceView和Surface都为空,至少需要一个不为空")
                  mStateListener?.decoderError(this, "显示器为空")
                  return false
              }
              return true
          }
    
          override fun initExtractor(path: String): IExtractor {
              return VideoExtractor(path)
          }
    
          override fun initSpecParams(format: MediaFormat) {
          }
    
          override fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean {
              if (mSurface != null) {
                  codec.configure(format, mSurface , null, 0)
                  notifyDecode()
              } else {
                  mSurfaceView?.holder?.addCallback(object : SurfaceHolder.Callback2 {
                      override fun surfaceRedrawNeeded(holder: SurfaceHolder) {
                      }
    
                      override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
                      }
    
                      override fun surfaceDestroyed(holder: SurfaceHolder) {
                      }
    
                      override fun surfaceCreated(holder: SurfaceHolder) {
                          mSurface = holder.surface
                          configCodec(codec, format)
                      }
                  })
    
                  return false
              }
              return true
          }
       
          override fun initRender(): Boolean {
              return true
          }
    
          override fun render(outputBuffers: ByteBuffer,
                            bufferInfo: MediaCodec.BufferInfo) {
          } 
    
          override fun doneDecode() {
          }
      }

    上篇文章中,定义好了解码流程框架,子类定义就很简单清晰了,只需按部就班,填写基类中预留的虚函数即可。

    • 检查参数

    可以看到,视频解码支持两种类型渲染表面,一个是SurfaceView,一个Surface。当其实最后都是传递Surface给MediaCodec

    1. SurfaceView应该是大家比较熟悉的View了,最常使用的就是用来做MediaPlayer的显示。当然也可以绘制图片、动画等。
    2. Surface应该不是很常用了,这里为了支持后续使用OpenGL来渲染视频,所以预先做了支持。
    • 生成数据提取器
      override fun initExtractor(path: String): IExtractor {
          return VideoExtractor(path)
      }

    配置解码器

    解码器的配置只需一句代码:

      codec.configure(format, mSurface , null, 0)

    不知道在上一篇文章,你有没有发现,在BaseDecoder初始化解码器的方法initCodec()中, 调用了configCodec方法后,会进入waitDecode方法,将线程挂起。

      abstract class BaseDecoder(private val mFilePath: String): IDecoder {
          //省略其他
          ......
        
          private fun initCodec(): Boolean {
              try {
                  val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
                  mCodec = MediaCodec.createDecoderByType(type)
                  if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
                      waitDecode()
                  }
                  mCodec!!.start()
            
                  mInputBuffers = mCodec?.inputBuffers
                  mOutputBuffers = mCodec?.outputBuffers
              } catch (e: Exception) {
                  return false
              }
              return true
          }
      }

    初始化Surface

    就是因为考虑到一个问题,SurfaceView的创建是有一个时间过程的,并非马上可以使用,要通过CallBack来监听它的状态。

    在surface初始化完毕后,再配置MediaCodec。

      override fun surfaceCreated(holder: SurfaceHolder) {
          mSurface = holder.surface
          configCodec(codec, format)
      }

    如果使用OpenGL直接传递surface进来,直接配置MediaCodec即可。

    渲染

    上文提到过,视频的渲染并不需要客户端手动去渲染,只需提供绘制表面surface,调用releaseOutputBuffer,将2个参数设置为true即可。所以,这里也不用在做什么操作了。

      mCodec!!.releaseOutputBuffer(index, true)

    三、音频播放

    有了上面视频播放器的基础以后,音频播放器也是分分钟搞定的事了。

      class AudioDecoder(path: String): BaseDecoder(path) {
          /**采样率*/
          private var mSampleRate = -1
        
          /**声音通道数量*/
          private var mChannels = 1
    
          /**PCM采样位数*/
          private var mPCMEncodeBit = AudioFormat.ENCODING_PCM_16BIT
    
          /**音频播放器*/
          private var mAudioTrack: AudioTrack? = null
    
          /**音频数据缓存*/
          private var mAudioOutTempBuf: ShortArray? = null
        
          override fun check(): Boolean {
              return true
          }
    
          override fun initExtractor(path: String): IExtractor {
              return AudioExtractor(path)
          }
    
          override fun initSpecParams(format: MediaFormat) {
              try {
                  mChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
                  mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
    
                  mPCMEncodeBit = if (format.containsKey(MediaFormat.KEY_PCM_ENCODING)) {
                      format.getInteger(MediaFormat.KEY_PCM_ENCODING)
                  } else {
                      //如果没有这个参数,默认为16位采样
                      AudioFormat.ENCODING_PCM_16BIT
                  }
              } catch (e: Exception) {
              }
          }
    
          override fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean {
              codec.configure(format, null , null, 0)
              return true
          }
    
          override fun initRender(): Boolean {
              val channel = if (mChannels == 1) {
                  //单声道
                  AudioFormat.CHANNEL_OUT_MONO
              } else {
                  //双声道
                  AudioFormat.CHANNEL_OUT_STEREO
              }
    
              //获取最小缓冲区
              val minBufferSize = AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)
    
              mAudioOutTempBuf = ShortArray(minBufferSize/2)
    
              mAudioTrack = AudioTrack(
                  AudioManager.STREAM_MUSIC,//播放类型:音乐
                  mSampleRate, //采样率
                  channel, //通道
                  mPCMEncodeBit, //采样位数
                  minBufferSize, //缓冲区大小
                  AudioTrack.MODE_STREAM) //播放模式:数据流动态写入,另一种是一次性写入
                
              mAudioTrack!!.play()
              return true
          }
    
          override fun render(outputBuffer: ByteBuffer,
                              bufferInfo: MediaCodec.BufferInfo) {
              if (mAudioOutTempBuf!!.size < bufferInfo.size / 2) {
                  mAudioOutTempBuf = ShortArray(bufferInfo.size / 2)
              }
              outputBuffer.position(0)
              outputBuffer.asShortBuffer().get(mAudioOutTempBuf, 0, bufferInfo.size/2)
              mAudioTrack!!.write(mAudioOutTempBuf!!, 0, bufferInfo.size / 2)
          }
    
          override fun doneDecode() {
              mAudioTrack?.stop()
              mAudioTrack?.release()
          }
      }

    初始化流程和视频是一样的,不一样的地方有三个:

    1. 初始化解码器

    音频不需要surface,直接传null

      codec.configure(format, null , null, 0)

    2. 获取参数不一样

    音频播放需要获取采样率,通道数,采样位数

    3. 需要初始化一个音频渲染器:AudioTrack
    由于解码出来的数据是PCM数据,所以直接使用AudioTrack播放即可。在initRender()
    中对其进行初始化。

    • 根据通道数量配置单声道和双声道
    • 根据采样率、通道数、采样位数计算获取最小缓冲区
      AudioTrack.getMinBufferSize(mSampleRate, channel, mPCMEncodeBit)
    • 创建AudioTrack,并启动
      mAudioTrack = AudioTrack(
                  AudioManager.STREAM_MUSIC,//播放类型:音乐
                  mSampleRate, //采样率
                  channel, //通道
                  mPCMEncodeBit, //采样位数
                  minBufferSize, //缓冲区大小
                  AudioTrack.MODE_STREAM) //播放模式:数据流动态写入,另一种是一次性写入
                
      mAudioTrack!!.play()

    4. 手动渲染音频数据,实现播放

    最后就是将解码出来的数据写入AudioTrack,实现播放。
    有一点注意的点是,需要把解码数据由ByteBuffer类型转换为ShortBuffer,这时Short数据类型的长度要减半。

    四、调用并播放

    以上,基本实现了音视频的播放流程,如无意外,在页面上调用以上音视频解码器,就可以实现播放了。

    简单看下页面和相关调用。

    main_activity.xml

      <?xml version="1.0" encoding="utf-8"?>
      <android.support.constraint.ConstraintLayout
              xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              tools:context=".MainActivity">
          <SurfaceView android:id="@+id/sfv"
                     app:layout_constraintTop_toTopOf="parent"
                     android:layout_width="match_parent"
                     android:layout_height="200dp"/>
      </android.support.constraint.ConstraintLayout>

    MainActivity.kt

      class MainActivity : AppCompatActivity() {
    
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_main)
              initPlayer()
          }
    
          private fun initPlayer() {
              val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"
            
              //创建线程池
              val threadPool = Executors.newFixedThreadPool(2)
            
              //创建视频解码器
              val videoDecoder = VideoDecoder(path, sfv, null)
              threadPool.execute(videoDecoder)
    
              //创建音频解码器
              val audioDecoder = AudioDecoder(path)
              threadPool.execute(audioDecoder)
              
              //开启播放
              videoDecoder.goOn()
              audioDecoder.goOn()
          }
      }

    至此,基本上实现音视频的解码和播放。但是如果你真正把代码跑起来的话,你会发现:视频和音频为什么不同步啊,视频就像倍速播放一样,一下就播完了,但是音频却很正常。

    这就要引出下一个不可避免的问题了,那就是音视频同步。

    五、音视频同步

    同步信号来源

    由于视频和音频是两个独立的任务在运行,视频和音频的解码速度也不一样,解码出来的数据也不一定马上就可以显示出来。

    在第一篇文章的时候有说过,解码有两个重要的时间参数:PTS和DTS,分别用于表示渲染的时间和解码时间,这里就需要用到PTS。

    播放器中一般存在三个时间,音频的时间,视频的时间,还有另外一个就是系统时间。这样可以用来实现同步的时间源就有三个:

    • 视频时间戳
    • 音频时间戳
    • 外部时间戳

     

    • 视频PTS

    通常情况下,由于人类对声音比较敏感,并且视频解码的PTS通常不是连续,而音频的PTS是比较连续的,如果以视频为同步信号源的话,基本上声音都会出现异常,而画面的播放也会像倍速播放一样。

    • 音频PTS

    那么剩下的两个选择中,以音频的PTS作为同步源,让画面适配音频是比较不错的一种选择。但是这里不采用,而是使用系统时间作为同步信号源。因为如果以音频PTS作为同步源的话,需要比较复杂的同步机制,音频和视频两者之间也有比较多的耦合。

    • 系统时间

    而系统时间作为统一信号源则非常适合,音视频彼此独立互不干扰,同时又可以保证基本一致。

    实现音视频同步

    要实现音视频之间的同步,这里需要考虑的有两个点:
    1. 比对

    在解码数据出来以后,检查PTS时间戳和当前系统流过的时间差距,快则延时,慢则直接播放
    2. 矫正
    在进入暂停或解码结束,重新恢复播放时,需要将系统流过的时间做一下矫正,将暂停的时间减去,恢复真正的流逝时间,即已播放时间。
    重新看回BaseDecoder解码流程:

      abstract class BaseDecoder(private val mFilePath: String): IDecoder {
          //省略其他
          ......
        
          /**
           * 开始解码时间,用于音视频同步
           */
          private var mStartTimeForSync = -1L
    
          final override fun run() {
              if (mState == DecodeState.STOP) {
                  mState = DecodeState.START
              }
              mStateListener?.decoderPrepare(this)
    
              //【解码步骤:1. 初始化,并启动解码器】
              if (!init()) return
      
              Log.i(TAG, "开始解码")
    
              while (mIsRunning) {
                  if (mState != DecodeState.START &&
                      mState != DecodeState.DECODING &&
                      mState != DecodeState.SEEKING) {
                      Log.i(TAG, "进入等待:$mState")
                    
                      waitDecode()
                    
                      // ---------【同步时间矫正】-------------
                      //恢复同步的起始时间,即去除等待流失的时间
                      mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
                  }
    
                  if (!mIsRunning ||
                      mState == DecodeState.STOP) {
                      mIsRunning = false
                      break
                  }
    
                  if (mStartTimeForSync == -1L) {
                      mStartTimeForSync = System.currentTimeMillis()
                  }
    
                  //如果数据没有解码完毕,将数据推入解码器解码
                  if (!mIsEOS) {
                      //【解码步骤:2. 见数据压入解码器输入缓冲】
                      mIsEOS = pushBufferToDecoder()
                  }
      
                  //【解码步骤:3. 将解码好的数据从缓冲区拉取出来】
                  val index = pullBufferFromDecoder()
                  if (index >= 0) {
                      // ---------【音视频同步】-------------
                      if (mState == DecodeState.DECODING) {
                          sleepRender()
                      }
                      //【解码步骤:4. 渲染】
                      render(mOutputBuffers!![index], mBufferInfo)
                      //【解码步骤:5. 释放输出缓冲】
                      mCodec!!.releaseOutputBuffer(index, true)
                      if (mState == DecodeState.START) {
                          mState = DecodeState.PAUSE
                      }
                  }
                  //【解码步骤:6. 判断解码是否完成】
                  if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                      Log.i(TAG, "解码结束")
                      mState = DecodeState.FINISH
                      mStateListener?.decoderFinish(this)
                  }
              }
              doneDecode()
              release()
          }
      }
    • 在不考虑暂停、恢复的情况下,什么时候进行时间同步呢?

    答案是:数据解码出来以后,渲染之前。

    解码器进入解码状态以后,来到【解码步骤:3. 将解码好的数据从缓冲区拉取出来】,这时如果数据是有效的,那么进入比对。

      // ---------【音视频同步】-------------
      final override fun run() {
        
          //......
        
          //【解码步骤:3. 将解码好的数据从缓冲区拉取出来】
          val index = pullBufferFromDecoder()
          if (index >= 0) {
               // ---------【音视频同步】-------------
             if (mState == DecodeState.DECODING) {
                  sleepRender()
              }
              //【解码步骤:4. 渲染】
              render(mOutputBuffers!![index], mBufferInfo)
              //【解码步骤:5. 释放输出缓冲】
              mCodec!!.releaseOutputBuffer(index, true)
              if (mState == DecodeState.START) {
                  mState = DecodeState.PAUSE
              }
          }
        
          //......
      }
    
      private fun sleepRender() {
          val passTime = System.currentTimeMillis() - mStartTimeForSync
          val curTime = getCurTimeStamp()
          if (curTime > passTime) {
              Thread.sleep(curTime - passTime)
          }
      }
    
      override fun getCurTimeStamp(): Long {
          return mBufferInfo.presentationTimeUs / 1000
      }  


    同步的原理如下:

    进入解码前,获取当前系统时间,存放在mStartTimeForSync,一帧数据解码出来以后,计算当前系统时间和mStartTimeForSync的距离,也就是已经播放的时间,如果当前帧的PTS大于流失的时间,进入sleep,否则直接渲染。

    • 考虑暂停情况下的时间矫正

    在进入暂停以后,由于系统时间一直在走,而mStartTimeForSync并没有随着系统时间累加,所以当恢复播放以后,重新将mStartTimeForSync加上这段暂停的时间段。
    只不过计算方法有多种:

    一种是记录暂停的时间,恢复时用系统时间减去暂停时间,就是暂停的时间段,然后用mStartTimeForSync加上这段暂停的时间段,就是新的mStartTimeForSync;
    另一个种是用恢复播放时的系统时间,减去当前正要播放的帧的PTS,得出的值就是mStartTimeForSync。

    这里采用第二种

      if (mState != DecodeState.START &&
          mState != DecodeState.DECODING &&
          mState != DecodeState.SEEKING) {
          Log.i(TAG, "进入等待:$mState")
    
          waitDecode()
    
          // ---------【同步时间矫正】-------------
          //恢复同步的起始时间,即去除等待流失的时间
          mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
      }

    至此,从解码到播放,再到音视频同步,一个简单的播放器就做完了。

    展开全文
  • android音视频.zip

    2019-09-11 16:36:43
    项目是进行android音视频进阶的整个流程学习流程代码,其中包括1.在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView,SurfaceView,自定义 View 2.在 Android 平台使用 AudioRecord 和 AudioTrack ...
  • Android音视频任务列表

    2020-08-30 12:07:16
    Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》。本文是Android音视频任务列表的具体内容(正在更新)。 Android音视频...
    展开全文
  • Android音视频开发学习思路
  •  HardwareVideoCodec是一个高效的Android音视频编码库,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,...

    HardwareVideoCodec是一个高效的Android音视频编码库,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。

    简介

      HardwareVideoCodec是个高性能、易用的Android音视频编码开源库,支持多款滤镜,支持RTMP直播推流,以及软编和硬编。硬编性能较好,在高通630的中端机子上实测1080p、30fps毫无问题。软编性能差一点,同样的机子,软编只能达到720p、24fps。硬编性能较好,软编兼容性较好,这个需要根据的业务需求进行选择。
      HardwareVideoCodec目前已经迭代到了1.5.1版本,更新了新的美颜滤镜,美颜更出色。支持RTMP推流,实测1080p、30fps局域网推流毫无性能压力。以下是主要的特性:

    1. 支持高性能的RTMP直播推流。
    2. 支持在不重启Camera的基础上,热切换画面分辨率。
    3. 支持包括美颜滤镜在内的20多款滤镜。
    4. 支持视频软编硬编
    5. 支持录制视频保存成mp4。
    6. 使用OpenGL进行画面渲染,更少的CPU和内存占用,高通630的中端机子硬编并开启RTMP推流实测仅12%的CPU占用。

    截图

     

    截图

    即将加入的特性

    1. 音频降噪、增益以及回音消除。

    使用

    1. 把以下代码加入到Project的build.gradle。
    buildscript {
        ext.kotlin_version = '1.2.30'//Latest kotlin version
        dependencies {
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        }
    }
    allprojects {
        repositories {
            maven {
                url 'https://dl.bintray.com/lmylr/maven'
            }
        }
    }
    
    1. 导入依赖,把以下代码加入到Module的build.gradle。
    dependencies {
        implementation 'com.lmy.codec:hardwarevideocodec:1.5.1'
        implementation 'com.lmy.codec:rtmp:1.0.1'//如果需要使用RTMP推流功能
    }
    
    1. 自定义Application继承自BaseApplication,只是为了使用Assets。
    class MyApplication : BaseApplication()
    
    1. 在Activity中使用HardwareVideoCodec。
    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val mTextureView = TextureView(this)
            setContentView(mTextureView)
            val mPresenter = RecordPresenter(CodecContext(this).apply {
                ioContext.path = "${Environment.getExternalStorageDirectory().absolutePath}/test.mp4"
                //ioContext.path = "rtmp://192.168.16.203:1935/live/livestream"//如果需要使用RTMP推流,把路径改为RTMP推流地址即可
            })
            mPresenter.setPreviewTexture(mTextureView)
            //For recording control
            mTextureView.setOnTouchListener { v, event ->
                when (event.action) {
                    MotionEvent.ACTION_DOWN -> {
                        mPresenter.start()
                    }
                    MotionEvent.ACTION_UP -> {
                        mPresenter.pause()
                    }
                }
                true
            }
        }
    }
    

      不出意外的话,你已经可以看到摄像头画面了。如果有什么问题,欢迎在评论区留言或者ISSUE,我会及时解答。

    开源协议

    HardwareVideoCodec is GPL 2.0.

    展开全文
  • Android音视频处理

    2013-09-23 17:38:26
    介绍android 音视频方面开发的接口和使用
  • Android 音视频录制

    2012-11-09 09:35:21
    Adnroid多媒体音视频录制Demo!另外还在思考Android音视频录制的另一种方式(LocalSocket)以流媒体形式写入录制音视频文件,知道怎么做的请留言,谢谢了!
  • Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。只能通过一点点的学习和积累把这块的知识串联积累起来。 初级入门篇: Android 音视频开发(一) : 通过三种方式绘制...
  • 主要为大家详细介绍了Android音视频之视频采集,系统API预览,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。只能通过一点点的学习和积累把这块的知识串联积累起来。 初级入门篇: Android 音视频开发(一) : 通过三种方式绘制...
  • Android 音视频开发

    千次阅读 2018-09-02 21:49:22
    Android 音视频开发(一) : 通过三种方式绘制图片 ...Android音视频处理之MediaExtractor https://www.jianshu.com/p/66acab100e4b PCM数据的采集和播放 https://blog.csdn.net/cyq7on/article/det...
  • Android音视频SDK

    2013-03-13 10:29:46
    一套不错的android音视频SDK,可用于二次开发,无回声问题,支持1080P高清视频通讯,采用了P2P技术,支持多平台互通,公开了代码跟开发文档,很适合做视频会议、视频监控、智能家居等项目。
  • 项目地址:wanliyang1990/wlmedia 简介:android 音视频播放 SDK,...FFmpeg-Android音视频- android 音视频播放 SDK,几句代码即可实现音视频播放功能~ 1、Usage Gradle: implementation 'ywl.ywl5320:wlmed...
  • Android音视频开发详解发布时间:2018-05-16 12:43,浏览次数:629, 标签:Android概要:1、课程从FFmpeg编译、AndroidStudio创建C++项目、Java调用C++、C++调用Java、C++多线程和线程锁、C++队列、OpenSLES集成、...
  • 一、Android音视频硬解码篇: 1,音视频基础知识 2,音视频硬解码流程:封装基础解码框架 3,音视频播放:音视频同步 4,音视频解封和封装:生成一个MP4 二、使用OpenGL渲染视频画面篇 1,初步了解OpenGL ES 2...
  • Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音);AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览、拍照功能 Android 音视频开发(三) – Camera2 实现预览、拍照功能 Android 音视频开发...
  • Android音视频开发学习

    2019-07-19 17:18:36
    本文原址链接:https://www.cnblogs.com/renhui/p/7452572.html 初级入门篇: Android 音视频开发(一) : 通过三种方式绘制图片 Android 音视频开发...
  • Android 音视频入门

    2018-06-11 02:10:41
    Android 音视频从入门到提高 —— 任务列表》 1. 在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView,SurfaceView,自定义 View 2. 在 Android 平台使用 AudioRecord 和 AudioTrack API 完成...
  • Android 音视频录制程序 视频h.263 音频 AMR_NB
  • Android音视频开发全套

    2019-12-19 10:32:16
    Android平台音视频开发全套,涉及:FFmpeg软解码解码、Mediacodec硬解码编码、Openssl音频播放、OpenGL ES视频渲染、RTMP推流等核心重要知识点。
  • [Cmake-Android音视频]SDK,NDK基本介绍 [Cmake-Android音视频]NDK-r14b编译ffmpeg3.4支持neon,硬解码 [Cmake-Android音视频]创建支持ffmpeg3.4的项目 [Cmake-Android音视频]ffmpeg3.4实现解封装 ...
  • Android音视频开发》— Android书籍

    千次阅读 2020-11-25 12:43:12
    文章目录第1章 音视频基础知识 1第2章 常用的系统播放器MediaPlayer 8第3章 管理调度的服务者...近年来,直播、短视频行业的相关业务发展迅猛,《Android音视频开发》主要介绍其中涉及的Android音视频开发相关技术
  • 1. 深入理解Android音视频同步机制(一)概述 2. 深入理解Android音视频同步机制(二)ExoPlayer的avsync逻辑 3. 深入理解Android音视频同步机制(三)NuPlayer的avsync逻辑 4. 深入理解Android音视频同步机制...
  • 直播间搭建Android 音视频开发的音视频基础知识本文你可以了解到 作为开篇的文章,我们先来看看音视频由什么构成的,以及一些常见的术语和概念。 一、视频是什么? 动画书 不知道大家小时候是否玩过一种动画...
  • Android 音视频开发学习之路

    千次阅读 2019-02-15 14:31:13
    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的。只能通过一点点的学习和积累把这块的知识串联积累起来。 初级入门篇: Android 音视频开发(一) : 通过三种方式绘制...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,243
精华内容 4,497
关键字:

android音视频