基于RTMP协议流媒体直播的整体解决方案

前言

随着自媒体的短视频和直播带货的流行,再加上这几年疫情的肆虐,直接把实体店给干趴下了。叫苦连天的实体店老板们,原来是不懂这些东西,而被割了韭菜。当然这只是开个玩笑,哈哈。他们没必要懂技术。今天我就来剖析直播的前端和后端的实现流程,干货满满,不要忘了先点个小赞,谢谢了。那么就开始我们今天要讲的内容。

后端

先讲后端,想看Android端实现的稍安勿躁。

RTMP协议

RTMP(Real-Time Messaging Protocol)是一种用于在互联网上传输音频、视频和数据的协议。它最初由Macromedia开发,后来由Adobe Systems继续开发和维护。RTMP最常用于实时数据传输,特别是在流媒体领域中。它被广泛用于在线直播、视频会议和其他实时通信应用中。

RTMP流媒体服务器搭建

首先,我们需要购置一台ECS。 操作系统:推荐Linux(CentOS 7.6 64位) 配置:1核CPU、512M内存、2Mbps服务器带宽(这是能流畅测试的最低配置,1Mbps丢帧会比较严重)。 软件环境:nginx、nginx-rtmp-module。

Nginx的反向代理和负载均衡

有人问我,Nginx是什么?Nginx是一个反向代理程序。既然有反向,那肯定就有正向咯!你怎么这么聪明。那么,什么是正向代理呢?简言之,正向代理就是代理我们客户端的请求,我们经常科学上网用的VPN软件就属于正向代理。反向代理恰恰相反,代理的是我们服务端接收请求。为什么还需要反向代理呢?这个问题问得好。我们客户端不也通常喜欢做拦截,插入一些业务逻辑吗?比如AOP。那么服务端是不是也喜欢搞这些框架的事情,反正都帮你考虑好了,你开箱即用就可以了。不是开棺啊,是开箱即用。如果所有的客户端请求都由同一个节点处理,就算这台服务器节点的性能再好,是不是也可能被打宕机。这是肯定的啊,你一天工作10几个小时,周末还加班,身体肯定也撑不住啊。超过一定的负载阈值,量变就是产生质变。Nginx就是把请求接过来,自己不处理,然后分配给其他的节点处理,称为服务器集群。Nginx来实时监测其他节点的负载状况,公平的分配任务。

安装所需依赖

安装Nginx前,需要先把这几个软件安装好。

shell 复制代码
yum install -y pcre pcre-devel zlib zlib-devel openssl openssl-devel

下载Nginx

shell 复制代码
mkdir nginx
cd /nginx
wget https://dorachat-sdk.oss-cn-hongkong.aliyuncs.com/nginx-1.9.11.tar.gz
tar zxvf nginx-1.9.11.tar.gz

下载Nginx的RTMP模块

shell 复制代码
wget https://dorachat-sdk.oss-cn-hongkong.aliyuncs.com/nginx-rtmp-module-1.2.2.zip
unzip nginx-rtmp-module-1.2.2.zip

编译

将Nginx的RTMP模块和Nginx的源码一起编译。

shell 复制代码
cd nginx-1.9.11
./configure --add-module=/root/nginx/nginx-rtmp-module-1.2.2  
make  
make install

然后我们查看下是否编译成功。nginx的默认安装目录/usr/local/nginx, 里面的sbin目录下有个nginx主程序,启动它。

shell 复制代码
sudo ./nginx -t

查看是否启动。

shell 复制代码
ps -ef | grep nginx

配置RTMP

回到Nginx安装目录,修改nginx配置添加rtmp。

shell 复制代码
sudo vi conf/nginx.conf

nginx的配置文件内容,你覆盖成以下的内容就好。

shell 复制代码
# events属于Nginx配置范畴,不属于rtmp配置范畴
events {
    worker_connections  8192;
}

rtmp {
   server {
         listen 1935;
         #server_name dorachat.com;
         chunk_size 4096;
         application live {
             live on;
             record off;
             # 设置推流和拉流鉴权地址
             # on_publish http://127.0.0.1:8686/auth; 
             # on_play http://127.0.0.1:8686/auth; 
             wait_key on; #对视频切片进行保护,这样就不会产生马赛克了。
             hls_path /opt/live/hls; #切片视频文件存放位置。
             hls_fragment  600s;     #设置HLS片段长度。
             hls_playlist_length 10m;  #设置HLS播放列表长度,这里设置的是10分钟。
             hls_continuous on; #连续模式。
             hls_cleanup on;    #对多余的切片进行删除。
             hls_nested on;     #嵌套模式。
         }
     }
}

我们的流媒体服务器监听的是1935端口,所以阿里云的安全组开放入方向的1935端口。最后我们重新加载nginx的配置。

shell 复制代码
sudo ./sbin/nginx -s reload

对于高并发和负载均衡我这里就不细说了,有兴趣的可以自行研究upstream和proxy_pass的配置。

测试流媒体服务器是否搭建成功

下载OBS进行推流

OBS官网 obsproject.com/welcome

⚠️:推流地址设置 rtmp://16.62.162.36:1935/live/home,如live为配置中的application live,home为推流码。home你也可以改成userId。

下载VLC(RTMP播放器)

VLC下载地址 dorachat-sdk.oss-cn-hongkong.aliyuncs.com/vlc-3.0.20-...

Android端

Android端的推流我们需要使用到NDK,而拉流播放就简单了,使用google官方的ExoPlayer播放器进行播放即可。ExoPlayer官方中文文档developer.android.com/media/media...

Java层

在Java层,我们做一些编码推流的流程控制。

代码实现

CameraHelper.kt

kt 复制代码
package site.doramusic.app.live

import android.app.Activity
import android.graphics.ImageFormat
import android.hardware.Camera
import android.hardware.Camera.CameraInfo
import android.hardware.Camera.PreviewCallback
import android.util.Log
import android.view.Surface
import android.view.SurfaceHolder

class CameraHelper(
    private val activity: Activity,
    private var cameraId: Int,
    private var width: Int,
    private var height: Int
) :
    SurfaceHolder.Callback, PreviewCallback {
    private var camera: Camera? = null
    private var buffer: ByteArray? = null
    private var surfaceHolder: SurfaceHolder? = null
    private var previewCallback: PreviewCallback? = null
    private var rotation = 0
    private var onChangedSizeListener: OnChangedSizeListener? = null
    var bytes: ByteArray? = null

    fun switchCamera() {
        cameraId = if (cameraId == CameraInfo.CAMERA_FACING_BACK) {
            CameraInfo.CAMERA_FACING_FRONT
        } else {
            CameraInfo.CAMERA_FACING_BACK
        }
        stopPreview()
        startPreview()
    }

    private fun stopPreview() {
        // 预览数据回调接口
        camera?.setPreviewCallback(null)
        // 停止预览
        camera?.stopPreview()
        // 释放摄像头
        camera?.release()
        camera = null
    }

    private fun startPreview() {
        try { 
            // 获得camera对象
            camera = Camera.open(cameraId)
            // 配置camera的属性
            val parameters = camera!!.getParameters()
            // 设置预览数据格式为nv21
            parameters.previewFormat = ImageFormat.NV21
            // 这是摄像头宽、高
            setPreviewSize(parameters)
            // 设置摄像头 图像传感器的角度、方向
            setPreviewOrientation(parameters)
            camera!!.setParameters(parameters)
            buffer = ByteArray(width * height * 3 / 2)
            bytes = ByteArray(buffer!!.size)
            // 数据缓存区
            camera!!.addCallbackBuffer(buffer)
            camera!!.setPreviewCallbackWithBuffer(this)
            // 设置预览画面
            camera!!.setPreviewDisplay(surfaceHolder)
            camera!!.startPreview()
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
    }

    private fun setPreviewOrientation(parameters: Camera.Parameters) {
        val info = CameraInfo()
        Camera.getCameraInfo(cameraId, info)
        rotation = activity.windowManager.defaultDisplay.rotation
        var degrees = 0
        when (rotation) {
            Surface.ROTATION_0 -> {
                degrees = 0
                onChangedSizeListener!!.onChanged(height, width)
            }
            Surface.ROTATION_90 -> {
                degrees = 90
                onChangedSizeListener!!.onChanged(width, height)
            }
            Surface.ROTATION_270 -> {
                degrees = 270
                onChangedSizeListener!!.onChanged(width, height)
            }
        }
        var result: Int
        if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360
            result = (360 - result) % 360 // compensate the mirror
        } else { // back-facing
            result = (info.orientation - degrees + 360) % 360
        }
        // 设置角度
        camera!!.setDisplayOrientation(result)
    }

    private fun setPreviewSize(parameters: Camera.Parameters) { 
        // 获取摄像头支持的宽、高
        val supportedPreviewSizes =
            parameters.supportedPreviewSizes
        var size = supportedPreviewSizes[0]
        Log.d(TAG, "支持 " + size.width + "x" + size.height)
        // 选择一个与设置的差距最小的支持分辨率
        // 10x10 20x20 30x30
        // 12x12
        var m = Math.abs(size.height * size.width - width * height)
        supportedPreviewSizes.removeAt(0)
        val iterator: Iterator<Camera.Size> =
            supportedPreviewSizes.iterator()
        // 遍历
        while (iterator.hasNext()) {
            val next = iterator.next()
            Log.d(TAG, "支持 " + next.width + "x" + next.height)
            val n = Math.abs(next.height * next.width - width * height)
            if (n < m) {
                m = n
                size = next
            }
        }
        width = size.width
        height = size.height
        parameters.setPreviewSize(width, height)
        Log.d(
            TAG,
            "设置预览分辨率 width:" + size.width + " height:" + size.height
        )
    }

    fun setPreviewDisplay(surfaceHolder: SurfaceHolder) {
        this.surfaceHolder = surfaceHolder
        this.surfaceHolder!!.addCallback(this)
    }

    fun setPreviewCallback(previewCallback: PreviewCallback) {
        this.previewCallback = previewCallback
    }

    override fun surfaceCreated(holder: SurfaceHolder) {}
    override fun surfaceChanged(
        holder: SurfaceHolder,
        format: Int,
        width: Int,
        height: Int
    ) {
        // 释放摄像头
        stopPreview()
        // 开启摄像头
        startPreview()
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        stopPreview()
    }

    override fun onPreviewFrame(
        data: ByteArray,
        camera: Camera
    ) {
        when (rotation) {
            Surface.ROTATION_0 -> rotation90(data)
            Surface.ROTATION_90 -> {
            }
            Surface.ROTATION_270 -> {
            }
        }
        // data数据依然是倒的
        previewCallback!!.onPreviewFrame(bytes, camera)
        camera.addCallbackBuffer(buffer)
    }

    private fun rotation90(data: ByteArray) {
        var index = 0
        val ySize = width * height
        // u和v
        val uvHeight = height / 2
        // 后置摄像头顺时针旋转90度
        if (cameraId == CameraInfo.CAMERA_FACING_BACK) { // 将y的数据旋转之后 放入新的byte数组
            for (i in 0 until width) {
                for (j in height - 1 downTo 0) {
                    bytes!![index++] = data[width * j + i]
                }
            }
            // 每次处理两个数据
            var i = 0
            while (i < width) {
                for (j in uvHeight - 1 downTo 0) { // v
                    bytes!![index++] = data[ySize + width * j + i]
                    // u
                    bytes!![index++] = data[ySize + width * j + i + 1]
                }
                i += 2
            }
        } else { // 逆时针旋转90度
            for (i in 0 until width) {
                var nPos = width - 1
                for (j in 0 until height) {
                    bytes!![index++] = data[nPos - i]
                    nPos += width
                }
            }
            // u v
            var i = 0
            while (i < width) {
                var nPos = ySize + width - 1
                for (j in 0 until uvHeight) {
                    bytes!![index++] = data[nPos - i - 1]
                    bytes!![index++] = data[nPos - i]
                    nPos += width
                }
                i += 2
            }
        }
    }

    fun setOnChangedSizeListener(listener: OnChangedSizeListener) {
        onChangedSizeListener = listener
    }

    fun release() {
        surfaceHolder!!.removeCallback(this)
        stopPreview()
    }

    interface OnChangedSizeListener {
        fun onChanged(w: Int, h: Int)
    }

    companion object {
        private const val TAG = "CameraHelper"
    }
}

这个类实现的是使用Android系统相机预览画面的功能,包括了切换前置后置摄像头以及旋转画面。

AudioChannel.kt

kt 复制代码
package site.doramusic.app.live

import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class AudioChannel(private val livePusher: LivePusher) {

    private val inputSamples: Int
    private val executor: ExecutorService
    private val audioRecord: AudioRecord
    private val channels = 1
    private var isLiving = false

    fun startLive() {
        isLiving = true
        executor.submit(AudioTask())
    }

    fun stopLive() {
        isLiving = false
    }

    fun release() {
        audioRecord.release()
    }

    internal inner class AudioTask : Runnable {
        override fun run() {
            // 启动录音机
            audioRecord.startRecording()
            val bytes = ByteArray(inputSamples)
            while (isLiving) {
                val len = audioRecord.read(bytes, 0, bytes.size)
                if (len > 0) {
                    // 送去编码
                    livePusher.native_pushAudio(bytes)
                }
            }
            // 停止录音机
            audioRecord.stop()
        }
    }

    init {
        executor = Executors.newSingleThreadExecutor()
        // 准备录音机,采集pcm数据
        val channelConfig: Int
        channelConfig = if (channels == 2) {
            AudioFormat.CHANNEL_IN_STEREO
        } else {
            AudioFormat.CHANNEL_IN_MONO
        }
        livePusher.native_setAudioEncodeInfo(44100, channels)
        // 16位,2个字节
        inputSamples = livePusher.inputSamples * 2
        // 最小需要的缓冲区
        val minBufferSize = AudioRecord.getMinBufferSize(
            44100,
            channelConfig,
            AudioFormat.ENCODING_PCM_16BIT
        ) * 2
        // 1、麦克风 2、采样率 3、声道数 4、采样位
        audioRecord = AudioRecord(
            MediaRecorder.AudioSource.MIC,
            44100,
            channelConfig,
            AudioFormat.ENCODING_PCM_16BIT,
            if (minBufferSize > inputSamples) minBufferSize else inputSamples
        )
    }
}

这个类中,我们实现了音频采样,即录音的功能。

VideoChannel.kt

kt 复制代码
package site.doramusic.app.live

import android.app.Activity
import android.hardware.Camera
import android.hardware.Camera.PreviewCallback
import android.view.SurfaceHolder
import site.doramusic.app.live.CameraHelper.OnChangedSizeListener

class VideoChannel(
    private val livePusher: LivePusher,
    activity: Activity,
    width: Int,
    height: Int,
    private val bitrate: Int,
    private val fps: Int,
    cameraId: Int
) : PreviewCallback, OnChangedSizeListener {

    private val cameraHelper: CameraHelper
    private var isLiving = false

    fun setPreviewDisplay(surfaceHolder: SurfaceHolder) {
        cameraHelper.setPreviewDisplay(surfaceHolder)
    }

    init {
        cameraHelper = CameraHelper(activity, cameraId, width, height)
        cameraHelper.setPreviewCallback(this)
        cameraHelper.setOnChangedSizeListener(this)
    }

    /**
     * 得到nv21数据,已经旋转好的。
     *
     * @param data
     * @param camera
     */
    override fun onPreviewFrame(
        data: ByteArray,
        camera: Camera
    ) {
        if (isLiving) {
            livePusher.native_pushVideo(data)
        }
    }

    fun switchCamera() {
        cameraHelper.switchCamera()
    }

    /**
     * 真实摄像头数据的宽、高。
     *
     * @param w
     * @param h
     */
    override fun onChanged(w: Int, h: Int) {
        // 初始化编码器
        livePusher.native_setVideoEncInfo(w, h, fps, bitrate)
    }

    fun startLive() {
        isLiving = true
    }

    fun stopLive() {
        isLiving = false
    }

    fun release() {
        cameraHelper.release()
    }
}

这个类中我们实现了视频编码的流程控制。

LivePusher.kt

kt 复制代码
package site.doramusic.app.live

import android.app.Activity
import android.view.SurfaceHolder

/**
 * 直播推流。
 */
class LivePusher(
    activity: Activity, width: Int, height: Int, bitrate: Int,
    fps: Int, cameraId: Int) {

    /**
     * 音频编码通道。
     */
    private val audioChannel: AudioChannel

    /**
     * 视频编码通道。
     */
    private val videoChannel: VideoChannel

    val inputSamples: Int external get

    companion object {
        init {
            System.loadLibrary("doralive")
        }
    }

    init {
        native_init()
        videoChannel = VideoChannel(this, activity, width, height, bitrate, fps, cameraId)
        audioChannel = AudioChannel(this)
    }

    fun setPreviewDisplay(surfaceHolder: SurfaceHolder?) {
        videoChannel.setPreviewDisplay(surfaceHolder)
    }

    /**
     * 切换前后摄像头。
     */
    fun switchCamera() {
        videoChannel.switchCamera()
    }

    fun startLive(path: String?) {
        native_start(path)
        videoChannel.startLive()
        audioChannel.startLive()
    }

    fun stopLive() {
        videoChannel.stopLive()
        audioChannel.stopLive()
        native_stop()
    }

    fun release() {
        videoChannel.release()
        audioChannel.release()
        native_release()
    }

    external fun native_init()
    external fun native_start(path: String?)
    external fun native_setVideoEncInfo(
        width: Int,
        height: Int,
        fps: Int,
        bitrate: Int
    )

    external fun native_setAudioEncodeInfo(sampleRateInHz: Int, channels: Int)
    external fun native_pushVideo(data: ByteArray?)
    external fun native_stop()
    external fun native_release()
    external fun native_pushAudio(data: ByteArray?)
}

最后,我们使用LivePusher这个类组合音频轨道和视频轨道数据的整合。

Native层

先看一下大致的目录结构,后文附有native层的完整代码。

NDK的目录结构
主要代码

AudioChannel.h

h 复制代码
#ifndef PUSHER_AUDIO_CHANNEL_H
#define PUSHER_AUDIO_CHANNEL_H

#include "librtmp/rtmp.h"
#include "faac/faac.h"
#include <sys/types.h>

class AudioChannel {
    typedef void (*AudioCallback)(RTMPPacket *packet);

public:
    AudioChannel();

    ~AudioChannel();

    void setAudioEncInfo(int samplesInHZ, int channels);

    void setAudioCallback(AudioCallback audioCallback);

    int getInputSamples();

    void encodeData(int8_t *data);

    RTMPPacket* getAudioTag();
private:
    AudioCallback m_audioCallback;
    int m_channels;
    faacEncHandle m_audioCodec = 0;
    u_long m_inputSamples;
    u_long m_maxOutputBytes;
    u_char *m_buffer = 0;
};

#endif //PUSHER_AUDIO_CHANNEL_H

AudioChannel.cpp

cpp 复制代码
#include <cstring>
#include "AudioChannel.h"
#include "macro.h"

AudioChannel::AudioChannel() {
}
AudioChannel::~AudioChannel() {
    DELETE(m_buffer);
    // 释放编码器
    if (m_audioCodec) {
        faacEncClose(m_audioCodec);
        m_audioCodec = 0;
    }
}

void AudioChannel::setAudioCallback(AudioCallback audioCallback) {
    this->m_audioCallback = audioCallback;
}

void AudioChannel::setAudioEncInfo(int samplesInHZ, int channels) {
    // 打开编码器
    m_channels = channels;
    // 参数3:一次最大能输入编码器的样本数量,也编码的数据的个数 (一个样本是16位 2字节)
    // 参数4:最大可能的输出数据,编码后的最大字节数
    m_audioCodec = faacEncOpen(samplesInHZ, channels, &m_inputSamples, &m_maxOutputBytes);

    // 设置编码器参数
    faacEncConfigurationPtr config = faacEncGetCurrentConfiguration(m_audioCodec);
    // 指定为 mpeg4 标准
    config->mpegVersion = MPEG4;
    // lc 标准
    config->aacObjectType = LOW;
    // 16位
    config->inputFormat = FAAC_INPUT_16BIT;
    // 编码出原始数据,既不是adts也不是adif
    config->outputFormat = 0;
    faacEncSetConfiguration(m_audioCodec, config);

    // 输出缓冲区,编码后的数据用这个缓冲区来保存
    m_buffer = new u_char[m_maxOutputBytes];
}

int AudioChannel::getInputSamples() {
    return m_inputSamples;
}

RTMPPacket *AudioChannel::getAudioTag() {
    u_char *buf;
    u_long len;
    faacEncGetDecoderSpecificInfo(m_audioCodec, &buf, &len);
    int bodySize = 2 + len;
    RTMPPacket *packet = new RTMPPacket;
    RTMPPacket_Alloc(packet, bodySize);
    // 双声道
    packet->m_body[0] = 0xAF;
    if (m_channels == 1) {
        packet->m_body[0] = 0xAE;
    }
    packet->m_body[1] = 0x00;
    // 图片数据
    memcpy(&packet->m_body[2], buf, len);

    packet->m_hasAbsTimestamp = 0;
    packet->m_nBodySize = bodySize;
    packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    packet->m_nChannel = 0x11;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    return packet;
}

void AudioChannel::encodeData(int8_t *data) {
    // 返回编码后数据字节的长度
    int bytelen = faacEncEncode(m_audioCodec, reinterpret_cast<int32_t *>(data), m_inputSamples, m_buffer,
                                m_maxOutputBytes);
    if (bytelen > 0) {
        int bodySize = 2 + bytelen;
        RTMPPacket *packet = new RTMPPacket;
        RTMPPacket_Alloc(packet, bodySize);
        // 双声道
        packet->m_body[0] = 0xAF;
        if (m_channels == 1) {
            packet->m_body[0] = 0xAE;
        }
        // 编码出的声音,都是0x01
        packet->m_body[1] = 0x01;
        // 图片数据
        memcpy(&packet->m_body[2], m_buffer, bytelen);

        packet->m_hasAbsTimestamp = 0;
        packet->m_nBodySize = bodySize;
        packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
        packet->m_nChannel = 0x11;
        packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
        m_audioCallback(packet);
    }
}

以上为音频编码的实现。无论是音频编码还是视频编码,最终都会使用到RTMPPacket这个结构体保存数据。音频编码我们采用的是faac的库。

VideoChannel.h

h 复制代码
#ifndef PUSHER_VIDEO_CHANNEL_H
#define PUSHER_VIDEO_CHANNEL_H

#include <inttypes.h>
#include "x264/x264.h"
#include <pthread.h>
#include "librtmp/rtmp.h"

class VideoChannel {
    typedef void (*VideoCallback)(RTMPPacket* packet);
public:
    VideoChannel();

    ~VideoChannel();

    // 创建x264编码器
    void setVideoEncInfo(int width, int height, int fps, int bitrate);

    void encodeData(int8_t *data);

    void setVideoCallback(VideoCallback videoCallback);

private:
    pthread_mutex_t m_mutex;
    int m_width;
    int m_height;
    int m_fps;
    int m_bitrate;
    x264_t *m_videoCodec = 0;
    x264_picture_t *m_picIn = 0;

    int m_ySize;
    int m_uvSize;
    VideoCallback m_videoCallback;
    void sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len);

    void sendFrame(int type, uint8_t *payload, int i_payload);
};


#endif //PUSHER_VIDEO_CHANNEL_H

VideoChannel.cpp

cpp 复制代码
#include "VideoChannel.h"
#include "librtmp/rtmp.h"
#include "macro.h"
#include "string.h"

VideoChannel::VideoChannel() {
    pthread_mutex_init(&m_mutex, 0);
}

VideoChannel::~VideoChannel() {
    pthread_mutex_destroy(&m_mutex);
    if (m_videoCodec) {
        x264_encoder_close(m_videoCodec);
        m_videoCodec = 0;
    }
    if (m_picIn) {
        x264_picture_clean(m_picIn);
        DELETE(m_picIn);
    }
}

void VideoChannel::setVideoEncInfo(int width, int height, int fps, int bitrate) {
    pthread_mutex_lock(&m_mutex);
    m_width = width;
    m_height = height;
    m_fps = fps;
    m_bitrate = bitrate;
    m_ySize = width * height;
    m_uvSize = m_ySize / 4;
    if (m_videoCodec) {
        x264_encoder_close(m_videoCodec);
        m_videoCodec = 0;
    }
    if (m_picIn) {
        x264_picture_clean(m_picIn);
        DELETE(m_picIn);
    }


    // 打开x264编码器
    // x264编码器的属性
    x264_param_t param;
    // 参数2:最快
    // 参数3:无延迟编码
    x264_param_default_preset(&param, "ultrafast", "zerolatency");
    // base_line 3.2 编码规格
    param.i_level_idc = 32;
    // 输入数据格式
    param.i_csp = X264_CSP_I420;
    param.i_width = width;
    param.i_height = height;
    // 无b帧
    param.i_bframe = 0;
    // 参数i_rc_method表示码率控制,CQP(恒定质量),CRF(恒定码率),ABR(平均码率)
    param.rc.i_rc_method = X264_RC_ABR;
    // 码率(比特率,单位Kbps)
    param.rc.i_bitrate = bitrate / 1000;
    // 瞬时最大码率
    param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2;
    // 设置了i_vbv_max_bitrate必须设置此参数,码率控制区大小,单位kbps
    param.rc.i_vbv_buffer_size = bitrate / 1000;

    // 帧率
    param.i_fps_num = fps;
    param.i_fps_den = 1;
    param.i_timebase_den = param.i_fps_num;
    param.i_timebase_num = param.i_fps_den;
//    param.pf_log = x264_log_default2;
    // 用fps而不是时间戳来计算帧间距离
    param.b_vfr_input = 0;
    // 帧距离(关键帧)  2s一个关键帧
    param.i_keyint_max = fps * 2;
    // 是否复制sps和pps放在每个关键帧的前面 该参数设置是让每个关键帧(I帧)都附带sps/pps。
    param.b_repeat_headers = 1;
    // 多线程
    param.i_threads = 1;

    x264_param_apply_profile(&param, "baseline");
    // 打开编码器
    m_videoCodec = x264_encoder_open(&param);
    m_picIn = new x264_picture_t;
    x264_picture_alloc(m_picIn, X264_CSP_I420, width, height);
    pthread_mutex_unlock(&m_mutex);
}

void VideoChannel::setVideoCallback(VideoCallback videoCallback) {
    this->m_videoCallback = videoCallback;
}

void VideoChannel::encodeData(int8_t *data) {
    pthread_mutex_lock(&m_mutex);
    // y数据
    memcpy(m_picIn->img.plane[0], data, m_ySize);
    for (int i = 0; i < m_uvSize; ++i) {
        // u数据
        *(m_picIn->img.plane[1] + i) = *(data + m_ySize + i * 2 + 1);
        *(m_picIn->img.plane[2] + i) = *(data + m_ySize + i * 2);
    }
    // 编码出来的数据(帧数据)
    x264_nal_t *pp_nal;
    // 编码出来有几个数据(多少帧)
    int pi_nal;
    x264_picture_t pic_out;
    x264_encoder_encode(m_videoCodec, &pp_nal, &pi_nal, m_picIn, &pic_out);
    // 如果是关键帧 3
    int sps_len;
    int pps_len;
    uint8_t sps[100];
    uint8_t pps[100];
    for (int i = 0; i < pi_nal; ++i) {
        if (pp_nal[i].i_type == NAL_SPS) {
            // 排除掉 h264的间隔 00 00 00 01
            sps_len = pp_nal[i].i_payload - 4;
            memcpy(sps, pp_nal[i].p_payload + 4, sps_len);
        } else if (pp_nal[i].i_type == NAL_PPS) {
            pps_len = pp_nal[i].i_payload - 4;
            memcpy(pps, pp_nal[i].p_payload + 4, pps_len);
            // pps肯定是跟着sps的
            sendSpsPps(sps, pps, sps_len, pps_len);
        } else {
            sendFrame(pp_nal[i].i_type, pp_nal[i].p_payload, pp_nal[i].i_payload);
        }
    }
    pthread_mutex_unlock(&m_mutex);
}

void VideoChannel::sendSpsPps(uint8_t *sps, uint8_t *pps, int sps_len, int pps_len) {
    int bodySize = 13 + sps_len + 3 + pps_len;
    RTMPPacket *packet = new RTMPPacket;
    RTMPPacket_Alloc(packet, bodySize);
    int i = 0;
    // 固定头
    packet->m_body[i++] = 0x17;
    // 类型
    packet->m_body[i++] = 0x00;
    // composition time 0x000000
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;

    // 版本
    packet->m_body[i++] = 0x01;
    // 编码规格
    packet->m_body[i++] = sps[1];
    packet->m_body[i++] = sps[2];
    packet->m_body[i++] = sps[3];
    packet->m_body[i++] = 0xFF;

    // 整个sps
    packet->m_body[i++] = 0xE1;
    // sps长度
    packet->m_body[i++] = (sps_len >> 8) & 0xff;
    packet->m_body[i++] = sps_len & 0xff;
    memcpy(&packet->m_body[i], sps, sps_len);
    i += sps_len;

    // pps
    packet->m_body[i++] = 0x01;
    packet->m_body[i++] = (pps_len >> 8) & 0xff;
    packet->m_body[i++] = (pps_len) & 0xff;
    memcpy(&packet->m_body[i], pps, pps_len);

    // 视频
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nBodySize = bodySize;
    // 随意分配一个管道(尽量避开rtmp.c中使用的)
    packet->m_nChannel = 10;
    // sps pps没有时间戳
    packet->m_nTimeStamp = 0;
    // 不使用绝对时间
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;

    m_videoCallback(packet);
}

void VideoChannel::sendFrame(int type, uint8_t *payload, int i_payload) {
    if (payload[2] == 0x00) {
        i_payload -= 4;
        payload += 4;
    } else {
        i_payload -= 3;
        payload += 3;
    }
    int bodySize = 9 + i_payload;
    RTMPPacket *packet = new RTMPPacket;
    RTMPPacket_Alloc(packet, bodySize);

    packet->m_body[0] = 0x27;
    if(type == NAL_SLICE_IDR){
        packet->m_body[0] = 0x17;
        LOGE("关键帧");
    }
    // 类型
    packet->m_body[1] = 0x01;
    // 时间戳
    packet->m_body[2] = 0x00;
    packet->m_body[3] = 0x00;
    packet->m_body[4] = 0x00;
    // 数据长度 int 4个字节
    packet->m_body[5] = (i_payload >> 24) & 0xff;
    packet->m_body[6] = (i_payload >> 16) & 0xff;
    packet->m_body[7] = (i_payload >> 8) & 0xff;
    packet->m_body[8] = (i_payload) & 0xff;

    // 图片数据
    memcpy(&packet->m_body[9], payload, i_payload);

    packet->m_hasAbsTimestamp = 0;
    packet->m_nBodySize = bodySize;
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nChannel = 0x10;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    m_videoCallback(packet);
}

以上为视频编码的实现。视频编码我们采用的是x264的库。这里牵扯到了IBP帧的概念,我来简单解释下。

"IPB帧"是视频编码中的一个概念,通常用于描述视频压缩中的帧类型。在IPB帧结构中,I代表"Intra-frame",P代表"Predictive frame",B代表"Bidirectional frame"。

  1. Intra-frame (I帧) :I帧是视频序列中的关键帧,也称为帧内编码帧。每个I帧都是独立编码的,不依赖于其他帧。它们包含了图像的完整信息,没有依赖其他帧的数据。I帧通常用于视频的起始点或者画面发生较大变化的地方。
  2. Predictive frame (P帧) :P帧是通过参考前面的帧进行编码的,也称为帧间预测帧。P帧存储了相对于前面的I帧或者P帧的差异信息,以便在解码时进行恢复。它们依赖于前面的帧来提供预测信息。
  3. Bidirectional frame (B帧) :B帧是通过参考前后的帧进行编码的,也称为双向预测帧。B帧存储了相对于前后帧的差异信息,可以最大程度地减小数据量。它们依赖于前后的帧来提供预测信息。

IPB帧结构的使用可以显著减小视频数据量,从而实现更高效的视频压缩和传输。

这里还有另外一个概念就是SPS和PPS。

SPS (Sequence Parameter Set) 和 PPS (Picture Parameter Set) 是 H.264/AVC(高级视频编码)标准中的两个重要概念,用于描述视频序列的参数和图像的参数。

  1. SPS (Sequence Parameter Set)

    • SPS 包含了描述视频序列的参数信息,如图像尺寸、帧率、颜色空间等。
    • SPS 是对整个视频序列共享的参数的描述,一个视频序列通常只有一个 SPS。
    • SPS 信息在视频编码开始时发送,并在整个视频序列中保持不变,直到视频编码器发送新的 SPS。
  2. PPS (Picture Parameter Set)

    • PPS 包含了描述图像的参数信息,如编码类型、量化参数等。
    • PPS 是对图像级别的参数的描述,每个图像都有一个对应的 PPS。
    • PPS 提供了一些特定于图像的参数,允许编码器在编码不同图像时进行灵活的设置。

这两个参数集合在 H.264/AVC 中的引入,使得视频编码器可以更加灵活地进行视频压缩和解码,并且可以提高编解码器的性能和效率。SPS 和 PPS 的发送通常会随着视频流的传输一起发送,以确保解码器能够正确地解析和解码视频数据。

这些都属于音视频编解码的范畴了。

doralive.cpp

cpp 复制代码
#include <jni.h>
#include <string>
#include "librtmp/rtmp.h"
#include "safe_queue.h"
#include "macro.h"
#include "VideoChannel.h"
#include "AudioChannel.h"

SafeQueue<RTMPPacket *> packets;
VideoChannel *videoChannel = 0;
int isStart = 0;
pthread_t pid;

int readyPushing = 0;
uint32_t start_time;

AudioChannel *audioChannel = 0;

void releasePackets(RTMPPacket *&packet) {
    if (packet) {
        RTMPPacket_Free(packet);
        delete packet;
        packet = 0;
    }
}

void callback(RTMPPacket *packet) {
    if (packet) {
        // 设置时间戳
        packet->m_nTimeStamp = RTMP_GetTime() - start_time;
        packets.push(packet);
    }
}

extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1init(JNIEnv *env, jobject instance) {
    // 准备一个Video编码器的工具类,进行编码
    videoChannel = new VideoChannel;
    videoChannel->setVideoCallback(callback);
    audioChannel = new AudioChannel;
    audioChannel->setAudioCallback(callback);
    // 准备一个队列,打包好的数据放入队列,在线程中统一的取出数据再发送给服务器
    packets.setReleaseCallback(releasePackets);
}

extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1setVideoEncInfo(JNIEnv *env, jobject instance,
                                                                jint width, jint height, jint fps,
                                                                jint bitrate) {

    if (videoChannel) {
        videoChannel->setVideoEncInfo(width, height, fps, bitrate);
    }
}

void *start(void *args) {
    char *url = static_cast<char *>(args);
    RTMP *rtmp = 0;
    do {
        rtmp = RTMP_Alloc();
        if (!rtmp) {
            LOGE("alloc rtmp失败");
            break;
        }
        RTMP_Init(rtmp);
        int ret = RTMP_SetupURL(rtmp, url);
        if (!ret) {
            LOGE("设置地址失败:%s", url);
            break;
        }
        // 5s超时时间
        rtmp->Link.timeout = 5;
        RTMP_EnableWrite(rtmp);
        ret = RTMP_Connect(rtmp, 0);
        if (!ret) {
            LOGE("连接服务器:%s", url);
            break;
        }
        ret = RTMP_ConnectStream(rtmp, 0);
        if (!ret) {
            LOGE("连接流:%s", url);
            break;
        }
        // 记录一个开始时间
        start_time = RTMP_GetTime();
        // 表示可以开始推流了
        readyPushing = 1;
        packets.setWork(1);
        // 保证第一个数据是aac解码数据包
        callback(audioChannel->getAudioTag());
        RTMPPacket *packet = 0;
        while (readyPushing) {
            packets.pop(packet);
            if (!readyPushing) {
                break;
            }
            if (!packet) {
                continue;
            }
            packet->m_nInfoField2 = rtmp->m_stream_id;
            // 发送rtmp包 1:队列
            // 意外断网?发送失败,rtmpdump 内部会调用RTMP_Close
            // RTMP_Close 又会调用 RTMP_SendPacket
            // RTMP_SendPacket  又会调用 RTMP_Close
            // 将rtmp.c 里面WriteN方法的 Rtmp_Close注释掉
            ret = RTMP_SendPacket(rtmp, packet, 1);
            releasePackets(packet);
            if (!ret) {
                LOGE("发送失败");
                break;
            }
        }
        releasePackets(packet);
    } while (0);
    isStart = 0;
    readyPushing = 0;
    packets.setWork(0);
    packets.clear();
    if (rtmp) {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    delete (url);
    return 0;
}

extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1start(JNIEnv *env, jobject instance,
                                                      jstring path_) {
    if (isStart) {
        return;
    }
    isStart = 1;
    const char *path = env->GetStringUTFChars(path_, 0);
    char *url = new char[strlen(path) + 1];
    strcpy(url, path);
    pthread_create(&pid, 0, start, url);
    env->ReleaseStringUTFChars(path_, path);
}

extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1pushVideo(JNIEnv *env, jobject instance,
                                                          jbyteArray data_) {
    if (!videoChannel || !readyPushing) {
        return;
    }
    jbyte *data = env->GetByteArrayElements(data_, NULL);
    videoChannel->encodeData(data);
    env->ReleaseByteArrayElements(data_, data, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1stop(JNIEnv *env, jobject instance) {
    readyPushing = 0;
    // 关闭队列工作
    packets.setWork(0);
    pthread_join(pid, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1release(JNIEnv *env, jobject instance) {
    DELETE(videoChannel);
    DELETE(audioChannel);
}

extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1setAudioEncodeInfo(JNIEnv *env, jobject instance,
                                                                jint sampleRateInHz,
                                                                jint channels) {
    if (audioChannel) {
        audioChannel->setAudioEncInfo(sampleRateInHz, channels);
    }
}

extern "C"
JNIEXPORT jint JNICALL
Java_site_doramusic_app_live_LivePusher_getInputSamples(JNIEnv *env, jobject instance) {

    if (audioChannel) {
        return audioChannel->getInputSamples();
    }
    return -1;
}

extern "C"
JNIEXPORT void JNICALL
Java_site_doramusic_app_live_LivePusher_native_1pushAudio(JNIEnv *env, jobject instance,
                                                          jbyteArray data_) {

    if (!audioChannel || !readyPushing) {
        return;
    }
    jbyte *data = env->GetByteArrayElements(data_, NULL);
    audioChannel->encodeData(data);
    env->ReleaseByteArrayElements(data_, data, 0);
}

这里的_1是对Java方法名称中_的转义。最后使用可移植操作系统的POSIX线程pthread发送rtmp包推流到服务器就完成了。

CMakeLists.txt

make 复制代码
cmake_minimum_required(VERSION 3.4.1)

# 引入指定目录下的CMakeLists.txt
add_subdirectory(src/main/cpp/librtmp)

add_library(
             doralive
             SHARED
             src/main/cpp/doralive.cpp
             src/main/cpp/VideoChannel.cpp
             src/main/cpp/AudioChannel.cpp)

include_directories(src/main/cpp/faac
            src/main/cpp/x264)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}")
target_link_libraries(
                        doralive
                       rtmp
                       x264
                       faac
                        log)

编译so库。最后附上cpp目录的代码。 dorachat-sdk.oss-cn-hongkong.aliyuncs.com/live-push-c...

总结

一套完整的直播推拉流的解决方案到此就结束了。对于后端云里雾里的同学,可以系统学习下Linux操作系统的使用。对于native层代码看得头晕的同学,建议先学习cpp、jni以及音视频编解码的相关内容。最后,祝大家五一节快乐!在放松休息的同时,不忘提升自己的技术硬实力。

相关推荐
QuantumStack17 分钟前
【C++ 真题】B2037 奇偶数判断
数据结构·c++·算法
结衣结衣.1 小时前
C++ 类和对象的初步介绍
java·开发语言·数据结构·c++·笔记·学习·算法
学习使我变快乐1 小时前
C++:静态成员
开发语言·c++
心怀花木1 小时前
【C++】多态
c++·多态
风清扬_jd1 小时前
Chromium 添加书签功能浅析c++
c++·chrome
吃椰子不吐壳1 小时前
c++类与对象二
c++
zaim13 小时前
计算机的错误计算(一百一十四)
java·c++·python·rust·go·c·多项式
学习使我变快乐3 小时前
C++:const成员
开发语言·c++
500了4 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵5 小时前
Android Debug Bridge(ADB)完全指南
android·adb