Android实现rtmp推拉流摄像头(三)
我负责的模块主体部分
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)有两个接口中的类要实现,分别负责判断连接成功和连接失败尝试重连
@Overridepublic void onConnectionSuccessRtmp() {runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(MainActivity.this, "Connection success", Toast.LENGTH_SHORT).show();}});}@Overridepublic void onConnectionFailedRtmp(@NonNull final String reason) {runOnUiThread(new Runnable() {@Overridepublic 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 事件描述*/@Overridepublic void onEventCallback(NodePlayer nodePlayer, int event, String msg) {Log.i("NodeMedia.NodePlayer","onEventCallback:"+event+" msg:"+msg);switch (event) {case 1000:// 正在连接视频runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(MainActivity.this, "Connecting", Toast.LENGTH_SHORT).show();}});break;case 1001:// 视频连接成功runOnUiThread(new Runnable() {@Overridepublic 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)拉流结束,停止播放
@Overrideprotected 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)定义一些相关的操作
//定位动态权限申请@Overridepublic 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() {@Overridepublic void onLocationChanged(Location location) {// 当GPS定位信息发生改变时,更新定位updateShow(location);loca_data = updateShow(location);}@Overridepublic void onStatusChanged(String provider, int status, Bundle extras) {}@Overridepublic void onProviderEnabled(String provider) {// 当GPS LocationProvider可用时,更新定位updateShow(lm.getLastKnownLocation(provider));loca_data = updateShow(lm.getLastKnownLocation(provider));}@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void run() {/**用定时器间隔发送*///定时执行任务TimerTask task = new TimerTask() {StringBuilder line;@Overridepublic 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:// 断开 客户端发送到服务器 的连接,即关闭输出流对象OutputStreamtry {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
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
