Android 腾讯音视频聊天的实现

前言

近期项目做即时聊天,以前已经实现,使用环信实现的,当时用时不到一个星期,但是视频通话质量不是很好,老板不是很喜欢,要求换成腾讯,然后开始调研腾讯im ,腾讯im即时聊天的文档中没有音视频通话,要自己实现,使用互动直播实现,因此要实现这个功能就要花钱,公司有钱任性,但是自己也可以接触下腾讯

这一番接触下,腾讯给我的感觉并不友好,demo功能不是很全,很多自己实现,文档写的很不完善,大数要靠自己猜,猜中了像中了500万,猜不中就跟谢谢惠顾的。。。吐槽无用还是要实现功能,现在我们来说一下实现音视频通话。

腾讯专门封装了一个callSdk 来实现音视频通话,Android Ios,两个客户端都有,通过callSdk 实现音视频会简单很多,主要设置监听,进行业务处理

CallSdk 的api文档:https://zhaoyang21cn.github.io/iLiveSDK_Help/callsdk/

音视频通话的demo:https://github.com/zhaoyang21cn/CallSDK_Android_Demo

看到这个github上的demo图片和运行出来的图片不符,可能会伤心了,具体的逻辑要自己实现,demo运行出来视频建立后会出现黑屏,是因为没有开权限的问题,希望不要陷坑太久。

互动直播的api文档:https://zhaoyang21cn.github.io/iLiveSDK_Help/android_help/

因为用到AVRootView 控件,所以参看这个文档

先来看一下我们视频通话实现的效果

视频呼叫页面


image.png

视频应答页面


image.png

一.在gradle中加入

"com.tencent.ilivesdk:ilivesdk:1.8.6.3",
 "com.tencent.callsdk:callsdk:1.0.31",

二.音视频的主要逻辑代码

package com.jyjt.ydyl.txim;

import android.Manifest;
import android.app.Activity;
import android.app.Application;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.support.v4.app.ActivityCompat;
import android.text.TextUtils;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.google.gson.JsonObject;
import com.jyjt.ydyl.BaseActivity;
import com.jyjt.ydyl.BasePresenter;
import com.jyjt.ydyl.R;
import com.jyjt.ydyl.application.MyApplication;
import com.jyjt.ydyl.tools.AppUtils;
import com.jyjt.ydyl.tools.ConfigUtils;
import com.jyjt.ydyl.tools.LogUtils;
import com.jyjt.ydyl.tools.SwitchActivityManager;
import com.jyjt.ydyl.tools.ToastUtil;
import com.jyjt.ydyl.txim.presentation.event.VcallEvent;
import com.tencent.av.sdk.AVAudioCtrl;
import com.tencent.callsdk.ILVBCallMemberListener;
import com.tencent.callsdk.ILVCallConstants;
import com.tencent.callsdk.ILVCallListener;
import com.tencent.callsdk.ILVCallManager;
import com.tencent.callsdk.ILVCallNotification;
import com.tencent.callsdk.ILVCallOption;
import com.tencent.ilivesdk.ILiveCallBack;
import com.tencent.ilivesdk.ILiveConstants;
import com.tencent.ilivesdk.ILiveSDK;
import com.tencent.ilivesdk.core.ILiveLoginManager;
import com.tencent.ilivesdk.view.AVRootView;
import com.tencent.ilivesdk.view.AVVideoView;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;

/**
 * 苏艳
 * <p>
 * 权限校验
 * 音视频呼叫:呼叫的时候校验麦克分或摄像头权限
 * 音视频接听:点击接受时校验麦克分或摄像头权限
 * 视频通话:通话的时候时候校验麦克分权限
 */


public class VoiceCallActivity extends BaseActivity implements ILVCallListener, ILVBCallMemberListener, ILiveLoginManager.TILVBStatusListener, AVRootView.onSubViewCreatedListener {
    //大小头像以及提示语========================================================
    @BindView(R.id.rl_big_samll_head)
    RelativeLayout rl_big_samll_head;
    @BindView(R.id.tv_waring)
    TextView tv_waring;
    @BindView(R.id.iv_video_big_head)
    ImageView iv_video_big_head;
    @BindView(R.id.iv_video_small_head)
    ImageView iv_video_small_head;
    @BindView(R.id.tv_yuyin_timer)
    public TextView tv_yuyin_timer;
    //两路视频通话界面================================================================
    @BindView(R.id.rl_video_conversation)
    RelativeLayout rl_video_conversation;
    @BindView(R.id.iv_switch_camera)
    ImageView iv_switch_camera;
    @BindView(R.id.av_root_view)
    AVRootView av_root_view;
    //视屏呼叫界面  1================================================================
    @BindView(R.id.ll_bootom_call)
    LinearLayout ll_bootom_call;
    //视频邀请界面 2================================================================
    @BindView(R.id.ll_bottom_invitation)
    LinearLayout ll_bottom_invitation;
    @BindView(R.id.ll_video_yes)
    LinearLayout ll_video_yes;
    @BindView(R.id.ll_video_no)
    LinearLayout ll_video_no;
    //视频通话界面 3==========================================================
    @BindView(R.id.ll_bootom_conversation)
    LinearLayout ll_bootom_conversation;
    @BindView(R.id.ll_hangup)
    LinearLayout ll_hangup;
    @BindView(R.id.ll_no_voice)
    LinearLayout ll_no_voice;
    @BindView(R.id.ll_big_voice)
    LinearLayout ll_big_voice;
    @BindView(R.id.iv_no_voice)
    ImageView iv_no_voice;
    @BindView(R.id.iv_big_voice)
    ImageView iv_big_voice;
    //基本数据================================================================
    //1.自己的id
    private String mHostId = "";
    //2.对方的id
    private String mUserId = "";
    //3.呼叫的类型    语音电话: CALL_TYPE_AUDIO  视频电话 : CALL_TYPE_VIDEO
    private int mCallType;
    //4.页面类型 1:视频呼叫 2:视频邀请  3:视频通话
    private int mType;
    //5.通话的id
    private int mCallId;
    //6.对方头像的url
    private String mHeadUrl = "";
    //7.对方的名字
    private String mUserName = "";
    //8.双人视频通话配置
    private ILVCallOption mOption;
    //9.麦克风切换设置
    private boolean mMicEnalbe = true;
    //10.扬声器切换设置
    private boolean mSpeaker = true;
    //11.默认设置前置摄像头
    private int mCurCameraId = ILiveConstants.FRONT_CAMERA;
    //12.计时器
    private Handler mHandler = new Handler();
    //13.当前毫秒数
    private long currentSecond = 0;

    //=======================================================================
    @Override
    protected BasePresenter loadPresenter() {
        return null;
    }

    @Override
    protected void initData() {
    }

    @Override
    protected void initView() {
        //1.获取页面类型
        mType = getIntent().getIntExtra("Type", 0);
        //2.获取type类型
        mCallType = getIntent().getIntExtra("CallType", ILVCallConstants.CALL_TYPE_VIDEO);
        //3.获取自己的id
        mHostId = getIntent().getStringExtra("HostId");
        //4.获取对方的id
        mUserId = getIntent().getStringExtra("UserId");
        //5.获取通话id
        mCallId = getIntent().getIntExtra("CallId", 0);
        //6.获取对方头像
        mHeadUrl = getIntent().getStringExtra("HeadUrl");
        //7.获取对方的名字,来电的时候展示
        mUserName = getIntent().getStringExtra("UserName");
        //8.初始化视频呼叫和接听的CallOption
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("name", ConfigUtils.getMyName());
        jsonObject.addProperty("head", ConfigUtils.getMyHead());
        String s = jsonObject.toString();
        mOption = new ILVCallOption(mHostId).callTips("").setMemberListener(this).setCallType(mCallType).customParam(s);
        //9.根据type 类型展示布局
        showLayout(mType);
    }

    @Override
    protected void initListener() {
        //1.设置视频通话监听
        ILVCallManager.getInstance().addCallListener(this);
        //2.被迫下线监听
        ILiveLoginManager.getInstance().setUserStatusListener(this);
        //3.AVRootView设置渲染后监听
        av_root_view.setSubCreatedListener(this);
        //2.结束监听
        ll_bootom_call.setOnClickListener(this);
        //3.接受监听
        ll_video_yes.setOnClickListener(this);
        //4.拒绝监听
        ll_video_no.setOnClickListener(this);
        //5.挂断监听
        ll_hangup.setOnClickListener(this);
        //6.静音监听
        ll_no_voice.setOnClickListener(this);
        //7.免提监听
        ll_big_voice.setOnClickListener(this);
        //8.摄像头切换
        iv_switch_camera.setOnClickListener(this);
    }

    /**
     * 根据mType 类型展示布局
     *
     * @param mType
     */
    public void showLayout(int mType) {
        switch (mType) {
            case 1:
                //1.判断摄像头权限
                if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
                    openCamerePermisson();
                } else {
                    openRecordPermisson();
                }
                //2.展示视频呼叫布局
                showDefault();
                showBigSmallHead();
                ll_bootom_call.setVisibility(View.VISIBLE);
                //3.修改提示语,展示头像
                tv_waring.setText(((mCallType == ILVCallConstants.CALL_TYPE_VIDEO) ? "视频电话拨通中..." : "聊天拨通中..."));
                //4.呼叫
                makecall(mUserId);
                break;
            case 2:
                //1.展示视频邀请布局
                showDefault();
                showBigSmallHead();
                ll_bottom_invitation.setVisibility(View.VISIBLE);
                //2.设置提示语
                tv_waring.setText(mUserName + ((mCallType == ILVCallConstants.CALL_TYPE_VIDEO) ? "邀请你加入视频通话" : "邀请你加入语音通话"));
                break;
            case 3:
                //1.展示视频通话布局
                showDefault();
                ll_bootom_conversation.setVisibility(View.VISIBLE);
                //2.根据视频通话和语音通话展示不同的布局
                if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
                    //1.校验麦克分权限(视频权限在呼叫和接听的时候校验了)
                    openRecordPermisson();
                    //2.展示两路视频界面
                    rl_video_conversation.setVisibility(View.VISIBLE);
                    //3.初始化AV基础
                    ILVCallManager.getInstance().initAvView(av_root_view);
                } else {
                    //1.展示大小头像界面
                    rl_big_samll_head.setVisibility(View.VISIBLE);
                    //2.设置提示语
                    tv_waring.setText("通话中");
                }
                //3.初始化计时器
                timeRunable.run();
                break;
            default:
                break;
        }
    }

    //打开相机权限
    public void openCamerePermisson() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN // api16之后需要权限
                && ActivityCompat.checkSelfPermission(VoiceCallActivity.this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {//权限不够时,提示
            ToastUtil.setToast("请打开摄像头权限");
        }
    }

    //打开麦克风权限
    public void openRecordPermisson() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN // api16之后需要权限
                && ActivityCompat.checkSelfPermission(VoiceCallActivity.this, Manifest.permission.RECORD_AUDIO)
                != PackageManager.PERMISSION_GRANTED) {//权限不够时,提示
            ToastUtil.setToast("请打开麦克风权限权限");
        }
    }

    //展示大小头像
    private void showBigSmallHead() {
        rl_big_samll_head.setVisibility(View.VISIBLE);
        AppUtils.loadCirclePic(this, R.mipmap.personal, mHeadUrl, iv_video_small_head);
        AppUtils.loadBitmapCicleIcon(this, R.mipmap.personal_center, mHeadUrl, iv_video_big_head);
    }

    //计时器
    private Runnable timeRunable = new Runnable() {
        @Override
        public void run() {
            currentSecond = currentSecond + 1000;
            tv_yuyin_timer.setText(getFormatHMS(currentSecond));
            //递归调用本runable对象,实现每隔一秒一次执行任务
            mHandler.postDelayed(this, 1000);
        }
    };

    // 根据毫秒返回时分秒
    public static String getFormatHMS(long time) {
        time = time / 1000;//总秒数
        int s = (int) (time % 60);//秒
        int m = (int) (time / 60);//分
        int h = (int) (time / 3600);//秒
        return String.format("%02d:%02d:%02d", h, m, s);
    }

    /**
     * 默认隐藏所有布局,根据需求展示
     */
    public void showDefault() {
        //1.大小头像界面
        rl_big_samll_head.setVisibility(View.GONE);
        //2.两路视频界面
        rl_video_conversation.setVisibility(View.GONE);
        //3.呼叫底部
        ll_bootom_call.setVisibility(View.GONE);
        //4.邀请底部
        ll_bottom_invitation.setVisibility(View.GONE);
        //5.通话底部
        ll_bootom_conversation.setVisibility(View.GONE);
    }

    /**
     * type 为1时,掉发起呼叫
     *
     * @param mUserId:对方id
     */
    public void makecall(String mUserId) {
        //1.发起呼叫生成id
        mCallId = ILVCallManager.getInstance().makeCall(mUserId + "", mOption, new ILiveCallBack() {
            @Override
            public void onSuccess(Object data) {
            }

            @Override
            public void onError(String module, int errCode, String errMsg) {
                toast(errMsg);
                finish();
            }
        });
        //2.设置呼叫类型
        VoiceService.isCallType = true;
        //3.设置通话Id
        VoiceService.isSendNOtifacation = false;

    }

    @Override
    protected int getLayoutId() {
        return R.layout.activity_voice_call;
    }

    @Override
    protected void otherViewClick(View view) {
        switch (view.getId()) {
            case R.id.ll_bootom_call://取消
                //1.结束挂断电话
                ILVCallManager.getInstance().endCall(mCallId);
                
                break;
            case R.id.ll_video_yes://接受
                //1.判断是否有网
                if (!AppUtils.isAccessNetwork(mContext)) {
                    toast("请检查您的网络");
                    return;
                }
                //2.权限验证
                if (mCallType == ILVCallConstants.CALL_TYPE_VIDEO) {
                    openCamerePermisson();
                } else {
                    openRecordPermisson();
                }
                //3.接受电话
                ILVCallManager.getInstance().acceptCall(mCallId, mOption);
                break;
            case R.id.ll_video_no://拒绝
                //1.若没有启动过app启动app
                showMainActivity();
                //2.拒绝通知对方已拒绝
                ILVCallManager.getInstance().rejectCall(mCallId);
                

                break;
            case R.id.ll_hangup://挂断
                //1.应答界面需要校验是否启动过app
                if (!TextUtils.isEmpty(mUserName)) {
                    showMainActivity();
                }
                //2.通话挂断
                ILVCallManager.getInstance().endCall(mCallId);
             
                break;
            case R.id.ll_no_voice: //静音
                changeMic();
                break;
            case R.id.ll_big_voice: //免提
                changeSpeaker();
                break;
            case R.id.iv_switch_camera: //摄像头切换
                switchCamera();
                break;
            default:
                break;
        }

    }

    private void showMainActivity() {
        List<Activity> activityList = MyApplication.getmApplication().getActivityList();
        int size = activityList.size();
        boolean isLanch = false;
        for (int i = 0; i < size; i++) {
            LogUtils.d("suyan", "=========栈里的acivity" + activityList.get(i).getLocalClassName());
            if (activityList.get(i).getLocalClassName().equals("activity.MainActivity")) {
                isLanch = true;
                break;
            }
        }
        if (!isLanch) {
            LogUtils.d("suyan", "=========启动");
            SwitchActivityManager.startMainActivity(mContext);
        }
    }

    //麦克风切换
    private void changeMic() {
        if (mMicEnalbe) {
            ILVCallManager.getInstance().enableMic(false);
        } else {
            ILVCallManager.getInstance().enableMic(true);
        }
        mMicEnalbe = !mMicEnalbe;
        //关闭麦克风
        iv_no_voice.setImageResource(mMicEnalbe ? R.mipmap.jingyin_f : R.mipmap.jingyin_t);
    }

    // 扬声器切换
    private void changeSpeaker() {
        if (mSpeaker) {
            ILiveSDK.getInstance().getAvAudioCtrl().setAudioOutputMode(AVAudioCtrl.OUTPUT_MODE_HEADSET);
        } else {
            ILiveSDK.getInstance().getAvAudioCtrl().setAudioOutputMode(AVAudioCtrl.OUTPUT_MODE_SPEAKER);
        }
        mSpeaker = !mSpeaker;
        iv_big_voice.setImageResource(mSpeaker ? R.mipmap.hands_free_f : R.mipmap.hands_free_t);
    }

    //切换摄像头
    private void switchCamera() {
        mCurCameraId = (ILiveConstants.FRONT_CAMERA == mCurCameraId) ? ILiveConstants.BACK_CAMERA : ILiveConstants.FRONT_CAMERA;
        ILVCallManager.getInstance().switchCamera(mCurCameraId);
    }

    @Override
    protected void onResume() {
        ILVCallManager.getInstance().onResume();
        super.onResume();
    }

    @Override
    protected void onPause() {
        ILVCallManager.getInstance().onPause();
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        ILVCallManager.getInstance().removeCallListener(this);
        ILVCallManager.getInstance().onDestory();
        tv_yuyin_timer = null;
        timeRunable = null;
        mHandler.removeMessages(0);

        super.onDestroy();
    }

    @Override
    public void showLoading() {

    }

    @Override
    public void hideLoading() {

    }

    //摄像头事件
    @Override
    public void onCameraEvent(String id, boolean bEnable) {

    }

    //麦克风事件
    @Override
    public void onMicEvent(String id, boolean bEnable) {

    }

    //通话建立成功,视频呼叫时回来的监听
    @Override
    public void onCallEstablish(int callId) {
        //1.修改mType类型
        mType = 3;
        //2.建立通话界面
        showLayout(mType);
        //3. 交换两路视频
        av_root_view.swapVideoView(0, 1);

    }

    //通话结束
    @Override
    public void onCallEnd(int callId, int endResult, String endInfo) {
        finish();
    }

    //通话异常
    @Override
    public void onException(int iExceptionId, int errCode, String errMsg) {
        toast(errMsg);
        finish();
    }

    /**
     * 监听Back键按下事件
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
            if (mType == 1) { //取消通话
                ILVCallManager.getInstance().endCall(mCallId);
            } else if (mType == 2) {//拒绝通话
                ILVCallManager.getInstance().rejectCall(mCallId);
            } else if (mType == 3) { //结束通话
                ILVCallManager.getInstance().endCall(mCallId);
            }
        }
        return super.onKeyDown(keyCode, event);


    }

    @Override
    public void onForceOffline(int error, String message) {
        //1.被迫下线监听提示
        toast("您的账号在其他地方登陆");
        //2.关闭界面
        finish();
    }

    //渲染后监听设置拖动已经切换两路视频
    @Override
    public void onSubViewCreated() {
        LogUtils.d("suyan", "====视频建立" + ILiveLoginManager.getInstance().getMyUserId() + "=" + av_root_view.getViewByIndex(0).getIdentifier() + "/" + av_root_view.getViewByIndex(1).getIdentifier());
        //4.设置点击小屏切换及可拖动
        for (int i = 1; i < ILiveConstants.MAX_AV_VIDEO_NUM; i++) {
            final int index = I;
            AVVideoView minorView = av_root_view.getViewByIndex(i);
            if (ILiveLoginManager.getInstance().getMyUserId().equals(minorView.getIdentifier())) {
                minorView.setMirror(true);      // 本地镜像
            }
            minorView.setDragable(true);    // 小屏可拖动
            minorView.setGestureListener(new GestureDetector.SimpleOnGestureListener() {
                @Override
                public boolean onSingleTapConfirmed(MotionEvent e) {
                    av_root_view.swapVideoView(0, index);     // 与大屏交换
                    return false;
                }
            });
        }
    }
}

activity_voice_call 的布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#66000000"
    tools:context="com.jyjt.ydyl.txim.VoiceCallActivity">

    <!--大小头像 ,呼叫提示-->
    <RelativeLayout
        android:id="@+id/rl_big_samll_head"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--大头像-->
        <ImageView
            android:id="@+id/iv_video_big_head"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop" />

        <View
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/frame_blcak" />
        <!--小头像-->
        <ImageView
            android:id="@+id/iv_video_small_head"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="125dp"
            android:scaleType="centerCrop" />
        <!--通话计时器-->
        <TextView
            android:id="@+id/tv_yuyin_timer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/iv_video_small_head"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="10dp"
            android:textColor="@color/white"
            android:textSize="15sp" />

        <TextView
            android:id="@+id/tv_waring"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/tv_yuyin_timer"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="3dp"
            android:text="通话中"
            android:textColor="@color/white"
            android:textSize="15sp" />
    </RelativeLayout>
    <!--两路视频通话界面-->
    <RelativeLayout
        android:id="@+id/rl_video_conversation"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        android:visibility="visible">

        <com.tencent.ilivesdk.view.AVRootView
            android:id="@+id/av_root_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <!--切换摄像头-->
        <ImageView
            android:id="@+id/iv_switch_camera"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_alignParentRight="true"
            android:layout_marginRight="20dp"
            android:layout_marginTop="20dp"
            android:src="@mipmap/ic_sw_camer" />
    </RelativeLayout>
    <!--底部,视屏呼叫界面,挂断-->
    <LinearLayout
        android:id="@+id/ll_bootom_call"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="20dp"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/btn_hangup_call"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@mipmap/huang_up" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:text="挂断"
            android:textColor="@color/white"
            android:textSize="12sp" />

    </LinearLayout>
    <!--底部,视屏邀请界面-->
    <LinearLayout
        android:id="@+id/ll_bottom_invitation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="20dp"
        android:orientation="horizontal">

        <!--挂断-->
        <LinearLayout
            android:id="@+id/ll_video_no"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_marginLeft="22dp"
            android:orientation="vertical"
            android:visibility="visible">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:src="@mipmap/huang_up" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="挂断"
                android:textColor="@color/white"
                android:textSize="12sp" />

        </LinearLayout>

        <View
            android:layout_width="0dp"
            android:layout_height="1dp"
            android:layout_weight="1" />
        <!--接听-->
        <LinearLayout
            android:id="@+id/ll_video_yes"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="22dp"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/btn_answer_call"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/jietong" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="接通"
                android:textColor="@color/white"
                android:textSize="12sp" />

        </LinearLayout>

    </LinearLayout>
    <!--底部,视频通话界面,静音,挂断,免提-->
    <LinearLayout
        android:id="@+id/ll_bootom_conversation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="20dp"
        android:orientation="horizontal">
        <!--静音-->
        <LinearLayout
            android:id="@+id/ll_no_voice"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_no_voice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:scaleType="centerCrop"
                android:src="@mipmap/jingyin_f" />

            <TextView
                android:id="@+id/tv_no_voice"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="@string/mute"
                android:textColor="@color/white"
                android:textSize="12sp" />
        </LinearLayout>
        <!--挂断-->
        <LinearLayout
            android:id="@+id/ll_hangup"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_weight="1"
            android:orientation="vertical"
            android:visibility="visible">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:src="@mipmap/huang_up" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="挂断"
                android:textColor="@color/white"
                android:textSize="12sp" />

        </LinearLayout>
        <!--免提-->
        <LinearLayout
            android:id="@+id/ll_big_voice"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/iv_big_voice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:scaleType="centerCrop"
                android:src="@mipmap/hands_free_f" />

            <TextView
                android:id="@+id/tv_big_voice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:gravity="center"
                android:text="@string/Hands_free"
                android:textColor="@color/white"
                android:textSize="12sp" />
        </LinearLayout>
    </LinearLayout>
</RelativeLayout>

三.视频呼叫

视频呼叫启动视频呼叫界面

 public void callVideo() {
        if (TextUtils.isEmpty(user_id) || TextUtils.isEmpty(ConfigUtils.getUid())) {
            toast("ID为空");
            return;
        }
       
     startVoiceCallActivity(mContext, ConfigUtils.getUid(), user_id, 1, TXChatHead.getInstance().getUserHead(user_id) == null ? "" : TXChatHead.getInstance().getUserHead(user_id), ILVCallConstants.CALL_TYPE_VIDEO);
    }
 /**
     * 音视频界面
     *
     * @param HostId :自己的id,由服务器生成的
     * @param UserId :对方的id,由服务器生成的
     * @param Type   :页面的类型 1:视频呼叫 2:视频邀请  3:视频通话,本方法只能传1
     */
    public static void startVoiceCallActivity(Context context, String HostId, String UserId, int Type, String HeadUrl, int CallType) {
        Intent intent = new Intent(context, VoiceCallActivity.class);
        intent.putExtra("HostId", HostId);
        intent.putExtra("CallType", CallType);
        intent.putExtra("UserId", UserId);
        intent.putExtra("Type", Type);
        intent.putExtra("HeadUrl", HeadUrl);
        context.startActivity(intent);
        ((Activity) context).overridePendingTransition(R.anim.left_out, R.anim.left_in);
    }

四.应答界面主要逻辑

应答界面,思路,callsdk 提供了来电监听ILVIncomingListener,实现了这个监听就能收到所有来电,但是有个问题这个监听写在哪里能让整个app 都收到,所以我写在了服务里面
注册服务器

<service android:name="com.jyjt.ydyl.txim.VoiceService" />

在应用启动的时候打开服务

 Intent serviceIntent = new Intent(MainActivity.this, VoiceService.class);
        startService(serviceIntent);

服务中的应答逻辑

package com.jyjt.ydyl.txim;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;

import com.jyjt.ydyl.application.MyApplication;
import com.jyjt.ydyl.tools.ConfigUtils;
import com.jyjt.ydyl.tools.LogUtils;
import com.jyjt.ydyl.tools.SwitchActivityManager;
import com.jyjt.ydyl.tools.ToastUtil;
import com.jyjt.ydyl.txim.model.TXChatHead;
import com.jyjt.ydyl.txim.presentation.event.VcallEvent;
import com.tencent.callsdk.ILVCallConfig;
import com.tencent.callsdk.ILVCallConstants;
import com.tencent.callsdk.ILVCallListener;
import com.tencent.callsdk.ILVCallManager;
import com.tencent.callsdk.ILVCallNotification;
import com.tencent.callsdk.ILVCallNotificationListener;
import com.tencent.callsdk.ILVIncomingListener;
import com.tencent.callsdk.ILVIncomingNotification;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;

/**
 * 视频邀请监听
 * 1.服务在闪屏页面启动
 * <p>
 * 苏艳
 **/
public class VoiceService extends Service implements ILVIncomingListener, ILVCallListener, ILVCallNotificationListener {
    //1.是否已经启动了应答界面
    boolean isStartActivity = false;
    //2.是否是主动呼叫,true :呼叫  false:应答
    public static boolean isCallType = false;
    //3.是否有心跳,true:有心跳  false :没有心跳
    public boolean havaHeardJump = false;
    //4.本次会话是否发过通知,默认没有发过
    public static boolean isSendNOtifacation = false;
    //5.通话后对方挂断,保留通话时长
    public static String mCallTime = "";
    //6.视频通话建立的时间点
    long mStartTime;
    //7.状态码和对应的提示文字
    public static HashMap<Integer, String> mWraingHashMap = new HashMap();

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        //1.设置监听
        ILVCallManager.getInstance().addCallListener(this);
        ILVCallManager.getInstance().addIncomingListener(this);
      ILVCallManager.getInstance().init(new ILVCallConfig().setAutoBusy(true).setNotificationListener(this));
        //2.设置通知提示语
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_DISCONNECT, "通话被服务器回收");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_FAILED, "请求失败");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_HANGUP, "通话结束");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_LOCAL_CANCEL, "聊天已取消");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_NOT_EXIST, "通话不存在");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_RESPONDER_LINEBUSY, "接听方占线");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_RESPONDER_REFUSE, "已拒绝");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_SPONSOR_CANCEL, "呼叫方取消");
        mWraingHashMap.put(ILVCallConstants.ERR_CALL_SPONSOR_TIMEOUT, "呼叫方超时");
        mWraingHashMap.put(ILVCallConstants.TCILiveCMD_Hangup, "对方结束通话");
        mWraingHashMap.put(ILVCallConstants.TCILiveCMD_Reject, "对方已拒绝");
        mWraingHashMap.put(ILVCallConstants.TCILiveCMD_SponsorCancel, "对方取消");
        mWraingHashMap.put(ILVCallConstants.TCILiveCMD_SponsorTimeout, "无人接听");
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        ILVCallManager.getInstance().removeIncomingListener(this);
        super.onDestroy();
    }

    @Override
    public void onNewIncomingCall(final int callId, final int callType, final ILVIncomingNotification notification) {
        Log.i("suyan", "========接听电话+service" + callId + "=" + callType + "=" + notification.getSponsorId() + "=自己的id==");

        if (callId != 0 && notification != null) {
            //1.判断呼叫是否超时,超时时间30m
            if ((System.currentTimeMillis() / 1000) - notification.getTimeStamp() > 30) {
                return;
            }
            //2.保存通话的id
            isSendNOtifacation = false;
            //3.延迟两面后判断是否有心跳
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (havaHeardJump) {
                        //4.启动应答界面
                        try {
                            isStartActivity = true;
                            isCallType = false;
                            String json = ((ILVCallNotification) notification).getUserInfo();
                            JSONObject jsonObj = new JSONObject(json);
                            String name = jsonObj.getString("name");
                            String head = jsonObj.getString("head");
                            //5.保存头像和昵称,以防止首次聊天,没有存过头像和昵称
                            TXChatHead.getInstance().addHead(head, notification.getSponsorId() + "", name);
                            //6.启动应答界面
                            SwitchActivityManager.startNewTaskVoiceCallActivity(VoiceService.this, 2, callId, ConfigUtils.getUid(), notification.getSponsorId(), TextUtils.isEmpty(head) ? "" : head, TextUtils.isEmpty(name) ? "" : name, callType);
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }, 2000);


        }

    }


    @Override
    public void onCallEstablish(int callId) {
        mStartTime = System.currentTimeMillis();
    }

    @Override
    public void onCallEnd(int callId, int endResult, String endInfo) {
        LogUtils.d("suyan", "========结束通话111");
        havaHeardJump = false;
        isSendNOtifacation = false;
        //关闭视频通话界面,处理弱网情况视频邀请唤起,但是对方已挂断问题
        if (isStartActivity) {
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    MyApplication.getmApplication().finishActivity(VoiceCallActivity.class);
                    isStartActivity = false;
                }
            }, 503);

        }

    }

    @Override
    public void onException(int iExceptionId, int errCode, String errMsg) {
    }


    @Override
    public void onRecvNotification(int callid, ILVCallNotification notification) {
        //心跳 10s  超时30s
        ILVCallNotification notification = (ILVCallNotification) arg;
        LogUtils.d("suyan", "===========通知的id" + notification.getNotifId() + "=====时间戳" + notification.getTimeStamp());
        //1.是否有过心跳
        if (notification.getNotifId() == ILVCallConstants.TCILiveCMD_HeartBeat) {
            havaHeardJump = true;
        }
        //3.设置提示语
        String waring = "";
        waring = mWraingHashMap.get(notification.getNotifId());
        String tostString = "";
        switch (notification.getNotifId()) {
            case ILVCallConstants.ERR_CALL_HANGUP: // 4 通话后,挂断,显示市场,自己发的通知
                waring = "聊天时长" + (TextUtils.isEmpty(notification.getUserInfo()) ? "" : notification.getUserInfo());
                tostString = "通话结束";
                break;
            case ILVCallConstants.TCILiveCMD_Hangup: //134  我呼叫,接通后对方挂断
                Long mCallTime = System.currentTimeMillis() - mStartTime;
                String formatHMS = getFormatHMS(mCallTime);
                waring = TextUtils.isEmpty(formatHMS) ? "对方结束通话" : "聊天时长" + formatHMS;
                tostString = "对方结束通话";
                break;
            default:
                break;
        }

        if (!TextUtils.isEmpty(waring) && !isSendNOtifacation) {
            //1.发本地消息,isCallType=true:我主动呼叫消息由我发出  isCallType=false:我应答消息消息有对方发出
            waring = "[音视频通话]" + waring;
            //次方法是给在会话中存一条本地消息,在会话界面中显示,
//ChatActivity.saveLocalMessage(notification.getSender(), waring, isCallType, isCallType);
            //2.本次通话已发送过通知
            isSendNOtifacation = true;
            //3.toast 提示
            if (isStartActivity || isCallType) {
                ToastUtil.setToast((notification.getNotifId() == ILVCallConstants.TCILiveCMD_Hangup || notification.getNotifId() == ILVCallConstants.ERR_CALL_HANGUP) ? tostString : waring);

            }
        }
    }

    // 根据毫秒返回时分秒
    public String getFormatHMS(long time) {
        time = time / 1000;//总秒数
        int s = (int) (time % 60);//秒
        int m = (int) (time / 60);//分
        int h = (int) (time / 3600);//秒
        return String.format("%02d:%02d:%02d", h, m, s);
    }
}

五.跳转应答界面

/**
     * 视频邀请界面
     *
     * @param mType   :页面的类型 1:视频呼叫 2:视频邀请  3:视频通话
     * @param mCallId :会话id
     */
    public static void startNewTaskVoiceCallActivity(Context context, int mType, int mCallId, String mHostId, String mUserId, String mHeadUrl, String mName, int CallType) {
        Intent intent = new Intent(context, VoiceCallActivity.class);
        intent.setAction(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra("CallType", CallType);
        intent.putExtra("Type", mType);
        intent.putExtra("CallId", mCallId);
        intent.putExtra("HostId", mHostId);
        intent.putExtra("HeadUrl", mHeadUrl);
        intent.putExtra("UserName", mName);
        intent.putExtra("UserId", mUserId);
        context.startActivity(intent);
    }

因为在service里面启动app,有可能这个app还没有启动没有栈,所以要单独开个栈,设置flag Intent.FLAG_ACTIVITY_NEW_TASK

六.总结

以上就是音视频通话的所有逻辑,主要都是参考callsdk中的demo 和callSdk的api文档去实现。其中有几个问题是demo中没有的要自己实现,本代码都已经实现
1.通话状态通知自己获取添加
2.呼叫,来电铃声
3.来电亮屏
4.通过心跳处理有效来电

总的来说,腾讯的音视频通话,大部分功能要自己实现,不像环信哪样细节都已经处理好了,所以自己实现时,要多考虑细节问题,callSdk Demo 很简单,只是实现了基本功能其余细小功能要多留意,api文档中有提供相应方法的调用,要参考文档去实现。什么SDK都不会保证百分之百不会出现问题,目前发现腾讯有时登录会失败,需要自己进行二次处理。

2018.11.16

最近得知callSdk 下架了,链接找不到callsdk 的demo ,咨询了互动直播的技术人员,的确如此。
回复如下:


image.png

经过咨询如果实现音视频通话,可以参考下面Demo。

Android_TRTC

腾讯实时音视频(TRTC),集成了账号登录、音视频通话、文本消息聊天等基础功能,可在无音视频基础技术的情况下,快速接入开发定制化的实时音视频产品。

参考地址:
https://github.com/zhaoyang21cn/Android_TRTC

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,142评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,656评论 2 59
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,961评论 3 119
  • 更: 侧边任务栏3D Touch iOS11.1已回归 1,以前的语境里,点击高于滑动,点击包含滑动 iOS:按键...
    JackYan阅读 510评论 0 0