項目首頁: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伺服器完成,優勢如下:
使用系統的類,不引入jni和c的庫,簡單可靠,一千行左右java代碼就可以完成。 硬體編碼而非軟體編碼,系統負載低,800kbps編碼cpu使用率13%左右。 低延遲和RTMP一樣,0.8秒到3秒,使用的協議是HTTP FLV流,原理和RTMP一樣。 安裝包小無複雜依賴,編譯出來的apk都只有1405KB左右。 方便整合,只需要引入一個SrsHttpFlv類,進行轉封裝和打包發送,可以用在任何app中。
Android直播有幾個大的環節:
開啟Camera,進行Preview擷取YUV映像資料,也就是未壓縮的映像。
設定picture和preview大小後,計算YUV的buffer的尺寸,不能簡單乘以1.5而應該按照文檔計算。
擷取YUV的同時,還可以進行預覽,只要綁定到SurfaceHolder就可以。 使用MediaCodec和MediaFormat對YUV進行編碼,其中MediaCodec是編碼,MediaFormat是打包成annexb封裝。
設定MediaCodec的colorFormat需要判斷是否MediaCodec支援,也就是從MediaCodec擷取colorFormat。 將YUV映像,送入MediaCodec的inputBuffer,並擷取outputBuffer中已經編碼的資料,格式是annexb。
其中queueInputBuffer時,需要指定pts,否則沒有編碼資料輸出,會被丟棄。 將編碼的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 = 0; 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 == 640) { 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 = 0; 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 == 640) { 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) * 16; // y_size = stride * height int y_size = stride * height; // c_stride = ALIGN(stride/2, 16) int c_stride = (int)Math.ceil(width / 32.0) * 16; // c_size = c_stride * height/2 int c_size = c_stride * height / 2; // size = y_size + c_size * 2 return y_size + c_size * 2; }
映像編碼 第二個環節,設定編碼器參數,並啟動:
// 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() * 1000; // 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, 125000); format.setInteger(MediaFormat.KEY_FRAME_RATE, 15); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, chooseColorFormat()); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5); 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 = 0; i < nbCodecs; i++) { MediaCodecInfo mci = MediaCodecList.getCodecInfoAt(i); if (!mci.isEncoder()) { continue; } String[] types = mci.getSupportedTypes(); for (int j = 0; 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 = 0; MediaCodecInfo.CodecCapabilities cc = ci.getCapabilitiesForType(VCODEC); for (int i = 0; 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(-1); //Log.i(TAG, String.format("try to dequeue input buffer, ii=%d", inBufferIndex)); if (inBufferIndex >= 0) { ByteBuffer bb = inBuffers[inBufferIndex]; bb.clear(); bb.put(data, 0, data.length); long pts = new Date().getTime() * 1000 - presentationTimeUs; //Log.i(TAG, String.format("feed YUV to encode %dB, pts=%d", data.length, pts / 1000)); encoder.queueInputBuffer(inBufferIndex, 0, data.length, pts, 0); } for (;;) { int outBufferIndex = encoder.dequeueOutputBuffer(ebi, 0); //Log.i(TAG, String.format("try to dequeue output buffer, ii=%d, oi=%d", inBufferIndex, outBufferIndex)); if (outBufferIndex >= 0) { ByteBuffer bb = outBuffers[outBufferIndex]; onEncodedAnnexbFrame(bb, ebi); encoder.releaseOutputBuffer(outBufferIndex, false); } if (outBufferIndex < 0) { 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 / 1000); 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(0) & 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(0); 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 <= 0) { return; } // write the 11B flv tag header ByteBuffer th = ByteBuffer.allocate(11); // Reserved UB [2] // Filter UB [1] // TagType UB [5] // DataSize UI24 int tag_size = (int)((frame.tag.size & 0x00FFFFFF) | ((frame.type & 0x1F) << 24)); th.putInt(tag_size); // Timestamp UI24 // TimestampExtended UI8 int time = (int)((frame.dts << 8) & 0xFFFFFF00) | ((frame.dts >> 24) & 0x000000FF); th.putInt(time); // StreamID UI24 Always 0. th.put((byte)0); th.put((byte)0); th.put((byte)0); bos.write(th.array()); // write the flv tag data. byte[] data = frame.tag.frame.array(); bos.write(data, 0, 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(4); pps.putInt((int)(frame.tag.size + 11)); 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,穩定性也高很多,我已經在上班路上直播過了,除了碼率低不太清楚,還沒有死掉過。
Winlin