互动准备

  1. 注册微吼开发者账号

  2. 创建应用 获取AppID

  3. 设置应用Bundle ID

  4. 获取互动房间ID roomId

  5. 获取互动房间包含各种权限的access_token

  1. 获取旁路直播房间ID liveRoomId

工程配置

在开始使用sdk之前,我们要配置好IDE和创建基础的工程代码。相关内容在【工程配置】中有详细说明。

注意:参与互动只能在真机环境下运行

时序图

注:虚线框部分请根据自身产品需求决定流程是否进行。

# 代码对接

1: activity或Fragment的布局文件中添加VHRenderView,渲染本地流(localView)

<com.vhall.vhallrtc.client.VHRenderView
            android:id="@+id/localView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:layout_below="@+id/hsv_streams"
            android:layout_weight="2" />

2: 创建 VHInteractive 对象

构建时传入互动直播房间的流状态监听器,设置互动房间的事件监听等操作。

 interactive = new VHInteractive(mContext, new RoomListener());
 interactive.setOnMessageListener(new MyMessageListener());
 监听示例:
class RoomListener implements Room.RoomDelegate {
        @Override
        public void onDidConnect(Room room, JSONObject jsonObject) {//进入房间
           //进入房间成功回调,一般操作是订阅当前房间所有流(room.subscribe(stream)),订阅成功后在onDidSubscribeStream回调中新建VHRenderView渲染
            //订阅房间内的其他流
            for (Stream stream : room.getRemoteStreams()) {
                room.subscribe(stream);
            }
			//刷新房间内人员状态
            refreshMembers();
        }

        @Override
        public void onDidError(Room room, Room.VHRoomErrorStatus vhRoomErrorStatus, String s) {
           //进入房间失败回调,取消订阅所有流
            for (Stream stream : room.getRemoteStreams()) {
                removeStream(stream);
            }
			//刷新房间内人员状态
            refreshMembers();
        }

        @Override
        public void onDidPublishStream(Room room, Stream stream) {//上麦
           //推流、上麦成功后回调
            isOnline = true;
        }

        @Override
        public void onDidUnPublishStream(Room room, Stream stream) {//下麦
           //取消推流、下麦成功后回调
            isOnline = false;
        }

        @Override
        public void onDidSubscribeStream(Room room, Stream stream) {//订阅其他流
            //订阅成功后回调,此处一般要新建VHRenderView来渲染订阅的流
            addStream(stream);
        }

        @Override
        public void onDidUnSubscribeStream(Room room, Stream stream) {//取消订阅
//取消订阅成功后回调,取消订阅后,该路流将不来拉取,用来渲染该路流的VHRenderView,需要回收
            removeStream(stream);
        }

        @Override
        public void onDidChangeStatus(Room room, Room.VHRoomStatus vhRoomStatus) {//状态改变
           //本地流状态发生变化时回调,连接失败等
        }

        @Override
        public void onDidAddStream(Room room, Stream stream) {//有流加入
           //有新的流加入房间时回调,在此回调中一般执行订阅该路流操作
            room.subscribe(stream);
        }

        @Override
        public void onDidRemoveStream(Room room, Stream stream) {//有流退出
           //有流退出房间时回调,此时一般要移除用户渲染该路流的VHRenderView
            removeStream(stream);
        }

        @Override
        public void onDidUpdateOfStream(Stream stream, JSONObject jsonObject) {//流状态更新
           //流状态发生变化时回调(宽高等)

        }
    }

    class MyMessageListener implements VHInteractive.OnMessageListener {

        @Override
        public void onMessage(JSONObject data) {
            refreshMembers();
            try {
                String event = data.getString("inav_event");
                int status = data.optInt("status");
                String userid = data.optString("third_party_user_id");
                switch (event) {
                    case VHInteractive.apply_inav_publish://申请上麦消息
                        showDialog(VHInteractive.apply_inav_publish, userid);
                        break;
                    case VHInteractive.audit_inav_publish://申请审核结果消息
                        if (status == 1) {//批准上麦
                            interactive.publish();
                        } else {
                            Toast.makeText(mContext, "您的上麦请求未通过!", Toast.LENGTH_SHORT).show();
                        }
                        break;
                    case VHInteractive.askfor_inav_publish://邀请上麦消息
                        showDialog(VHInteractive.askfor_inav_publish, userid);
                        break;
                    case VHInteractive.kick_inav_stream:
                        Toast.makeText(mContext, "您已被请下麦!", Toast.LENGTH_SHORT).show();
                        break;
                    case VHInteractive.kick_inav:
                        Toast.makeText(mContext, "您已被踢出房间!", Toast.LENGTH_SHORT).show();
                        getActivity().finish();
                        break;
                    case VHInteractive.user_publish_callback:
                        String action = "";
                        switch (status) {
                            case 1:
                                action = "上麦啦!";
                                break;
                            case 2:
                                action = "下麦啦!";
                                break;
                            case 3:
                                action = "拒绝上麦!";
                                break;
                        }
                        Toast.makeText(mContext, userid + ":" + action, Toast.LENGTH_SHORT).show();
                        break;
                    case VHInteractive.inav_close:
                        Toast.makeText(mContext, "直播间已关闭", Toast.LENGTH_SHORT).show();
                        getActivity().finish();
                        break;

                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

3: 初始化渲染本地流的LocalVHRenderView和LocalStream

   localView.init(SharedContext, RenderEvents);
   localStream = interactive.createLocalStream(Stream.VhallFrameResolutionValue.VhallFrameResolution320x240.getValue(), "paassdk");
    localView.setStream(localStream);

4: 初始化VHInteractive

初始化操作为异步操作,获取当前互动直播间信息

  interactive.init(mRoomId, mAccessToken, new VHInteractive.InitCallback());

初始化成功后,准备工作基本就绪,本地流开始渲染,接下来就可以进入房间,并进行上线麦操作。

房间内部操作流程为:进入房间、上麦或申请上麦、下麦、离开房间。

5: 进入房间、离开房间

   interactive.enterRoom();
   interactive.leaveRoom();

6: 上麦(需要权限,无权限可申请上麦)、下麦

   interactive.publish();
   interactive.unpublish();

7:邀请上麦、强制下麦、踢出房间(需要权限)

    mInteractive.invitePublish(mMember.userid, null);
    mInteractive.kickoutStream(mMember.userid, null);

8获取房间内成员状态信息

//成员状态为主动拉取,状态改变时要及时更新
   mInteractive.getMembers(Callback callback);

9推旁路直播

//旁路直播是将互动直播间多路流,混合为一直,以直播的形式发出,需要事先创建好一个直播间,将直播ID传入
   interactive.broadcastRoom(mBroadcastid, type, Callback callback);

流(stream)操作包括:静音与取消静音;开户与关闭视频流;如果为本地流(localStream),可以切换摄像头。本地流,可以在创建的interactive里取出,以本地流操作为例,远程流在回调中取到stream,调用相应接口即可。

10静音与取消静音

interactive.getLocalStream().muteVideo();
interactive.getLocalStream().unmuteVideo();

11开启与关闭视频

  interactive.getLocalStream().unmuteVideo();
  interactive.getLocalStream().muteVideo();

12切换摄像头

	//只有本地流才能切换摄像头
    interactive.getLocalStream().switchCamera();

#示例代码

package com.vhall.opensdk.interactive;


import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.Toast;
import android.widget.ToggleButton;

import com.vhall.ilss.VHInteractive;
import com.vhall.opensdk.R;
import com.vhall.vhallrtc.client.Room;
import com.vhall.vhallrtc.client.Stream;
import com.vhall.vhallrtc.client.VHRenderView;

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

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;

public class InteractiveFragment extends Fragment implements View.OnClickListener {
    private static final String TAG = "InteractiveFragment";
    Context mContext;
    LinearLayout mLayoutGroup;
    LinearLayout mMenuLayout;
    VHRenderView localView;
    Button mEnterBtn, mReqBtn, mJoinBtn, mQuitBtn, mLeaveBtn;

    Handler mHandler = new Handler();
    public String mRoomId;
    public String mAccessToken;
    VHInteractive interactive = null;
    boolean isEnable = false;//是否可用
    //    boolean isEnter = false;//是否进入房间
    boolean isOnline = false;//是否上麦
    OnMemberChangedListener mMemberChangedListener;

    AlertDialog mDialog;
    ToggleButton mBroadcastTB, mVideoTB, mAudioTB;

    String mBroadcastid = "";

    public interface OnMemberChangedListener {
        void onMemberChanged(VHInteractive interactive, List<Member> list);
    }

    public void setOnMemberChangedListener(OnMemberChangedListener listener) {
        mMemberChangedListener = listener;
    }

    public static InteractiveFragment getInstance(String roomid, String accessToken, String broadcastid) {
        InteractiveFragment fragment = new InteractiveFragment();
        fragment.mRoomId = roomid;
        fragment.mAccessToken = accessToken;
        fragment.mBroadcastid = broadcastid;
        return fragment;
    }

    @Override
    public void onAttach(Context context) {
        mContext = context;
        super.onAttach(context);
    }


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.frag_interactive, container, false);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        initView();
        interactive = new VHInteractive(mContext, new RoomListener());
        interactive.setOnMessageListener(new MyMessageListener());
        localView.init(interactive.getEglBase().getEglBaseContext(), null);
        interactive.setLocalView(localView);
        localView.setTag(interactive.getLocalStream());
        interactive.init(mRoomId, mAccessToken, new VHInteractive.InitCallback() {
            @Override
            public void onSuccess() {
                isEnable = true;
                ClickEventEnter();
            }

            @Override
            public void onFailure(int errorCode, String errorMsg) {
                isEnable = false;
                Toast.makeText(mContext, errorMsg, Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    public void onClick(View v) {

        switch (v.getId()) {
            case R.id.btn_enter:
                ClickEventEnter();
                break;
            case R.id.btn_join:
                clickEventJoin();
                break;
            case R.id.btn_quit:
                clickEventQuit();
                break;
            case R.id.btn_leave:
                clickEventLeave();
                break;
            case R.id.btn_request:
                clickEventReq();
                break;
            case R.id.localView:
                clickEventMenu();
                break;
        }
    }

    public void ClickEventEnter() {//进入房间
        if (!isEnable)
            return;
        interactive.enterRoom();
    }

    public void clickEventReq() {//申请上麦
        if (!isEnable)
            return;
        interactive.requestPublish(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                try {
                    final JSONObject obj = new JSONObject(result);
                    int code = obj.optInt("code");
                    if (code != 200) {
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(mContext, obj.optString("msg"), Toast.LENGTH_SHORT).show();
                            }
                        });
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void clickEventJoin() {//上麦
        if (!isEnable)
            return;
        if (isOnline)
            return;
        if (!interactive.isPushAvailable()) {
            Toast.makeText(mContext, "无上麦权限", Toast.LENGTH_SHORT).show();
            return;
        }
        interactive.publish();
    }

    public void clickEventQuit() {//下麦
        if (!isEnable)
            return;
        interactive.unpublish();
    }

    public void clickEventLeave() {//离开互动房间
        if (!isEnable)
            return;
        interactive.leaveRoom();
    }

    public void clickEventMenu() {
        if (mMenuLayout.getVisibility() == View.GONE)
            mMenuLayout.setVisibility(View.VISIBLE);
        else
            mMenuLayout.setVisibility(View.GONE);
    }

    public void refreshMembers() {
        interactive.getMembers(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String res = response.body().string();
                Log.i(TAG, "members:" + res);
                try {
                    JSONObject result = new JSONObject(res);
                    String msg = result.optString("msg");
                    int code = result.optInt("code");
                    if (code == 200) {
                        JSONObject data = result.getJSONObject("data");
                        JSONArray list = data.getJSONArray("lists");
                        if (list != null && list.length() > 0) {
                            final List<Member> members = new LinkedList<>();
                            for (int i = 0; i < list.length(); i++) {
                                JSONObject obj = list.getJSONObject(i);
                                Member member = new Member();
                                member.userid = obj.getString("third_party_user_id");
                                member.status = obj.getInt("status");
                                members.add(member);
                            }
                            if (mMemberChangedListener != null) {
                                mHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        mMemberChangedListener.onMemberChanged(interactive, members);
                                    }
                                });
                            }
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    class RoomListener implements Room.RoomDelegate {

        @Override
        public void onDidConnect(Room room, JSONObject jsonObject) {//进入房间
            Log.e(TAG, "onDidConnect");
            //订阅房间内的其他流
            for (Stream stream : room.getRemoteStreams()) {
                room.subscribe(stream);
            }
            refreshMembers();
        }

        @Override
        public void onDidError(Room room, Room.VHRoomErrorStatus vhRoomErrorStatus, String s) {//进入房间失败
            Log.e(TAG, "onDidError");
            for (Stream stream : room.getRemoteStreams()) {
                removeStream(stream);
            }
            refreshMembers();
        }

        @Override
        public void onDidPublishStream(Room room, Stream stream) {//上麦
            Log.e(TAG, "onDidPublishStream");
            isOnline = true;
        }

        @Override
        public void onDidUnPublishStream(Room room, Stream stream) {//下麦
            Log.e(TAG, "onDidUnPublishStream");
            isOnline = false;
        }

        @Override
        public void onDidSubscribeStream(Room room, Stream stream) {//订阅其他流
            Log.e(TAG, "onDidSubscribeStream");
            addStream(stream);
        }

        @Override
        public void onDidUnSubscribeStream(Room room, Stream stream) {//取消订阅
            Log.e(TAG, "onDidUnSubscribeStream");
            removeStream(stream);
        }

        @Override
        public void onDidChangeStatus(Room room, Room.VHRoomStatus vhRoomStatus) {//状态改变
            Log.e(TAG, "onDidChangeStatus");
        }

        @Override
        public void onDidAddStream(Room room, Stream stream) {//有流加入
            Log.e(TAG, "onDidAddStream");
            room.subscribe(stream);
        }

        @Override
        public void onDidRemoveStream(Room room, Stream stream) {//有流退出
            Log.e(TAG, "onDidRemoveStream");
            removeStream(stream);
        }

        @Override
        public void onDidUpdateOfStream(Stream stream, JSONObject jsonObject) {//流状态更新
            Log.e(TAG, "onDidUpdateOfStream");

        }
    }

    class MyMessageListener implements VHInteractive.OnMessageListener {

        @Override
        public void onMessage(JSONObject data) {
            refreshMembers();
            try {
                String event = data.getString("inav_event");
                int status = data.optInt("status");
                String userid = data.optString("third_party_user_id");
                switch (event) {
                    case VHInteractive.apply_inav_publish://申请上麦消息
                        showDialog(VHInteractive.apply_inav_publish, userid);
                        break;
                    case VHInteractive.audit_inav_publish://申请审核结果消息
                        if (status == 1) {//批准上麦
                            interactive.publish();
                        } else {
                            Toast.makeText(mContext, "您的上麦请求未通过!", Toast.LENGTH_SHORT).show();
                        }
                        break;
                    case VHInteractive.askfor_inav_publish://邀请上麦消息
                        showDialog(VHInteractive.askfor_inav_publish, userid);
                        break;
                    case VHInteractive.kick_inav_stream:
                        Toast.makeText(mContext, "您已被请下麦!", Toast.LENGTH_SHORT).show();
                        break;
                    case VHInteractive.kick_inav:
                        Toast.makeText(mContext, "您已被踢出房间!", Toast.LENGTH_SHORT).show();
                        getActivity().finish();
                        break;
                    case VHInteractive.user_publish_callback:
                        String action = "";
                        switch (status) {
                            case 1:
                                action = "上麦啦!";
                                break;
                            case 2:
                                action = "下麦啦!";
                                break;
                            case 3:
                                action = "拒绝上麦!";
                                break;
                        }
                        Toast.makeText(mContext, userid + ":" + action, Toast.LENGTH_SHORT).show();
                        break;
                    case VHInteractive.inav_close:
                        Toast.makeText(mContext, "直播间已关闭", Toast.LENGTH_SHORT).show();
                        getActivity().finish();
                        break;

                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

    private void addStream(final Stream stream) {
        if (stream == null)
            return;
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                int height = mLayoutGroup.getHeight();
                int width = (3 * height) / 2;
                final VHRenderView view = new VHRenderView(mContext);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(width, height);
                view.setLayoutParams(params);
                view.init(interactive.getEglBase().getEglBaseContext(), null);
                view.setStream(stream);
                view.setTag(stream);
                view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        exchangeView(view);
                    }
                });
                mLayoutGroup.addView(view);


            }
        });
    }

    private void removeStream(final Stream stream) {
        if (stream == null)
            return;
        int childCount = mLayoutGroup.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = mLayoutGroup.getChildAt(i);
            if ((view.getTag()) == stream) {
                Log.e(TAG, "存在该stream,移除!");
                mLayoutGroup.removeView(view);
            }
        }
    }

    @Override
    public void onStart() {
        super.onStart();
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onStop() {
        super.onStop();

    }

    @Override
    public void onDestroy() {
        if (mDialog != null && mDialog.isShowing())
            mDialog.dismiss();
        interactive.destory();
        super.onDestroy();
    }


    private void initView() {
        mLayoutGroup = getView().findViewById(R.id.ll_streams);
        mMenuLayout = getView().findViewById(R.id.ll_menu);
        localView = getView().findViewById(R.id.localView);
        mEnterBtn = getView().findViewById(R.id.btn_enter);
        mJoinBtn = getView().findViewById(R.id.btn_join);
        mQuitBtn = getView().findViewById(R.id.btn_quit);
        mLeaveBtn = getView().findViewById(R.id.btn_leave);
        mReqBtn = getView().findViewById(R.id.btn_request);
        mBroadcastTB = getView().findViewById(R.id.tb_broadcast);
        mVideoTB = getView().findViewById(R.id.tb_video);
        mAudioTB = getView().findViewById(R.id.tb_audio);
        localView.setOnClickListener(this);
        mEnterBtn.setOnClickListener(this);
        mJoinBtn.setOnClickListener(this);
        mQuitBtn.setOnClickListener(this);
        mLeaveBtn.setOnClickListener(this);
        mReqBtn.setOnClickListener(this);
        mBroadcastTB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if (TextUtils.isEmpty(mBroadcastid)) {
                    mBroadcastTB.setChecked(false);
                    return;
                }
                int type = isChecked ? 1 : 2;
                interactive.broadcastRoom(mBroadcastid, type, null);
            }
        });
        mVideoTB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                try {
                    if (isChecked)
                        interactive.getLocalStream().unmuteVideo();
                    else
                        interactive.getLocalStream().muteVideo();

                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });
        mAudioTB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                try {
                    if (isChecked)
                        interactive.getLocalStream().unmuteAudio();
                    else
                        interactive.getLocalStream().muteAudio();
                 
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * @param event
     * @param userid
     */
    private void showDialog(String event, final String userid) {
        if (getActivity().isFinishing())
            return;
        if (mDialog != null && mDialog.isShowing()) {
            mDialog.dismiss();
        }
        AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
        switch (event) {
            case VHInteractive.apply_inav_publish:
                mDialog = builder.setTitle("申请上麦")
                        .setMessage(userid + " 申请上麦,是否批准!")
                        .setNegativeButton("不批", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                interactive.checkPublishRequest(userid, 2, null);
                            }
                        })
                        .setPositiveButton("批准", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                interactive.checkPublishRequest(userid, 1, null);
                            }
                        })
                        .create();
                mDialog.show();
                break;
            case VHInteractive.askfor_inav_publish:
                mDialog = builder.setTitle("邀请上麦")
                        .setMessage(userid + " 邀请您上麦,是否同意!")
                        .setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                interactive.refusePublish();
                            }
                        })
                        .setPositiveButton("同意", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                interactive.publish();
                            }
                        })
                        .create();
                mDialog.show();
                break;
        }


    }

    private void exchangeView(VHRenderView clickView) {
        Stream clickStream = (Stream) clickView.getTag();
        Stream targetStream = (Stream) localView.getTag();
        localView.setStream(clickStream);
        localView.setTag(clickStream);
        clickView.setStream(targetStream);
        clickView.setTag(targetStream);
    }
}

其他相关文档

互动模块参考手册