Android直播解决方案

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a992036795/article/details/64460049

思路

  1. 要实现直播我们必须有3个东西 推流端、流媒体服务器、播放端。
  2. 流媒体服务器我们可以暂时不考虑,可以直接使用开源的服务器red5或nginx等。
  3. 推流端设计:
    • 如何进行音频、视频采集
    • 如何进行音频、视频编码
    • 如何音视频一起实时发送
  4. 播放端设计:
    • 如何接收音频、视频包
    • 如何进行音频、视频解码
    • 如何播放音频、视频

本文采用的协议

  1. 我们音频采用AAC编码、视频采用H264编码。
  2. 推送采用RTMP协议。

推送端实现

推送端的实现我在曾经的一遍文章中已经讲过,文章地址 http://blog.csdn.net/a992036795/article/details/54583571 ,这篇文章中代码为了便于阅读,当时所有的核心代码都放在了MainActivity中,并且都运行在主线程所以造成和很多问题,我已经将代码整理并上传了github,末尾我将奉上项目地址。
由于代码量较多,我就只贴出部分代码,大家可以直接去我的github中查看完整代码。

音频采集

音频采集我使用一个单独的线程去采集,并将采集的数据放入到一个队列中。这个队列中的数据将会由音频编码器去消费。

采集音频代码:

     public void start() {
        workThread = new Thread() {
            @Override
            public void run() {
                mAudioRecord.startRecording();
                while (loop && !Thread.interrupted()) {
                    int size = mAudioRecord.read(buffer, 0, buffer.length);
                    if (size < 0) {
                        Log.i(TAG, "audio ignore ,no data to read");
                        break;
                    }
                    if (loop) {
                        byte[] audio = new byte[size];
                        System.arraycopy(buffer, 0, audio, 0, size);
                        if (mCallback != null) {
                            mCallback.audioData(audio);
                        }
                    }
                }

            }
        };

        loop = true;
        workThread.start();
    }

回调传递数据:

     mAudioGatherer.setCallback(new AudioGatherer.Callback() {
            @Override
            public void audioData(byte[] data) {
                if (isPublish) {
                    mMediaEncoder.putAudioData(data);
                }
            }
        });

加入到队列:

    private LinkedBlockingQueue<byte[]> audioQueue;

      public void putAudioData(byte[] data) {
        try {
            audioQueue.put(data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

视频采集

视频采集和音频采集类似,也是启动一个线程将采集到的数据放入一个队列然后等待视频编码器去消费。只不过这里我们需要将采集到的视频处理成编码器支持的像素格式。我们设置摄像头参数的时候设置成:parameters.setPreviewFormat(ImageFormat.Nv21),这样我们在回调函数:public void onPreviewFrame(final byte[] data, final Camera camera) 拿到的数据像素格式就是Nv21格式。(Nv21是Yuv420格式的一种,Yuv420格式还分为Yuv420SP,Yuv420P等,大家可以搜索相关资料,网上也有许多转换的方法)
设置回调方法:

mCamera.setPreviewCallbackWithBuffer(getPreviewCallback());
mCamera.addCallbackBuffer(new byte[calculateFrameSize(ImageFormat.NV21)]);

回调出拿到NV21数据并放入一个队列,这个队列将有一个线程消费,负责将他的像素格式转换成编码器需要的像素格式:

public Camera.PreviewCallback getPreviewCallback() {
    return new Camera.PreviewCallback() {
        //            byte[] dstByte = new byte[calculateFrameSize(ImageFormat.NV21)];
        @Override
        public void onPreviewFrame(final byte[] data, final Camera camera) {
            if (data != null) {
                if (isPublished) {
                    try {
                        mQueue.put(new PixelData(colorFormat, data));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else {
                camera.addCallbackBuffer(new byte[calculateFrameSize(ImageFormat.NV21)]);
            }
        }
    };
}

转换像素格式并放入一个队列,这个队列将由视频编码器消费:

private void initWorkThread() {
    workThread = new Thread() {
        private long preTime;
        //YUV420
        byte[] dstByte = new byte[calculateFrameSize(ImageFormat.NV21)];

        @Override
        public void run() {
            while (loop && !Thread.interrupted()) {
                try {
                    PixelData pixelData = mQueue.take();
                    // 处理
                    if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) {
                        Yuv420Util.Nv21ToYuv420SP(pixelData.data, dstByte, previewSize.width, previewSize.height);
                    } else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar) {
                        Yuv420Util.Nv21ToI420(pixelData.data, dstByte, previewSize.width, previewSize.height);
                    } else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) {
                        // Yuv420_888
                    } else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar) {
                        // Yuv420packedPlannar 和 yuv420sp很像
                        // 区别在于 加入 width = 4的话 y1,y2,y3 ,y4公用 u1v1
                        // 而 yuv420dp 则是 y1y2y5y6 共用 u1v1
                        //这样处理的话颜色核能会有些失真。
                        Yuv420Util.Nv21ToYuv420SP(pixelData.data, dstByte, previewSize.width, previewSize.height);
                    } else if (colorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar) {
                    } else {
                        System.arraycopy(pixelData.data, 0, dstByte, 0, pixelData.data.length);
                    }

                    if (mCallback != null) {
                        mCallback.onReceive(dstByte, colorFormat);
                    }
                    //处理完成之后调用 addCallbackBuffer()
                    if (preTime != 0) {
                        // 延时
                        int shouldDelay = (int) (1000.0 / FPS);
                        int realDelay = (int) (System.currentTimeMillis() - preTime);
                        int delta = shouldDelay - realDelay;
                        if (delta > 0) {
                            sleep(delta);
                        }
                    }
                    addCallbackBuffer(pixelData.data);
                    preTime = System.currentTimeMillis();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }
    };
}

音频编码

音视频编码我都采用Andorid自带的MediaCodec进行编码。

音频编码,读取音频采集线程放入队列的数据,进行编码

/**
 * 开始音频编码
 */
public void startAudioEncode() {
    if (aEncoder == null) {
        throw new RuntimeException("请初始化音频编码器");
    }

    if (audioEncoderLoop) {
        throw new RuntimeException("必须先停止");
    }
    audioEncoderThread = new Thread() {
        @Override
        public void run() {
            presentationTimeUs = System.currentTimeMillis() * 1000;
            aEncoder.start();
            while (audioEncoderLoop && !Thread.interrupted()) {
                try {
                    byte[] data = audioQueue.take();
                    encodeAudioData(data);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }

        }
    };
    audioEncoderLoop = true;
    audioEncoderThread.start();
}
/**
 * 音频编码
 *
 * @param data
 */
private void encodeAudioData(byte[] data) {
    ByteBuffer[] inputBuffers = aEncoder.getInputBuffers();
    ByteBuffer[] outputBuffers = aEncoder.getOutputBuffers();
    int inputBufferId = aEncoder.dequeueInputBuffer(-1);
    if (inputBufferId >= 0) {
        ByteBuffer bb = inputBuffers[inputBufferId];
        bb.clear();
        bb.put(data, 0, data.length);
        long pts = new Date().getTime() * 1000 - presentationTimeUs;
        aEncoder.queueInputBuffer(inputBufferId, 0, data.length, pts, 0);
    }

    int outputBufferId = aEncoder.dequeueOutputBuffer(aBufferInfo, 0);
    if (outputBufferId >= 0) {
        // outputBuffers[outputBufferId] is ready to be processed or rendered.
        ByteBuffer bb = outputBuffers[outputBufferId];
        if (mCallback != null) {
            mCallback.outputAudioData(bb, aBufferInfo);
        }
        aEncoder.releaseOutputBuffer(outputBufferId, false);
    }

}

视频编码

视频编码和音频编码类似,也是使用MediaCodec进行编码

读取队列中的数据:

  /**
     * 开始视频编码
     */
    public void startVideoEncode() {
        if (vEncoder == null) {
            throw new RuntimeException("请初始化视频编码器");
        }
        if (videoEncoderLoop) {
            throw new RuntimeException("必须先停止");
        }

        videoEncoderThread = new Thread() {
            @Override
            public void run() {
                presentationTimeUs = System.currentTimeMillis() * 1000;
                vEncoder.start();
                while (videoEncoderLoop && !Thread.interrupted()) {
                    try {

                        byte[] data = videoQueue.take(); //待编码的数据
                        encodeVideoData(data);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

            }
        };
        videoEncoderLoop = true;
        videoEncoderThread.start();
    }

编码:

  /**
     * 视频编码
     *
     * @param dstByte
     */
    private void encodeVideoData(byte[] dstByte) {
        ByteBuffer[] inputBuffers = vEncoder.getInputBuffers();
        ByteBuffer[] outputBuffers = vEncoder.getOutputBuffers();

        int inputBufferId = vEncoder.dequeueInputBuffer(-1);
        if (inputBufferId >= 0) {
            // fill inputBuffers[inputBufferId] with valid data
            ByteBuffer bb = inputBuffers[inputBufferId];
            bb.clear();
            bb.put(dstByte, 0, dstByte.length);
            long pts = new Date().getTime() * 1000 - presentationTimeUs;
            vEncoder.queueInputBuffer(inputBufferId, 0, dstByte.length, pts, 0);
        }

        int outputBufferId = vEncoder.dequeueOutputBuffer(vBufferInfo, 0);
        if (outputBufferId >= 0) {
            // outputBuffers[outputBufferId] is ready to be processed or rendered.
            ByteBuffer bb = outputBuffers[outputBufferId];
            if (null != mCallback) {
                mCallback.outputVideoData(bb, vBufferInfo);
            }
            vEncoder.releaseOutputBuffer(outputBufferId, false);
        }

    }

最后我们将编码中的数据,和对应的发送操作封装成一个Runable对象,放入一个队列,等待推送线程推送。

音视频推送

推送使用开源的librtmp,这里将要使用到jni。详细的实现就在这里不说了,大家可以去看我的代码就好好了。jni层实现也只有一点。
Jni类,其中的发送就是用来发送h264数据,和aac数据。

public final class PublishJni {
    static {
        System.loadLibrary("publish");
    }

    static native long init(String url, int w, int h, int timeOut);

    static native int sendSpsAndPps(long cptr, byte[] sps, int spsLen, byte[] pps, int ppsLen, long timestamp);

    static native int sendVideoData(long cptr, byte[] data, int len, long timestamp);

    static native int sendAacSpec(long cptr, byte[] data, int len);

    static native int sendAacData(long cptr, byte[] data, int len, long timestamp);

    static native int stop(long cptr);

}

服务器搭建

大家可以直接使用开源的服务器,我这里使用red5。下载安装、并配置环境变量。完毕之后输入red5命令就可以启动服务器了。

播放端

我使用开源的ijplayer框架,使用它示例中提供的ijplayer example 就可以直接播放了。

项目地址

https://github.com/blueberryCoder/LiveStream

猜你喜欢

转载自blog.csdn.net/a992036795/article/details/64460049