728x90
반응형
이제 라이브 화면 Ui를 만들어보겠습니다.
public class VideoGridContainer extends RelativeLayout implements Runnable {
private static final int MAX_USER = 2;
private static final int STATS_REFRESH_INTERVAL = 2000;
private static final int STAT_LEFT_MARGIN = 34;
private static final int STAT_TEXT_SIZE = 10;
private SparseArray<ViewGroup> mUserViewList = new SparseArray<>(MAX_USER);
private List<Integer> mUidList = new ArrayList<>(MAX_USER);
private StatsManager mStatsManager;
private Handler mHandler;
RelativeLayout.LayoutParams mParams;
private boolean screenChange = false;
private boolean screenChangeFlag = false;
public VideoGridContainer(Context context) {
super(context);
init();
}
public VideoGridContainer(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public VideoGridContainer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//setBackgroundResource(R.mipmap.live_room_bg_logo);
mHandler = new Handler(getContext().getMainLooper());
}
public void setStatsManager(StatsManager manager) {
mStatsManager = manager;
}
//라이브커머스 전용
public void addUserVideoSurface(int uid, SurfaceView surface, boolean isLocal) {
if (surface == null) {
return;
}
int id = -1;
if (isLocal) {
if (mUidList.contains(0)) {
mUidList.remove((Integer) 0);
mUserViewList.remove(0);
}
if (mUidList.size() == MAX_USER) {
mUidList.remove(0);
mUserViewList.remove(0);
}
id = 0;
} else {
if (mUidList.contains(uid)) {
mUidList.remove((Integer) uid);
mUserViewList.remove(uid);
}
if (mUidList.size() < MAX_USER) {
id = uid;
}
}
if (id == 0) mUidList.add(0, uid);
else mUidList.add(uid);
if (id != -1) {
mUserViewList.append(uid, createVideoView(surface));
if (mStatsManager != null) {
mStatsManager.addUserStats(uid, isLocal);
if (mStatsManager.isEnabled()) {
mHandler.removeCallbacks(this);
mHandler.postDelayed(this, STATS_REFRESH_INTERVAL);
}
}
requestGridLayout();
}
}
Runnable r = new Runnable() {
@Override
public void run() {
addView(mUserViewList.get(mUidList.get(0)), mParams);
}
};
Runnable r2 = new Runnable() {
@Override
public void run() {
addView(mUserViewList.get(mUidList.get(1)), mParams);
}
};
private ViewGroup createVideoView(SurfaceView surface) {
RelativeLayout layout = new RelativeLayout(getContext());
layout.setId(surface.hashCode());
RelativeLayout.LayoutParams videoLayoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
layout.addView(surface, videoLayoutParams);
return layout;
}
public void removeUserVideo(int uid, boolean isLocal) {
if (isLocal && mUidList.contains(0)) {
mUidList.remove((Integer) 0);
mUserViewList.remove(0);
} else if (mUidList.contains(uid)) {
mUidList.remove((Integer) uid);
mUserViewList.remove(uid);
}
mStatsManager.removeUserStats(uid);
requestGridLayout();
if (getChildCount() == 0) {
mHandler.removeCallbacks(this);
}
}
public void requestGridLayout() {
removeAllViews();
layout(mUidList.size()); //라이브커머스
}
//라이브 스트리밍 사용시 사용
private void layout(int size) {
RelativeLayout.LayoutParams[] params = getParams(size);
for (int i = 0; i < size; i++) {
addView(mUserViewList.get(mUidList.get(i)), params[i]);
}
}
// 라이브 스트리밍 시 사용
private RelativeLayout.LayoutParams[] getParams(int size) {
int width = getMeasuredWidth();
int height = getMeasuredHeight();
RelativeLayout.LayoutParams[] array =
new RelativeLayout.LayoutParams[size];
for (int i = 0; i < size; i++) {
array[0] = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
array[0].addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
array[0].addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
}
return array;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
clearAllVideo();
}
private void clearAllVideo() {
removeAllViews();
mUserViewList.clear();
mUidList.clear();
mHandler.removeCallbacks(this);
}
@Override
public void run() {
if (mStatsManager != null && mStatsManager.isEnabled()) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
RelativeLayout layout = (RelativeLayout) getChildAt(i);
}
mHandler.postDelayed(this, STATS_REFRESH_INTERVAL);
}
}
}
이렇게 커스텀 ui를 하나 만들어줍니다.
그다음에 XML 보겠습니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Activity.LiveActivity">
<com.uaram.rtsp.LiveView.VideoGridContainer
android:id="@+id/live_video_grid_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.uaram.rtsp.LiveView.VideoGridContainer>
<Button
android:id="@+id/camara"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="카메라\n전환"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent">
</Button>
<Button
android:id="@+id/mic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="마이크\n음소거"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
</Button>
<Button
android:id="@+id/change"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="화면\n전환"
app:layout_constraintBottom_toBottomOf="@+id/live_video_grid_layout"
app:layout_constraintEnd_toStartOf="@+id/mic"
app:layout_constraintStart_toEndOf="@+id/camara">
</Button>
<EditText
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:hint="채팅을 입력해주세요."
android:textColor="#000000"
app:layout_constraintBottom_toTopOf="@+id/change"
app:layout_constraintEnd_toStartOf="@+id/send"
app:layout_constraintStart_toStartOf="parent">
</EditText>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="보내기"
app:layout_constraintBottom_toBottomOf="@+id/message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/message">
</Button>
<TextView
android:id="@+id/chatList"
android:layout_width="match_parent"
android:layout_height="250dp"
android:textColor="#000000"
android:scrollbars="vertical"
android:scrollbarSize="2dp"
android:scrollbarFadeDuration="0"
android:layout_marginBottom="15dp"
android:textSize="16dp"
app:layout_constraintBottom_toTopOf="@+id/message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</TextView>
</androidx.constraintlayout.widget.ConstraintLayout>
----------------------------------------------------------------------------------------------------------------------
public class LiveActivity extends RtcBaseActivity {
private final String TAG = "로그 ";
private VideoGridContainer mVideoGridContainer;
private VideoEncoderConfiguration.VideoDimensions mVideoDimension;
private Button mic;
private Button camara;
private Button change;
private Button send;
private TextView chatList;
private EditText message;
private int mUid = 0;
private RtmClient mRtmClient;
private ChatManager mChatManager;
// private RtmClientListener mClientListener;
private RtmChannel mRtmChannel;
private int role = 0;
private int videoBitCount = 0;
Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live);
handler = new Handler();
mVideoGridContainer = findViewById(R.id.live_video_grid_layout);
mVideoGridContainer.bringToFront();
mic = findViewById(R.id.mic);
camara = findViewById(R.id.camara);
change = findViewById(R.id.change);
send = findViewById(R.id.send);
message = findViewById(R.id.message);
chatList = findViewById(R.id.chatList);
message.bringToFront();
chatList.bringToFront();
mVideoGridContainer.setStatsManager(statsManager());
mChatManager = AgoraApplication.the().getChatManager();
mRtmClient = mChatManager.getRtmClient();
// mClientListener = new MyRtmClientListener();
// mChatManager.registerListener(mClientListener);
Intent intent = getIntent();
role = intent.getIntExtra("key_client_role",1);
mVideoDimension = com.uaram.rtsp.Constants.VIDEO_DIMENSIONS[
config().getVideoDimenIndex()];
createAndJoinChannel();
rtcEngine().setClientRole(role);
if (role == 1) startBroadcast();
else{
mic.setVisibility(View.GONE);
camara.setVisibility(View.GONE);
change.setVisibility(View.GONE);
}
mic.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onMuteAudioClicked();
}
});
camara.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onSwitchCameraClicked();
}
});
change.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//mVideoGridContainer.layout(2,true); //화상채팅 전용
}
});
send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onClickSendChannelMsg();
}
});
}
private void createAndJoinChannel() {
// step 1: create a channel instance
mRtmChannel = mRtmClient.createChannel(config().getChannelName(), new MyChannelListener());
if (mRtmChannel == null) {
finish();
return;
}
// step 2: join the channel
mRtmChannel.join(new ResultCallback<Void>() {
@Override
public void onSuccess(Void responseInfo) {
Log.i(TAG, "join channel success");
getChannelMemberList();
}
@Override
public void onFailure(ErrorInfo errorInfo) {
Log.e(TAG, "join channel failed");
runOnUiThread(() -> {
finish();
});
}
});
}
private void getChannelMemberList() {
mRtmChannel.getMembers(new ResultCallback<List<RtmChannelMember>>() {
@Override
public void onSuccess(final List<RtmChannelMember> responseInfo) {
runOnUiThread(() -> {
});
}
@Override
public void onFailure(ErrorInfo errorInfo) {
Log.e(TAG, "failed to get channel members, err: " + errorInfo.getErrorCode());
}
});
}
public void onClickSendChannelMsg(){ //보내기 버튼
String msg = message.getText().toString();
if (!msg.equals("")) {
RtmMessage message = mRtmClient.createMessage();
message.setText(msg);
//MessageBean messageBean = new MessageBean(nickName, message, true);
sendChannelMessage(message);
}
message.setText("");
}
private void sendChannelMessage(RtmMessage message) {
mRtmChannel.sendMessage(message, new ResultCallback<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG,"메세지 전송 성공");
chatList.append(config().getNicklName() + " : " + message.getText()+"\n");
}
@Override
public void onFailure(ErrorInfo errorInfo) {
// refer to RtmStatusCode.ChannelMessageState for the message state
final int errorCode = errorInfo.getErrorCode();
runOnUiThread(() -> {
switch (errorCode) {
case RtmStatusCode.ChannelMessageError.CHANNEL_MESSAGE_ERR_TIMEOUT:
case RtmStatusCode.ChannelMessageError.CHANNEL_MESSAGE_ERR_FAILURE:
break;
}
});
}
});
}
private void startBroadcast() {
// message.setVisibility(View.GONE);
// send.setVisibility(View.GONE);
rtcEngine().setClientRole(1);
SurfaceView surface = prepareRtcVideo(0, true);
mVideoGridContainer.addUserVideoSurface(0, surface, true); //라이브커머스 전용
mic.setActivated(true); //마이크 활성화여부
}
private void stopBroadcast() {
rtcEngine().setClientRole(io.agora.rtc.Constants.CLIENT_ROLE_BROADCASTER);
removeRtcVideo(1, true);
mVideoGridContainer.removeUserVideo(1, true);
statsManager().clearAllData();
}
public void onSwitchCameraClicked() { //카메라 전면 후면 전환
rtcEngine().switchCamera();
}
public void onMuteAudioClicked() { //마이크 활성화 / 비활성화
rtcEngine().muteLocalAudioStream(mic.isActivated());
mic.setActivated(!mic.isActivated());
}
private void renderRemoteUser(int uid) {
Log.d("로그 renderRemoteUser ", String.valueOf(uid));
SurfaceView surface = prepareRtcVideo(uid, false);
mVideoGridContainer.addUserVideoSurface(uid, surface, false); //라이브커머스 전용
}
private void removeRemoteUser(int uid) {
removeRtcVideo(uid, false);
mVideoGridContainer.removeUserVideo(uid, false);
finish();
}
@Override
public void onUserOffline(int uid, int reason) {
Log.d("로그 ","onUserOffline");
mUid = uid;
}
@Override
public void onFirstRemoteVideoDecoded(int uid, int width, int height, int elapsed) {
//방송화면이 있다면 호출되어 보여짐.
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d("로그 ","onFirstRemoteVideoDecoded");
renderRemoteUser(uid);
}
});
}
@Override
public void onRtcStats(IRtcEngineEventHandler.RtcStats stats) {
//비디오의 비트수가 0비트로 지속적으로 3번 날라온다면 방송이 종료된걸로 판단.
Log.d(TAG+"비디오 비트 : ", String.valueOf(stats.rxVideoKBitRate));
if(stats.rxVideoKBitRate < 1) videoBitCount++;
if(videoBitCount > 2) {
videoBitCount = 0;
runOnUiThread(new Runnable() {
@Override
public void run() {
if(role != 1) removeRemoteUser(mUid);
}
});
}
super.onRtcStats(stats);
}
@Override
public void onRemoteAudioStats(IRtcEngineEventHandler.RemoteAudioStats stats) {
if (!statsManager().isEnabled()) return;
RemoteStatsData data = (RemoteStatsData) statsManager().getStatsData(stats.uid);
if (data == null) return;
data.setAudioNetDelay(stats.networkTransportDelay);
data.setAudioNetJitter(stats.jitterBufferDelay);
data.setAudioLoss(stats.audioLossRate);
data.setAudioQuality(statsManager().qualityToString(stats.quality));
super.onRemoteAudioStats(stats);
}
@Override
public void onRemoteVideoStats(IRtcEngineEventHandler.RemoteVideoStats stats) {
if (!statsManager().isEnabled()) return;
RemoteStatsData data = (RemoteStatsData) statsManager().getStatsData(stats.uid);
if (data == null) return;
data.setWidth(stats.width);
data.setHeight(stats.height);
data.setFramerate(stats.rendererOutputFrameRate);
data.setVideoDelay(stats.delay);
super.onRemoteVideoStats(stats);
}
@Override
protected void onDestroy() {
stopBroadcast();
mRtmClient.logout(null);
MessageUtil.cleanMessageListBeanList();
super.onDestroy();
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
class MyChannelListener implements RtmChannelListener {
@Override
public void onMemberCountUpdated(int i) {
}
@Override
public void onAttributesUpdated(List<RtmChannelAttribute> list) {
}
@Override
public void onMessageReceived(final RtmMessage message, final RtmChannelMember fromMember) {
runOnUiThread(() -> {
Log.d(TAG,"onMessageReceived : "+fromMember.getUserId());
try {
String resultNickName = URLDecoder.decode(fromMember.getUserId(),"UTF-8");
chatList.append(resultNickName + " : " +message.getText()+"\n");
} catch (UnsupportedEncodingException e) { }
});
}
@Override
public void onImageMessageReceived(final RtmImageMessage rtmImageMessage, final RtmChannelMember rtmChannelMember) {
runOnUiThread(() -> {
String account = rtmChannelMember.getUserId();
Log.i(TAG, account + " : " + rtmImageMessage);
});
}
@Override
public void onFileMessageReceived(RtmFileMessage rtmFileMessage, RtmChannelMember rtmChannelMember) {
}
@Override
public void onMemberJoined(RtmChannelMember member) {
runOnUiThread(() -> {
});
}
@Override
public void onMemberLeft(RtmChannelMember member) {
runOnUiThread(() -> {
});
}
}
}
화면전환 버튼은 없애셔도 상관없습니다.
채팅이 보여지는 TextView는 추후에 리스트뷰나 리사이클러뷰로 변경하셔도 좋습니다.
그다음 BaseActivity 하나 생성하겠습니다.
public abstract class RtcBaseActivity extends BaseActivity implements EventHandler {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
registerRtcEventHandler(this);
joinChannel();
}
private void configVideo() {
VideoEncoderConfiguration configuration = new VideoEncoderConfiguration(
Constants.VIDEO_DIMENSIONS[config().getVideoDimenIndex()],
VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15,
VideoEncoderConfiguration.STANDARD_BITRATE,
VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT
);
configuration.mirrorMode = Constants.VIDEO_MIRROR_MODES[config().getMirrorEncodeIndex()];
rtcEngine().setVideoEncoderConfiguration(configuration);
rtcEngine().setAudioProfile(1, 5); //선명도, 노이즈 제거
rtcEngine().adjustAudioMixingPlayoutVolume(100);
}
private void joinChannel() {
RtcTokenBuilder token = new RtcTokenBuilder();
int timestamp = (int)(System.currentTimeMillis() / 1000 + 3600);
String result = token.buildTokenWithUid(appId, appCertificate, config().getChannelName(), 0, RtcTokenBuilder.Role.Role_Publisher, timestamp);
//String result = getBaseContext().getString(R.string.rtcToken); //테스트 토큰
Log.d("로그 RTC토큰 ",result);
if (TextUtils.isEmpty(result) || TextUtils.equals(result, "#YOUR ACCESS TOKEN#")) {
result = null; // default, no token
}
rtcEngine().setChannelProfile(io.agora.rtc.Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);
rtcEngine().enableVideo();
configVideo();
rtcEngine().joinChannel(result, config().getChannelName(), "", 0);
}
protected SurfaceView prepareRtcVideo(int uid, boolean local) {
// Render local/remote video on a SurfaceView
SurfaceView surface = RtcEngine.CreateRendererView(getApplicationContext());
if (local) {
rtcEngine().setupLocalVideo(
new VideoCanvas(
surface,
VideoCanvas.RENDER_MODE_HIDDEN,
0,
Constants.VIDEO_MIRROR_MODES[config().getMirrorLocalIndex()]
)
);
} else {
rtcEngine().setupRemoteVideo(
new VideoCanvas(
surface,
VideoCanvas.RENDER_MODE_HIDDEN,
uid,
Constants.VIDEO_MIRROR_MODES[config().getMirrorRemoteIndex()]
)
);
}
return surface;
}
protected void removeRtcVideo(int uid, boolean local) {
if (local) {
rtcEngine().setupLocalVideo(null);
} else {
rtcEngine().setupRemoteVideo(new VideoCanvas(null, VideoCanvas.RENDER_MODE_HIDDEN, uid));
}
}
@Override
protected void onDestroy() {
super.onDestroy();
removeRtcEventHandler(this);
rtcEngine().leaveChannel();
}
}
테스트 토큰은 콘솔페이지에서도 생성 해 볼 수 있습니다.
채널네임과 라이브토큰값이 같은곳으로 동시접속하면 같은 영상을 공유할수있습니다.
테스트 하실때에는
해당 버튼을 누르고 채널명을 입력하시면 테스트용 토큰이 발급됩니다.
소스에서 중요하게 보실 부분은 버튼클릭 리스너부분과 오버라이드된 이벤트리스너들을 중점으로 보시면 이해하시기 편하십니다.
라이브 토큰을 발급받는 소스는 다음 게시글에서 보겠습니다.
ㄱ ㅏ ㄱ 푸시
ㅗ o ㅗ 푸시 :D
728x90
반응형
'Android' 카테고리의 다른 글
Android intent(인텐트)로 설정화면 호출하기 (0) | 2022.06.09 |
---|---|
아고라 플랫폼을 이용한 안드로이드 라이브스트리밍(RTSP) - 6 (5) | 2022.05.18 |
아고라 플랫폼을 이용한 안드로이드 라이브스트리밍(RTSP) - 4 (0) | 2022.05.12 |
아고라 플랫폼을 이용한 안드로이드 라이브스트리밍(RTSP) - 3 (0) | 2022.05.12 |
아고라 플랫폼을 이용한 안드로이드 라이브스트리밍(RTSP) - 2 (5) | 2022.05.12 |
댓글