前言
随着自媒体的短视频和直播带货的流行,再加上这几年疫情的肆虐,直接把实体店给干趴下了。叫苦连天的实体店老板们,原来是不懂这些东西,而被割了韭菜。当然这只是开个玩笑,哈哈。他们没必要懂技术。今天我就来剖析直播的前端和后端的实现流程,干货满满,不要忘了先点个小赞,谢谢了。那么就开始我们今天要讲的内容。
后端
先讲后端,想看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前,需要先把这几个软件安装好。
yum install -y pcre pcre-devel zlib zlib-devel openssl openssl-devel
下载Nginx
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模块
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的源码一起编译。
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主程序,启动它。
sudo ./nginx -t
查看是否启动。
ps -ef | grep nginx
配置RTMP
回到Nginx安装目录,修改nginx配置添加rtmp。
sudo vi conf/nginx.conf
nginx的配置文件内容,你覆盖成以下的内容就好。
# 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的配置。
sudo ./sbin/nginx -s reload
对于高并发和负载均衡我这里就不细说了,有兴趣的可以自行研究upstream和proxy_pass的配置。
测试流媒体服务器是否搭建成功
下载OBS进行推流
OBS官网 https://obsproject.com/welcome 。
⚠️:推流地址设置 rtmp://16.62.162.36:1935/live/home,如live为配置中的application live,home为推流码。home你也可以改成userId。
下载VLC(RTMP播放器)
VLC下载地址 https://dorachat-sdk.oss-cn-hongkong.aliyuncs.com/vlc-3.0.20-intel64.dmg 。
Android端
Android端的推流我们需要使用到NDK,而拉流播放就简单了,使用google官方的ExoPlayer播放器进行播放即可。ExoPlayer官方中文文档https://developer.android.com/media/media3/exoplayer?hl=zh-cn 。
Java层
在Java层,我们做一些编码推流的流程控制。
代码实现
CameraHelper.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