본문 바로가기
Android

아고라 플랫폼을 이용한 안드로이드 라이브스트리밍(RTSP) - 5

by 일용직 코딩노동자 2022. 5. 13.
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
반응형

댓글