視頻服務器(11) Kurento[6] Android播放

 

之前做的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:[email protected]: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播放視頻。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章