Android 直播流程3()

3、使用

MediaCodec创建之后,需要通过start()方法进行开启。MediaCodec有输入缓冲区队列和输出缓冲区队列,不断通过往输入缓冲区队列传递数据,经过MediaCodec处理后就可以得到响应的输出数据。当在编码的时候,需要向输入缓冲区传入采集到的原始的视音频数据,然后获取输出缓冲区的数据,输出出来的数据也就是编码处理后的数据。当在解码的时候,往输入缓冲区输入需要解码的数据,然后获取输出缓冲区的数据,输出出来的数据也就是解码后得到的原始的视音频数据。当需要清空输入和输出缓冲区的时候,可以调用MediaCodec的flush()方法。当编码或者解码结束时,通过往输入缓冲区输入带结束标记的数据,然后从输出缓冲区可以得到这个结束标记,从而完成整个编解码过程。下面一张图片很好地展示了MediaCodec的状态变化。

                                                                MediaCodec状态

对于MediaCodec通过处理输入的数据,从而得到输出数据。MediaCodec通过一系列的输入和输出缓冲区来处理数据。如下图所示,输入客户端通过查询得到空的输入缓冲区,然后往里面填充数据,然后将输入缓冲区传递给MediaCodec;输出客户端通过查询得到塞满的输出缓冲区,然后得到里面的数据,然后通知MediaCodec释放这个输出缓冲区。

                                                                MediaCodec过程

在API 21及以后可以通过下面这种异步的方式来使用MediaCodec。

MediaCodec codec = MediaCodec.createByCodecName(name);

MediaFormat mOutputFormat; // member variable

codec.setCallback(new MediaCodec.Callback() {

  @Override

  void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {

    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);

    // fill inputBuffer with valid data

    …

    codec.queueInputBuffer(inputBufferId, …);

  }

  @Override

  void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {

    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);

    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A

    // bufferFormat is equivalent to mOutputFormat

    // outputBuffer is ready to be processed or rendered.

    …

    codec.releaseOutputBuffer(outputBufferId, …);

  }

  @Override

  void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {

    // Subsequent data will conform to new format.

    // Can ignore if using getOutputFormat(outputBufferId)

    mOutputFormat = format; // option B

  }

  @Override

  void onError(…) {

    …

  }

});

codec.configure(format, …);

mOutputFormat = codec.getOutputFormat(); // option B

codec.start();

// wait for processing to complete

codec.stop();

codec.release();

从API 21开始,可以使用下面这种同步的方式来使用MediaCodec。

MediaCodec codec = MediaCodec.createByCodecName(name);

codec.configure(format, …);

MediaFormat outputFormat = codec.getOutputFormat(); // option B

codec.start();

for (;;) {

  int inputBufferId = codec.dequeueInputBuffer(timeoutUs);

  if (inputBufferId >= 0) {

    ByteBuffer inputBuffer = codec.getInputBuffer(…);

    // fill inputBuffer with valid data

    …

    codec.queueInputBuffer(inputBufferId, …);

  }

  int outputBufferId = codec.dequeueOutputBuffer(…);

  if (outputBufferId >= 0) {

    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);

    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A

    // bufferFormat is identical to outputFormat

    // outputBuffer is ready to be processed or rendered.

    …

    codec.releaseOutputBuffer(outputBufferId, …);

  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {

    // Subsequent data will conform to new format.

    // Can ignore if using getOutputFormat(outputBufferId)

    outputFormat = codec.getOutputFormat(); // option B

  }

}

codec.stop();

codec.release();

在API版本21之前,获取缓冲区的方式有所不同,不能直接得到相应的缓冲区,需要根据索引序号从缓冲区列表中得到相应的缓冲区,具体的代码如下所示:

MediaCodec codec = MediaCodec.createByCodecName(name);

codec.configure(format, …);

codec.start();

ByteBuffer[] inputBuffers = codec.getInputBuffers();

ByteBuffer[] outputBuffers = codec.getOutputBuffers();

for (;;) {

  int inputBufferId = codec.dequeueInputBuffer(…);

  if (inputBufferId >= 0) {

    // fill inputBuffers[inputBufferId] with valid data

    …

    codec.queueInputBuffer(inputBufferId, …);

  }

  int outputBufferId = codec.dequeueOutputBuffer(…);

  if (outputBufferId >= 0) {

    // outputBuffers[outputBufferId] is ready to be processed or rendered.

    …

    codec.releaseOutputBuffer(outputBufferId, …);

  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {

    outputBuffers = codec.getOutputBuffers();

  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {

    // Subsequent data will conform to new format.

    MediaFormat format = codec.getOutputFormat();

  }

}

codec.stop();

codec.release();

当数据输入结束的时候,通过queueInputBuffer的输入标记设置为BUFFER_FLAG_END_OF_STREAM来通知MediaCodec结束编码。当MediaCodec作为编码器的时候, dequeueOutputBuffer方法能够得到当前编码输出缓冲区数据的相关信息,这些信息存储在bufferInfo里面,通过bufferInfo信息能够得到数据的真实长度,当前数据为关键帧或者非关键帧等等信息。

当使用Output Surface作为解码的输出的时候,可以根据以下情况来设置是否将视频渲染到Surface上。

releaseOutputBuffer(bufferId, false)  //不渲染buffer里面的数据

releaseOutputBuffer(bufferId, true)  //渲染buffer里面的数据

releaseOutputBuffer(bufferId, timestamp)  //在特定时间渲染buffer里面的数据

当使用Input Surface作为编码器输入的时候,不允许使用dequeueInputBuffer。当输入结束的时候,使用signalEndOfInputStream()来使得编码器停止。

MediaMuxer

前面讲述了MediaExtractor(视音频分离器),现在讲述MediaMuxer(视音频合成器)。MediaMuxer是Android提供的视音频合成器,目前只支持mp4和webm两种格式的视音频合成。一般来时视音频媒体都有视频轨道和音频轨道,有些时候也还有字母轨道,MediaMuxer将这些轨道糅合在一起存储在一个文件中。

MediaMuxer在Android中一个最常使用的场景是录制mp4文件。一般来说当存储为mp4文件时,视频轨道一般是经过编码处理后的h264视频,音频轨道一般是经过编码后处理的aac音频。前面已经讲述了如何对采集的视频和音频进行硬编,那么这时候如果对硬编后的视频和音频使用MediaMuxer进行合成,那么就可以合成为mp4文件。

下面是MediaMuxer一般的使用方法。

MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);

// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()

// or MediaExtractor.getTrackFormat().

MediaFormat audioFormat = new MediaFormat(...);

MediaFormat videoFormat = new MediaFormat(...);

int audioTrackIndex = muxer.addTrack(audioFormat);

int videoTrackIndex = muxer.addTrack(videoFormat);

ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);

boolean finished = false;

BufferInfo bufferInfo = new BufferInfo();

muxer.start();

while(!finished) {

// getInputBuffer() will fill the inputBuffer with one frame of encoded

// sample from either MediaCodec or MediaExtractor, set isAudioSample to

// true when the sample is audio data, set up all the fields of bufferInfo,

// and return true if there are no more samples.

    finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);

    if (!finished) {

        int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;

        muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);

    }

};

muxer.stop();

muxer.release();

其实上面的注释很好说明了MediaMuxer的使用场景。视音频轨道的初始化需要传入MediaFormat,而MediaFormat可以通过MediaCodec.getOutputFormat()获取(采集后进行硬编得到MediaFormat),也可以通过MediaExtractor.getTrackFormat()获取(分离器分离出视音频得到MediaFormat)。上面包含了两个应用场景,一个是采集,一个是转码。

结合

MediaExtractor和MediaCodec结合使用可以实现视频的播放功能,MediaCodec和MediaMuxer结合使用可以实现视频的录制功能,MediaExtractor、MediaCodec和MediaMuxer三者一起使用可以实现视频的转码功能。下面讲述一下这几个功能的实现。

1、视音频录制

之前讲述了视频的采集和音频的采集,将采集到的视音频通过MediaCodec进行编码处理,之后将编码数据传递到MediaMuxer进行合成,也就完成了视音频录制的功能。

                                                                        视频录制

根据视音频采集的相关参数创建MediaCodec,当MediaCodec的outputBufferId为INFO_OUTPUT_FORMAT_CHANGED时,可以通过codec.getOutputFormat()得到相应的MediaFormat,之后便可以用这个MediaFormat为MediaMuxer添加相应的视音频轨道。通过codec.dequeueOutputBuffer(…)可以得到编码后的数据的bufferInfo信息和相应的数据,之后将这个数据和bufferInfo通过muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo)传递给Muxer,也就将整个视音频数据合成到了mp4中。

2、视音频播放

                                                                                视音频播放

利用Android提供的Media API来实现一个播放器也是可以的,实际上Google著名的开源项目ExoPlayer就是这么做的。

上面的示意图简要描述了一个简单的本地播放器的结构。利用MediaExtractor分离视音频文件,得到相应的音频轨道和视频轨道。之后通过MediaExtractor从相应的轨道中获取数据,并且将这些数据传递给MediaCodec的输入缓冲区,经过MediaCodec的解码便可以得到相应的原始数据。音频解码后可以得到PCM数据,从而可以传递给AudioTrack进行播放。视频解码后可以渲染到相应的Surface,这个Surface可以是通过SurfaceTexture创建,而SurfaceTexture是可以通过纹理创建的,从而将解码后的视频数据传递到纹理上了。

MediaExtractor解析视音频文件,可以得到相应数据的pts,之后pts可以传输到MediaCodec,之后在MediaCodec的输出里面可以得到相应的pts,之后在根据视音频的pts来控制视音频的渲染,从而实现视音频的同步。

3、视音频转码

视音频的转码,其实就是通过MediaExtractor解析相应的文件,之后得到相应的视频轨道和音频轨道,之后将轨道里的数据传输到MediaCodec进行解码,然后将解码后的数据进行相应的处理(例如音频变声、视频裁剪、视频滤镜),之后将处理后的数据传递给MediaCodec进行编码,最后利用MediaMuxer将视频轨道和音频轨道进行合成,从而完成了整个转码过程。

                                                                           视音频转码

讲述了,如何使用Media API进行相应的录制、播放、转码,讲述了如何将视音频编解码和纹理相结合

以上就是直播功能的基本流程:

采集视频、音频数据 ---- 将视频数据通过h264/aac进行编码 ---- 将编码好的音频视频数据混合封装成flv的格式 ---- 把flv数据推送到支持rtmp的服务器-----获取音视频数据解码播放。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,841评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,415评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,904评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,051评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,055评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,255评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,729评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,377评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,517评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,420评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,467评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,144评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,735评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,812评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,029评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,528评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,126评论 2 341

推荐阅读更多精彩内容