1. 程式人生 > >Android實時直播,一千行java搞定不依賴jni,延遲0.8至3秒,強悍移動端來襲

Android實時直播,一千行java搞定不依賴jni,延遲0.8至3秒,強悍移動端來襲

在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 = 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