项目首页:https://github.com/ossrs/srs-sea

SRS服务器项目:https://github.com/ossrs/srs

一个支持RTMP推流的版本:https://github.com/begeekmyfriend/yasea

Android高版本中,特别是4.1引入了MediaCodec可以对摄像头的图像进行硬件编码,实现直播。

一般Android推流到服务器,使用ffmpeg居多,也就是软编码,实际上使用Android的硬件编码会有更好的体验。

看了下网上的文章也不少,但是都缺乏一个整体跑通的方案,特别是如何推送的服务器。本文把Android推直播流的过程梳理一遍。

AndroidPublisher提出了Android直播的新思路,主要配合SRS服务器完成,优势如下:

  1. 使用系统的类,不引入jni和c的库,简单可靠,一千行左右java代码就可以完成。
  2. 硬件编码而非软件编码,系统负载低,800kbps编码cpu使用率13%左右。
  3. 低延迟和RTMP一样,0.8秒到3秒,使用的协议是HTTP FLV流,原理和RTMP一样。
  4. 安装包小无复杂依赖,编译出来的apk都只有1405KB左右。
  5. 方便集成,只需要引入一个SrsHttpFlv类,进行转封装和打包发送,可以用在任何app中。

Android直播有几个大的环节:

  1. 打开Camera,进行Preview获取YUV图像数据,也就是未压缩的图像。
    设置picture和preview大小后,计算YUV的buffer的尺寸,不能简单乘以1.5而应该按照文档计算。
    获取YUV的同时,还可以进行预览,只要绑定到SurfaceHolder就可以。
  2. 使用MediaCodec和MediaFormat对YUV进行编码,其中MediaCodec是编码,MediaFormat是打包成annexb封装。
    设置MediaCodec的colorFormat需要判断是否MediaCodec支持,也就是从MediaCodec获取colorFormat。
  3. 将YUV图像,送入MediaCodec的inputBuffer,并获取outputBuffer中已经编码的数据,格式是annexb。
    其中queueInputBuffer时,需要指定pts,否则没有编码数据输出,会被丢弃。
  4. 将编码的annexb数据,发送到服务器。
    一般使用rtmp(librtmp/srslibrtmp/ffmpeg),因为流媒体服务器的输入一般是rtmp。
    若服务器支持http-flv流POST,那么可以直接发送给服务器。
秀一个运行起来的图:

下面是各个重要环节的分解。

YUV图像

第一个环节,打开Camera并预览:
                camera = Camera.open();
Camera.Parameters parameters = camera.getParameters(); parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
parameters.setPreviewFormat(ImageFormat.YV12); Camera.Size size = null;
List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
for (int i = ; i < sizes.size(); i++) {
//Log.i(TAG, String.format("camera supported picture size %dx%d", sizes.get(i).width, sizes.get(i).height));
if (sizes.get(i).width == ) {
size = sizes.get(i);
}
}
parameters.setPictureSize(size.width, size.height);
Log.i(TAG, String.format("set the picture size in %dx%d", size.width, size.height)); sizes = parameters.getSupportedPreviewSizes();
for (int i = ; i < sizes.size(); i++) {
//Log.i(TAG, String.format("camera supported preview size %dx%d", sizes.get(i).width, sizes.get(i).height));
if (sizes.get(i).width == ) {
vsize = size = sizes.get(i);
}
}
parameters.setPreviewSize(size.width, size.height);
Log.i(TAG, String.format("set the preview size in %dx%d", size.width, size.height)); camera.setParameters(parameters); // set the callback and start the preview.
buffer = new byte[getYuvBuffer(size.width, size.height)];
camera.addCallbackBuffer(buffer);
camera.setPreviewCallbackWithBuffer(onYuvFrame);
try {
camera.setPreviewDisplay(preview.getHolder());
} catch (IOException e) {
Log.e(TAG, "preview video failed.");
e.printStackTrace();
return;
}
Log.i(TAG, String.format("start to preview video in %dx%d, buffer %dB", size.width, size.height, buffer.length));
camera.startPreview();

计算YUV的buffer的函数,需要根据文档计算,而不是简单“*3/2”:

    // for the buffer for YV12(android YUV), @see below:
// https://developer.android.com/reference/android/hardware/Camera.Parameters.html#setPreviewFormat(int)
// https://developer.android.com/reference/android/graphics/ImageFormat.html#YV12
private int getYuvBuffer(int width, int height) {
// stride = ALIGN(width, 16)
int stride = (int)Math.ceil(width / 16.0) * ;
// y_size = stride * height
int y_size = stride * height;
// c_stride = ALIGN(stride/2, 16)
int c_stride = (int)Math.ceil(width / 32.0) * ;
// c_size = c_stride * height/2
int c_size = c_stride * height / ;
// size = y_size + c_size * 2
return y_size + c_size * ;
}

图像编码

第二个环节,设置编码器参数,并启动:
                // encoder yuv to 264 es stream.
// requires sdk level 16+, Android 4.1, 4.1.1, the JELLY_BEAN
try {
encoder = MediaCodec.createEncoderByType(VCODEC);
} catch (IOException e) {
Log.e(TAG, "create encoder failed.");
e.printStackTrace();
return;
}
ebi = new MediaCodec.BufferInfo();
presentationTimeUs = new Date().getTime() * ; // start the encoder.
// @see https://developer.android.com/reference/android/media/MediaCodec.html
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, vsize.width, vsize.height);
format.setInteger(MediaFormat.KEY_BIT_RATE, );
format.setInteger(MediaFormat.KEY_FRAME_RATE, );
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, chooseColorFormat());
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, );
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
Log.i(TAG, "encoder start");

其中,colorFormat需要从编码器支持的格式中选取,否则会有不支持的错误:

    // choose the right supported color format. @see below:
// https://developer.android.com/reference/android/media/MediaCodecInfo.html
// https://developer.android.com/reference/android/media/MediaCodecInfo.CodecCapabilities.html
private int chooseColorFormat() {
MediaCodecInfo ci = null; int nbCodecs = MediaCodecList.getCodecCount();
for (int i = ; i < nbCodecs; i++) {
MediaCodecInfo mci = MediaCodecList.getCodecInfoAt(i);
if (!mci.isEncoder()) {
continue;
} String[] types = mci.getSupportedTypes();
for (int j = ; j < types.length; j++) {
if (types[j].equalsIgnoreCase(VCODEC)) {
//Log.i(TAG, String.format("encoder %s types: %s", mci.getName(), types[j]));
ci = mci;
break;
}
}
} int matchedColorFormat = ;
MediaCodecInfo.CodecCapabilities cc = ci.getCapabilitiesForType(VCODEC);
for (int i = ; i < cc.colorFormats.length; i++) {
int cf = cc.colorFormats[i];
//Log.i(TAG, String.format("encoder %s supports color fomart %d", ci.getName(), cf)); // choose YUV for h.264, prefer the bigger one.
if (cf >= cc.COLOR_FormatYUV411Planar && cf <= cc.COLOR_FormatYUV422SemiPlanar) {
if (cf > matchedColorFormat) {
matchedColorFormat = cf;
}
}
} Log.i(TAG, String.format("encoder %s choose color format %d", ci.getName(), matchedColorFormat));
return matchedColorFormat;
}

第三个环节,在YUV图像回调中,送给编码器,并获取输出:

        // when got YUV frame from camera.
// @see https://developer.android.com/reference/android/media/MediaCodec.html
final Camera.PreviewCallback onYuvFrame = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//Log.i(TAG, String.format("got YUV image, size=%d", data.length)); // feed the encoder with yuv frame, got the encoded 264 es stream.
ByteBuffer[] inBuffers = encoder.getInputBuffers();
ByteBuffer[] outBuffers = encoder.getOutputBuffers();
if (true) {
int inBufferIndex = encoder.dequeueInputBuffer(-);
//Log.i(TAG, String.format("try to dequeue input buffer, ii=%d", inBufferIndex));
if (inBufferIndex >= ) {
ByteBuffer bb = inBuffers[inBufferIndex];
bb.clear();
bb.put(data, , data.length);
long pts = new Date().getTime() * - presentationTimeUs;
//Log.i(TAG, String.format("feed YUV to encode %dB, pts=%d", data.length, pts / 1000));
encoder.queueInputBuffer(inBufferIndex, , data.length, pts, );
} for (;;) {
int outBufferIndex = encoder.dequeueOutputBuffer(ebi, );
//Log.i(TAG, String.format("try to dequeue output buffer, ii=%d, oi=%d", inBufferIndex, outBufferIndex));
if (outBufferIndex >= ) {
ByteBuffer bb = outBuffers[outBufferIndex];
onEncodedAnnexbFrame(bb, ebi);
encoder.releaseOutputBuffer(outBufferIndex, false);
} if (outBufferIndex < ) {
break;
}
}
} // to fetch next frame.
camera.addCallbackBuffer(buffer);
}
};

MUX为FLV流

获取编码的annexb数据后,调用函数发送到服务器:
    // when got encoded h264 es stream.
private void onEncodedAnnexbFrame(ByteBuffer es, MediaCodec.BufferInfo bi) {
try {
muxer.writeSampleData(videoTrack, es, bi);
} catch (Exception e) {
Log.e(TAG, "muxer write sample failed.");
e.printStackTrace();
}
}
最后这个环节,一般会用librtmp或者srslibrtmp,或者ffmpeg发送。如果服务器能直接支持http post,那么就可以使用HttpURLConnection直接发送了。SRS3将会支持HTTP-FLV推流;因此只需要将编码的annexb格式的数据,转换成flv后发送给SRS服务器。
 
SRS2支持了HTTP FLV Stream caster,也就是支持POST一个flv流到服务器,就相当于RTMP的publish了。可以直接使用android-publisher提供的FlvMuxer,将annexb数据打包发送,参考:https://github.com/simple-rtmp-server/android-publisher
 
其中,annexb打包的过程如下:
        public void writeVideoSample(final ByteBuffer bb, MediaCodec.BufferInfo bi) throws Exception {
int pts = (int)(bi.presentationTimeUs / );
int dts = (int)pts; ArrayList<SrsAnnexbFrame> ibps = new ArrayList<SrsAnnexbFrame>();
int frame_type = SrsCodecVideoAVCFrame.InterFrame;
//Log.i(TAG, String.format("video %d/%d bytes, offset=%d, position=%d, pts=%d", bb.remaining(), bi.size, bi.offset, bb.position(), pts)); // send each frame.
while (bb.position() < bi.size) {
SrsAnnexbFrame frame = avc.annexb_demux(bb, bi); // 5bits, 7.3.1 NAL unit syntax,
// H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
// 7: SPS, 8: PPS, 5: I Frame, 1: P Frame
int nal_unit_type = (int)(frame.frame.get() & 0x1f);
if (nal_unit_type == SrsAvcNaluType.SPS || nal_unit_type == SrsAvcNaluType.PPS) {
Log.i(TAG, String.format("annexb demux %dB, pts=%d, frame=%dB, nalu=%d", bi.size, pts, frame.size, nal_unit_type));
} // for IDR frame, the frame is keyframe.
if (nal_unit_type == SrsAvcNaluType.IDR) {
frame_type = SrsCodecVideoAVCFrame.KeyFrame;
} // ignore the nalu type aud(9)
if (nal_unit_type == SrsAvcNaluType.AccessUnitDelimiter) {
continue;
} // for sps
if (avc.is_sps(frame)) {
byte[] sps = new byte[frame.size];
frame.frame.get(sps); if (utils.srs_bytes_equals(h264_sps, sps)) {
continue;
}
h264_sps_changed = true;
h264_sps = sps;
continue;
} // for pps
if (avc.is_pps(frame)) {
byte[] pps = new byte[frame.size];
frame.frame.get(pps); if (utils.srs_bytes_equals(h264_pps, pps)) {
continue;
}
h264_pps_changed = true;
h264_pps = pps;
continue;
} // ibp frame.
SrsAnnexbFrame nalu_header = avc.mux_ibp_frame(frame);
ibps.add(nalu_header);
ibps.add(frame);
} write_h264_sps_pps(dts, pts); write_h264_ipb_frame(ibps, frame_type, dts, pts);
}

至于发送到服务器,其实就是使用系统的HTTP客户端。代码如下:

    private void reconnect() throws Exception {
// when bos not null, already connected.
if (bos != null) {
return;
} disconnect(); URL u = new URL(url);
conn = (HttpURLConnection)u.openConnection(); Log.i(TAG, String.format("worker: connect to SRS by url=%s", url));
conn.setDoOutput(true);
conn.setChunkedStreamingMode();
conn.setRequestProperty("Content-Type", "application/octet-stream");
bos = new BufferedOutputStream(conn.getOutputStream());
Log.i(TAG, String.format("worker: muxer opened, url=%s", url)); // write 13B header
// 9bytes header and 4bytes first previous-tag-size
byte[] flv_header = new byte[]{
'F', 'L', 'V', // Signatures "FLV"
(byte) 0x01, // File version (for example, 0x01 for FLV version 1)
(byte) 0x00, // 4, audio; 1, video; 5 audio+video.
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x09, // DataOffset UI32 The length of this header in bytes
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
};
bos.write(flv_header);
bos.flush();
Log.i(TAG, String.format("worker: flv header ok.")); sendFlvTag(bos, videoSequenceHeader);
} private void sendFlvTag(BufferedOutputStream bos, SrsFlvFrame frame) throws IOException {
if (frame == null) {
return;
} if (frame.frame_type == SrsCodecVideoAVCFrame.KeyFrame) {
Log.i(TAG, String.format("worker: got frame type=%d, dts=%d, size=%dB", frame.type, frame.dts, frame.tag.size));
} else {
//Log.i(TAG, String.format("worker: got frame type=%d, dts=%d, size=%dB", frame.type, frame.dts, frame.tag.size));
} // cache the sequence header.
if (frame.type == SrsCodecFlvTag.Video && frame.avc_aac_type == SrsCodecVideoAVCType.SequenceHeader) {
videoSequenceHeader = frame;
} if (bos == null || frame.tag.size <= ) {
return;
} // write the 11B flv tag header
ByteBuffer th = ByteBuffer.allocate();
// Reserved UB [2]
// Filter UB [1]
// TagType UB [5]
// DataSize UI24
int tag_size = (int)((frame.tag.size & 0x00FFFFFF) | ((frame.type & 0x1F) << ));
th.putInt(tag_size);
// Timestamp UI24
// TimestampExtended UI8
int time = (int)((frame.dts << ) & 0xFFFFFF00) | ((frame.dts >> ) & 0x000000FF);
th.putInt(time);
// StreamID UI24 Always 0.
th.put((byte));
th.put((byte));
th.put((byte));
bos.write(th.array()); // write the flv tag data.
byte[] data = frame.tag.frame.array();
bos.write(data, , frame.tag.size); // write the 4B previous tag size.
// @remark, we append the tag size, this is different to SRS which write RTMP packet.
ByteBuffer pps = ByteBuffer.allocate();
pps.putInt((int)(frame.tag.size + ));
bos.write(pps.array()); bos.flush();
if (frame.frame_type == SrsCodecVideoAVCFrame.KeyFrame) {
Log.i(TAG, String.format("worker: send frame type=%d, dts=%d, size=%dB, tag_size=%#x, time=%#x",
frame.type, frame.dts, frame.tag.size, tag_size, time
));
}
}

全部使用Java代码,最后apk编译出来才1405KB,稳定性也高很多,我已经在上班路上直播过了,除了码率低不太清楚,还没有死掉过。

Android实时直播,一千行java搞定不依赖jni,延迟0.8至3秒,强悍移动端来袭的更多相关文章

  1. 30行代码搞定WCF并发性能测试

    [以下只是个人观点,欢迎交流] 30行代码搞定WCF并发性能 轻量级测试. 1. 调用并发测试接口 static void Main()         {               List< ...

  2. (转)面试大总结之一:Java搞定面试中的链表题目

    面试大总结之一:Java搞定面试中的链表题目 分类: Algorithm Interview2013-11-16 05:53 11628人阅读 评论(40) 收藏 举报 链表是面试中常出现的一类题目, ...

  3. 10行代码搞定移动web端自定义tap事件

    发发牢骚 移动web端里摸爬滚打这么久踩了不少坑,有一定移动web端经验的同学一定被click困扰过.我也不列外.一路走来被虐的不行,fastclick.touchend.iscroll什么的都用过, ...

  4. BaseHttpListActivity,几行代码搞定Android Http列表请求、加载和缓存

    Android开发中,向服务器请求一个列表并显示是非常常见的需求,但实现起来比较麻烦,代码繁杂. 随着应用的更新迭代,这种需求越来越多,我渐渐发现了实现这种需求的代码的共同点. 于是我将Activit ...

  5. (转载)Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup

    [置顶] [Android]快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup 标签: androidAdapter快速开发0耦合 2016-12-1 ...

  6. 面试大总结之二:Java搞定面试中的二叉树题目

    package BinaryTreeSummary; import java.util.ArrayList; import java.util.Iterator; import java.util.L ...

  7. [转] Android:用GSON 五招之内搞定任何JSON数组

    [From] http://www.open-open.com/lib/view/open1472632967912.html 写在前面 关于GSON的入门级使用,这里就不提了,如有需要可以看这篇博文 ...

  8. 面试大总结:Java搞定面试中的链表题目总结

    package LinkedListSummary; import java.util.HashMap; import java.util.Stack; /** * http://blog.csdn. ...

  9. [Unity Editor]10行代码搞定Hierarchy排序

    在日常的工作和研究中,当给我们的场景摆放过多的物件的时候,Hierarchy面板就会变得杂乱不堪.比如这样:    过多的层次结构充斥在里面,根层的物件毫无序列可言,整个层次面板显示非常的杂乱不堪,如 ...

随机推荐

  1. python进阶学习笔记(一)

    python进阶部分要学习的内容: 学习目标: 1.函数式编程 1.1,什么是函数式编程 函数式编程是一种抽象计算的编程模式 不同语言的抽象层次不同: 函数式编程的特点: python支持的函数式编程 ...

  2. service不死之身

    为了应用常驻后台,避免被第三方杀掉的方法 1)Service设置成START-STICKY kill 后会被重启(等待5秒左右),重传Intent,保持与重启前一样 2)通过startForegrou ...

  3. Oracle总结【SQL细节、多表查询、分组查询、分页】

    前言 在之前已经大概了解过Mysql数据库和学过相关的Oracle知识点,但是太久没用过Oracle了,就基本忘了...印象中就只有基本的SQL语句和相关一些概念....写下本博文的原因就是记载着Or ...

  4. Math.round(11.5)等于多少? Math.round(-11.5)等于多少?

    1.先说下怎么理解 round()方法可以这样理解: 将括号内的数+0.5之后,向下取值, 比如:round(3.4)就是3.4+0.5=3.9,向下取值是3,所以round(3.4)=3; roun ...

  5. javascript-深入理解&&和||

    先从两个问题看起: 第一个问题 为什么 a && b 返回的是true,b && a 返回的是6 var user = 6; var both = true; cons ...

  6. IO多路复用,同步,异步,阻塞和非阻塞 区别

    一.什么是socket?什么是I/O操作? 我们都知道unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO.管道.终端,对我们来说,一切都是 ...

  7. Windows PowerShell漫谈-win7下没有超级终端

    Windows PowerShell是我在研究win7新特性的时候发现的新工具,起初没有对它产生太大的兴趣,只是简单看看了有关它的介绍.简单使用了一下,感觉上它和cmd.exe没有本质区别.对它产生兴 ...

  8. 【javascript】jQuery判断用户右击事件

    jquery 判断用户是鼠标是右击还是左击, // 1 = 鼠标左键 left; 2 = 鼠标中键; 3 = 鼠标右键 $(document).mousedown(function(e) { if(3 ...

  9. 【linux】安装mysql出现 no curses错误解决方法

    安装mysql,在./configure时出现错误:error: No curses/termcap library found的解决办法 mysql版本:5.1.30 已经不记得这次是第几次安装my ...

  10. Linux显示所有可更新的软件清单命令

    Linux显示所有可更新的软件清单命令 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ yum check-update 程序"yum"尚未 ...