web-dev-qa-db-ja.com

Android向けWebRTCネイティブコードを使用して3方向電話会議ビデオチャットを実装する方法

Androidアプリを使用して Android用のWebRTCネイティブコードパッケージ を使用して(つまり、WebViewを使用せずに)3ウェイビデオチャットを実装しようとしています。 node.jsを使用してシグナリングサーバーを記述し、クライアントアプリ内の Gottox socket.io Java client ライブラリを使用してサーバーに接続し、SDPパケットを交換して、双方向のビデオチャット接続。

しかし、今ではそれを超えて三者通話に移行するのに問題があります。 WebRTCネイティブコードパッケージに付属するAppRTCDemoアプリは、双方向通話のみを示します(サードパーティが部屋に参加しようとすると、「部屋がいっぱいです」というメッセージが返されます)。

この答え (特にAndroidとは関係ありません)によると、複数のPeerConnectionを作成することでそれを行うことになっているため、各チャット参加者は他の2人の参加者。

ただし、2つ以上のPeerConnectionClient(libjingle_peerconnection_so.soのネイティブ側に実装されているPeerConectionをラップするJavaクラス)を作成すると、ライブラリ内から例外がスローされ、結果としてカメラにアクセスしようとしている2人との競合から:

E/VideoCapturerAndroid(21170): startCapture failed
E/VideoCapturerAndroid(21170): Java.lang.RuntimeException: Fail to connect to camera service
E/VideoCapturerAndroid(21170):  at Android.hardware.Camera.native_setup(Native Method)
E/VideoCapturerAndroid(21170):  at Android.hardware.Camera.<init>(Camera.Java:548)
E/VideoCapturerAndroid(21170):  at Android.hardware.Camera.open(Camera.Java:389)
E/VideoCapturerAndroid(21170):  at org.webrtc.VideoCapturerAndroid.startCaptureOnCameraThread(VideoCapturerAndroid.Java:528)
E/VideoCapturerAndroid(21170):  at org.webrtc.VideoCapturerAndroid.access$11(VideoCapturerAndroid.Java:520)
E/VideoCapturerAndroid(21170):  at org.webrtc.VideoCapturerAndroid$6.run(VideoCapturerAndroid.Java:514)
E/VideoCapturerAndroid(21170):  at Android.os.Handler.handleCallback(Handler.Java:733)
E/VideoCapturerAndroid(21170):  at Android.os.Handler.dispatchMessage(Handler.Java:95)
E/VideoCapturerAndroid(21170):  at Android.os.Looper.loop(Looper.Java:136)
E/VideoCapturerAndroid(21170):  at org.webrtc.VideoCapturerAndroid$CameraThread.run(VideoCapturerAndroid.Java:484)

これは、接続の確立を試みる前であってもローカルクライアントを初期化するときに発生するため、node.js、socket.ioなどのシグナリングサーバー関連のものとは関係ありません。

複数のピアに同じビデオを送信できるように、カメラを共有するために複数のピア接続を取得するにはどうすればよいですか?

私が持っていた1つのアイデアは、複数の接続間で共有できるVideoCapturerAndroidを置き換えるためにある種のシングルトンカメラクラスを実装することでしたが、それが機能するかどうかさえわからず、3を実行する方法があるかどうか知りたいのですが。ライブラリ内のハッキングを開始する前に、APIを使用して方法を呼び出します。

それは可能ですか?そうであれば、どうですか?

更新:

VideoCapturerAndroidオブジェクトを複数のPeerConnectionClients間で共有して、最初の接続に対してのみ作成し、それを後続の接続の初期化関数に渡してみましたが、その結果、この「Capturerは一度しか取得できません!」 2番目のピア接続のVideoCapturerオブジェクトから2番目のVideoTrackを作成するときの例外:

E/AndroidRuntime(18956): FATAL EXCEPTION: Thread-1397
E/AndroidRuntime(18956): Java.lang.RuntimeException: Capturer can only be taken once!
E/AndroidRuntime(18956):    at org.webrtc.VideoCapturer.takeNativeVideoCapturer(VideoCapturer.Java:52)
E/AndroidRuntime(18956):    at org.webrtc.PeerConnectionFactory.createVideoSource(PeerConnectionFactory.Java:113)
E/AndroidRuntime(18956):    at com.example.rtcapp.PeerConnectionClient.createVideoTrack(PeerConnectionClient.Java:720)
E/AndroidRuntime(18956):    at com.example.rtcapp.PeerConnectionClient.createPeerConnectionInternal(PeerConnectionClient.Java:482)
E/AndroidRuntime(18956):    at com.example.rtcapp.PeerConnectionClient.access$20(PeerConnectionClient.Java:433)
E/AndroidRuntime(18956):    at com.example.rtcapp.PeerConnectionClient$2.run(PeerConnectionClient.Java:280)
E/AndroidRuntime(18956):    at Android.os.Handler.handleCallback(Handler.Java:733)
E/AndroidRuntime(18956):    at Android.os.Handler.dispatchMessage(Handler.Java:95)
E/AndroidRuntime(18956):    at Android.os.Looper.loop(Looper.Java:136)
E/AndroidRuntime(18956):    at com.example.rtcapp.LooperExecutor.run(LooperExecutor.Java:56)

PeerConnectionClients間でVideoTrackオブジェクトを共有しようとすると、ネイティブコードから次のエラーが発生しました。

E/libjingle(19884): Local fingerprint does not match identity.
E/libjingle(19884): P2PTransportChannel::Connect: The ice_ufrag_ and the ice_pwd_ are not set.
E/libjingle(19884): Local fingerprint does not match identity.
E/libjingle(19884): Failed to set local offer sdp: Failed to Push down transport description: Local fingerprint does not match identity.

PeerConnectionClients間でMediaStreamを共有すると、アプリが突然終了し、Logcatにエラーメッセージが表示されません。

25
samgak

あなたが持っている問題は、PeerConnectionClientがではないPeerConnection itのラッパーcontainsであることです-)PeerConnection。

この質問に答えられないことに気付いたので、少し手助けできるかどうかを確認したいと思いました。私はソースコードを調べましたが、PeerConnectionClientは単一のリモートピア用に非常にハードコーディングされています。次の行ではなく、PeerConnectionオブジェクトのコレクションを作成する必要があります。

private PeerConnection peerConnection;

もう少し見回すと、それよりも少し複雑になります。

CreatePeerConnectionInternalのmediaStreamロジックは一度だけ実行する必要があり、次のようにPeerConnectionオブジェクト間でストリームを共有する必要があります。

peerConnection.addStream(mediaStream);

WebRTC仕様 を参照するか、この stackoverflow の質問を見て、PeerConnectionタイプが1つのピアのみを処理するように設計されていることを確認できます。また、漠然と暗示されています here

したがって、維持するmediaStreamオブジェクトは1つだけです。

private MediaStream mediaStream;

したがって、主なアイデアは、1つのMediaStreamオブジェクトと、接続したいピアと同じ数のPeerConnectionオブジェクトです。したがって、複数のPeerConnectionClientオブジェクトを使用するのではなく、単一のPeerConnectionClientを変更して、マルチクライアント処理をカプセル化します。何らかの理由で複数のPeerConnectionClientオブジェクトのデザインを使いたい場合は、メディアストリームロジック(および1度だけ作成する必要のあるサポートタイプ)を抽象化する必要があります。

また、既存のビデオトラックではなく、複数のリモートビデオトラックを維持する必要があります。

private VideoTrack remoteVideoTrack;

明らかに、1つのローカルカメラをレンダリングし、リモート接続用に複数のレンダラーを作成することだけに関心があります。

これがあなたを軌道に戻すのに十分な情報であることを願っています。

17
Matthew Sanders

マシューサンダースの回答を利用して、なんとか機能させることができたので、この回答では、サンプルコードをビデオ会議通話をサポートするように調整する1つの方法について詳しく説明します。

ほとんどの変更はPeerConnectionClientで行う必要がありますが、PeerConnectionClientを使用するクラスでも行う必要があります。これは、シグナリングサーバーと通信して接続をセットアップする場所です。

PeerConnectionClient内では、次のメンバー変数を接続ごとに格納する必要があります。

private VideoRenderer.Callbacks remoteRender;
private final PCObserver pcObserver = new PCObserver();
private final SDPObserver sdpObserver = new SDPObserver();
private PeerConnection peerConnection;
private LinkedList<IceCandidate> queuedRemoteCandidates;
private boolean isInitiator;
private SessionDescription localSdp;
private VideoTrack remoteVideoTrack;

私のアプリケーションでは最大4つの接続(4方向のチャットの場合)が必要だったので、それぞれの配列を格納しましたが、それらをすべてオブジェクト内に配置してオブジェクトの配列を持つことができます。

private static final int MAX_CONNECTIONS = 3;
private VideoRenderer.Callbacks[] remoteRenders;
private final PCObserver[] pcObservers = new PCObserver[MAX_CONNECTIONS];
private final SDPObserver[] sdpObservers = new SDPObserver[MAX_CONNECTIONS];
private PeerConnection[] peerConnections = new PeerConnection[MAX_CONNECTIONS];
private LinkedList<IceCandidate>[] queuedRemoteCandidateLists = new LinkedList[MAX_CONNECTIONS];
private boolean[] isConnectionInitiator = new boolean[MAX_CONNECTIONS];
private SessionDescription[] localSdps = new SessionDescription[MAX_CONNECTIONS];
private VideoTrack[] remoteVideoTracks = new VideoTrack[MAX_CONNECTIONS];

connectionIdフィールドをPCObserverおよびSDPObserverクラスに追加し、PeerConnectionClientコンストラクター内で配列にオブザーバーオブジェクトを割り当て、connectionIdフィールドを設定しましたオブザーバーオブジェクトごとに、配列内のインデックスに。上記のメンバー変数を参照するPCObserverおよびSDPObserverのすべてのメソッドは、connectionIdフィールドを使用して適切な配列にインデックスを付けるように変更する必要があります。

PeerConnectionClientコールバックを変更する必要があります。

public static interface PeerConnectionEvents {
    public void onLocalDescription(final SessionDescription sdp, int connectionId);
    public void onIceCandidate(final IceCandidate candidate, int connectionId);
    public void onIceConnected(int connectionId);
    public void onIceDisconnected(int connectionId);
    public void onPeerConnectionClosed(int connectionId);
    public void onPeerConnectionStatsReady(final StatsReport[] reports);
    public void onPeerConnectionError(final String description);
}

また、次のPeerConnectionClientメソッド:

private void createPeerConnectionInternal(int connectionId)
private void closeConnectionInternal(int connectionId)
private void getStats(int connectionId)
public void createOffer(final int connectionId)
public void createAnswer(final int connectionId)
public void addRemoteIceCandidate(final IceCandidate candidate, final int connectionId)
public void setRemoteDescription(final SessionDescription sdp, final int connectionId)
private void drainCandidates(int connectionId)

オブザーバークラスのメソッドと同様に、以前の単一オブジェクトを参照するのではなく、connectionIdを使用して接続ごとのオブジェクトの適切な配列にインデックスを付けるように、これらすべての関数を変更する必要があります。コールバック関数の呼び出しも、connectionIdを返すように変更する必要があります。

createPeerConnectioncreateMultiPeerConnectionという新しい関数に置き換えました。これには、リモートビデオストリームを表示するためのVideoRenderer.Callbacksオブジェクトの配列が1つではなく渡されます。関数は、createMediaConstraintsInternal()を1回呼び出し、PeerConnectionsごとにcreatePeerConnectionInternal()を呼び出し、0からMAX_CONNECTIONS - 1にループします。 mediaStreamオブジェクトは、初期化コードをcreatePeerConnectionInternal()チェックでラップするだけで、if(mediaStream == null)への最初の呼び出しでのみ作成されます。

私が遭遇した複雑さの1つは、アプリがシャットダウンし、PeerConnectionインスタンスが閉じてMediaStreamが破棄されたときでした。サンプルコードでは、addStream(mediaStream)を使用してmediaStreamPeerConnectionに追加されていますが、対応するremoveStream(mediaStream)関数は呼び出されません(dispose()が代わりに呼び出されます) )。ただし、dispose()PeerConnectionをファイナライズするため、MediaStreamオブジェクトを共有するMediaStreamが複数ある場合、これにより問題(ネイティブコードのMediaStreamInterfaceの参照カウントアサート)が作成されます。これは、最後のPeerConnectionが閉じているときにのみ発生します。 removeStream()close()を呼び出すだけでも十分ではありません。PeerConnectionが完全にシャットダウンされず、PeerConnectionFactoryオブジェクトを破棄するときにアサートがクラッシュするためです。私が見つけた唯一の修正は、次のコードをPeerConnectionクラスに追加することでした。

public void freeConnection()
{
    localStreams.clear();
    freePeerConnection(nativePeerConnection);
    freeObserver(nativeObserver);
}

そして、最後を除く各PeerConnectionのファイナライズ時にこれらの関数を呼び出します。

peerConnections[connectionId].removeStream(mediaStream);
peerConnections[connectionId].close();
peerConnections[connectionId].freeConnection();
peerConnections[connectionId] = null;

そして、次のように最後のものをシャットダウンします:

peerConnections[connectionId].dispose();
peerConnections[connectionId] = null;

PeerConnectionClientを変更した後、正しい順序で接続を設定し、正しい接続インデックスを各関数に渡し、コールバックを適切に処理するために、シグナリングコードを変更する必要があります。私は、socket.ioソケットIDと接続IDの間のハッシュを維持することでこれを行いました。新しいクライアントがルームに参加すると、既存の各メンバーがオファーを新しいクライアントに送信し、順番に回答を受け取ります。また、複数のVideoRenderer.Callbacksオブジェクトを初期化し、それらをPeerConnectionClientインスタンスに渡して、電話会議用に画面を分割することも必要です。

8
samgak