成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

使用 Agora SDK 實(shí)現(xiàn)視頻對(duì)話應(yīng)用 HouseParty-附 Android 源碼

CocoaChina / 2574人閱讀

摘要:叔想做個(gè)直播很久了,最近終于得空,做了一個(gè)視頻群聊,以饗觀眾。主界面在主界面,我們需要檢查先和權(quán)限,以適配及以上版本。但提供了相關(guān)可以直接實(shí)現(xiàn)前置攝像頭預(yù)覽的功能。最多支持六人同時(shí)聊天。直接繼承,根據(jù)不同的顯示模式來(lái)完成孩子的測(cè)量和布局。

叔想做個(gè)直播demo很久了,最近終于得空,做了一個(gè)視頻群聊Demo,以饗觀眾。 直播云有很多大廠在做,經(jīng)老鐵介紹,Agora不錯(cuò),遂入坑。Agora提供多種模式,一個(gè)頻道可以設(shè)置一種模式。

Agora SDK集成

叔專注SDK集成幾十年,Agora SDK集成也并沒(méi)有搞什么事情,大家按照下面步驟上車就行。

1.注冊(cè)

登錄官網(wǎng),注冊(cè)個(gè)人賬號(hào),這個(gè)叔就不介紹了。

2.創(chuàng)建應(yīng)用

注冊(cè)賬號(hào)登錄后,進(jìn)入后臺(tái),找到“添加新項(xiàng)目”按鈕,點(diǎn)擊創(chuàng)建新項(xiàng)目,創(chuàng)建好后就會(huì)獲取到一個(gè)App ID, 做過(guò)SDK集成的老鐵們都知道這是干啥用的。

3.下載SDK

進(jìn)入官方下載界面, 這里我們選擇視頻通話 + 直播 SDK中的Android版本下載。下載后解壓之后又兩個(gè)文件夾,分別是libs和samples, libs文件夾存放的是庫(kù)文件,samples是官方Demo源碼,大叔曾說(shuō)過(guò)欲練此SDK,必先跑Sample, 有興趣的同學(xué)可以跑跑。

4.集成SDK

1. 導(dǎo)入庫(kù)文件

將libs文件夾的下的文件導(dǎo)入Android Studio, 最終效果如下:

2.添加必要權(quán)限

在AndroidManifest.xml中添加如下權(quán)限




3.配置APP ID

在values文件夾下創(chuàng)建strings-config.xml, 配置在官網(wǎng)創(chuàng)建應(yīng)用的App ID。


    6ffa586315ed49e6a8cdff064ad8a0b0
主界面(MainActivity)

在主界面,我們需要檢查先Camera和Audio權(quán)限,以適配Andriod6.0及以上版本。

private static final int PERMISSION_REQ_ID_RECORD_AUDIO = 0;
private static final int PERMISSION_REQ_ID_CAMERA = 1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //檢查Audio權(quán)限
    if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) {
        //檢查Camera權(quán)限
        checkSelfPermission(Manifest.permission.CAMERA, PERMISSION_REQ_ID_CAMERA);
    }
}

public boolean checkSelfPermission(String permission, int requestCode) {
    if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
        return false;
    }
    return true;
}
頻道界面 (ChannelActivity)

點(diǎn)擊開PA!,進(jìn)入頻道選擇界面

創(chuàng)建頻道列表

這里使用RecyclerView創(chuàng)建頻道列表。

/**
 * 初始化頻道列表
 */private void initRecyclerView() {
    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    mRecyclerView.setHasFixedSize(true);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    mRecyclerView.setAdapter(new ChannelAdapter(this, mockChannelList()));
}
前置攝像頭預(yù)覽

頻道界面背景為前置攝像頭預(yù)覽,這個(gè)可以使用Android SDK自己實(shí)現(xiàn)。但Agora SDK提供了相關(guān)API可以直接實(shí)現(xiàn)前置攝像頭預(yù)覽的功能。具體實(shí)現(xiàn)如下:

1. 初始化RtcEngineZ

RtcEngine是Agora SDK的核心類,叔用一個(gè)管理類AgoraManager進(jìn)行了簡(jiǎn)單的封裝,提供操作RtcEngine的核心功能。

初始化如下:

/**
 * 初始化RtcEngine
 */
public void init(Context context) {
    //創(chuàng)建RtcEngine對(duì)象, mRtcEventHandler為RtcEngine的回調(diào)
    mRtcEngine = RtcEngine.create(context, context.getString(R.string.private_app_id), mRtcEventHandler);
    //開啟視頻功能
    mRtcEngine.enableVideo();
    //視頻配置,設(shè)置為360P
    mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false);
    mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION);//設(shè)置為通信模式(默認(rèn))
    //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);設(shè)置為直播模式
    //mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_GAME);設(shè)置為游戲模式
}


/**
 * 在Application類中初始化RtcEngine,注意在AndroidManifest.xml中配置下Application
 */
public class LaoTieApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        AgoraManager.getInstance().init(getApplicationContext());
    }
}

2. 設(shè)置本地視頻

/**
 * 設(shè)置本地視頻,即前置攝像頭預(yù)覽
 */
public AgoraManager setupLocalVideo(Context context) {
    //創(chuàng)建一個(gè)SurfaceView用作視頻預(yù)覽
    SurfaceView surfaceView = RtcEngine.CreateRendererView(context);
    //將SurfaceView保存起來(lái)在SparseArray中,后續(xù)會(huì)將其加入界面。key為視頻的用戶id,這里是本地視頻, 默認(rèn)id是0
    mSurfaceViews.put(mLocalUid, surfaceView);
    //設(shè)置本地視頻,渲染模式選擇VideoCanvas.RENDER_MODE_HIDDEN,如果選其他模式會(huì)出現(xiàn)視頻不會(huì)填充滿整個(gè)SurfaceView的情況,
    //具體渲染模式的區(qū)別是什么,官方也沒(méi)有詳細(xì)的說(shuō)明
    mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, mLocalUid));
    return this;//返回AgoraManager以作鏈?zhǔn)秸{(diào)用
}

3. 添加SurfaceView到布局

@Override
protected void onResume() {
    super.onResume();
    //先清空容器
    mFrameLayout.removeAllViews();
    //設(shè)置本地前置攝像頭預(yù)覽并啟動(dòng)
    AgoraManager.getInstance().setupLocalVideo(getApplicationContext()).startPreview();
    //將本地?cái)z像頭預(yù)覽的SurfaceView添加到容器中
    mFrameLayout.addView(AgoraManager.getInstance().getLocalSurfaceView());
}

4. 停止預(yù)覽

/**
 * 停止預(yù)覽
 */
@Override
protected void onPause() {
    super.onPause();
    AgoraManager.getInstance().stopPreview();
}
聊天室 (PartyRoomActivity)

點(diǎn)擊頻道列表中的選項(xiàng),跳轉(zhuǎn)到聊天室界面。聊天室界面顯示規(guī)則是:1個(gè)人是全屏,2個(gè)人是2分屏,3-4個(gè)人是4分屏,5-6個(gè)人是6分屏, 4分屏和6分屏模式下,雙擊一個(gè)小窗,窗會(huì)變大,其余小窗在底部排列。最多支持六人同時(shí)聊天?;谶@種需求,叔決定寫一個(gè)自定義控件PartyRoomLayout來(lái)完成。PartyRoomLayout直接繼承ViewGroup,根據(jù)不同的顯示模式來(lái)完成孩子的測(cè)量和布局。

1人全屏


1人全屏其實(shí)就是前置攝像頭預(yù)覽效果。

前置攝像頭預(yù)覽
//設(shè)置前置攝像頭預(yù)覽并開啟
AgoraManager.getInstance()
        .setupLocalVideo(getApplicationContext())
        .startPreview();
//將攝像頭預(yù)覽的SurfaceView加入PartyRoomLayout
mPartyRoomLayout.addView(AgoraManager.getInstance().getLocalSurfaceView());
PartyRoomLayout處理1人全屏
/**
 * 測(cè)量一個(gè)孩子的情況,孩子的寬高和父容器即PartyRoomLayout一樣
 */
private void measureOneChild(int widthMeasureSpec, int heightMeasureSpec) {
    View child = getChildAt(0);
    child.measure(widthMeasureSpec, heightMeasureSpec);
}

/**
 * 布局一個(gè)孩子的情況
 */
private void layoutOneChild() {
    View child = getChildAt(0);
    child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
}
加入頻道

從頻道列表跳轉(zhuǎn)過(guò)來(lái)后,需要加入到用戶所選的頻道。

//更新頻道的TextView

mChannel = (TextView) findViewById(R.id.channel);
String channel = getIntent().getStringExtra(“Channel”);
mChannel.setText(channel);

//在AgoraManager中封裝了加入頻道的API

AgoraManager.getInstance()
            .setupLocalVideo(getApplicationContext())
            .joinChannel(channel)//加入頻道
            .startPreview();
掛斷

mEndCall = (ImageButton) findViewById(R.id.end_call);
mEndCall.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //AgoraManager里面封裝了掛斷的API, 退出頻道
        AgoraManager.getInstance().leaveChannel();
        finish();
    }
});
二分屏

事件監(jiān)聽(tīng)器

IRtcEngineEventHandler類里面封裝了Agora SDK里面的很多事件回調(diào),在AgoraManager中我們創(chuàng)建了IRtcEngineEventHandler的一個(gè)對(duì)象mRtcEventHandler,并在創(chuàng)建RtcEngine時(shí)傳入。
private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() { /**

private IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {

    /**
     * 當(dāng)獲取用戶uid的遠(yuǎn)程視頻的回調(diào)
     */
    @Override
    public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
        if (mOnPartyListener != null) {
            mOnPartyListener.onGetRemoteVideo(uid);
        }
    }

    /**
     * 加入頻道成功的回調(diào)
     */
    @Override
    public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
        if (mOnPartyListener != null) {
            mOnPartyListener.onJoinChannelSuccess(channel, uid);
        }
    }

    /**
     * 退出頻道
     */
    @Override
    public void onLeaveChannel(RtcStats stats) {
        if (mOnPartyListener != null) {
            mOnPartyListener.onLeaveChannelSuccess();
        }
    }

    /**
     * 用戶uid離線時(shí)的回調(diào)
     */
    @Override
    public void onUserOffline(int uid, int reason) {
        if (mOnPartyListener != null) {
            mOnPartyListener.onUserOffline(uid);
        }
    }
};

同時(shí),我們也提供了一個(gè)接口,暴露給AgoraManager外部。

public interface OnPartyListener {

    void onJoinChannelSuccess(String channel, int uid);

    void onGetRemoteVideo(int uid);

    void onLeaveChannelSuccess();

    void onUserOffline(int uid);
}

在PartyRoomActivity中監(jiān)聽(tīng)事件

AgoraManager.getInstance()
        .setupLocalVideo(getApplicationContext())
        .setOnPartyListener(mOnPartyListener)//設(shè)置監(jiān)聽(tīng)
        .joinChannel(channel)
        .startPreview();

設(shè)置遠(yuǎn)程用戶視頻

private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() {

    /**
     * 獲取遠(yuǎn)程用戶視頻的回調(diào)
     */
    @Override
    public void onGetRemoteVideo(final int uid) {
        //操作UI,需要切換到主線程
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //設(shè)置遠(yuǎn)程用戶的視頻
                AgoraManager.getInstance().setupRemoteVideo(PartyRoomActivity.this, uid);
                //將遠(yuǎn)程用戶視頻的SurfaceView添加到PartyRoomLayout中,這會(huì)觸發(fā)PartyRoomLayout重新走一遍繪制流程
                mPartyRoomLayout.addView(AgoraManager.getInstance().getSurfaceView(uid));
            }
        });
    }

};

測(cè)量布局二分屏

當(dāng)?shù)谝淮位卣{(diào)onGetRemoteVideo時(shí),說(shuō)明現(xiàn)在有兩個(gè)用戶了,所以在PartyRoomLayout中需要對(duì)二分屏模式進(jìn)行處理

/**
 * 二分屏?xí)r的測(cè)量
 */
private void measureTwoChild(int widthMeasureSpec, int heightMeasureSpec) {
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        int size = MeasureSpec.getSize(heightMeasureSpec);
        //孩子高度為父容器高度的一半
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
        child.measure(widthMeasureSpec, childHeightMeasureSpec);
    }
}

/**
 * 二分屏模式的布局
 */
private void layoutTwoChild() {
    int left = 0;
    int top = 0;
    int right = getMeasuredWidth();
    int bottom = getChildAt(0).getMeasuredHeight();
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        child.layout(left, top, right, bottom);
        top += child.getMeasuredHeight();
        bottom += child.getMeasuredHeight();
    }
}

用戶離線時(shí)的處理

當(dāng)有用戶離線時(shí),我們需要移除該用戶視頻對(duì)應(yīng)的SurfaceView

private AgoraManager.OnPartyListener mOnPartyListener = new AgoraManager.OnPartyListener() {

    @Override
    public void onUserOffline(final int uid) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //從PartyRoomLayout移除遠(yuǎn)程視頻的SurfaceView
                mPartyRoomLayout.removeView(AgoraManager.getInstance().getSurfaceView(uid));
                //清除緩存的SurfaceView
                AgoraManager.getInstance().removeSurfaceView(uid);
            }
        });
    }
};
四分屏和六分屏

當(dāng)有3個(gè)或者4個(gè)老鐵開趴,界面顯示成四分屏, 當(dāng)有5個(gè)或者6個(gè)老鐵開趴,界面切分成六分屏

由于之前已經(jīng)處理了新進(jìn)用戶就會(huì)創(chuàng)建SurfaceView加入PartyRoomLayout的邏輯,所以這里只需要處理四六分屏?xí)r的測(cè)量和布局

四六分屏測(cè)量
private void measureMoreChildSplit(int widthMeasureSpec, int heightMeasureSpec) {
    //列數(shù)為兩列,計(jì)算行數(shù)
    int row = getChildCount() / 2;
    if (getChildCount() % 2 != 0) {
        row = row + 1;
    }
    //根據(jù)行數(shù)平分高度
    int childHeight = MeasureSpec.getSize(heightMeasureSpec) / row;
    //寬度為父容器PartyRoomLayout的寬度一般,即屏寬的一半
    int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}
四六分屏布局
private void layoutMoreChildSplit() {
    int left = 0;
    int top = 0;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        int right = left + child.getMeasuredWidth();
        int bottom = top + child.getMeasuredHeight();
        child.layout(left, top, right, bottom);
        if ( (i + 1 )% 2 == 0) {//滿足換行條件,更新left和top,布局下一行
            left = 0;
            top += child.getMeasuredHeight();
        } else {
            //不滿足換行條件,更新left值,繼續(xù)布局一行中的下一個(gè)孩子
            left += child.getMeasuredWidth();
        }
    }
}
雙擊上下分屏布局

在四六分屏模式下,雙擊一個(gè)小窗,窗會(huì)變大,其余小窗在底部排列, 成上下分屏模式。實(shí)現(xiàn)思路就是監(jiān)聽(tīng)PartyRoomLayout的觸摸時(shí)間,當(dāng)是雙擊時(shí),則重新布局。

觸摸事件處理

/**
 *  攔截所有的事件
 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return true;
}

/**
 * 讓GestureDetector處理觸摸事件
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
    return true;
}



//四六分屏模式
private static int DISPLAY_MODE_SPLIT = 0;
//上下分屏模式
private static int DISPLAY_MODE_TOP_BOTTOM = 1;
//顯示模式的變量,默認(rèn)是四六分屏
private int mDisplayMode = DISPLAY_MODE_SPLIT;
//上下分屏?xí)r上面View的下標(biāo)
private int mTopViewIndex = -1;

private GestureDetector.SimpleOnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        handleDoubleTap(e);//處理雙擊事件
        return true;
    }

    private void handleDoubleTap(MotionEvent e) {
        //遍歷所有的孩子
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            //獲取孩子view的矩形
            Rect rect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
            if (rect.contains((int)e.getX(), (int)e.getY())) {//找到雙擊位置的孩子是誰(shuí)
                if (mTopViewIndex == i) {//如果點(diǎn)擊的位置就是上面的view, 則切換成四六分屏模式
                    mDisplayMode = DISPLAY_MODE_SPLIT;
                    mTopViewIndex = -1;//重置上面view的下標(biāo)
                } else {
                    //切換成上下分屏模式,
                    mTopViewIndex = i;//保存雙擊位置的下標(biāo),即上面View的下標(biāo)
                    mDisplayMode = DISPLAY_MODE_TOP_BOTTOM;
                }
                requestLayout();//請(qǐng)求重新布局
                break;
            }
        }
    }
};
上下分屏測(cè)量

處理完雙擊事件后,切換顯示模式,請(qǐng)求重新布局,這時(shí)候又會(huì)觸發(fā)測(cè)量和布局。

/**
 * 上下分屏模式的測(cè)量
 */
private void measureMoreChildTopBottom(int widthMeasureSpec, int heightMeasureSpec) {
    for (int i = 0; i < getChildCount(); i++) {
        if (i == mTopViewIndex) {
            //測(cè)量上面View
            measureTopChild(widthMeasureSpec, heightMeasureSpec);
        } else {
            //測(cè)量下面View
            measureBottomChild(i, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

/**
 *  上下分屏模式時(shí)上面View的測(cè)量
 */
private void measureTopChild(int widthMeasureSpec, int heightMeasureSpec) {
    int size = MeasureSpec.getSize(heightMeasureSpec);
    //高度為PartyRoomLayout的一半
    int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
    getChildAt(mTopViewIndex).measure(widthMeasureSpec, childHeightMeasureSpec);
}

/**
 * 上下分屏模式時(shí)底部View的測(cè)量
 */
private void measureBottomChild(int i, int widthMeasureSpec, int heightMeasureSpec) {
    //除去頂部孩子后還剩的孩子個(gè)數(shù)
    int childCountExcludeTop = getChildCount() - 1;
    //當(dāng)?shù)撞亢⒆觽€(gè)數(shù)小于等于3時(shí)
    if (childCountExcludeTop <= 3) {
        //平分孩子寬度
        int childWidth = MeasureSpec.getSize(widthMeasureSpec) / childCountExcludeTop;
        int size = MeasureSpec.getSize(heightMeasureSpec);
        //高度為PartyRoomLayout的一半
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size / 2, MeasureSpec.EXACTLY);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } else if (childCountExcludeTop == 4) {//當(dāng)?shù)撞亢⒆觽€(gè)數(shù)為4個(gè)時(shí)
        int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 2;//寬度為PartyRoomLayout的一半
        int childHeight = MeasureSpec.getSize(heightMeasureSpec) / 4;//高度為PartyRoomLayout的1/4
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } else {//當(dāng)?shù)撞亢⒆哟笥?個(gè)時(shí)
        //計(jì)算行的個(gè)數(shù)
        int row = childCountExcludeTop / 3;
        if (row  % 3 != 0) {
            row ++;
        }
        //孩子的寬度為PartyRoomLayout寬度的1/3
        int childWidth = MeasureSpec.getSize(widthMeasureSpec) / 3;
        //底部孩子平分PartyRoomLayout一半的高度
        int childHeight = (MeasureSpec.getSize(heightMeasureSpec) / 2) / row;
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        getChildAt(i).measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
}
上下分屏布局
private void layoutMoreChildTopBottom() {
    //布局上面View
    View topView = getChildAt(mTopViewIndex);
    topView.layout(0, 0, topView.getMeasuredWidth(), topView.getMeasuredHeight());
    int left = 0;
    int top = topView.getMeasuredHeight();
    for (int i = 0; i < getChildCount(); i++) {
        //上面已經(jīng)布局過(guò)上面的View, 這里就跳過(guò)
        if (i == mTopViewIndex) {
            continue;
        }
        View view = getChildAt(i);
        int right = left + view.getMeasuredWidth();
        int bottom = top + view.getMeasuredHeight();
        //布局下面的一個(gè)View
        view.layout(left, top, right, bottom);
        left = left + view.getMeasuredWidth();
        if (left >= getWidth()) {//滿足換行條件則換行
            left = 0;
            top += view.getMeasuredHeight();
        }
    }
}

至此,一個(gè)功能類似Houseparty的demo就完成了,github地址:

https://github.com/uncleleonf...

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/70046.html

相關(guān)文章

  • Android實(shí)現(xiàn)多人音視頻聊天應(yīng)用(一)

    作者:聲網(wǎng)Agora用戶,資深A(yù)ndroid開發(fā)者吳東洋。本系列文章分享了基于Agora SDK 2.1實(shí)現(xiàn)多人視頻通話的實(shí)踐經(jīng)驗(yàn)。 自從2016年,鼓吹互聯(lián)網(wǎng)寒冬的論調(diào)甚囂塵上,2017年亦有愈演愈烈之勢(shì)。但連麥直播、在線抓娃娃、直播問(wèn)答、遠(yuǎn)程狼人殺等類型的項(xiàng)目卻異軍突起,成了投資人的風(fēng)口,創(chuàng)業(yè)者的藍(lán)海和用戶的必裝App,而這些方向的項(xiàng)目都有一個(gè)共同的特點(diǎn)——都依賴視頻通話和全互動(dòng)直播技術(shù)。 聲...

    raoyi 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<