Android实现rtmp推拉流摄像头(三)
我负责的模块主体部分1.实现推实时摄像头到nginx-rtmp服务器上,然后从服务器拉取处理后的视频流播放。主要是推流器、服务器、拉流器的选择与搭建。【因为本人水平有限,这块只是基于大神的项目进行了修改】2.实现GPS定位信息的获取与发送。主要是获取GPS定位信息和如何发送到服务器上。目录推流器的搭建rtmp服务器拉流器的搭建合并视频传输模块与非视频传输模块1. 推流器的搭建参考pedroSG94
![](https://csdnimg.cn/release/devpress/public/img/ic-book.4f347164.png)
我负责的模块主体部分
1.实现推实时摄像头到nginx-rtmp服务器上,然后从服务器拉取处理后的视频流播放。主要是推流器、服务器、拉流器的选择与搭建。【因为本人水平有限,这块只是基于大神的项目进行了修改】
2.实现GPS定位信息的获取与发送。主要是获取GPS定位信息和如何发送到服务器上。
目录
- 推流器的搭建
- rtmp服务器
- 拉流器的搭建
- 合并视频传输模块与非视频传输模块
1. 推流器的搭建
参考pedroSG94/rtmp-rtsp-stream-client-java
1)将github上的推流库拉取到自己的项目中,在build.gradle最下面加入
implementation 'com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:1.7.7'
2)在MainActivity中要申请存储空间、相机音频等一系列权限
private final String[] PERMISSIONS = {
Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA,//音频和相机权限申请
Manifest.permission.WRITE_EXTERNAL_STORAGE //存储空间权限申请
};
3)在onCreate中直接调用推流库中的类
//推流
rtmpCamera1 = new RtmpCamera1(this, this);
rtmpCamera1.setReTries(10);//设置重连次数
4)在onClock中注册按钮点击事件
switch (view.getId()) {
//推流按钮
case R.id.b_start_stop:
if (!rtmpCamera1.isStreaming()) {
if (rtmpCamera1.isRecording()
//这里的rotation为相机角度,设置90会正常,如果为0是头朝左的
|| rtmpCamera1.prepareVideo(640, 480, 30, 1200 * 1024, false, 90) && rtmpCamera1.prepareAudio()) {
pushBut.setText(R.string.stop_button);
rtmpCamera1.startStream(urlpush.getText().toString());
} else {
Toast.makeText(this, "Error preparing stream, This device cant do it",
Toast.LENGTH_SHORT).show();
}
} else {
pushBut.setText(R.string.start_button);
rtmpCamera1.stopStream();
}
break;
5)有两个接口中的类要实现,分别负责判断连接成功和连接失败尝试重连
@Override
public void onConnectionSuccessRtmp() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "Connection success", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onConnectionFailedRtmp(@NonNull final String reason) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (rtmpCamera1.reTry(5000, reason)) {
Toast.makeText(MainActivity.this, "Retry", Toast.LENGTH_SHORT)
.show();
} else {
Toast.makeText(MainActivity.this, "Connection failed. " + reason, Toast.LENGTH_SHORT)
.show();
rtmpCamera1.stopStream();
pushBut.setText(R.string.start_button);
}
}
});
}
推流模块就这样设置好了
2. rtmp服务器
关于rtmp服务器,我尝试过两种,nginx-rtmp和srs-rtmp,分别在linux系统下(虚拟机上)进行了搭建。nginx搭建相比srs较为繁琐,二者较多搭建在linux系统下,windows系统下也有搭建方法,不过比较少见。
这是srs-rtmp在linux下的搭建教程v1_CN_SampleRTMP,很简单快速。
nginx-rtmp后边会写。目前测试使用了一个在windows下搭建好的服务器。
这是地址illuspas/nginx-rtmp-win32
3. 拉流器的搭建
使用的拉流基础库NodeMedia/NodeMediaClient-Android
1)将github上的推流库拉取到自己的项目中,在build.gradle最下面加入
implementation 'com.github.NodeMedia:NodeMediaClient-Android:2.9.7'
2)在onCreate中进行拉流相关的初始化
// 拉流
np = new NodePlayer(this);
NodePlayerView npv = findViewById(R.id.surfaceView_bottom);
npv.setRenderType(NodePlayerView.RenderType.SURFACEVIEW);
npv.setUIViewContentMode(NodePlayerView.UIViewContentMode.ScaleAspectFit);
np.setPlayerView(npv);
np.setNodePlayerDelegate(this);
np.setHWEnable(true);
3)在onClock中注册按钮点击事件
//拉流按钮
case R.id.start_pull:
if(!np.isPlaying()){
np.setInputUrl(urlpush.getText().toString());
np.start();
pullBut.setText(getString(R.string.stop_player));
}else {
np.stop();
pullBut.setText(getString(R.string.start_player));
}
break;
4)拉流事件回调
//拉流事件回调
/**
* 事件回调
* @param nodePlayer 对象
* @param event 事件状态码
* @param msg 事件描述
*/
@Override
public void onEventCallback(NodePlayer nodePlayer, int event, String msg) {
Log.i("NodeMedia.NodePlayer","onEventCallback:"+event+" msg:"+msg);
switch (event) {
case 1000:
// 正在连接视频
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "Connecting", Toast.LENGTH_SHORT).show();
}
});
break;
case 1001:
// 视频连接成功
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "Connection success", Toast.LENGTH_SHORT).show();
}
});
break;
case 1002:
// 视频连接失败 流地址不存在,或者本地网络无法和服务端通信,回调这里。5秒后重连, 可停止
// nodePlayer.stopPlay();
break;
case 1003:
// 视频开始重连,自动重连总开关
// nodePlayer.stopPlay();
break;
case 1004:
// 视频播放结束
break;
case 1005:
// 网络异常,播放中断,播放中途网络异常,回调这里。1秒后重连,如不需要,可停止
// nodePlayer.stopPlay();
break;
case 1006:
//RTMP连接播放超时
break;
case 1100:
// 播放缓冲区为空
// System.out.println("NetStream.Buffer.Empty");
break;
case 1101:
// 播放缓冲区正在缓冲数据,但还没达到设定的bufferTime时长
// System.out.println("NetStream.Buffer.Buffering");
break;
case 1102:
// 播放缓冲区达到bufferTime时长,开始播放.
// 如果视频关键帧间隔比bufferTime长,并且服务端没有在缓冲区间内返回视频关键帧,会先开始播放音频.直到视频关键帧到来开始显示画面.
// System.out.println("NetStream.Buffer.Full");
break;
case 1103:
// System.out.println("Stream EOF");
// 客户端明确收到服务端发送来的 StreamEOF 和 NetStream.Play.UnpublishNotify时回调这里
// 注意:不是所有云cdn会发送该指令,使用前请先测试
// 收到本事件,说明:此流的发布者明确停止了发布,或者因其网络异常,被服务端明确关闭了流
// 本sdk仍然会继续在1秒后重连,如不需要,可停止
// nodePlayer.stopPlay();
break;
case 1104:
//解码后得到的视频高宽值 格式为:{width}x{height}
break;
default:
break;
}
}
5)拉流结束,停止播放
@Override
protected void onDestroy() {
super.onDestroy();
/**
* 停止播放
*/
np.stop();
/**
* 释放资源
*/
np.release();
}
拉流模块就完成了
4. 合并视频传输模块与非视频传输模块
这里就是将前面写好的获取GPS定位信息并和服务器建立socket连接进行传输信息两块和以上部分合并
1)在onCreate中进行初始化
// 初始化线程池
mThreadPool = Executors.newCachedThreadPool();
//初始化位置变量
lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
//判断GPS是否可用
if (!isGpsAble(lm)) {
Toast.makeText(MainActivity.this, "请打开GPS", Toast.LENGTH_SHORT).show();
openGPS2();
}
//判断定位权限是否开启
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {//未开启定位权限
//开启定位权限,200是标识码
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 200);
} else {
startLocation();
Toast.makeText(MainActivity.this, "已开启定位权限", Toast.LENGTH_LONG).show();
}
2)定义一些相关的操作
//定位动态权限申请
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 200://刚才的识别码
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){//用户同意权限,执行我们的操作
startLocation();//开始定位
}else{//用户拒绝之后,当然我们也可以弹出一个窗口,直接跳转到系统设置页面
Toast.makeText(MainActivity.this,"未开启定位权限,请手动到设置去开启权限",Toast.LENGTH_LONG).show();
}
break;
default:break;
}
}
//开始定位
@SuppressLint("MissingPermission")
private void startLocation(){
//从GPS获取最近的定位信息
Location lc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
updateShow(lc);
loca_data = updateShow(lc);
//设置间隔100毫秒获得一次GPS定位信息
lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 100, 3, new LocationListener() {
@Override
public void onLocationChanged(Location location) {
// 当GPS定位信息发生改变时,更新定位
updateShow(location);
loca_data = updateShow(location);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
// 当GPS LocationProvider可用时,更新定位
updateShow(lm.getLastKnownLocation(provider));
loca_data = updateShow(lm.getLastKnownLocation(provider));
}
@Override
public void onProviderDisabled(String provider) {
updateShow(null);
}
});
}
//定义一个更新显示的方法
private StringBuilder updateShow(Location location) {
if (location != null) {
StringBuilder sb1 = new StringBuilder();
int id = 111;
sb1.append("{'id':'"+ id + "',");
sb1.append("'accuracy':'"+ location.getAccuracy() + "',");
sb1.append("'speed':'"+ location.getSpeed()+ "',");
sb1.append("'direction':'"+ location.getBearing() + "',");
sb1.append("'wgs84':["+ location.getLongitude() + "," + location.getLatitude() + "],");
sb1.append("'altitude':'"+ location.getAltitude() + "',");
sb1.append("'timestamp':'"+ timeGetTime + "'}");
return sb1;
} else {
return null;
}
}
//判断GPS是否可用
private boolean isGpsAble(LocationManager lm) {
return lm.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER) ? true : false;
}
//打开设置页面让用户自己设置
private void openGPS2() {
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivityForResult(intent, 0);
}
3)注册按钮点击事件:建立非视频连接
//建立非视频连接
case R.id.connect:
mThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
// 创建Socket对象 & 指定服务端的IP 及 端口号
temp = urlpush.getText().toString().replaceAll("rtmp://|:1935/live/test", "");
socket = new Socket(temp, 8989);
// 判断客户端和服务器是否连接成功
System.out.println(socket.isConnected());
} catch (IOException e) {
e.printStackTrace();
}
}
});
break;
4)注册按钮事件:在推视频流的同时进行非视频信息的发送
在推流按钮中开一个子线程负责进行非视频信息的发送
//推流按钮
case R.id.b_start_stop:
if (!rtmpCamera1.isStreaming()) {
if (rtmpCamera1.isRecording()
//这里的rotation为相机角度,设置90会正常,如果为0是头朝左的
|| rtmpCamera1.prepareVideo(640, 480, 30, 1200 * 1024, false, 90) && rtmpCamera1.prepareAudio()) {
pushBut.setText(R.string.stop_button);
rtmpCamera1.startStream(urlpush.getText().toString());
} else {
Toast.makeText(this, "Error preparing stream, This device cant do it",
Toast.LENGTH_SHORT).show();
}
} else {
pushBut.setText(R.string.start_button);
rtmpCamera1.stopStream();
}
// 利用线程池直接开启一个线程 & 执行该线程
mThreadPool.execute(new Runnable() {
@Override
public void run() {
/**用定时器间隔发送*/
//定时执行任务
TimerTask task = new TimerTask() {
StringBuilder line;
@Override
public void run() {
try {
outputStream = socket.getOutputStream();
line = loca_data;
outputStream.write((line + "\n").getBytes("utf-8"));
//这里将缓冲区的数据冲入
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
};
timer.schedule(task, 1000, 50);
}
});
break;
5)注册按钮事件:断开非视频信息的传输连接
case R.id.disconnect:
// 断开 客户端发送到服务器 的连接,即关闭输出流对象OutputStream
try {
outputStream.close();
// 最终关闭整个Socket连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
// 判断客户端和服务器是否已经断开连接
System.out.println(socket.isConnected());
break;
这样就合并完成了
下面是最终的效果
初始界面
推流界面
画面中的eclipse程序窗口中不断接收着一行行非视频信息
ferry-hhh/rtmp-push-pull-stream
上述程序源代码放在这里
上述成果有很大一部分是基于优秀学长提供的信息才得以完成的,感恩的心
下面这应该是学长实现的推拉流器集成在一起的app
yurensmile/rtmp-player-pulisher
更多推荐
所有评论(0)