视频服务器(11) Kurento[6] Android播放

8 篇文章 0 订阅
6 篇文章 0 订阅

 

之前做的WebGL加载速度慢,尝试做成App,需要Android中能够播放Kurento视频。

目录

一、调研资料

1.1、考察1

1.2、考察2

1.3、找别人的Demo

二、开发

2.1、播放本地视频

2.1.1 获取权限

2.1.2 界面

2.2.3 播放本地视频

2.2.4 生命周期相关

2.2.5 startLocalMedia

2.2、和服务端通信

2.3、获取远程视频

2.3.1 客户端发送部分

2.3.2 客户端接收部分

2.3.3 客户端显示视频

2.3.4 其他处理


 

一、调研资料

参考1:Android端WebRtc+Kurento详解

参考2:Kurento WebRTC Peer For Android(Kurento官方)

1.1、考察1

下载参考1里面的https://github.com/nubomedia-vtt/kurento-room-client-androidhttps://github.com/BaeBae33/webrtc_android

无法用androidstudio直接打包androidapp,只能看看代码。

kurento-room-client-android是一个library,感觉像是基于websocket的一层封装。

webrtc_android则是一个jar包(org.webrtc)的源代码,可能是后面会引用到的核心jar包的源代码(的一个老的版本),同时可能也是上面的kurento-room-client-android后续迭代版本。

1.2、考察2

参考2是Kurento官方的接教程,就3个部分,OverviewInstallation GuideDeveloper Guide

Overview里面下载https://github.com/nubomedia-vtt/webrtcpeer-android,感觉像是又对上面的webrtc_android的一层封装,

Installation Guide里面教你怎么导入jar包到项目中。

https://bintray.com/nubomedia-vtt/maven/webrtcpeer-android

compile 'fi.vtt.nubomedia:webrtcpeer-android:1.1.2'

实际我用的androidstudio版本是3.5.2现在已经不用compile了,用implementation,

Developer Guide教你怎么写功能了,但是我按着教程,加上个SurfaceViewRender后打包app,结果界面是黑屏了。

对了官方教程也有问题(笔误吧)

这里的localRender,应该是localView的。

这里卡住了.....

我的android知识不行,很久以前(Android2.*时代)学了一点,所以不知道怎么应变了。

要么先再学习一下android。

1.3、找别人的Demo

尝试用“NBMWebRTCPeer.Observer”在github上找demo。

找到几个能直接打包的

https://github.com/nubomedia-vtt/nubo-test/,3年前,好像是NUBOMEDIA官方的?加载有错误。

https://github.com/nhancv/nc-kurento-android,2年前,能打包,有界面。似乎是基于org.webrtc的另一套封装

这里提供了服务端的部分

Setup
Install server: https://www.youtube.com/watch?v=02X7HOyhAkA
Start server demo: https://www.youtube.com/watch?v=b44IKU2pl3U
Start app: https://www.youtube.com/watch?v=W407V5T_aW4
Demo server
https://github.com/nhancv/ot-kurento-node-webrtc

Kurento tutorial flow
http://doc-kurento.readthedocs.io/en/stable/

Android webrtc-peer lib
https://github.com/nhancv/nc-android-webrtcpeer

https://github.com/satriyaPhincon/CallVideo,3个月前,能打包,有界面。logo(OCBC NISP)是新加坡银行的??

https://github.com/gaopj/webrtcdemo,7个月前,能打包,有界面。在这个基础上学习一下吧。

-----------------------------------------

https://github.com/kries2333/nubo-android-base,11个月前,有界面,加载有错误,因为是NUBOMEDIA官方的,有点期待,尝试处理一下问题。

问题1.

不用管 ok下去

----------

问题2.

compile改成implementation

---------

问题3.

Execution failed for task ':app:compileDebugJavaWithJavac'

 

错误: -source 1.7 中不支持方法引用
(请使用 -source 8 或更高版本以启用方法引用)

错误: -source 1.7 中不支持 lambda 表达式
(请使用 -source 8 或更高版本以启用 lambda 表达式)

参考:https://blog.csdn.net/w1227976200/article/details/79542943

加上

出现更多错误(问题4)

不过应该是已经向前一步了。

为什么有些org.webrtc下面的类可以,有些不行呢?

org.webrtc实际上上libjingle包里面的.

implementation ('io.pristine:libjingle:11139@aar') { transitive=true }

把不行的import拷贝到其他可以打包app的项目中也是不行的。

怀疑是现在的libjingle和11个月前不一样了。

结论:暂时不行。看看代码好了。

-----------------------------------------

二、开发

学习上面的项目代码的基础上,开发AndroidKurentoPlayer。

2.1、播放本地视频

记得要加上:

implementation 'fi.vtt.nubomedia:webrtcpeer-android:1.1.2'

2.1.1 获取权限

需要dexter,按我理解这是一个权限管理插件,参考:Android Dexter 分析

原本不知道这个,本地摄像头视频无法显示,代码运行后日志显示

E/VideoCapturerAndroid: VideoCapturerAndroid: startCapture failed
    VideoCapturerAndroid: java.lang.RuntimeException: Fail to connect to camera service
E/VideoCapturerAndroid: VideoCapturerAndroid: java.lang.RuntimeException: Fail to connect to camera service

首先AndroidManifest.xml里面要加上权限

    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.CALL_PHONE"/>

在build.gradle里面加上dexter

implementation 'com.karumi:dexter:5.0.0'

然后在Activity的onStart中询问权限(不然要手动开启权限)

    private void permission(){
        Dexter.withActivity(MainActivity.this)
                .withPermissions(
                        Manifest.permission.CALL_PHONE,
                        Manifest.permission.CAMERA,
                        Manifest.permission.ANSWER_PHONE_CALLS,
                        Manifest.permission.READ_EXTERNAL_STORAGE,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.RECORD_AUDIO
                )
                .withListener(new MultiplePermissionsListener() {
                    @Override
                    public void onPermissionsChecked(MultiplePermissionsReport report) {
                        Log.d("checkpermission", String.valueOf(report.areAllPermissionsGranted()));
                        if (report.areAllPermissionsGranted()){
                            Log.d("checkpermission", "granted");
                        } else if(report.isAnyPermissionPermanentlyDenied()) {
                            Log.d("checkpermission", "not granted");
                        }
                    }
                    @Override
                    public void onPermissionRationaleShouldBeShown(List<com.karumi.dexter.listener.PermissionRequest> permissions, PermissionToken token) {

                    }
                }).onSameThread().check();
    }

2.1.2 界面

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:background="@color/black">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <org.webrtc.SurfaceViewRenderer
            android:id="@+id/gl_surface"
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:visibility="visible"/>
        <org.webrtc.SurfaceViewRenderer
            android:id="@+id/gl_surface_local"
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:visibility="visible"/>
        <Button
            android:id="@+id/btnInit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Init" />
        <Button
            android:id="@+id/btnLocal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Start Local" />
        <Button
            android:id="@+id/btnLocalSwitch"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Switch Local" />
        <Button
            android:id="@+id/btnLocalEnd"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="End Local" />
    </LinearLayout>
</RelativeLayout>

核心是需要在界面上添加<org.webrtc.SurfaceViewRenderer ..../>

2.2.3 播放本地视频

在onStart(或者onCreate)初始化WebRTCPeer

    @Override
    protected void onStart() {
        Log.e(TAG,"onStart");
        super.onStart();
        initWebRtc();
    }

    private void initWebRtc(){
        localView = findViewById(R.id.gl_surface_local);//界面上有个gl_surface_local的<org.webrtc.SurfaceViewRenderer
        localView.init(EglBase.create().getEglBaseContext(), null);
        mediaConfiguration = new NBMMediaConfiguration();//本地播放的效果比下面的好
//        mediaConfiguration = new NBMMediaConfiguration(
//                NBMMediaConfiguration.NBMRendererType.OPENGLES,
//                NBMMediaConfiguration.NBMAudioCodec.OPUS, 0,
//                NBMMediaConfiguration.NBMVideoCodec.VP9, 0,
//                new NBMMediaConfiguration.NBMVideoFormat(640, 480, PixelFormat.RGBX_8888, 15),
//                NBMMediaConfiguration.NBMCameraPosition.FRONT);//本地播放效果比前面差,不过传输应该是这个比较快的。
        nbmWebRTCPeer = new NBMWebRTCPeer(mediaConfiguration, this, localView, this);
        nbmWebRTCPeer.initialize();//=>onInitialize
    }

    @Override
    public void onInitialize() {
        Log.d(TAG,"onInitialize");
        nbmWebRTCPeer.generateOffer("local", true);//这样就能播放本地视频了
        //nbmWebRTCPeer.startLocalMedia();//只有这个会崩溃啊,日志:JNI DETECTED ERROR IN APPLICATION: java_object == null
    }

2.2.4 生命周期相关

参考:Android Activity生命周期解析

测试时:onCreate->onStart->onResume->onPause->onResume

在onPause之前有个:E/LB: fail to open file: No such file or directory

启动过程中间都有失去焦点过?

加上nbmWebRTCPeer.initialize();的话,是 onCreate->onStart(initialize)->onResume->onPause->onInitialize->onResume

-------------------------------------------

切换程序:onPause->onStop

切回程序:onStart->OnResume

这里除了点问题,onStart里面的initWebRtc()被再次调用了。

那放到onCreate中吧,onCreate(initialize)->onStart->onResume->onPause->onInitialize->onResume 

2.2.5 startLocalMedia

参考1:基于Kurento的WebRTC移动视频群聊解决方案0000

参考2:NUBOMEDIA 起步

我原本以为播放本地视频需要startLocalMedia,结果从代码上看是不需要的。

看代码,startLocalMedia是和stopLocalMedia成对使用

    @Override
    protected void onPause() {
        Log.e(TAG,"onPause ");
        nbmWebRTCPeer.stopLocalMedia();
        //不加上这个会提示:I/SurfaceViewRenderer: SurfaceViewRenderer: gl_surface_local: No surface to draw on
        super.onPause();
    }

    @Override
    protected void onResume() {
        Log.e(TAG,"onResume");
        super.onResume();
        nbmWebRTCPeer.startLocalMedia();
    }

完整的生命周期:

onCreate(initialize)->onStart->onResume->onPause->onInitialize->onResume ->

切出 ->onPause(stopLocalMedia)->onStop

切回->onStart->OnResume(startLocalMedia)

但是我这样使用会出现程序闪退的情况,日志是

E/MainActivity: onPause 
D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.myapplication, PID: 21412
    java.lang.RuntimeException: Unable to pause activity {com.example.myapplication/com.example.myapplication.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void fi.vtt.nubomedia.webrtcpeerandroid.MediaResourceManager.stopVideoSource()' on a null object reference
       ......
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
     Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void fi.vtt.nubomedia.webrtcpeerandroid.MediaResourceManager.stopVideoSource()' on a null object reference
        at fi.vtt.nubomedia.webrtcpeerandroid.NBMWebRTCPeer.stopLocalMedia(NBMWebRTCPeer.java:565)
        at com.example.myapplication.MainActivity.onPause(MainActivity.java:164)
        at android.app.Activity.performPause(Activity.java:7444)
        at android.app.Instrumentation.callActivityOnPause(Instrumentation.java:1466)
        at android.app.ActivityThread.performPauseActivityIfNeeded(ActivityThread.java:4068)

关键是:

Attempt to invoke virtual method 'void fi.vtt.nubomedia.webrtcpeerandroid.MediaResourceManager.stopVideoSource()' on a null

修改stopLocalMedia()调用代码

    protected void onPause() {
        Log.e(TAG,"onPause ");
        if(nbmWebRTCPeer!=null && nbmWebRTCPeer.isInitialized()){
            nbmWebRTCPeer.stopLocalMedia();
        }
        //不加上这个会提示:I/SurfaceViewRenderer: SurfaceViewRenderer: gl_surface_local: No surface to draw on
        super.onPause();
    }

startLocalMedia()又出错了,发现是为了测试把generateOffer注释掉了,看源代码,发现,startLocalMedia和generateOffer里面都调用了startLocalMediaSync

--------------------

现在的问题是startLocalMedia没有效果,调用stopLocalMedia后再调用startLocalMedia本地视频也出不来了

startLocalMedia的源码是:

    public boolean startLocalMedia() {
        if (mediaResourceManager != null && mediaResourceManager.getLocalMediaStream() == null) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    startLocalMediaSync();
                }
            });
            return true;
        } else {
            return false;
        }
    }

发现不管那次的返回值都是false。

应该是后面的判断mediaResourceManager.getLocalMediaStream() == null进不去

stopLocalMedial的源码是:

    public void stopLocalMedia() {
        mediaResourceManager.stopVideoSource();
    }

MediaResourceManager里面的close才是把localMediaStream设置为null的地方

    void close(){
        // Uncomment only if you know what you are doing
        localMediaStream.dispose();
        localMediaStream = null;
        //videoCapturer.dispose();
        //videoCapturer = null;
    }

考虑修改源代码,源代码https://github.com/nubomedia-vtt/webrtcpeer-android

不改源代码的情况下,不用startLocalMedia,用initialize。

现在暂时改成这样

    @Override
    protected void onPause() {
        Log.e(TAG,"onPause ");
        if(nbmWebRTCPeer!=null && nbmWebRTCPeer.isInitialized()){
            nbmWebRTCPeer.stopLocalMedia();
            isStop=true;
        }
        //不加上这个会提示:I/SurfaceViewRenderer: SurfaceViewRenderer: gl_surface_local: No surface to draw on
        super.onPause();
    }

    private boolean isStop=false;
    @Override
    protected void onResume() {
        super.onResume();
        Log.e(TAG,"onResume");
        if(nbmWebRTCPeer!=null && nbmWebRTCPeer.isInitialized() && isStop){
            boolean b=nbmWebRTCPeer.startLocalMedia();
            Log.e(TAG,"startLocalMedia:"+b);
            if(b==false){//都是false
                nbmWebRTCPeer.initialize();
            }
        }
    }

从结果上,只是查看本地摄像头视频没什么问题,不知道对于传输视频信息会怎样。

2.2、和服务端通信

通信是基于WebSocket的,也可以是Http的吧。现有的例子都是视频通话的例子,都是使用KurentoRoomAPI连接服务端的。

我是播放rtsp视频用的,需要自己改一下,先写个KurentoPlayerAPI继承自KurentoRoomAPI。

public class KurentoPlayerAPI extends KurentoRoomAPI {
    /**
     * Constructor that initializes required instances and parameters for the API calls.
     * WebSocket connections are not established in the constructor. User is responsible
     * for opening, closing and checking if the connection is open through the corresponding
     * API calls.
     *
     * @param executor is the asynchronous UI-safe executor for tasks.
     * @param uri      is the web socket link to the room web services.
     * @param listener interface handles the callbacks for responses, notifications and errors.
     */
    public KurentoPlayerAPI(LooperExecutor executor, String uri, RoomListener listener) {
        super(executor, uri, listener);
        Log.e("KurentoPlayerAPI","uri:"+uri);
    }

    public void startPlayer(String videourl, String sdpOffer, int id){
        HashMap<String, Object> namedParameters = new HashMap<>();
        namedParameters.put("videourl", videourl);
        namedParameters.put("sdpOffer", sdpOffer);
        send("start", namedParameters, id);
    }
}

在onCreate或者onStart里面初始化

    private void initkurentoRoomAPI() {
        executor = new LooperExecutor();
        executor.requestStart();
        //wsUri = mSharedPreferences.getString(Constants.SERVER_NAME, Constants.DEFAULT_SERVER);
        kurentoRoomAPI = new KurentoPlayerAPI(executor, Constants.DEFAULT_SERVER, MainActivity.this);
        kurentoRoomAPI.connectWebSocket();

        findViewById(R.id.btnJoinRoom).setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                Log.i(TAG,"onClick sendJoinRoom");
                kurentoRoomAPI.sendJoinRoom("userId","roomId",true,roomId);
            }
        });
    }

测试了一下,KurentoRoomPlayer发送过去的,服务端收到的是

{"method":"joinRoom","id":1,"params":{"dataChannels":true,"user":"userId","room":"roomId"},"jsonrpc":"2.0"}

我的服务端是原来的Kurento-Player-Java的,改一下,

 public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
   // JsonObject jsonMessage = gson.fromJson(message.getPayload(), JsonObject.class);
    JsonObject jsonMessage=GetJsonMessage(message);
    if(jsonMessage==null)return;
    String sessionId = session.getId();
    log.debug("Incoming message {} from sessionId", jsonMessage, sessionId);

    JsonElement jsonrpcElement=jsonMessage.get("jsonrpc");
    if(jsonrpcElement!=null){//KurentoRoomAPI发送过来的
      String jsonrpc=jsonrpcElement.getAsString();
      String method=jsonMessage.get("method").getAsString();
      log.info(">>>>>>>>>>>>>>>>> PlayerHandler.handleTextMessage method="+method+",jsonrpc="+jsonrpc);
      switch(method){
        case "joinRoom":
          break;
        case "leaveRoom":
          break;
        case "publishVideo":
          break;
        case "unpublishVideo":
          break;
        case "receiveVideoFrom":
          break;
        case "unsubscribeFromVideo":
          break;
        case "onIceCandidate":
          break;
        case "sendMessage":
          break;
        case "customRequest":
          break;
        default:
          doPlayerCommand(session, jsonMessage, sessionId, method);//
          break;
      }
    }
    else{ //原来的player
      JsonElement idElement=jsonMessage.get("id");
      if(idElement==null){
        log.error("no id element:"+jsonMessage.toString());
        return;
      }
      String id=idElement.getAsString();
      //log.info("id="+id);
      log.info(">>>>>>>>>>>>>>>>> PlayerHandler.handleTextMessage id="+id);
      doPlayerCommand(session, jsonMessage, sessionId, id);//根据id,进行不同操作
    }
  }

------------------------------------------------------

https://github.com/Kurento/kurento-room,聊天室相关代码,3年前的,服务端后续参考改一下。先试着播放视频。

他们是不是3年前开始去做云服务了.....新的NUBOMEDIA项目(https://github.com/nubomedia)。

另外,找到一个博客:Kurento应用开发指南(以Kurento 6.0为模板) 之八: Kurento协议,介绍了一下JSON-RPC 消息格式。这里还有很多webrtc相关的博客,有空看看。

------------------------------------------------------

服务端收客户端消息,还是按照原来的JsonObject收,获取JsonElement,再获取具体参数,也是没问题的。

但是因为客户端接收处理的部分封装好了,KurentoRoomAPI->KurentoAPI->JsonRpcWebSocketClient->ExtendedWebSocketClient->onMessage

@Override
		public void onMessage(final String message) {
			executor.execute(new Runnable() {
				@Override
				public void run() {
					if (connectionState == WebSocketConnectionState.CONNECTED) {
						try {
							JSONRPC2Message msg = JSONRPC2Message.parse(message);

							if (msg instanceof JSONRPC2Request) {
								JsonRpcRequest request = new JsonRpcRequest();
								request.setId(((JSONRPC2Request) msg).getID());
								request.setMethod(((JSONRPC2Request) msg).getMethod());
								request.setNamedParams(((JSONRPC2Request) msg).getNamedParams());
								request.setPositionalParams(((JSONRPC2Request) msg).getPositionalParams());
								events.onRequest(request);
							} else if (msg instanceof JSONRPC2Notification) {
								JsonRpcNotification notification = new JsonRpcNotification();
								notification.setMethod(((JSONRPC2Notification) msg).getMethod());
								notification.setNamedParams(((JSONRPC2Notification) msg).getNamedParams());
								notification.setPositionalParams(((JSONRPC2Notification) msg).getPositionalParams());
								events.onNotification(notification);
							} else if (msg instanceof JSONRPC2Response) {
								JsonRpcResponse notification = new JsonRpcResponse(message);
								events.onResponse(notification);
							}
						} catch (JSONRPC2ParseException e) {
							// TODO: Handle exception
						}
					}
				}
			});
		}

到KurentoRoomAPI转换成了RoomListener里面的OnRoomResponse(JSONRPC2Response)和OnRoomNotification(JSONRPC2Notification)。

也就是说服务端发回来的信息必须是JSONRPC2Response或者JSONRPC2Notification,不然到业务层代码就收不到消息了。

服务端加上引用

		<dependency>
			<groupId>com.thetransactioncompany</groupId>
			<artifactId>jsonrpc2-base</artifactId>
			<version>1.38</version>
		</dependency>

最终服务端代码没找到具体能直接用的,前端参考的https://github.com/satriyaPhincon/CallVideo。如果要配合前端改后端很麻烦,要把不同的指令类型分成Response和Notification。麻烦,我改成全部信息都用JSONRPC2Notification发回。

另外还要兼容原来的js客户端的代码,创建了个JsonResponse类,把JsonObject发回的消息封装一下,添加用JSONRPC2Notification发回的接口。根据前端信息内容确定是否用JSONRPC返回。

package org.kurento.tutorial.player;

import com.google.gson.JsonObject;
import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification;
import com.thetransactioncompany.jsonrpc2.JSONRPC2Response;
import org.kurento.client.IceCandidate;
import org.kurento.jsonrpc.JsonUtils;

import java.util.HashMap;
import java.util.Map;

public class JsonResponse {

    private boolean isRpc;
    private String id = "id";
    private HashMap<String, Object> map;

    public JsonResponse(boolean isRpc,String id) {
        this.isRpc=isRpc;
        this.id = id;
        map = new HashMap<String, Object>();
    }

    public void addParam(String key, Object value) {
        this.map.put(key, value);
    }
    public void add(String key, Object value) {
        this.map.put(key, value);
    }
    public void addProperty(String key, Object value) {
        this.map.put(key, value);
    }

    @Override
    public String toString() {
        return getJson(this.isRpc);
    }

    public String getJson(boolean isRpc) {
        //System.out.println("getJson:"+isRpc);
        if (isRpc) {
            JSONRPC2Notification notification =getJSONRPC2Notification();
            return notification.toString();
        } else {
            JsonObject response = getJsonObject();
            return response.toString();
        }
    }

    public JsonObject getJsonObject() {
        JsonObject response = new JsonObject();
        response.addProperty("id", id);
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String mapKey = entry.getKey();
            Object mapValue = entry.getValue();
            String className=mapValue.getClass().getName();
            //System.out.println("className:"+className);
            if(className=="String"){
                System.out.println("className:"+className);
                response.addProperty(mapKey, (String)mapValue);
            }
//            else if(className=="com.google.gson.JsonObject"){
//                //System.out.println("className:"+className);
//                response.add(mapKey, (JsonObject)mapValue);
//            }
            else if(className=="org.kurento.client.IceCandidate"){
                IceCandidate candidate=(IceCandidate)mapValue;
                response.add(mapKey, JsonUtils.toJsonObject(candidate));
            }
            else{
                System.out.println("className4:"+className);
                response.addProperty(mapKey, mapValue.toString());
            }
        }
        return response;
    }

    public JSONRPC2Notification getJSONRPC2Notification() {
        JSONRPC2Notification notification = new JSONRPC2Notification(id);
        Map<String, Object> params = new HashMap<>();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String mapKey = entry.getKey();
            Object mapValue = entry.getValue();
            String className=mapValue.getClass().getName();
            if(className=="String"){
                System.out.println("className:"+className);
                params.put(mapKey, (String)mapValue);
            }
            else if(className=="org.kurento.client.IceCandidate"){
                System.out.println("className1:"+className);
                IceCandidate candidate=(IceCandidate)mapValue;
                params.put("sdpMid", candidate.getSdpMid());
                params.put("sdpMLineIndex", candidate.getSdpMLineIndex());
                params.put("candidate", candidate.getCandidate());
            }
            else{
                System.out.println("className4:"+className);
                params.put(mapKey, mapValue.toString());
            }
        }
        notification.setNamedParams(params);
        return notification;
    }
}

IceCandidate部分需要特殊处理一下。

既然添加了jsonrpc2-base,handleTextMessage部分实际上也能用JSONRPC2Notification解析出来处理。

@Override
  public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    JsonObject jsonMessage=GetJsonMessage(message);
    if(jsonMessage==null)return;
    String sessionId = session.getId();
    log.debug("Incoming message {} from sessionId", jsonMessage, sessionId);
    JsonElement jsonrpcElement=jsonMessage.get("jsonrpc");
    if(jsonrpcElement!=null){//KurentoRoomAPI发送过来的
      isRpc=true;
      String jsonString=jsonMessage.toString();
      log.info(">>>>>>>>>>>>>>>>> jsonString="+jsonString);
      JSONRPC2Notification jsonRpc=JSONRPC2Notification.parse(jsonString);
      log.info(">>>>>>>>>>>>>>>>> jsonRpc="+jsonRpc);
      String method=jsonRpc.getMethod();
      log.info(">>>>>>>>>>>>>>>>> method="+method);
      String jsonrpc=jsonrpcElement.getAsString();
//      String method=jsonMessage.get("method").getAsString();
      JsonObject paramsObject=jsonMessage.getAsJsonObject("params");
      log.info(">>>>>>>>>>>>>>>>> PlayerHandler.handleTextMessage method="+method+",jsonrpc="+jsonrpc);
      switch(method){
        case "joinRoom":
          break;
        case "leaveRoom":
          break;
        case "publishVideo":
          break;
        case "unpublishVideo":
          break;
        case "receiveVideoFrom":
          break;
        case "unsubscribeFromVideo":
          break;
        case "onIceCandidate":
          onIceCandidate(sessionId,jsonRpc);//jsonRpc处理
          break;
        case "sendMessage":
          break;
        case "customRequest":
          break;
        default:
          doPlayerCommand(session, paramsObject, sessionId, method);//
          break;
      }
    }
    else{ //原来的player
      isRpc=false;
      JsonElement idElement=jsonMessage.get("id");
      if(idElement==null){
        log.error("no id element:"+jsonMessage.toString());
        return;
      }
      String id=idElement.getAsString();
      log.info(">>>>>>>>>>>>>>>>> PlayerHandler.handleTextMessage id="+id);
      doPlayerCommand(session, jsonMessage, sessionId, id);//根据id,进行不同操作
    }
  }

如果不是要兼容js客户端,可以整个改掉的,或者是修改js客户端发送JSONRPC信息。先这样吧。

2.3、获取远程视频

本来立刻开始改前后端代码了,结果不成功,没办法,静下心来整理一下整个获取视频的过程。

另外参考:https://blog.csdn.net/fanhenghui/article/details/80229811

总之两边都收到candidate并addCandidate后,连接就建立的,就能获取视频了。

另外发现之前写的代码是漏了服务端添加candidate部分了,补上后视频就出来了。

webrtc是p2p的,这里的服务端其实也相当于p2p的一边。

-----------------

同时姑且整理了一下CallVideo的流程,虽然现在没用。

------------------

接下来就是参考https://github.com/satriyaPhincon/CallVideo,android播放webrtc视频的过程。

2.3.1 客户端发送部分

1.开始,生成offer

在onCreate或者onStart中初始化NBMWebRTCPeer

    private void initWebRtc(){
        rootEglBase = EglBase.create();
        masterView = findViewById(R.id.gl_surface);
        masterView.init(rootEglBase.getEglBaseContext(), null);
        masterView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED);
        //masterView.setMirror(true);//镜像,左右颠倒

        localView = findViewById(R.id.gl_surface_local);//界面上有个gl_surface_local的<org.webrtc.SurfaceViewRenderer
        localView.init(rootEglBase.getEglBaseContext(), null);
        mediaConfiguration = new NBMMediaConfiguration();//本地播放的效果比下面的好
        nbmWebRTCPeer = new NBMWebRTCPeer(mediaConfiguration, this, localView, this);
        nbmWebRTCPeer.registerMasterRenderer(masterView);
        nbmWebRTCPeer.initialize();//=>onInitialize
    }

在onInitialize中generateOffer

    @Override
    public void onInitialize() {
        Log.e(TAG,"onInitialize:"+false);
        nbmWebRTCPeer.generateOffer("remote", false);
    }

2. 发送sdpOffer

在onLocalSdpOfferGenerated中发送sdpOffer

    @Override
    public void onLocalSdpOfferGenerated(SessionDescription localSdpOffer, NBMPeerConnection connection) {
        String sdpOffer=localSdpOffer.description;
        connectionId = connection.getConnectionId();
        int publishVideoRequestId = ++Constants.id;//感觉没什么用
        String videoUrl="rtsp://admin:admin12345@192.168.1.56:554/cam/realmonitor?channel=1&subtype=0";
        kurentoRoomAPI.startPlay(videoUrl,sdpOffer,publishVideoRequestId);
    }

3. 发送localCandidate

在onIceCandidate中发送candidate

    @Override
    public void onIceCandidate(IceCandidate iceCandidate, NBMPeerConnection connection) {
        String endpointName=connection.getConnectionId();//暂时没用
        int id=12;//不知道什么意思
        kurentoRoomAPI.sendOnIceCandidate(endpointName, iceCandidate.sdp,
                iceCandidate.sdpMid, Integer.toString(iceCandidate.sdpMLineIndex), id);
    }

2.3.2 客户端接收部分

在onRoomNotification中处理服务端消息

@Override
    public void onRoomNotification(RoomNotification notification) {
        Log.e("RoomListener","onRoomNotification:"+notification);
        String method=notification.getMethod();
        switch (method) {
            case "startResponse":
                startResponse(notification);
                break;
            case "error":
                break;
            case "playEnd":
                //playEnd();
                break;
            case "videoInfo":
                //showVideoData(parsedMessage);
                break;
            case "iceCandidate":
                addIceCandidate(notification);
            break;
            case "seek":
                break;
            case "position":
                break;
            default:
                break;
        }
    }

1.processAnswer

    private void startResponse(RoomNotification response){
        String sdpAnswer=response.getParam("sdpAnswer").toString();
        SessionDescription sd = new SessionDescription(SessionDescription.Type.ANSWER,sdpAnswer );
        nbmWebRTCPeer.processAnswer(sd, connectionId);
    }

2 添加remoteCandidate

    public void addIceCandidate(RoomNotification notification) {
        String sdpMid = notification.getParam("sdpMid").toString();
        int sdpMLineIndex = Integer.valueOf(notification.getParam("sdpMLineIndex").toString());
        String sdp = notification.getParam("candidate").toString();
        IceCandidate ic = new IceCandidate(sdpMid, sdpMLineIndex, sdp);
        nbmWebRTCPeer.addRemoteIceCandidate(ic, connectionId);
    }

2.3.3 客户端显示视频

    @Override
    public void onRemoteStreamAdded(MediaStream stream, NBMPeerConnection connection) {
        Log.e("NBMWebRTCPeerObserver", String.format("onRemoteStreamAdded:" + stream + "|" + stream.videoTracks.get(0)));
        nbmWebRTCPeer.setActiveMasterStream(stream);
        nbmWebRTCPeer.attachRendererToRemoteStream(masterView, stream);
    }

2.3.4 其他处理

因为我这个只是一个视频播放客户端,不需要本地视频。但是generateOffer("remote", false)还是会显示本地视频,要有个地方stopLocalMedia。

1.在generateOffer后面马上stopLocalMedia,会导致远程视频也看不到

2.手动按钮点击stopLocalMedia,则不影响远程视频的播放。

3.手动按钮点击nbmWebRTCPeer.initialize()则本地和远程都会出来。

最终在onResum和onLocalSdpOfferGenerated最后加上了stopLocalMedia。

后续还有封装成类库,给其他app调用;结合Unity播放视频。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值