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

目首页:https://github.com/simple-rtmp-server/srs-sea

SRS服务器项目:https://github.com/simple-rtmp-server/srs

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并预览:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. camera = Camera.open();  
  2. Camera.Parameters parameters = camera.getParameters();  
  3.   
  4. parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);  
  5. parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);  
  6. parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);  
  7. parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);  
  8. parameters.setPreviewFormat(ImageFormat.YV12);  
  9.   
  10. Camera.Size size = null;  
  11. List<Camera.Size> sizes = parameters.getSupportedPictureSizes();  
  12. for (int i = 0; i < sizes.size(); i++) {  
  13.     //Log.i(TAG, String.format("camera supported picture size %dx%d", sizes.get(i).width, sizes.get(i).height));  
  14.     if (sizes.get(i).width == 640) {  
  15.         size = sizes.get(i);  
  16.     }  
  17. }  
  18. parameters.setPictureSize(size.width, size.height);  
  19. Log.i(TAG, String.format("set the picture size in %dx%d", size.width, size.height));  
  20.   
  21. sizes = parameters.getSupportedPreviewSizes();  
  22. for (int i = 0; i < sizes.size(); i++) {  
  23.     //Log.i(TAG, String.format("camera supported preview size %dx%d", sizes.get(i).width, sizes.get(i).height));  
  24.     if (sizes.get(i).width == 640) {  
  25.         vsize = size = sizes.get(i);  
  26.     }  
  27. }  
  28. parameters.setPreviewSize(size.width, size.height);  
  29. Log.i(TAG, String.format("set the preview size in %dx%d", size.width, size.height));  
  30.   
  31. camera.setParameters(parameters);  
  32.   
  33. // set the callback and start the preview.  
  34. buffer = new byte[getYuvBuffer(size.width, size.height)];  
  35. camera.addCallbackBuffer(buffer);  
  36. camera.setPreviewCallbackWithBuffer(onYuvFrame);  
  37. try {  
  38.     camera.setPreviewDisplay(preview.getHolder());  
  39. catch (IOException e) {  
  40.     Log.e(TAG, "preview video failed.");  
  41.     e.printStackTrace();  
  42.     return;  
  43. }  
  44. Log.i(TAG, String.format("start to preview video in %dx%d, buffer %dB", size.width, size.height, buffer.length));  
  45. camera.startPreview();  

计算YUV的buffer的函数,需要根据文档计算,而不是简单“*3/2”:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // for the buffer for YV12(android YUV), @see below:  
  2. // https://developer.android.com/reference/android/hardware/Camera.Parameters.html#setPreviewFormat(int)  
  3. // https://developer.android.com/reference/android/graphics/ImageFormat.html#YV12  
  4. private int getYuvBuffer(int width, int height) {  
  5.     // stride = ALIGN(width, 16)  
  6.     int stride = (int)Math.ceil(width / 16.0) * 16;  
  7.     // y_size = stride * height  
  8.     int y_size = stride * height;  
  9.     // c_stride = ALIGN(stride/2, 16)  
  10.     int c_stride = (int)Math.ceil(width / 32.0) * 16;  
  11.     // c_size = c_stride * height/2  
  12.     int c_size = c_stride * height / 2;  
  13.     // size = y_size + c_size * 2  
  14.     return y_size + c_size * 2;  
  15. }  

图像编码

第二个环节,设置编码器参数,并启动:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // encoder yuv to 264 es stream.  
  2. // requires sdk level 16+, Android 4.1, 4.1.1, the JELLY_BEAN  
  3. try {  
  4.     encoder = MediaCodec.createEncoderByType(VCODEC);  
  5. catch (IOException e) {  
  6.     Log.e(TAG, "create encoder failed.");  
  7.     e.printStackTrace();  
  8.     return;  
  9. }  
  10. ebi = new MediaCodec.BufferInfo();  
  11. presentationTimeUs = new Date().getTime() * 1000;  
  12.   
  13. // start the encoder.  
  14. // @see https://developer.android.com/reference/android/media/MediaCodec.html  
  15. MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, vsize.width, vsize.height);  
  16. format.setInteger(MediaFormat.KEY_BIT_RATE, 125000);  
  17. format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);  
  18. format.setInteger(MediaFormat.KEY_COLOR_FORMAT, chooseColorFormat());  
  19. format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);  
  20. encoder.configure(format, nullnull, MediaCodec.CONFIGURE_FLAG_ENCODE);  
  21. encoder.start();  
  22. Log.i(TAG, "encoder start");  

其中,colorFormat需要从编码器支持的格式中选取,否则会有不支持的错误:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // choose the right supported color format. @see below:  
  2. // https://developer.android.com/reference/android/media/MediaCodecInfo.html  
  3. // https://developer.android.com/reference/android/media/MediaCodecInfo.CodecCapabilities.html  
  4. private int chooseColorFormat() {  
  5.     MediaCodecInfo ci = null;  
  6.   
  7.     int nbCodecs = MediaCodecList.getCodecCount();  
  8.     for (int i = 0; i < nbCodecs; i++) {  
  9.         MediaCodecInfo mci = MediaCodecList.getCodecInfoAt(i);  
  10.         if (!mci.isEncoder()) {  
  11.             continue;  
  12.         }  
  13.   
  14.         String[] types = mci.getSupportedTypes();  
  15.         for (int j = 0; j < types.length; j++) {  
  16.             if (types[j].equalsIgnoreCase(VCODEC)) {  
  17.                 //Log.i(TAG, String.format("encoder %s types: %s", mci.getName(), types[j]));  
  18.                 ci = mci;  
  19.                 break;  
  20.             }  
  21.         }  
  22.     }  
  23.   
  24.     int matchedColorFormat = 0;  
  25.     MediaCodecInfo.CodecCapabilities cc = ci.getCapabilitiesForType(VCODEC);  
  26.     for (int i = 0; i < cc.colorFormats.length; i++) {  
  27.         int cf = cc.colorFormats[i];  
  28.         //Log.i(TAG, String.format("encoder %s supports color fomart %d", ci.getName(), cf));  
  29.   
  30.         // choose YUV for h.264, prefer the bigger one.  
  31.         if (cf >= cc.COLOR_FormatYUV411Planar && cf <= cc.COLOR_FormatYUV422SemiPlanar) {  
  32.             if (cf > matchedColorFormat) {  
  33.                 matchedColorFormat = cf;  
  34.             }  
  35.         }  
  36.     }  
  37.   
  38.     Log.i(TAG, String.format("encoder %s choose color format %d", ci.getName(), matchedColorFormat));  
  39.     return matchedColorFormat;  
  40. }  

第三个环节,在YUV图像回调中,送给编码器,并获取输出:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // when got YUV frame from camera.  
  2. // @see https://developer.android.com/reference/android/media/MediaCodec.html  
  3. final Camera.PreviewCallback onYuvFrame = new Camera.PreviewCallback() {  
  4.     @Override  
  5.     public void onPreviewFrame(byte[] data, Camera camera) {  
  6.         //Log.i(TAG, String.format("got YUV image, size=%d", data.length));  
  7.   
  8.         // feed the encoder with yuv frame, got the encoded 264 es stream.  
  9.         ByteBuffer[] inBuffers = encoder.getInputBuffers();  
  10.         ByteBuffer[] outBuffers = encoder.getOutputBuffers();  
  11.         if (true) {  
  12.             int inBufferIndex = encoder.dequeueInputBuffer(-1);  
  13.             //Log.i(TAG, String.format("try to dequeue input buffer, ii=%d", inBufferIndex));  
  14.             if (inBufferIndex >= 0) {  
  15.                 ByteBuffer bb = inBuffers[inBufferIndex];  
  16.                 bb.clear();  
  17.                 bb.put(data, 0, data.length);  
  18.                 long pts = new Date().getTime() * 1000 - presentationTimeUs;  
  19.                 //Log.i(TAG, String.format("feed YUV to encode %dB, pts=%d", data.length, pts / 1000));  
  20.                 encoder.queueInputBuffer(inBufferIndex, 0, data.length, pts, 0);  
  21.             }  
  22.   
  23.             for (;;) {  
  24.                 int outBufferIndex = encoder.dequeueOutputBuffer(ebi, 0);  
  25.                 //Log.i(TAG, String.format("try to dequeue output buffer, ii=%d, oi=%d", inBufferIndex, outBufferIndex));  
  26.                 if (outBufferIndex >= 0) {  
  27.                     ByteBuffer bb = outBuffers[outBufferIndex];  
  28.                     onEncodedAnnexbFrame(bb, ebi);  
  29.                     encoder.releaseOutputBuffer(outBufferIndex, false);  
  30.                 }  
  31.   
  32.                 if (outBufferIndex < 0) {  
  33.                     break;  
  34.                 }  
  35.             }  
  36.         }  
  37.   
  38.         // to fetch next frame.  
  39.         camera.addCallbackBuffer(buffer);  
  40.     }  
  41. };  

MUX为FLV流

获取编码的annexb数据后,调用函数发送到服务器:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // when got encoded h264 es stream.  
  2. private void onEncodedAnnexbFrame(ByteBuffer es, MediaCodec.BufferInfo bi) {  
  3.     try {  
  4.         muxer.writeSampleData(videoTrack, es, bi);  
  5.     } catch (Exception e) {  
  6.         Log.e(TAG, "muxer write sample failed.");  
  7.         e.printStackTrace();  
  8.     }  
  9. }  

最后这个环节,一般会用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打包的过程如下:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public void writeVideoSample(final ByteBuffer bb, MediaCodec.BufferInfo bi) throws Exception {  
  2.     int pts = (int)(bi.presentationTimeUs / 1000);  
  3.     int dts = (int)pts;  
  4.   
  5.     ArrayList<SrsAnnexbFrame> ibps = new ArrayList<SrsAnnexbFrame>();  
  6.     int frame_type = SrsCodecVideoAVCFrame.InterFrame;  
  7.     //Log.i(TAG, String.format("video %d/%d bytes, offset=%d, position=%d, pts=%d", bb.remaining(), bi.size, bi.offset, bb.position(), pts));  
  8.   
  9.     // send each frame.  
  10.     while (bb.position() < bi.size) {  
  11.         SrsAnnexbFrame frame = avc.annexb_demux(bb, bi);  
  12.   
  13.         // 5bits, 7.3.1 NAL unit syntax,  
  14.         // H.264-AVC-ISO_IEC_14496-10.pdf, page 44.  
  15.         //  7: SPS, 8: PPS, 5: I Frame, 1: P Frame  
  16.         int nal_unit_type = (int)(frame.frame.get(0) & 0x1f);  
  17.         if (nal_unit_type == SrsAvcNaluType.SPS || nal_unit_type == SrsAvcNaluType.PPS) {  
  18.             Log.i(TAG, String.format("annexb demux %dB, pts=%d, frame=%dB, nalu=%d", bi.size, pts, frame.size, nal_unit_type));  
  19.         }  
  20.   
  21.         // for IDR frame, the frame is keyframe.  
  22.         if (nal_unit_type == SrsAvcNaluType.IDR) {  
  23.             frame_type = SrsCodecVideoAVCFrame.KeyFrame;  
  24.         }  
  25.   
  26.         // ignore the nalu type aud(9)  
  27.         if (nal_unit_type == SrsAvcNaluType.AccessUnitDelimiter) {  
  28.             continue;  
  29.         }  
  30.   
  31.         // for sps  
  32.         if (avc.is_sps(frame)) {  
  33.             byte[] sps = new byte[frame.size];  
  34.             frame.frame.get(sps);  
  35.   
  36.             if (utils.srs_bytes_equals(h264_sps, sps)) {  
  37.                 continue;  
  38.             }  
  39.             h264_sps_changed = true;  
  40.             h264_sps = sps;  
  41.             continue;  
  42.         }  
  43.   
  44.         // for pps  
  45.         if (avc.is_pps(frame)) {  
  46.             byte[] pps = new byte[frame.size];  
  47.             frame.frame.get(pps);  
  48.   
  49.             if (utils.srs_bytes_equals(h264_pps, pps)) {  
  50.                 continue;  
  51.             }  
  52.             h264_pps_changed = true;  
  53.             h264_pps = pps;  
  54.             continue;  
  55.         }  
  56.   
  57.         // ibp frame.  
  58.         SrsAnnexbFrame nalu_header = avc.mux_ibp_frame(frame);  
  59.         ibps.add(nalu_header);  
  60.         ibps.add(frame);  
  61.     }  
  62.   
  63.     write_h264_sps_pps(dts, pts);  
  64.   
  65.     write_h264_ipb_frame(ibps, frame_type, dts, pts);  
  66. }  

至于发送到服务器,其实就是使用系统的HTTP客户端。代码如下:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. private void reconnect() throws Exception {  
  2.     // when bos not null, already connected.  
  3.     if (bos != null) {  
  4.         return;  
  5.     }  
  6.   
  7.     disconnect();  
  8.   
  9.     URL u = new URL(url);  
  10.     conn = (HttpURLConnection)u.openConnection();  
  11.   
  12.     Log.i(TAG, String.format("worker: connect to SRS by url=%s", url));  
  13.     conn.setDoOutput(true);  
  14.     conn.setChunkedStreamingMode(0);  
  15.     conn.setRequestProperty("Content-Type""application/octet-stream");  
  16.     bos = new BufferedOutputStream(conn.getOutputStream());  
  17.     Log.i(TAG, String.format("worker: muxer opened, url=%s", url));  
  18.   
  19.     // write 13B header  
  20.     // 9bytes header and 4bytes first previous-tag-size  
  21.     byte[] flv_header = new byte[]{  
  22.             'F''L''V'// Signatures "FLV"  
  23.             (byte0x01// File version (for example, 0x01 for FLV version 1)  
  24.             (byte0x00// 4, audio; 1, video; 5 audio+video.  
  25.             (byte0x00, (byte0x00, (byte0x00, (byte0x09// DataOffset UI32 The length of this header in bytes  
  26.             (byte0x00, (byte0x00, (byte0x00, (byte0x00  
  27.     };  
  28.     bos.write(flv_header);  
  29.     bos.flush();  
  30.     Log.i(TAG, String.format("worker: flv header ok."));  
  31.   
  32.     sendFlvTag(bos, videoSequenceHeader);  
  33. }  
  34.   
  35. private void sendFlvTag(BufferedOutputStream bos, SrsFlvFrame frame) throws IOException {  
  36.     if (frame == null) {  
  37.         return;  
  38.     }  
  39.   
  40.     if (frame.frame_type == SrsCodecVideoAVCFrame.KeyFrame) {  
  41.         Log.i(TAG, String.format("worker: got frame type=%d, dts=%d, size=%dB", frame.type, frame.dts, frame.tag.size));  
  42.     } else {  
  43.         //Log.i(TAG, String.format("worker: got frame type=%d, dts=%d, size=%dB", frame.type, frame.dts, frame.tag.size));  
  44.     }  
  45.   
  46.     // cache the sequence header.  
  47.     if (frame.type == SrsCodecFlvTag.Video && frame.avc_aac_type == SrsCodecVideoAVCType.SequenceHeader) {  
  48.         videoSequenceHeader = frame;  
  49.     }  
  50.   
  51.     if (bos == null || frame.tag.size <= 0) {  
  52.         return;  
  53.     }  
  54.   
  55.     // write the 11B flv tag header  
  56.     ByteBuffer th = ByteBuffer.allocate(11);  
  57.     // Reserved UB [2]  
  58.     // Filter UB [1]  
  59.     // TagType UB [5]  
  60.     // DataSize UI24  
  61.     int tag_size = (int)((frame.tag.size & 0x00FFFFFF) | ((frame.type & 0x1F) << 24));  
  62.     th.putInt(tag_size);  
  63.     // Timestamp UI24  
  64.     // TimestampExtended UI8  
  65.     int time = (int)((frame.dts << 8) & 0xFFFFFF00) | ((frame.dts >> 24) & 0x000000FF);  
  66.     th.putInt(time);  
  67.     // StreamID UI24 Always 0.  
  68.     th.put((byte)0);  
  69.     th.put((byte)0);  
  70.     th.put((byte)0);  
  71.     bos.write(th.array());  
  72.   
  73.     // write the flv tag data.  
  74.     byte[] data = frame.tag.frame.array();  
  75.     bos.write(data, 0, frame.tag.size);  
  76.   
  77.     // write the 4B previous tag size.  
  78.     // @remark, we append the tag size, this is different to SRS which write RTMP packet.  
  79.     ByteBuffer pps = ByteBuffer.allocate(4);  
  80.     pps.putInt((int)(frame.tag.size + 11));  
  81.     bos.write(pps.array());  
  82.   
  83.     bos.flush();  
  84.     if (frame.frame_type == SrsCodecVideoAVCFrame.KeyFrame) {  
  85.         Log.i(TAG, String.format("worker: send frame type=%d, dts=%d, size=%dB, tag_size=%#x, time=%#x",  
  86.                 frame.type, frame.dts, frame.tag.size, tag_size, time  
  87.         ));  
  88.     }  
  89. }  

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

Winlin

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一: 使用javacv来实现,最终也是用过ffmpeg来进编码和推流,javacv实现到可以直接接收摄像头的帧数据 需要自己实现的代码只是打开摄像头,写一个SurfaceView进预览,然后实现PreviewCallback将摄像头每一帧的数据交给javacv即可 javacv地址:https://github.com/bytedeco/javacv demo地址:https://github.com/beautifulSoup/RtmpRecoder/tree/master 二: 使用Android自带的编码工具,可实现硬编码,这里有一个国内大神开源的封装很完善的的库yasea,第一种方法需要实现的Camera采集部分也一起封装好了,进一些简单配置就可以实现编码推流,并且yasea目前已经直接支持摄像头的热切换,和各种滤镜效果 yasea地址(内置demo):https://github.com/begeekmyfriend/yasea 服务器 流媒体服务器我用的是srs,项目地址:https://github.com/ossrs/srs 关于srs的编译、配置、部署、在官方wiki中已经写的很详细了,并且srs同样是国内开发人员开源的项目,有全中文的文档,看起来很方便 这里有最基本的简单编译部署过程 Android直播实现(二)srs流媒体服务器部署 播放器 android端的播放使用vitamio,还是国内的开源播放器,是不是感觉国内的前辈们越来越屌了^~^! vitamio支持几乎所有常见的的视频格式和流媒体协议 vitamio地址(内置demo):https://github.com/yixia/VitamioBundle 这里使用的是yaesa库,先介绍一下直播实现的流程:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值