移动端视频开发通过什么方式实现直播?十分钟带你快速了解

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}

相机的初始化就在这里啦:

public void InitCamera() {
Camera.Parameters p = _mCamera.getParameters();

Size prevewSize = p.getPreviewSize();
showlog(“Original Width:” + prevewSize.width + “, height:” + prevewSize.height);

List PreviewSizeList = p.getSupportedPreviewSizes();
List PreviewFormats = p.getSupportedPreviewFormats();
showlog(“Listing all supported preview sizes”);
for (Camera.Size size : PreviewSizeList) {
showlog(" w: " + size.width + ", h: " + size.height);
}

showlog(“Listing all supported preview formats”);
Integer iNV21Flag = 0;
Integer iYV12Flag = 0;
for (Integer yuvFormat : PreviewFormats) {
showlog(“preview formats:” + yuvFormat);
if (yuvFormat == android.graphics.ImageFormat.YV12) {
iYV12Flag = android.graphics.ImageFormat.YV12;
}
if (yuvFormat == android.graphics.ImageFormat.NV21) {
iNV21Flag = android.graphics.ImageFormat.NV21;
}
}

if (iNV21Flag != 0) {
_iCameraCodecType = iNV21Flag;
} else if (iYV12Flag != 0) {
_iCameraCodecType = iYV12Flag;
}
p.setPreviewSize(HEIGHT_DEF, WIDTH_DEF);
p.setPreviewFormat(_iCameraCodecType);
p.setPreviewFrameRate(FRAMERATE_DEF);

showlog(“_iDegrees=”+_iDegrees);
_mCamera.setDisplayOrientation(_iDegrees);
p.setRotation(_iDegrees);
_mCamera.setPreviewCallback(_previewCallback);
_mCamera.setParameters§;
try {
_mCamera.setPreviewDisplay(_mSurfaceView.getHolder());
} catch (Exception e) {
return;
}
_mCamera.cancelAutoFocus();//只有加上了这一句,才会自动对焦。
_mCamera.startPreview();
}

还记得之前初始化完成之后开始推流函数吗?

private void RtmpStartMessage() {
Message msg = new Message();
msg.what = ID_RTMP_PUSH_START;
Bundle b = new Bundle();
b.putInt(“ret”, 0);
msg.setData(b);
mHandler.sendMessage(msg);
}

Handler处理:

public Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
Bundle b = msg.getData();
int ret;
switch (msg.what) {
case ID_RTMP_PUSH_START: {
Start();
break;
}
}
}
};
}

真正的推流实现原来在这里:

private void Start() {
if (DEBUG_ENABLE) {
File saveDir = Environment.getExternalStorageDirectory();
String strFilename = saveDir + “/aaa.h264”;
try {
if (!new File(strFilename).exists()) {
new File(strFilename).createNewFile();
}
_outputStream = new DataOutputStream(new FileOutputStream(strFilename));
} catch (Exception e) {
e.printStackTrace();
}
}
//_rtmpSessionMgr.Start(“rtmp://192.168.0.110/live/12345678”);
_rtmpSessionMgr = new RtmpSessionManager();
_rtmpSessionMgr.Start(_rtmpUrl); //------point 1

int iFormat = _iCameraCodecType;
_swEncH264 = new SWVideoEncoder(WIDTH_DEF, HEIGHT_DEF, FRAMERATE_DEF, BITRATE_DEF);
_swEncH264.start(iFormat); //------point 2

_bStartFlag = true;

_h264EncoderThread = new Thread(_h264Runnable);
_h264EncoderThread.setPriority(Thread.MAX_PRIORITY);
_h264EncoderThread.start(); //------point 3

_AudioRecorder.startRecording();
_AacEncoderThread = new Thread(_aacEncoderRunnable);
_AacEncoderThread.setPriority(Thread.MAX_PRIORITY);
_AacEncoderThread.start(); //------point 4
}

里面主要的函数有四个,我分别标出来了,现在我们逐一看一下。首先是point 1,这已经走到SDK里面了

public int Start(String rtmpUrl){
int iRet = 0;

_rtmpUrl = rtmpUrl;
_rtmpSession = new RtmpSession();

_bStartFlag = true;
_h264EncoderThread.setPriority(Thread.MAX_PRIORITY);
_h264EncoderThread.start();

return iRet;
}

其实就是启动了一个线程,这个线程稍微有点复杂

private Thread _h264EncoderThread = new Thread(new Runnable() {

private Boolean WaitforReConnect(){
for(int i=0; i < 500; i++){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(_h264EncoderThread.interrupted() || (!_bStartFlag)){
return false;
}
}
return true;
}
@Override
public void run() {
while (!_h264EncoderThread.interrupted() && (_bStartFlag)) {
if(_rtmpHandle == 0) {
_rtmpHandle = _rtmpSession.RtmpConnect(_rtmpUrl);
if(_rtmpHandle == 0){
if(!WaitforReConnect()){
break;
}
continue;
}
}else{
if(_rtmpSession.RtmpIsConnect(_rtmpHandle) == 0){
_rtmpHandle = _rtmpSession.RtmpConnect(_rtmpUrl);
if(_rtmpHandle == 0){
if(!WaitforReConnect()){
break;
}
continue;
}
}
}

if((_videoDataQueue.size() == 0) && (_audioDataQueue.size()==0)){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
//Log.i(TAG, “VideoQueue length=”+_videoDataQueue.size()+“, AudioQueue length=”+_audioDataQueue.size());
for(int i = 0; i < 100; i++){
byte[] audioData = GetAndReleaseAudioQueue();
if(audioData == null){
break;
}
//Log.i(TAG, “###RtmpSendAudioData:”+audioData.length);
_rtmpSession.RtmpSendAudioData(_rtmpHandle, audioData, audioData.length);
}

byte[] videoData = GetAndReleaseVideoQueue();
if(videoData != null){
//Log.i(TAG, “$$$RtmpSendVideoData:”+videoData.length);
_rtmpSession.RtmpSendVideoData(_rtmpHandle, videoData, videoData.length);
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
_videoDataQueueLock.lock();
_videoDataQueue.clear();
_videoDataQueueLock.unlock();
_audioDataQueueLock.lock();
_audioDataQueue.clear();
_audioDataQueueLock.unlock();

if((_rtmpHandle != 0) && (_rtmpSession != null)){
_rtmpSession.RtmpDisconnect(_rtmpHandle);
}
_rtmpHandle = 0;
_rtmpSession = null;
}
});

看18行,主要就是一个while循环,每隔一段时间去_audioDataQueue和_videoDataQueue两个缓冲数组中取数据发送给服务器,发送方法_rtmpSession.RtmpSendAudioData和_rtmpSession.RtmpSendVideoData都是Native方法,通过jni调用so库文件的内容,每隔一段时间,这个时间是多少呢?看第4行,原来是5秒钟,也就是说我们的视频数据会在缓冲中存放5秒才被取出来发给服务器,所有直播会有5秒的延时,我们可以修改这块来控制直播延时。

上面说了我们会从_audioDataQueue和_videoDataQueue两个Buffer里面取数据,那么数据是何时放进去的呢?看上面的point 2,3,4。首先是point 2,同样走进了SDK:

public boolean start(int iFormateType){
int iType = OpenH264Encoder.YUV420_TYPE;

if(iFormateType == android.graphics.ImageFormat.YV12){
iType = OpenH264Encoder.YUV12_TYPE;
}else{
iType = OpenH264Encoder.YUV420_TYPE;
}
_OpenH264Encoder = new OpenH264Encoder();
_iHandle = _OpenH264Encoder.InitEncode(_iWidth, _iHeight, _iBitRate, _iFrameRate, iType);
if(_iHandle == 0){
return false;
}

_iFormatType = iFormateType;
return true;
}

其实这是初始化编码器,具体的初始化过程也在so文件,jni调用。point 3,4其实就是开启两个线程,那我们看看线程中具体实现吧。

private Thread _h264EncoderThread = null;
private Runnable _h264Runnable = new Runnable() {
@Override
public void run() {
while (!_h264EncoderThread.interrupted() && _bStartFlag) {
int iSize = _YUVQueue.size();
if (iSize > 0) {
_yuvQueueLock.lock();
byte[] yuvData = _YUVQueue.poll();
if (iSize > 9) {
Log.i(LOG_TAG, “###YUV Queue len=” + _YUVQueue.size() + “, YUV length=” + yuvData.length);
}

_yuvQueueLock.unlock();
if (yuvData == null) {
continue;
}

if (_bIsFront) {
_yuvEdit = _swEncH264.YUV420pRotate270(yuvData, HEIGHT_DEF, WIDTH_DEF);
} else {
_yuvEdit = _swEncH264.YUV420pRotate90(yuvData, HEIGHT_DEF, WIDTH_DEF);
}
byte[] h264Data = _swEncH264.EncoderH264(_yuvEdit);
if (h264Data != null) {
_rtmpSessionMgr.InsertVideoData(h264Data);
if (DEBUG_ENABLE) {
try {
_outputStream.write(h264Data);
int iH264Len = h264Data.length;
//Log.i(LOG_TAG, “Encode H264 len=”+iH264Len);
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
_YUVQueue.clear();
}
}

也是一个循环线程,第9行,从_YUVQueue中取出摄像头获取的数据,然后进行视频旋转,第24行,对数据进行编码,然后执行26行,InsertVideoData:

public void InsertVideoData(byte[] videoData){
if(!_bStartFlag){
return;
}
_videoDataQueueLock.lock();
if(_videoDataQueue.size() > 50){
_videoDataQueue.clear();
}
_videoDataQueue.offer(videoData);
_videoDataQueueLock.unlock();
}

果然就是插入之前提到的_videoDataQueue的Buffer。这里插入的是视频数据,那么音频数据呢?在另外一个线程,内容大致相同

private Runnable _aacEncoderRunnable = new Runnable() {
@Override
public void run() {
DataOutputStream outputStream = null;
if (DEBUG_ENABLE) {
File saveDir = Environment.getExternalStorageDirectory();
String strFilename = saveDir + “/aaa.aac”;
try {
if (!new File(strFilename).exists()) {
new File(strFilename).createNewFile();
}
outputStream = new DataOutputStream(new FileOutputStream(strFilename));
} catch (Exception e1) {
e1.printStackTrace();
}
}

long lSleepTime = SAMPLE_RATE_DEF * 16 * 2 / _RecorderBuffer.length;

while (!_AacEncoderThread.interrupted() && _bStartFlag) {
int iPCMLen = _AudioRecorder.read(_RecorderBuffer, 0, _RecorderBuffer.length); // Fill buffer
if ((iPCMLen != _AudioRecorder.ERROR_BAD_VALUE) && (iPCMLen != 0)) {
if (_fdkaacHandle != 0) {
byte[] aacBuffer = _fdkaacEnc.FdkAacEncode(_fdkaacHandle, _RecorderBuffer);
if (aacBuffer != null) {
long lLen = aacBuffer.length;

_rtmpSessionMgr.InsertAudioData(aacBuffer);
//Log.i(LOG_TAG, “fdk aac length=”+lLen+" from pcm="+iPCMLen);
if (DEBUG_ENABLE) {
try {
outputStream.write(aacBuffer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
} else {
Log.i(LOG_TAG, “######fail to get PCM data”);
}
try {
Thread.sleep(lSleepTime / 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.i(LOG_TAG, “AAC Encoder Thread ended …”);
}
};
private Thread _AacEncoderThread = null;
}

这就是通过循环将音频数据插入_audioDataQueue这个Buffer。 以上就是视频采集和推流的代码分析,Demo中并没有对视频进行任何处理,只是摄像头采集,编码后推流到服务器端。
###三.第二部分:Nginx服务器搭建
流媒体服务器有诸多选择,如商业版的Wowza。但我选择的是免费的Nginx(nginx-rtmp-module)。Nginx本身是一个非常出色的HTTP服务器,它通过nginx的模块nginx-rtmp-module可以搭建一个功能相对比较完善的流媒体服务器。这个流媒体服务器可以支持RTMP和HLS。 Nginx配合SDK做流媒体服务器的原理是: Nginx通过rtmp模块提供rtmp服务, SDK推送一个rtmp流到Nginx, 然后客户端通过访问Nginx来收看实时视频流。 HLS也是差不多的原理,只是最终客户端是通过HTTP协议来访问的,但是SDK推送流仍然是rtmp的。 下面是一款已经集成rtmp模块的windows版本的Nginx。下载后,即可直接使用 下载链接:https://github.com/illuspas/nginx-rtmp-win32

1、rtmp端口配置 配置文件在/conf/nginx.conf RTMP监听 1935 端口,启用live 和hls 两个application

所以你的流媒体服务器url可以写成:rtmp://(服务器IP地址):1935/live/xxx 或 rtmp://(服务器IP地址):1935/hls/xxx 例如我们上面写的 rtmp://192.168.1.104:1935/live/12345

HTTP监听 8080 端口,

  • :8080/stat 查看stream状态
  • :8080/index.html 为一个直播播放与直播发布测试器
  • :8080/vod.html 为一个支持RTMP和HLS点播的测试器

2、启动nginx服务 双击nginx.exe文件或者在dos窗口下运行nginx.exe,即可启动nginx服务:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

[外链图片转存中…(img-EgmTnmL7-1712031526985)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-Vk4JWVTW-1712031526985)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值