我负责的模块主体部分
1.实现推实时摄像头到nginx-rtmp服务器上,然后从服务器拉取处理后的视频流播放。主要是推流器、服务器、拉流器的选择与搭建。【因为本人水平有限,这块只是基于大神的项目进行了修改】
2.实现GPS定位信息的获取与发送。主要是获取GPS定位信息和如何发送到服务器上。

目录

  1. 推流器的搭建
  2. rtmp服务器
  3. 拉流器的搭建
  4. 合并视频传输模块与非视频传输模块

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

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐