互动准备
-
获取互动房间ID roomId
-
获取互动房间包含各种权限的access_token
工程配置
在开始使用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);
}
}