之前做的WebGL加載速度慢,嘗試做成App,需要Android中能夠播放Kurento視頻。
目錄
一、調研資料
參考2:Kurento WebRTC Peer For Android(Kurento官方)
1.1、考察1
下載參考1裏面的https://github.com/nubomedia-vtt/kurento-room-client-android和https://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個部分,Overview,Installation Guide,Developer 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 生命週期相關
測試時: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播放視頻。