WebRTC ビデオ Android 実装の原則の解釈

導入:

入社して最初にやったのが映像関連のプロジェクトで、他社が提供するSDKを使ったので機能の実装が簡単だったので、プロジェクトが終わった時点で「これだけではダメだ」と思いましたが、少なくとも使い方を知っておいてください。原理的なプロセスです。それでは、WebRTC のビデオ接続プロセスについての私の説明をいくつか説明しましょう。

WebRTC ライブラリに関しては、ポイントツーポイント通信を提供しますが、双方がサーバーに接続することが前提であり、まず、通信を確立するためにブラウザ間で交換されるメタデータ (実際にはシグナリング) がサーバーを経由する必要があります。公式の NAT やファイアウォールもサーバーを経由する必要があります (実際には、接続を確立する方法を見つけるために穴を開けることと理解できます)。サーバーについては、私はあまり詳しくないので、説明しません。あまり言いません。

アンドロイド側:

これはコンパイルされたWebRTC プロジェクトです。そうでない場合、初心者が自分でコンパイルするのは困難です。Android クライアントに関しては、RTCPeerConnection インターフェイスを理解することだけが必要です。RTCPeerConnection インターフェイスは、ローカル コンピューターからリモート エンドへの WebRTC 接続を表し、接続を作成、維持、監視、閉じるためのメソッドの実装を提供します。まだ次の 2 つのことを理解する必要があります。 1. このマシン上のメディア ストリームの特性 (解像度、エンコード機能など) を決定します (これは実際には SDP の説明に含まれており、後で説明します)。 2. ネットワーク接続の両端のホストのアドレス (実際には、ICE 候補です)

原則 (重要):

オファー アンド アンサーを通じて SDP 記述子を交換します: (たとえば、A が B に対してビデオ リクエストを開始します) たとえば、A と B はポイントツーポイント接続を確立する必要があります。おおよそのプロセスは次のとおりです: 両端が最初に PeerConnection インスタンスを確立します (ここでは pc と呼ばれます)、A は pc を使用します。提供された createOffer() メソッドは、SDP 記述子を含むオファー シグナリングを確立します。同様に、A は、pc によって提供された setLocalDescription() メソッドを通じて A の SDP 記述子を A の pc オブジェクトに渡し、A はオファーを送信しますサーバー経由で B に送信します。B は、A のオファー シグナリングに含まれる SDP 記述子を抽出し、pc が提供する setRemoteDescription() メソッドを介して B の PC インスタンス オブジェクトに渡します。B は、pc が提供する createAnswer() メソッドを使用して、B を含む SDP を作成します。記述子応答シグナリング、B PC が提供する setLocalDescription() メソッドを使用して、SDP 記述子を自身の PC インスタンス オブジェクトに渡し、応答シグナリングをサーバー経由で A に送信します。最後に、A は B の応答シグナリングを受信します。最後に、SDP 記述子を抽出して呼び出します。 setRemoteDescription() メソッドを使用して、それを A 自身の PC インスタンス オブジェクトに渡します。

したがって、両端のビデオ接続のプロセスは大まかに上記のプロセスであり、一連のシグナリング交換を通じて、A と B が作成した PC インスタンス オブジェクトに A と B の SDP 記述子が含まれ、上記 2 つのうちの 1 つ目が完了します。次に、次のように、接続の両端のホストのネットワーク アドレスを取得します。

ICE フレームワークを介して NAT/ファイアウォール トラバーサル接続 (ホール パンチング) を確立します。この URL は外部から直接アクセスできる必要があります。WebRTC は ICE フレームワークを使用してこの URL を取得します。PeerConnection が作成されると、ICE サーバーのアドレスが次のように渡されます。
 

 private void init(Context context) {
        PeerConnectionFactory.initializeAndroidGlobals(context, true, true, true);
        this.factory = new PeerConnectionFactory();
        this.iceServers.add(new IceServer("turn:turn.realtimecat.com:3478", "learningtech", "learningtech"));
    }
注意:“turn:turn.realtimecat.com:3478”这段字符其实就是该ICE服务器的地址。

もちろん、このアドレスも交換する必要があります。AB を例にとると、交換プロセスは次のとおりです (PeerConnection は PC と呼ばれます): A と B はそれぞれ、ICE サーバーで構成された PC インスタンスを作成し、onicecandidate イベント コールバックをネットワーク候補が利用可能な場合、onicecandidate 関数はコールバック関数内で呼び出されます。A または B はネットワーク候補メッセージを ICE Candidate シグナリングにカプセル化し、サーバーを介して中継し、相手に渡します。A または B は受信します。サーバーリレーを通じて相手から送信された ICE Candidate シグナリングを解析し、ネットワーク候補を取得したら、PC インスタンスの addIceCandidate() メソッドを通じて PC インスタンスに追加します。

このようにして、接続が確立され、addStream() を介してストリームを RTCPeerConnection に追加して、メディア ストリーム データを送信できます。ストリームを RTCPeerConnection インスタンスに追加すると、相手は onaddstream にバインドされたコールバック関数を通じてリッスンできるようになります。接続が完了する前に addStream() を呼び出します。接続が確立された後も、相手は引き続きメディア ストリームを監視できます。

SDKを使用して作成したコードの実装手順は以下のとおりです:

1. まず、インターフェイスのレイアウトで、ビデオを表示するxmlファイルにGLSurfaceViewコントロールを記述します。もちろん、コントロールを動的に追加することもできます(私が書いた)それは静的に、これはランダムです)

2. 最初にコントロールを初期化します。(もちろん、インターフェイスに入ったらすぐに初期化することも、後でサーバーに接続した後に任意の順序で初期化することもできます)

public void initPlayView(GLSurfaceView glSurfaceView) {
        VideoRendererGui.setView(glSurfaceView, (Runnable)null);
        this.isVideoRendererGuiSet = true;
    }

この手順では、表示するインターフェイスとして glSurfaceView を VideoRendererGui に追加します。

3. ビデオ サーバーにログインします。実際には、このステップが最初になります (ステップ 2 と 3 の順序は制限されません)。

public void connect(String url) throws URISyntaxException {
        //先初始化配置网络ping的一些信息
        this.init(url);
        //然后在连接服务器
        this.client.connect();
    }

    private void init(String url) throws URISyntaxException {
        if (!this.init) {
            Options opts = new Options();
            opts.forceNew = true;
            opts.reconnection = false;
            opts.query = "user_id=" + this.username;
            this.client = IO.socket(url, opts);
            this.client.on("connect", new Listener() {
                public void call(Object... args) {
                    if (Token.this.mEventHandler != null) {
                        Message msg = Token.this.mEventHandler.obtainMessage(10010);
                        Token.this.mEventHandler.sendMessage(msg);
                    }
                }
            }).on("disconnect", new Listener() {
                public void call(Object... args) {
                    if (Token.this.mEventHandler != null) {
                        Message msg = Token.this.mEventHandler.obtainMessage(10014);
                        Token.this.mEventHandler.sendMessage(msg);
                    }
                }
            }).on("error", new Listener() {
                public void call(Object... args) {
                    if (Token.this.mEventHandler != null) {
                        Error error = null;
                        if (args.length > 0) {
                            try {
                                error = (Error) (new Gson()).fromJson((String) args[0], Error.class);
                            } catch (Exception var4) {
                                var4.printStackTrace();
                            }
                        }
                        Message msg = Token.this.mEventHandler.obtainMessage(10013, error);
                        Token.this.mEventHandler.sendMessage(msg);
                    }
                }
            }).on("connect_timeout", new Listener() {
                public void call(Object... args) {
                    if (Token.this.mEventHandler != null) {
                        Message msg = Token.this.mEventHandler.obtainMessage(10012);
                        Token.this.mEventHandler.sendMessage(msg);
                    }
                }
            }).on("connect_error", new Listener() {
                public void call(Object... args) {
                    if (Token.this.mEventHandler != null) {
                        Message msg = Token.this.mEventHandler.obtainMessage(10011);
                        Token.this.mEventHandler.sendMessage(msg);
                    }
                }
            }).on("message", new Listener() {
                public void call(Object... args) {
                    try {
                        Token.this.handleMessage(cn.niusee.chat.sdk.Message.parseMessage((JSONObject) args[0]));
                    } catch (MessageErrorException var3) {
                        var3.printStackTrace();
                    }
                }
            });
            this.init = true;
        }
    }

4. ログイン時に、トークンのいくつかのモニターを設定します。

public interface OnTokenCallback {
    void onConnected();//视频连接成功的回调
    void onConnectFail();
    void onConnectTimeOut();
    void onError(Error var1);//视频连接错误的回调
    void onDisconnect();//视频断开的回调
    void onSessionCreate(Session var1);//视频打洞成功的回调
}

5. 以下は、サーバーにログインするためのコードです。

public void login(String username) {
        try {
            SingleChatClient.getInstance(getApplication()).setOnConnectListener(new SingleChatClient.OnConnectListener() {
                @Override
                public void onConnect() {
//                    loadDevices();
                    Log.e(TAG, "连接视频服务器成功");
                    state.setText("登录视频服务器成功!");
                }
                @Override
                public void onConnectFail(String reason) {
                    Log.e(TAG, "连接视频服务器失败");
                    state.setText("登录视频服务器失败!" + reason);
                }
                @Override
                public void onSessionCreate(Session session) {
                    Log.e(TAG, "来电者名称:" + session.callName);
                    mSession = session;
                    accept.setVisibility(View.VISIBLE);
                    requestPermission(new String[]{Manifest.permission.CAMERA}, "请求设备权限", new GrantedResult() {
                        @Override
                        public void onResult(boolean granted) {
                            if(granted){
                                createLocalStream();
                            }else {
                                Toast.makeText(MainActivity.this,"权限拒绝",Toast.LENGTH_SHORT).show();
                            }
                        }
                    });
                    mSession.setOnSessionCallback(new OnSessionCallback() {
                        @Override
                        public void onAccept() {
                            Toast.makeText(MainActivity.this, "视频接收", Toast.LENGTH_SHORT).show();
                        }
                        @Override
                        public void onReject() {
                            Toast.makeText(MainActivity.this, "拒绝通话", Toast.LENGTH_SHORT).show();
                        }
                        @Override
                        public void onConnect() {
                            Toast.makeText(MainActivity.this, "视频建立成功", Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void onClose() {
                            Log.e(TAG, "onClose  我是被叫方");
                            hangup();
                        }
                        @Override
                        public void onRemote(Stream stream) {
                            Log.e(TAG, "onRemote  我是被叫方");
                            mRemoteStream = stream;
                           mSingleChatClient.getChatClient().playStream(stream, new Point(0, 0, 100, 100, false));
                            mSingleChatClient.getChatClient().playStream(mLocalStream, new Point(72, 72, 25, 25, false));
                        }
                        @Override
                        public void onPresence(Message message) {
                        }
                    });
                }
            });
//            SingleChatClient.getInstance(getApplication()).connect(UUID.randomUUID().toString(), WEB_RTC_URL);
            Log.e("MainActicvity===",username);
            SingleChatClient.getInstance(getApplication()).connect(username, WEB_RTC_URL);
        } catch (URISyntaxException e) {
            e.printStackTrace();
            Log.d(TAG, "连接失败");
        }
    }

知らせ:

onSessionCreate(Session session)这个回调是当检测到有视频请求来的时候才会触发,所以这里可以设置当触发该回调是显示一个接受按钮,一个拒绝按钮,session中携带了包括对方的userName,以及各种信息(上面所说的SDP描述信息等),这个时候通过session来设置OnSessionCallback的回调信息,public interface OnSessionCallback {
    void onAccept();//用户同意
    void onReject();//用户拒绝
    void onConnect();//连接成功
    void onClose();//连接掉开
    void onRemote(Stream var1);//当远程流开启的时候,就是对方把他的本地流传过来的时候
    void onPresence(Message var1);//消息通道过来的action消息,action是int型,远程控制的时候可以使用这个int型信令发送指令
}

知らせ:

 @Override
    public void onRemote(Stream stream) {
    Log.e(TAG, "onRemote  我是被叫方");
    mRemoteStream = stream;
    mSingleChatClient.getChatClient().playStream(stream, new Point(0, 0, 100, 100, false));
    mSingleChatClient.getChatClient().playStream(mLocalStream, new Point(72, 72, 25, 25, false));
} 

ここで、リモートストリームコールバックを実行すると、相手の画面を表示したり、自身のローカルストリーム小ウィンドウを更新したりすることができる。(最も重要な前提は、送信したローカル ストリームを相手に受信してもらいたい場合は、まず自分で playStream を呼び出す必要があります。そうすることで、onRemote コールバックを通じて送信したローカル ストリームを相手が受信できるようになります)

6. A が B にビデオ チャットの開始を積極的に要求する場合、手動で呼び出す必要があります。
 

private void call() {
        try {
            Log.e("MainActivity===","对方username:"+userName);
            mSession = mSingleChatClient.getToken().createSession(userName);
            //userName是指对方的用户名,并且这里要新建session对象,因为你是主动发起呼叫的,如果是被呼叫的则在onSessionCreate(Session session)回调中会拿到session对象的。(主叫方和被叫方不太一样)
        } catch (SessionExistException e) {
            e.printStackTrace();
        }
        requestPermission(new String[]{Manifest.permission.CAMERA}, "请求设备相机权限", new GrantedResult() {
            @Override
            public void onResult(boolean granted) {
                if(granted){//表示用户允许
                    createLocalStream();//权限允许之后,首先打开本地流,以及摄像头开启
                }else {//用户拒绝
                    Toast.makeText(MainActivity.this,"权限拒绝",Toast.LENGTH_SHORT).show();
                    return;
                }
            }
        });
        mSession.setOnSessionCallback(new OnSessionCallback() {
            @Override
            public void onAccept() {
                Toast.makeText(MainActivity.this, "通话建立成功", Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onReject() {
                Toast.makeText(MainActivity.this, "对方拒绝了您的视频通话请求", Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onConnect() {
            }
            @Override
            public void onClose() {
                mSingleChatClient.getToken().closeSession(userName);
                Log.e(TAG, "onClose  我是呼叫方");
                hangup();
                Toast.makeText(MainActivity.this, "对方已中断视频通话", Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onRemote(Stream stream) {
                mStream = stream;
                Log.e(TAG, "onRemote  我是呼叫方");
                Toast.makeText(MainActivity.this, "视频建立成功", Toast.LENGTH_SHORT).show();
                mSingleChatClient.getChatClient().playStream(stream, new Point(0, 0, 100, 100, false));
                mSingleChatClient.getChatClient().playStream(mLocalStream, new Point(72, 72, 25, 25, false));
            }
            @Override
            public void onPresence(Message message) {
            }
        });
        if (mSession != null) {
            mSession.call();//主动开启呼叫对方
        }
    }

ローカル ストリームを作成します。

private void createLocalStream() {
        if (mLocalStream == null) {
            try {
                String camerName = CameraDeviceUtil.getFrontDeviceName();
                if(camerName==null){
                    camerName = CameraDeviceUtil.getBackDeviceName();
                }
                mLocalStream = mSingleChatClient.getChatClient().createStream(camerName,
                        new Stream.VideoParameters(640, 480, 12, 25), new Stream.AudioParameters(true, false, true, true), null);
            } catch (StreamEmptyException | CameraNotFoundException e) {
                e.printStackTrace();
            }
        } else {
            mLocalStream.restart();
        }
        mSingleChatClient.getChatClient().playStream(mLocalStream, new Point(72, 72, 25, 25, false));
    }

要約:

上記は、SDK の原理と使用法を簡単に紹介したものです (SDK について詳しく知りたい場合は、以下のコメントにメッセージを残していただければ送信します)。原則については今後さらに詳しく説明しますが、もう 1 つ重要な点があります。問題はマルチネットワークの相互運用性に関するもので、当事者 A はチャイナユニコム 4G 状態にあり、当事者 B はテレコム WIFI 状態にあり、当事者 B はモバイル 4G 状態にあります。これらの異なるネットワーク オペレータ間の相互運用性に問題がある可能性があるため、事前にテストしてください。その際、特別なパケット キャプチャとデバッグを実施したところ、A が Unicom 4G を使用しており、B (モバイル 4G) へのビデオを開始したときの結果が示されました。 , A は常にホールメイキング状態にありましたが、ホールがブロックされて消えませんでした。転送 (つまりインターネット) 理論的に言えば、転送は最後の状況です。つまり、以前のすべての方法が失敗します。転送は確かに可能ですが、転送には多くの帯域幅を必要とする転送サーバーのセットアップが必要です。これによりビデオ接続が保証されるため、現在のビデオはデフォルトでイントラネット (同じ Wi-Fi の下) をサポートするか、同じサーバー間の相互運用性をサポートします。他の異なるネットワーク事業者間の相互運用性については、100% の相互運用性が保証されていないため、これは難しい質問です。

著者: Ai Shen Accidentally
元記事では Android 側での WebRTC ビデオ実装の原則を説明しています - Nuggets

★記事末尾の名刺では、(FFmpeg、webRTC、rtmp、hls、rtsp、ffplay、srs)を含むオーディオおよびビデオ開発学習教材やオーディオおよびビデオ学習ロードマップなどを無料で受け取ることができます。

以下を参照してください! ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

おすすめ

転載: blog.csdn.net/yinshipin007/article/details/132758969