安卓音视频整理(二)—— 音视频编解码

这是关于安卓音视频的一个系列文章,大家可以从这里随意跳跃:

0.安卓音视频整理

1.安卓音视频整理(一)—— 音频模块

2.安卓音视频整理(二)—— 音视频编解码

3.安卓音视频整理(三)—— 图像模块

4.安卓音视频整理(四)—— 音视频播放器

5.安卓音视频整理(五)—— MediaSDK的封装

摘要:多媒体视频不光包括了音频部分,还有图像部分。一个视频文件的录制和播放,伴随着音视频的编解码过程,而编解码也是多媒体开发中很重要的一个知识点。上2篇文章我们对安卓音频的知识点进行了梳理。在这篇文章中,我会基于之前整理的内容,同时重点围绕多媒体视频来叙述。

1.容器(封装)格式与编码方式

视频是现在电脑中多媒体系统中的重要一环。为了适应储存视频的需要,人们设定了不同的视频文件格式来把视频和音频放在一个文件中,以方便同时回放。一个视频文件实际上是在一个文件里面包含了编码过的视频轨道、编码过的音频轨道,还有各种文件描述信息。

1.1 编码格式

编码格式就是指通过特定的压缩技术,将某个视频格式的文件转换成另一种视频格式文件存在的方法。音视频编码格式分为视频编码格式和音频编码格式。

视频编码的主要作用是将视频像素数据(RGB,YUV等)压缩成为视频码流,从而降低视频的数据量。不同编码方法的区别主要是压缩算法的不同。视频编码的目的主要是压缩数据体积。

在这里插入图片描述

视频中常用的编码标准有H.26X系列MPEG系列,移动端使用最多的是MP4视频文件,主要使用H.264 AVC编码格式。

有关视频编码的原理可以看一看 雷霄骅 的这篇文章:视频压缩编码和音频压缩编码的基本原理

音频编码的主要作用是将音频采样数据(PCM 等)压缩成为音频码流,从而降低音频的数据量,偏于存储和传输,跟视频编码的作用类似。

音频中常用的编码格式有以下:

① WAV

PCM(脉冲编码调制)是 Pulse Code Modulation 的缩写。WAV 编码的一种实现(有多种实现方式,但是都不会进行压缩操作)就是在 PCM 数据格式的前面加上 44 字节,分别用来描述 PCM 的采样率、声道数、数据格式等信息。

特点:音质非常好,大量软件都支持。

适用场合:多媒体开发的中间文件、保存音乐和音效素材。

② MP3(有损)

MP3 具有不错的压缩比,使用 LAME 编码(MP3 编码格式的一种实现)的中高码率的 MP3 文件,听感上非常接近源 WAV 文件,当然在不同的应用场景下,应该调整合适的参数以达到最好的效果。

特点:音质在 128Kbit/s 以上表现还不错,压缩比比较高,大量软件和硬件都支持,兼容性好。

适用场合:高比特率下对兼容性有要求的音乐欣赏。

③ AAC(有损)

AAC 是新一代的音频有损压缩技术,它通过一些附加的编码技术(比如 PS、SBR 等),衍生出了 LC-AAC、HE-AAC、HE-AAC v2 三种主要的编码格式。

LC-AAC 是比较传统的 AAC,相对而言,其主要应用于中高码率场景的编码(≥80Kbit/s);

HE-AAC(相当于AAC+SBR)主要应用于中低码率场景的编码(≤80Kbit/s);

而新近推出的 HE-AAC v2(相当于AAC+SBR+PS)主要应用于低码率场景的编码(≤48Kbit/s)。事实上大部分编码器都设置为 ≤48Kbit/s 自动启用 PS 技术,而 >48Kbit/s 则不加PS,相当于普通的 HE-AAC。

特点:在小于 128Kbit/s 的码率下表现优异,并且多用于视频中的音频编码。

适用场合:128Kbit/s 以下的音频编码,多用于视频中音频轨的编码。

④ Ogg(有损)

Ogg 是一种非常有潜力的编码,在各种码率下都有比较优秀的表现,尤其是在中低码率场景下。Ogg 除了音质好之外,还是完全免费的,这为 Ogg 获得更多的支持打好了基础。Ogg 有着非常出色的算法,可以用更小的码率达到更好的音质,128Kbit/s 的 Ogg 比 192Kbit/s 甚至更高码率的 MP3 还要出色。但目前因为还没有媒体服务软件的支持,因此基于 Ogg 的数字广播还无法实现。Ogg 目前受支持的情况还不够好,无论是软件上的还是硬件上的支持,都无法和 MP3 相提并论。

特点:可以用比 MP3 更小的码率实现比 MP3 更好的音质,高中低码率下均有良好的表现,兼容性不够好,流媒体特性不支持。

适用场合:语音聊天的音频消息场景。

⑤ APE(无损)

APE 是流行的数字音乐无损压缩格式之一,因出现较早,在全世界特别是中国大陆有着广泛的用户群。与 MP3 这类有损压缩格式不可逆转地删除(人耳听力不敏感的)数据以缩减源文件体积不同,APE 这类无损压缩格式,是以更精炼的记录方式来缩减体积,还原后数据与源文件一样,从而保证了文件的完整性。

APE 由软件 Monkey’s audio 压制得到,开发者为 Matthew T. Ashland,源代码开放,因其界面上有只 “猴子” 标志而出名。相较同类文件格式 FLAC,ape 有查错能力但不提供纠错功能,以保证文件的无损和纯正;其另一个特色是压缩率约为 55%,比 FLAC 高,体积大概为原 CD 的一半,便于存储。

APE 作为一种无损压缩音频格式,通过 Monkey’s Audio 这个软件可以将庞大的 WAV 音频文件压缩为 APE,,体积虽然变小了,但音质和原来一样。通过 Monkey’s Audio 解压缩还原以后得到的 WAV 文件可以做到与压缩前的源文件完全一致。所以 APE 被誉为“无损音频压缩格式”,Monkey’'s Audio 被誉为“无损音频压缩软件”。

简单来讲,APE 压缩与 WinZip 或 WinRAR 这类专业数据压缩软件压缩原理类似,只是 APE 等无损压缩数字音乐之后的 APE 音频文件是可以直接被播放的。APE 的压缩速率是动态的,压缩时只压缩可被压缩部分,不能被压缩的部分还是会保留下来。

⑥ FLAC(无损)

FLAC 中文可解释为无损音频压缩编码。FLAC 是一套著名的自由音频压缩编码,其特点是无损压缩。不同于其他有损压缩编码如 MP3 及 AAC,它不会破坏任何原有的音频资讯,所以可以还原音乐光盘音质。2012 年以来它已被很多软件及硬件音频产品(如 CD 等)所支持.

FLAC 与 MP3 不同,MP3 是音频压缩编码,但 FLAC 是无损压缩,也就是说音频以 FLAC 编码压缩后不会丢失任何信息,将 FLAC 件还原为 WAV 文件后,与压缩前的 WAV 文件内容相同。这种压缩与 ZIP 的方式类似,但 FLAC 的压缩比率大于 ZIP 和 RAR,因为 FLAC 是专门针对 PCM 音频的特点设计的压缩方式。而且可以使用播放器直接播放 FLAC 压缩的文件,就象通常播放你的 MP3 文件一样(近几年已经有许多汽车播放器和家用音响设备支持 FLAC,在 FLAC 的网站上你可以找到这些设备厂家的链接)。

有关音频编码的内容可以看一看这篇文章:常见音频编码格式解析

1.2 容器(封装)格式

我们知道,视频文件需要同时包含音频数据和图像数据,有时还会加入一些其它的数据,例如字幕。这些数据可能会被不同的程序或者硬件处理,但是当它们要进行传输或者存储的时候,这三种数据通常是被封装在一起的,这个存放封装数据的东西,就可以称为 容器,也就是我们日常所知道的 视频 文件格式。例如常见的*.mpg, *.avi, *.mov, *.mp4, *.rm, *.ogg, *.tta等格式,都可以说是一种视频容器。视频容器的格式会关系到这个视频文件的可扩展性。

在这里插入图片描述

2. 音视频 MPEG 介绍

MPEG 是Moving Picture Experts Group的简称。这个名字本来的含义是指一个研究视频和音频编码标准的小组。现在我们所说的MPEG泛指又该小组制定的一系列视频编码标准。该小组于 1988年组成,至今已经制定了MPEG-1、MPEG-2、MPEG-3、MPEG-4、MPEG-7等多个标准,MPEG-21正在制定中。

2.1 AAC音频

AAC是高级音频编码(Advanced Audio Coding)的缩写,出现于1997年,最初是基于MPEG-2的音频编码技术。由Fraunhofer IIS、Dolby Laboratories、AT&T、Sony等公司共同开发,目的是取代MP3格式。

AAC是新一代的音频有损压缩技术,它通过一些附加的编码技术(比如PS,SBR等),衍生出了 LC-AAC, HE-AAC , HE-AACv2 三种主要的编码。

  • LC-AAC 就是比较传统的AAC,相对而言,主要用于中高码率(>=80Kbps);
  • HE-AAC (相当于AAC+SBR)主要用于中低码(<=80Kbps);
  • HE-AACv2 (相当于AAC+SBR+PS)为最新推出,主要用于低码率(<=48Kbps)

目前使用最多的是 LC-AACHE-AAC (适合低码率)。流行的Nero AAC编码程序只支持LC,HE,HEv2这三种规格,编码后的AAC音频,规格显示都是LC。HE其实就是AAC(LC)+SBR技术,HEv2就是AAC(LC)+SBR+PS技术;事实上大部分编码器设成<=48Kbps自动启用PS技术,而>48Kbps就不加PS,就相当于普通的HE-AAC。

扩展名:.m4a, .m4b, .m4p, .m4v, .m4r, .3gp, .mp4, .aac
互联网媒体类型:audio/aac, audio/aacp, audio/3gpp, audio/3gpp2,audio/mp4, audio/MP4A-LATM, audio/mpeg4-generic
格式:有损数据压缩
延伸自:MPEG-2 音频
标准:ISO/IEC 13818-7(MPEG-2第7部), ISO/IEC 14496-3(MPEG-4第3部)

AAC格式的主要扩展名有三种:

  • AAC - 使用MPEG-2 Audio Transport Stream( ADTS,参见MPEG-2 )容器,区别于使用MPEG-4容器的MP4/M4A格式,属于传统的AAC编码(FAAC默认的封装,但FAAC亦可输出 MPEG-4 封装的AAC)
  • MP4 - 使用了MPEG-4 Part 14(第14部分)的简化版即3GPP Media Release 6 Basic (3gp6,参见3GP ) 进行封装的AAC编码(Nero AAC 编码器仅能输出MPEG-4封装的AAC);
  • M4A - 为了区别纯音频MP4文件和包含视频的MP4文件而由苹果(Apple)公司使用的扩展名,Apple iTunes 对纯音频MP4文件采用了".M4A"命名。M4A的本质和音频MP4相同,故音频MP4文件亦可直接更改扩展名为M4A。

作为一种高压缩比的音频压缩算法,AAC压缩比通常为18:1,也有资料说为20:1,远胜mp3; 在音质方面,由于采用多声道,和使用低复杂性的描述方式,使其比几乎所有的传统编码方式在同规格的情况下更胜一筹。不过直到2006年, 使用这一格式储存音乐的并不多。

2.1.1 AAC音频文件格式

AAC的音频文件格式有 ADIFADTS & LATM

  • ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。

  • ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。

  • LATM 的全称为“Low-overhead MPEG-4 Audio TransportMultiplex”(低开销音频传输复用),是MPEG-4 AAC制定的一种高效率的码流传输方式,MPEG-2 TS流也采用LATM作为AAC音频码流的封装格式之一。

简单的说,ADTS可以在任意帧解码,也就是说它每一帧都有头信息。ADIF只有一个统一的头,所以必须得到所有的数据后解码。且这两种的header的格式也是不同的,目前一般编码后的和抽取出的都是ADTS格式的音频流。

2.1.2 AAC文件处理流程

  • (1) 判断文件格式,确定为ADIF或ADTS
  • (2) 若为ADIF,解ADIF头信息,跳至第6步。
  • (3) 若为ADTS,寻找同步头。
  • (4) 解ADTS帧头信息。
  • (5) 若有错误检测,进行错误检测。
  • (6) 解块信息。
  • (7) 解元素信息。

2.2 H.26x介绍

H.26x 有H.261,H.262,H.263, H.263v2以及H.264,H.261基本上已经不再使用。这里重点讲一下H.264。

H.264/AVC 是一种视频高压缩技术,同时称为MPEG-4 AVC,或MPEG-4 Part10。 H.264 可工作于多种速率,广泛应用于Internet/intranet上的多媒体流服务、视频点播、可视游戏、低码率移动多媒体通信 (视频 手机等)、交互式多媒体应用、实时多媒体监控、数字电视与演播电视和虚拟视频会议等,大有在上述领域一统天下的趋势,有非常广泛的开发和应用前景。H.264集中体现了当今国际视频编码解码技术的最新成果。在相同的重建图像质量下,H.264比其他视频压缩编码具有更高的压缩比、 更好的IP和无线网络信道适应性。

H.264 标准分为三档:基本档次主要档次(可用于SDTV、HDTV和DVD等);以及扩展档次(用于网络的视频流)

2.2.1 序列

在H264中,图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。一个序列的第一个图像叫做IDR图像(立即刷新图像),IDR图像都是I帧图像。H.264引入IDR图像是为了解码的重同步,当解码器解码到IDR图像时,立即将参考帧队列清空,将已编码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会,IDR图像之后的图像永远不会使用IDR之前的图片的数据来编码。

一个序列就是一段内容差异不太大的图像编码后生产的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以遍一个I镇,然后一个P帧,B帧了。当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3,4个P帧。

一个 I 帧可以不依赖其他帧就解码出一幅完整的图像,而 P 帧、B 帧不行。P 帧需要依赖视频流中排在它前面的帧才能解码出图像。B 帧则需要依赖视频流中排在它前面或后面的帧才能解码出图像。

2.2.2 DTS & PTS

DTS:解码时间戳,这个事件戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
PTS:显示时间戳,这个事件戳用来告诉播放器该什么时候显示这一帧的数据。

当视频流中没有 B 帧时,通常 DTS 和 PTS 的顺序是一致的。但如果有 B 帧时,解码顺序和播放顺序就不一致了。

比如一个视频中,帧的显示顺序是:I B B P,现在我们需要在解码 B 帧时知道 P 帧中信息,因此这几帧在视频流中的顺序可能是:I P B B,这时候就体现出每帧都有 DTS 和 PTS 的作用了。它们的顺序大概如下:

在这里插入图片描述

3. 安卓 MP4 视频文件的编解码举例

以最多见的MP4为例,简单整理一下安卓开发中的视频编解码流程。下图可以看作是MP4文件的录制与播放流程。我将从这2个图对其展开叙述。

视频录制流程:

在这里插入图片描述

视频播放流程:

在这里插入图片描述

应该是能够看得懂我画的是什么,要是看不懂那就过,往下看。

3.1 MP4 文件编码录制

先来看一下MP4文件录制流程的简单框图,为了不影响看图,我把图再贴一下在下面。从图中可以看出,MP4录制的过程包含了3个重要部分:

  • 声音的录制
  • 图像的录制
  • 声音与图像的合并

在声音的录制、图像的录制中就分别涉及到了音频、视频的编码。图中音频数据需要被编码成AAC;图像数据需要被编码成H264。然后通过安卓中的 MediaMuxer 进行音视频的合并,合并中同步音频和图像2者的时间戳,这样一个MP4文件就诞生了。

关于MP4的录制,重点也就是 音频数据编码到AAC图像数据编码到H264以及音视频数据合并成MP4 这3块内容。安卓获取声音数据的方法我已经在上一篇文章《安卓音视频整理(一)—— 音频模块》里讲过了,而图像数据的获取我会在下一篇文章《安卓音视频整理(三)—— 图像模块》里讲解。本篇文章重点讲解音视频的编解码。编码使用到的编码器 为MediaCodec,对此我已做好了封装。一个录制MP4的过程,我们有这3个线程。分别是 AudioEncodecThread;VideoEncodecThread;EGLMediaThread(预览)。以下是编码初始化代码。

    private void initMediaEncodec(String savePath, int width, int height, int sampleRate, int channelCount) {
        try {
            LogAAR.e("initMediaEncodec:-->savePath="+savePath+",size: w * h ="+width+"*"+height+",sampleRate="+sampleRate+",channelCount="+channelCount);
            mediaMuxer = new MediaMuxer(savePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            initVideoEncodec(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
            initAudioEncodec(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, channelCount);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

代码中,初始化时已经将MP4需要的编码格式传进编码器,音频和视频分别是MediaFormat.MIMETYPE_AUDIO_AA 和 MediaFormat.MIMETYPE_VIDEO_AVC。这样编码器会按照我们所传参数分别对音频、视频编码进行初始化。以下是分别对音频、视频编码器进行初始化代码。

private void initVideoEncodec(String mimeType, int width, int height) {
        try {
            videoBufferinfo = new MediaCodec.BufferInfo();
            videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
            videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 4);
            videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
            videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            videoEncodec = MediaCodec.createEncoderByType(mimeType);
            videoEncodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            surface = videoEncodec.createInputSurface();

        } catch (IOException e) {
            e.printStackTrace();
            videoEncodec = null;
            videoFormat = null;
            videoBufferinfo = null;
        }

    }

    private void initAudioEncodec(String mimeType, int sampleRate, int channelCount) {
        try {
            audioBufferinfo = new MediaCodec.BufferInfo();
            audioFormat = MediaFormat.createAudioFormat(mimeType, sampleRate, channelCount);
            audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, sampleRate*channelCount*16);
            audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 96000);

            audioEncodec = MediaCodec.createEncoderByType(mimeType);
            audioEncodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

        } catch (IOException e) {
            e.printStackTrace();
            audioBufferinfo = null;
            audioFormat = null;
            audioEncodec = null;
        }
    }

接下来分开讲 音频数据编码到AAC图像数据编码到H264以及音视频数据合并成MP4 的内容。

在这里插入图片描述

3.1.1 音频数据编码到AAC

音频数据编码到AAC,过程是需要将录音机得到的音频PCM数据实时传入 MediaCodec,由MediaCodec完成音频数据编码得到AAC的过程,音频数据被编码为AAC,我们再将音频的AAC数据传入到 MediaMuxer。于此同时,视频数据的编码以及写入MediaMuxer也在同步进行。音频编码线程的代码如下:


public void putPCMData(byte[] buffer, int size) {
        if (audioEncodecThread != null && !audioEncodecThread.isExit && buffer != null && size > 0) {
            if (audioEncodec!=null){
                try {
                    int inputBufferindex = audioEncodec.dequeueInputBuffer(0);
                    if (inputBufferindex >= 0) {
                        ByteBuffer byteBuffer = audioEncodec.getInputBuffers()[inputBufferindex];
                        byteBuffer.clear();
                        byteBuffer.put(buffer);
                        long result = System.nanoTime() ;
                        long time = (result - audioEncodecThread.baseTimeStamp ) / 1000;

                        audioEncodec.queueInputBuffer(inputBufferindex, 0, size, time, 0);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
static class AudioEncodecThread extends Thread {

        private WeakReference<YorkBaseMediaEncoder> encoder;
        private boolean isExit;

        private MediaCodec audioEncodec;
        private MediaCodec.BufferInfo bufferInfo;
        private MediaMuxer mediaMuxer;

        private int audioTrackIndex = -1;
        long pts;


        public AudioEncodecThread(WeakReference<YorkBaseMediaEncoder> encoder) {
            this.encoder = encoder;
            audioEncodec = encoder.get().audioEncodec;
            bufferInfo = encoder.get().audioBufferinfo;
            mediaMuxer = encoder.get().mediaMuxer;
            audioTrackIndex = -1;
        }

        @Override
        public void run() {
            super.run();
            pts = 0;
            isExit = false;
            audioEncodec.start();
            while (true) {
                if (isExit) {
                    audioEncodec.stop();
                    audioEncodec.release();
                    audioEncodec = null;
                    encoder.get().audioExit = true;
                    if (encoder.get().videoExit) {
                        mediaMuxer.stop();
                        mediaMuxer.release();
                        mediaMuxer = null;
                    }
                    break;
                }
                if (audioEncodec==null){
                    isExit=true;
                    break;
                }
                int outputBufferIndex = audioEncodec.dequeueOutputBuffer(bufferInfo, 0);
                if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    if (mediaMuxer != null) {
                        audioTrackIndex = mediaMuxer.addTrack(audioEncodec.getOutputFormat());
                        if (encoder.get().videoEncodecThread.videoTrackIndex != -1) {
                            mediaMuxer.start();
                            encoder.get().encodecStart = true;
                        }
                    }
                } else {
                    while (outputBufferIndex >= 0) {
                        if (encoder.get().encodecStart) {
                            ByteBuffer outputBuffer = audioEncodec.getOutputBuffers()[outputBufferIndex];
                            outputBuffer.position(bufferInfo.offset);
                            outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                            bufferInfo.presentationTimeUs = getPTSUs();
                            mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo);
                        }
                        audioEncodec.releaseOutputBuffer(outputBufferIndex, false);
                        outputBufferIndex = audioEncodec.dequeueOutputBuffer(bufferInfo, 0);
                    }
                }
            }
        }
        private long baseTimeStamp = -1;
        private long getPTSUs() {
            long result = System.nanoTime() ;
            long time = (result - baseTimeStamp ) / 1000;
            if (time < 0){
                return  0;
            }
            return time;
        }
        public void exit() {
            isExit = true;
        }
    }

3.1.2 图像数据编码到H264

图像数据编码到H264,原理是将Camera得到的图像数据实时传入 MediaCodec,由 MediaCodec 进行编码得到H264数据,当数据被编码得到了H264数据,我们再将H264数据传入到 MediaMuxer。图像数据编码线程的代码如下:

 static class VideoEncodecThread extends Thread {
        private WeakReference<YorkBaseMediaEncoder> encoder;
        private boolean isExit;
        private MediaCodec videoEncodec;
        private MediaCodec.BufferInfo videoBufferinfo;
        private MediaMuxer mediaMuxer;
        private int videoTrackIndex = -1;


        public VideoEncodecThread(WeakReference<YorkBaseMediaEncoder> encoder) {
            this.encoder = encoder;
            videoEncodec = encoder.get().videoEncodec;
            videoBufferinfo = encoder.get().videoBufferinfo;
            mediaMuxer = encoder.get().mediaMuxer;
            videoTrackIndex = -1;
        }

        @Override
        public void run() {
            super.run();
            videoTrackIndex = -1;
            isExit = false;
            videoEncodec.start();
            while (true) {
                if (isExit) {
                    videoEncodec.stop();
                    videoEncodec.release();
                    videoEncodec = null;
                    encoder.get().videoExit = true;
                    if (encoder.get().audioExit) {
                        mediaMuxer.stop();
                        mediaMuxer.release();
                        mediaMuxer = null;
                    }
                    if (encoder.get()!=null&&encoder.get().onMediaInfoListener!=null){
                        encoder.get().onMediaInfoListener.onMediaRecordComple();
                    }
                    break;
                }
                if (videoEncodec==null){
                    isExit=true;
                    break;
                }
                int outputBufferIndex = videoEncodec.dequeueOutputBuffer(videoBufferinfo, 0);

                if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    videoTrackIndex = mediaMuxer.addTrack(videoEncodec.getOutputFormat());
                    if (encoder.get().audioEncodecThread.audioTrackIndex != -1) {
                        mediaMuxer.start();
                        encoder.get().encodecStart = true;
                    }
                } else {
                    while (outputBufferIndex >= 0) {
                        if (encoder.get().encodecStart) {
                            ByteBuffer outputBuffer = videoEncodec.getOutputBuffers()[outputBufferIndex];
                            outputBuffer.position(videoBufferinfo.offset);
                            outputBuffer.limit(videoBufferinfo.offset + videoBufferinfo.size);
                            videoBufferinfo.presentationTimeUs = getPTSUs();
                            mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, videoBufferinfo);

                            if (encoder.get()!=null&&encoder.get().onMediaInfoListener != null) {
                                encoder.get().onMediaInfoListener.onMediaTime((int) (videoBufferinfo.presentationTimeUs / 1000));
                            }
                        }
                        videoEncodec.releaseOutputBuffer(outputBufferIndex, false);
                        outputBufferIndex = videoEncodec.dequeueOutputBuffer(videoBufferinfo, 0);
                    }
                }
            }
        }
        private long baseTimeStamp = -1;
        private long getPTSUs() {
            long result = System.nanoTime() ;
            long time = (result - baseTimeStamp ) / 1000;
            if (time < 0){
                return  0;
            }
            return time;
        }

        public void exit() {
            isExit = true;
        }

    }

3.1.3 音视频数据合并成MP4

       mediaMuxer = new MediaMuxer(savePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

如上,在初始化时,已经设置了视频的文件保存路径 savePath, 以及其输出格式 MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 。经过上面2个线程的同时写入。此时的 savePath 路径的MP4文件中已经有数据了。
使用 MediaMuxer 合并音视频过程有一个注意点,那就是保证音频视频时间戳的对应,否则将导致音视频不同步,或者程序报错。

音视频数据合并成MP4主要就是上面代码中的:

    mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo);

   mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, videoBufferinfo);

这2步,分别将已经编码好的音频轨道数据、和视频轨道数据传入了MediaMuxer。MediaMuxer进行音视频合并得到的也就是我们想要的.mp4文件。

完整代码:YorkBaseMediaEncoder.java

package com.york.opensdk.opengl.encodec;

import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.view.Surface;
import com.york.opensdk.opengl.egl.EglHelper;
import com.york.opensdk.opengl.egl.YorkEGLSurfaceView;
import com.york.opensdk.utils.LogAAR;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;

import javax.microedition.khronos.egl.EGLContext;

public abstract class YorkBaseMediaEncoder {

    private Surface surface;
    private EGLContext eglContext;

    private int width;
    private int height;
    private MediaCodec videoEncodec;
    private MediaFormat videoFormat;
    private MediaCodec.BufferInfo videoBufferinfo;
    private MediaCodec audioEncodec;
    private MediaFormat audioFormat;
    private MediaCodec.BufferInfo audioBufferinfo;
    private MediaMuxer mediaMuxer;
    private boolean encodecStart;
    private boolean audioExit;
    private boolean videoExit;

    private yorkEGLMediaThread yorkEGLMediaThread;
    private VideoEncodecThread videoEncodecThread;
    private AudioEncodecThread audioEncodecThread;


    private YorkEGLSurfaceView.YorkGLRender yorkGLRender;

    public final static int RENDERMODE_WHEN_DIRTY = 0;
    public final static int RENDERMODE_CONTINUOUSLY = 1;
    private int mRenderMode = RENDERMODE_CONTINUOUSLY;

    private OnMediaInfoListener onMediaInfoListener;


    public YorkBaseMediaEncoder(Context context) {
    }

    public void setRender(YorkEGLSurfaceView.YorkGLRender yorkGLRender) {
        this.yorkGLRender = yorkGLRender;
    }

    public void setmRenderMode(int mRenderMode) {
        if (yorkGLRender == null) {
            throw new RuntimeException("must set render before");
        }
        this.mRenderMode = mRenderMode;
    }

    public void setOnMediaInfoListener(OnMediaInfoListener onMediaInfoListener) {
        this.onMediaInfoListener = onMediaInfoListener;
    }

    public void initEncodec(EGLContext eglContext, String savePath, int width, int height, int sampleRate, int channelCount) {
        this.width = width;
        this.height = height;
        this.eglContext = eglContext;
        initMediaEncodec(savePath, width, height, sampleRate, channelCount);
    }

    public void startRecord() {
        if (surface != null && eglContext != null) {
            audioExit = false;
            videoExit = false;
            encodecStart = false;
            yorkEGLMediaThread = new yorkEGLMediaThread(new WeakReference<YorkBaseMediaEncoder>(this));
            videoEncodecThread = new VideoEncodecThread(new WeakReference<YorkBaseMediaEncoder>(this));
            audioEncodecThread = new AudioEncodecThread(new WeakReference<YorkBaseMediaEncoder>(this));
            audioEncodecThread.baseTimeStamp=System.nanoTime();
            videoEncodecThread.baseTimeStamp=System.nanoTime();
            yorkEGLMediaThread.isCreate = true;
            yorkEGLMediaThread.isChange = true;
            yorkEGLMediaThread.start();
            videoEncodecThread.start();
            audioEncodecThread.start();
            if (onMediaInfoListener!=null){
                onMediaInfoListener.onMediaRecordStart();
            }
        }
    }

    public void stopRecord() {
        if (yorkEGLMediaThread != null && videoEncodecThread != null && audioEncodecThread != null) {
            videoEncodecThread.exit();
            audioEncodecThread.exit();
            yorkEGLMediaThread.onDestory();
            audioEncodecThread.baseTimeStamp=-1;
            videoEncodecThread.baseTimeStamp=-1;
            videoEncodecThread = null;
            yorkEGLMediaThread = null;
            audioEncodecThread = null;
        }
    }

    private void initMediaEncodec(String savePath, int width, int height, int sampleRate, int channelCount) {
        try {
            LogAAR.e("initMediaEncodec:-->savePath="+savePath+",size: w * h ="+width+"*"+height+",sampleRate="+sampleRate+",channelCount="+channelCount);
            mediaMuxer = new MediaMuxer(savePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            initVideoEncodec(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
            initAudioEncodec(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, channelCount);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    private void initVideoEncodec(String mimeType, int width, int height) {
        try {
            videoBufferinfo = new MediaCodec.BufferInfo();
            videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
            videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 4);
            videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
            videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            videoEncodec = MediaCodec.createEncoderByType(mimeType);
            videoEncodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            surface = videoEncodec.createInputSurface();

        } catch (IOException e) {
            e.printStackTrace();
            videoEncodec = null;
            videoFormat = null;
            videoBufferinfo = null;
        }

    }

    private void initAudioEncodec(String mimeType, int sampleRate, int channelCount) {
        try {
            audioBufferinfo = new MediaCodec.BufferInfo();
            audioFormat = MediaFormat.createAudioFormat(mimeType, sampleRate, channelCount);
            audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, sampleRate*channelCount*16);
            audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 96000);

            audioEncodec = MediaCodec.createEncoderByType(mimeType);
            audioEncodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

        } catch (IOException e) {
            e.printStackTrace();
            audioBufferinfo = null;
            audioFormat = null;
            audioEncodec = null;
        }
    }

    public void putPCMData(byte[] buffer, int size) {
        if (audioEncodecThread != null && !audioEncodecThread.isExit && buffer != null && size > 0) {
            if (audioEncodec!=null){
                try {
                    int inputBufferindex = audioEncodec.dequeueInputBuffer(0);
                    if (inputBufferindex >= 0) {
                        ByteBuffer byteBuffer = audioEncodec.getInputBuffers()[inputBufferindex];
                        byteBuffer.clear();
                        byteBuffer.put(buffer);
                        long result = System.nanoTime() ;
                        long time = (result - audioEncodecThread.baseTimeStamp ) / 1000;

                        audioEncodec.queueInputBuffer(inputBufferindex, 0, size, time, 0);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

    static class yorkEGLMediaThread extends Thread {
        private WeakReference<YorkBaseMediaEncoder> encoder;
        private EglHelper eglHelper;
        private Object object;

        private boolean isExit = false;
        private boolean isCreate = false;
        private boolean isChange = false;
        private boolean isStart = false;

        public yorkEGLMediaThread(WeakReference<YorkBaseMediaEncoder> encoder) {
            this.encoder = encoder;
        }

        @Override
        public void run() {
            super.run();
            isExit = false;
            isStart = false;
            object = new Object();
            eglHelper = new EglHelper();
            eglHelper.initEgl(encoder.get().surface, encoder.get().eglContext);

            while (true) {
                if (isExit) {
                    release();
                    break;
                }

                if (isStart) {
                    if (encoder.get().mRenderMode == RENDERMODE_WHEN_DIRTY) {
                        synchronized (object) {
                            try {
                                object.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    } else if (encoder.get().mRenderMode == RENDERMODE_CONTINUOUSLY) {
                        try {
                            Thread.sleep(1000 / 60);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        throw new RuntimeException("mRenderMode is wrong value");
                    }
                }
                onCreate();
                onChange(encoder.get().width, encoder.get().height);
                onDraw();
                isStart = true;
            }

        }

        private void onCreate() {
            if (isCreate && encoder.get().yorkGLRender != null) {
                isCreate = false;
                encoder.get().yorkGLRender.onSurfaceCreated();
            }
        }

        private void onChange(int width, int height) {
            if (isChange && encoder.get().yorkGLRender != null) {
                isChange = false;
                encoder.get().yorkGLRender.onSurfaceChanged(width, height);
            }
        }

        private void onDraw() {
            if (encoder.get().yorkGLRender != null && eglHelper != null) {
                encoder.get().yorkGLRender.onDrawFrame();
                if (!isStart) {
                    encoder.get().yorkGLRender.onDrawFrame();
                }
                eglHelper.swapBuffers();

            }
        }

        private void requestRender() {
            if (object != null) {
                synchronized (object) {
                    object.notifyAll();
                }
            }
        }

        public void onDestory() {
            isExit = true;
            requestRender();
        }

        public void release() {
            if (eglHelper != null) {
                eglHelper.destoryEgl();
                eglHelper = null;
                object = null;
                encoder = null;
            }
        }
    }

    static class VideoEncodecThread extends Thread {
        private WeakReference<YorkBaseMediaEncoder> encoder;
        private boolean isExit;
        private MediaCodec videoEncodec;
        private MediaCodec.BufferInfo videoBufferinfo;
        private MediaMuxer mediaMuxer;
        private int videoTrackIndex = -1;


        public VideoEncodecThread(WeakReference<YorkBaseMediaEncoder> encoder) {
            this.encoder = encoder;
            videoEncodec = encoder.get().videoEncodec;
            videoBufferinfo = encoder.get().videoBufferinfo;
            mediaMuxer = encoder.get().mediaMuxer;
            videoTrackIndex = -1;
        }

        @Override
        public void run() {
            super.run();
            videoTrackIndex = -1;
            isExit = false;
            videoEncodec.start();
            while (true) {
                if (isExit) {
                    videoEncodec.stop();
                    videoEncodec.release();
                    videoEncodec = null;
                    encoder.get().videoExit = true;
                    if (encoder.get().audioExit) {
                        mediaMuxer.stop();
                        mediaMuxer.release();
                        mediaMuxer = null;
                    }
                    if (encoder.get()!=null&&encoder.get().onMediaInfoListener!=null){
                        encoder.get().onMediaInfoListener.onMediaRecordComple();
                    }
                    break;
                }
                if (videoEncodec==null){
                    isExit=true;
                    break;
                }
                int outputBufferIndex = videoEncodec.dequeueOutputBuffer(videoBufferinfo, 0);

                if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    videoTrackIndex = mediaMuxer.addTrack(videoEncodec.getOutputFormat());
                    if (encoder.get().audioEncodecThread.audioTrackIndex != -1) {
                        mediaMuxer.start();
                        encoder.get().encodecStart = true;
                    }
                } else {
                    while (outputBufferIndex >= 0) {
                        if (encoder.get().encodecStart) {
                            ByteBuffer outputBuffer = videoEncodec.getOutputBuffers()[outputBufferIndex];
                            outputBuffer.position(videoBufferinfo.offset);
                            outputBuffer.limit(videoBufferinfo.offset + videoBufferinfo.size);
                            videoBufferinfo.presentationTimeUs = getPTSUs();
                            mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, videoBufferinfo);

                            if (encoder.get()!=null&&encoder.get().onMediaInfoListener != null) {
                                encoder.get().onMediaInfoListener.onMediaTime((int) (videoBufferinfo.presentationTimeUs / 1000));
                            }
                        }
                        videoEncodec.releaseOutputBuffer(outputBufferIndex, false);
                        outputBufferIndex = videoEncodec.dequeueOutputBuffer(videoBufferinfo, 0);
                    }
                }
            }
        }
        private long baseTimeStamp = -1;
        private long getPTSUs() {
            long result = System.nanoTime() ;
            long time = (result - baseTimeStamp ) / 1000;
            if (time < 0){
                return  0;
            }
            return time;
        }

        public void exit() {
            isExit = true;
        }

    }

    static class AudioEncodecThread extends Thread {

        private WeakReference<YorkBaseMediaEncoder> encoder;
        private boolean isExit;

        private MediaCodec audioEncodec;
        private MediaCodec.BufferInfo bufferInfo;
        private MediaMuxer mediaMuxer;

        private int audioTrackIndex = -1;
        long pts;


        public AudioEncodecThread(WeakReference<YorkBaseMediaEncoder> encoder) {
            this.encoder = encoder;
            audioEncodec = encoder.get().audioEncodec;
            bufferInfo = encoder.get().audioBufferinfo;
            mediaMuxer = encoder.get().mediaMuxer;
            audioTrackIndex = -1;
        }

        @Override
        public void run() {
            super.run();
            pts = 0;
            isExit = false;
            audioEncodec.start();
            while (true) {
                if (isExit) {
                    audioEncodec.stop();
                    audioEncodec.release();
                    audioEncodec = null;
                    encoder.get().audioExit = true;
                    if (encoder.get().videoExit) {
                        mediaMuxer.stop();
                        mediaMuxer.release();
                        mediaMuxer = null;
                    }
                    break;
                }
                if (audioEncodec==null){
                    isExit=true;
                    break;
                }
                int outputBufferIndex = audioEncodec.dequeueOutputBuffer(bufferInfo, 0);
                if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    if (mediaMuxer != null) {
                        audioTrackIndex = mediaMuxer.addTrack(audioEncodec.getOutputFormat());
                        if (encoder.get().videoEncodecThread.videoTrackIndex != -1) {
                            mediaMuxer.start();
                            encoder.get().encodecStart = true;
                        }
                    }
                } else {
                    while (outputBufferIndex >= 0) {
                        if (encoder.get().encodecStart) {
                            ByteBuffer outputBuffer = audioEncodec.getOutputBuffers()[outputBufferIndex];
                            outputBuffer.position(bufferInfo.offset);
                            outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                            bufferInfo.presentationTimeUs = getPTSUs();
                            mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo);
                        }
                        audioEncodec.releaseOutputBuffer(outputBufferIndex, false);
                        outputBufferIndex = audioEncodec.dequeueOutputBuffer(bufferInfo, 0);
                    }
                }
            }
        }
        private long baseTimeStamp = -1;
        private long getPTSUs() {
            long result = System.nanoTime() ;
            long time = (result - baseTimeStamp ) / 1000;
            if (time < 0){
                return  0;
            }
            return time;
        }
        public void exit() {
            isExit = true;
        }
    }


    public interface OnMediaInfoListener {
        void onMediaRecordStart();
        void onMediaTime(int times);
        void onMediaRecordComple();
    }

}

以上就是一个.MP4文件的录制过程

3.2 MP4 文件解码播放

关于MP4 文件的播放,先再看一下MP4文件播放流程的简单框图:

在这里插入图片描述

图中MP4播放的过程主要有以下流程:

  • FFmpeg 解码得到 Avpacket
  • 音频重采样 Avframe 得到 PCM
  • OpenSL ES 播放 PCM 扬声器发出声音
  • 视频硬/软解码得到图像数据
  • OpenGL 渲染 YUV 数据在屏幕上显示

之所以采用这种方式播放MP4文件,是因为这种方式更能体现出一个视频文件播放的每一个具体步骤。我们还可以了解音频解码、重采样;视频的硬解码、软解码过程,同时也能加深对安卓音视频开发的理解。

关于FFmpeg的使用,本篇文章里不会对它的每一个步骤细讲,之后我会将这部分内容单独开一篇文章《安卓音视频整理(四)—— 音视频播放器》来叙述,在这篇文章中,我们会涉及到FFmpeg的编译、集成、使用,以及音视频播放器SDK的封装等,重点也是在 NDK开发。有兴趣的盆友可以看一下。

预告一下,播放器中会有这些基本功能,代码如下:

package com.york.opensdk.media;

import android.media.MediaCodec;
import android.media.MediaFormat;
import android.text.TextUtils;
import android.view.Surface;
import com.york.opensdk.listner.PlayerObservable;
import com.york.opensdk.listner.PlayerOnFileCodecListener;
import com.york.opensdk.listner.PlayerOnParpareListener;
import com.york.opensdk.listner.PlayerStatusListener;
import com.york.opensdk.utils.BitConverter;
import com.york.opensdk.utils.VideoSupportUitl;
import com.york.opensdk.utils.LogAAR;
import com.york.opensdk.utils.ToolUtils;
import com.york.opensdk.opengl.videoplay.YorkGLSurfacePlayView;
import com.york.opensdk.opengl.videoplay.YorkRender;

import java.nio.ByteBuffer;
import java.util.Observable;
import java.util.Observer;

/**
 * Created by 86158 on 2019/8/3.
 * 支持音频和视频播放
 */

public class OpenPlayer implements Observer {

    private String source;
    private boolean playNext = false;
    private PlayerStatusListener mPlayerStatusListener;
    private PlayerOnParpareListener mPlayerOnParpareListener;
    private PlayerOnFileCodecListener mPlayerOnFileCodecListener;
    private YorkGLSurfacePlayView mGLSurfacePlayView;
    private MediaFormat mediaFormat;
    private MediaCodec mediaCodec;
    private Surface surface;
    private MediaCodec.BufferInfo info;
    private PlayerObservable mPlayerObservable;
    public final int PLAY_STOP = 1;
    public final int PLAY_PAUSE = 2;
    public final int PLAYING = 3;

    public OpenPlayer() {
        mPlayerObservable = new PlayerObservable();
        mPlayerObservable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {

    }

    static {
        System.loadLibrary("player_lib");
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avformat-57");
        System.loadLibrary("avutil-55");
        System.loadLibrary("swresample-2");
        System.loadLibrary("swscale-4");
    }

    public void setYorkGLSurfaceView(YorkGLSurfacePlayView yorkGLSurfacePlayView) {
        this.mGLSurfacePlayView = yorkGLSurfacePlayView;
        if (mGLSurfacePlayView != null) {
            mGLSurfacePlayView.getWlRender().setOnSurfaceCreateListener(new YorkRender.OnSurfaceCreateListener() {
                @Override
                public void onSurfaceCreate(Surface mSurface) {
                    if (surface == null) {
                        surface = mSurface;
                        LogAAR.d("onSurfaceCreate");
                    }
                }
            });
        }
    }

    public YorkGLSurfacePlayView getGLSurfacePlayView() {
        return mGLSurfacePlayView;
    }

    public void setPlayerStatusListener(PlayerStatusListener mPlayerStatusListener) {
        this.mPlayerStatusListener = mPlayerStatusListener;
    }

    public void setOpenPlayOnParpare(PlayerOnParpareListener mPlayerOnParpareListener) {
        this.mPlayerOnParpareListener = mPlayerOnParpareListener;
    }

    public void setPlayerOnFileCodecListener(PlayerOnFileCodecListener mPlayerOnFileCodecListener) {
        this.mPlayerOnFileCodecListener = mPlayerOnFileCodecListener;
    }

    public PlayerObservable getPlayerObservable() {
        return mPlayerObservable;
    }

    /*********************************************** player callback ************************************************/

    private void onCallParpared() {
        LogAAR.i("onParpared");
        if (mPlayerStatusListener != null) {
            mPlayerStatusListener.onParpared();
        }
        if (mPlayerOnParpareListener != null) {
            mPlayerOnParpareListener.OnParpare();
        }
        mPlayerObservable.onParpare();
    }

    private void onCallLoad(boolean load) {
        if (mPlayerStatusListener != null) {
            mPlayerStatusListener.onLoad(load);
        }
    }

    private void onCallComplete(boolean isDoStop) {
        LogAAR.i("onComplete ->isDoStop=" + isDoStop);
        if (mPlayerStatusListener != null) {
            mPlayerStatusListener.onOnComplete(isDoStop);
        }
        mPlayerObservable.onOnComplete(isDoStop);
    }

    private void onCallPlayValumeDB(int chenell, int db, int left_db, int right_db) {
        if (mPlayerStatusListener != null) {
            mPlayerStatusListener.onPlayValumeDB(chenell, db, left_db, right_db);
        }
    }

    private void onCallError(int cord, String msg) {
        LogAAR.e("cord = " + cord + ",msg = " + msg);
        if (mPlayerStatusListener != null) {
            mPlayerStatusListener.onError(cord, msg);
        }
        mPlayerObservable.onError();
    }

    private void onCallNext() {
        if (playNext) {
            playNext = false;
            parparePlay();
        }
    }

    private void onAudioData(byte[] buffer, int chenell, int size) {

        byte[] leftData = new byte[buffer.length];
        byte[] rightData = new byte[buffer.length];
        for (int i = 0; i < buffer.length / 2; i++) {
            if (i % 2 == 0) {
                System.arraycopy(buffer, i * 2, leftData, i, 2);
            } else {
                System.arraycopy(buffer, i * 2, rightData, i - 1, 2);
            }
        }
        short[] left = BitConverter.toShorts(leftData);
        short[] right = BitConverter.toShorts(rightData);
        short[] midle = BitConverter.toShorts(buffer);
        int left_db = (int) ToolUtils.getDB(left, size);
        int right_db = (int) ToolUtils.getDB(right, size);
        int midle_db = (int) ToolUtils.getDB(midle, size);
        if (mPlayerStatusListener != null) {
            mPlayerStatusListener.onPcmData(buffer, size);
            mPlayerStatusListener.onPlayValumeDB(chenell, midle_db, left_db, right_db);
            if ( getPlayStates() == PLAYING){
                mPlayerObservable.onTimeInfo(n_get_current(),n_get_duration());
            }
        }
    }

    private void onPCMdown() {
        LogAAR.i("onPCMdown");
        if (mPlayerOnFileCodecListener != null) {
            mPlayerOnFileCodecListener.onPcmDown();
        }
        n_release_decode();
    }

    private void onDecodePosition(double pos) {
        if (mPlayerOnFileCodecListener != null) {
            mPlayerOnFileCodecListener.onDecodePosition(pos);
        }
    }

    public void onCallRenderYUV(int width, int height, byte[] y, byte[] u, byte[] v) {
        if (mGLSurfacePlayView != null) {
            mGLSurfacePlayView.getWlRender().setRenderType(YorkRender.RENDER_YUV);
            mGLSurfacePlayView.setYUVData(width, height, y, u, v);
        }
    }

    public boolean onCallIsSupportMediaCodec(String ffcodecname) {
        return VideoSupportUitl.isSupportCodec(ffcodecname);
    }


    /**
     * 初始化MediaCodec,供底层用
     *
     * @param codecName
     * @param width
     * @param height
     * @param csd_0
     * @param csd_1
     */
    public void initMediaCodec(String codecName, int width, int height, byte[] csd_0, byte[] csd_1) {
        if (surface != null) {
            try {
                mGLSurfacePlayView.getWlRender().setRenderType(YorkRender.RENDER_MEDIACODEC);
                String mime = VideoSupportUitl.findVideoCodecName(codecName);
                mediaFormat = MediaFormat.createVideoFormat(mime, width, height);
                mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, width * height);
                mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(csd_0));
                mediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(csd_1));
                LogAAR.d(mediaFormat.toString());
                mediaCodec = MediaCodec.createDecoderByType(mime);

                info = new MediaCodec.BufferInfo();
                mediaCodec.configure(mediaFormat, surface, null, 0);
                mediaCodec.start();

            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            if (mPlayerStatusListener != null) {
                onCallError(2001, "surface is null");
            }
        }
    }

    /**
     * 供底层用
     *
     * @param datasize
     * @param data
     */
    public void decodeAVPacket(int datasize, byte[] data) {
        if (surface != null && datasize > 0 && data != null && mediaCodec != null) {

            int intputBufferIndex = mediaCodec.dequeueInputBuffer(10);
            if (intputBufferIndex >= 0) {
                ByteBuffer byteBuffer = mediaCodec.getInputBuffers()[intputBufferIndex];
                byteBuffer.clear();
                byteBuffer.put(data);
                mediaCodec.queueInputBuffer(intputBufferIndex, 0, datasize, 0, 0);
            }
            if (info == null) {
                info = new MediaCodec.BufferInfo();
            }
            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(info, 10);
            while (outputBufferIndex >= 0) {
                if (mediaCodec != null) {
                    mediaCodec.releaseOutputBuffer(outputBufferIndex, true);
                    outputBufferIndex = mediaCodec.dequeueOutputBuffer(info, 10);
                }
            }
        }
    }

    private void releaseMediacodec() {
        if (mediaCodec != null) {
            try {
                mediaCodec.flush();
                mediaCodec.stop();
                mediaCodec.release();
            } catch (Exception e) {
                //e.printStackTrace();
            }
            mediaFormat = null;
            info = null;
        }

    }


    /********************************************** player method *************************************************/
    public void setSource(String source) {
        LogAAR.d("setSource:source="+source);
        stopPlay();
        this.source = source;
    }

    public void parparePlay() {
        if (TextUtils.isEmpty(source)) {
            LogAAR.e("source not be null");
            return;
        }
        LogAAR.d("parparePlay");
        n_parpare_play(source);
    }

    public void startPlay() {
        if (TextUtils.isEmpty(source)) {
            LogAAR.e("source not be null");
            return;
        }
        LogAAR.d("startPlay");
        n_start_play();
        mPlayerObservable.onPlaying();
    }

    public void seekPlay(double secd) {
        n_seek_play(secd);
    }

    public void pausePlay() {
        if (getPlayStates() == 3) {
            LogAAR.d("pausePlay");
            n_pause_play();
            if (mPlayerStatusListener != null) {
                mPlayerStatusListener.onPause(true);
            }
        }
        mPlayerObservable.onPause();
    }

    public void resumePlay() {
        LogAAR.d("resumePlay");
        n_resume_play();
        if (mPlayerStatusListener != null) {
            mPlayerStatusListener.onPause(false);
        }
        mPlayerObservable.onPlaying();
    }

    public void stopPlay() {
        LogAAR.d("stopPlay");
        if (!TextUtils.isEmpty(source)) {
            n_stop_play();
            releaseMediacodec();
        }
    }

    public void playNext(String url) {
        LogAAR.d("playNext: url="+url);
        this.source = url;
        playNext = true;
        stopPlay();
    }

    public void setVolume(int percent) {
        LogAAR.d("setVolume: percent="+percent);
        if (percent >= 0 && percent <= 100) {
            n_volume(percent);
        }
    }


    public void setMute(int mute) {
        LogAAR.d("setMute: mute="+mute);
        n_mute(mute);
    }

    public void setPitch(float pitch) {
        LogAAR.d("setPitch: pitch="+pitch);
        n_pitch(pitch);
    }

    public void setSpeed(float speed) {
        LogAAR.d("setSpeed: speed="+speed);
        n_speed(speed);
    }

    public long getDuration() {
        return n_get_duration();
    }

    public long getCurrent() {
        return n_get_current();
    }

    /**
     * @return 1.stop;2.pause;3.playing;
     */
    public int getPlayStates() {
        int states=n_get_play_states();
        return states;
    }


    public void decodeFile(String source) {
        n_decode_audio(source);
    }

    public void setRecordTempPath(String path) {
        n_set_sava_dir(path);
    }

    public void release() {
        stopPlay();
        n_release();
        if (mediaCodec != null) {
            mediaCodec = null;
        }
        mPlayerObservable.deleteObserver(this);
        mPlayerStatusListener = null;
        mPlayerOnParpareListener = null;
        mPlayerOnFileCodecListener = null;
        mGLSurfacePlayView = null;
    }


    /********************************** 音视频 ****************************************/

    private native void n_parpare_play(String url);

    private native void n_start_play();

    private native void n_pause_play();

    private native void n_resume_play();

    private native void n_seek_play(double secd);

    private native int n_duration();

    private native void n_stop_play();

    private native void n_set_sava_dir(String path);

    private native void n_release();

    private native void n_release_decode();

    private native void n_decode_audio(String path);

    private native int n_get_play_states();

    private native void n_volume(int percent);

    private native void n_mute(int mute);

    private native void n_pitch(float pitch);

    private native void n_speed(float speed);// 视频0.5--2.0倍,音频0--5倍都可以

    private native long n_get_current();

    private native long n_get_duration();

}

好了,以上就是音视频编解码的内容整理,更多的内容可以看一下我的后续文章。感谢观看



上一篇:1.安卓音视频整理(一)—— 音频模块

下一篇:3.安卓音视频整理(三)—— 图像模块

个人网站:www.yorkyue.com

个人网站欢迎界面

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值