├── Agora-Online-PK-Android ├── .gitignore ├── LICENSE.md ├── app │ ├── .gitignore │ ├── build.gradle │ ├── libs │ │ └── PLACEHOLDER │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── io │ │ │ └── agora │ │ │ └── pk │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── io │ │ │ │ └── agora │ │ │ │ └── pk │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── PKApplication.java │ │ │ │ ├── PKBroadcasterActivity.java │ │ │ │ ├── PKConfig.java │ │ │ │ ├── model │ │ │ │ ├── AGEventHandler.java │ │ │ │ ├── MyEngineEventHandler.java │ │ │ │ └── WorkerThread.java │ │ │ │ ├── ui │ │ │ │ └── CircleImageView.java │ │ │ │ └── utils │ │ │ │ ├── KeyboardStatusDetector.java │ │ │ │ ├── MessageUtils.java │ │ │ │ ├── PKConstants.java │ │ │ │ └── StringUtils.java │ │ ├── jniLibs │ │ │ ├── arm64-v8a │ │ │ │ └── PLACEHOLDER │ │ │ ├── armeabi-v7a │ │ │ │ └── PLACEHOLDER │ │ │ └── x86 │ │ │ │ └── PLACEHOLDER │ │ └── res │ │ │ ├── drawable │ │ │ ├── chat_room_et_style_2.xml │ │ │ ├── chat_room_main_exit.png │ │ │ ├── chat_room_main_like.png │ │ │ ├── chat_room_main_pk_bg.xml │ │ │ ├── chat_room_main_pk_support.png │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── main_btn_style.xml │ │ │ ├── pk_exit_btn_style.xml │ │ │ ├── user_icon_01.png │ │ │ ├── user_icon_02.png │ │ │ ├── user_icon_03.png │ │ │ ├── user_icon_04.png │ │ │ ├── user_icon_05.png │ │ │ ├── user_icon_06.png │ │ │ ├── user_icon_07.png │ │ │ └── user_icon_08.png │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── activity_pk_broadcaster.xml │ │ │ └── pop_view_pk.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── io │ │ └── agora │ │ └── pk │ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── tools │ └── gradle-on-demand.gradle ├── Agora-Online-PK-iOS ├── .gitignore ├── Agora-Online-PK.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Agora-Online-PK │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── agoraLog.imageset │ │ │ ├── Contents.json │ │ │ ├── agoraLog.png │ │ │ ├── agoraLog@2x.png │ │ │ └── agoraLog@3x.png │ │ ├── agoraLogText.imageset │ │ │ ├── Contents.json │ │ │ ├── agoraLogText.png │ │ │ ├── agoraLogText@2x.png │ │ │ └── agoraLogText@3x.png │ │ ├── back.imageset │ │ │ ├── Contents.json │ │ │ └── back.png │ │ ├── background_button.imageset │ │ │ ├── Contents.json │ │ │ ├── background_button.png │ │ │ ├── background_button@2x.png │ │ │ └── background_button@3x.png │ │ ├── background_room.imageset │ │ │ ├── Contents.json │ │ │ ├── background_room.png │ │ │ ├── background_room@2x.png │ │ │ └── background_room@3x.png │ │ ├── cancel.imageset │ │ │ ├── Contents.json │ │ │ ├── cancel.png │ │ │ ├── cancel@2x.png │ │ │ └── cancel@3x.png │ │ ├── heart.imageset │ │ │ ├── Contents.json │ │ │ ├── heart.png │ │ │ ├── heart@2x.png │ │ │ └── heart@3x.png │ │ ├── heart_room.imageset │ │ │ ├── Contents.json │ │ │ ├── heart_room.png │ │ │ ├── heart_room@2x.png │ │ │ └── heart_room@3x.png │ │ ├── profile.imageset │ │ │ ├── Contents.json │ │ │ └── profile_06.png │ │ └── user_login.imageset │ │ │ ├── Contents.json │ │ │ ├── user_login.png │ │ │ ├── user_login@2x.png │ │ │ └── user_login@3x.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Contants │ │ ├── Constants.swift │ │ └── KeyCenter.swift │ ├── Controllers │ │ ├── LoginViewController.swift │ │ └── RoomViewController.swift │ ├── Info.plist │ ├── Utils │ │ ├── AlertUtil.swift │ │ └── CommonExtensions.swift │ └── Views │ │ ├── PopView.swift │ │ ├── PopView.xib │ │ └── VideoSession.swift └── LICENSE.md ├── Image ├── API_list.png ├── API_list_EN.png ├── ArchitectureDesign.png ├── ArchitectureDesign_EN.png └── descritpion.md ├── README.md └── README.zh.md /Agora-Online-PK-Android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | .idea/ 12 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Agora.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "io.agora.pk" 7 | minSdkVersion 16 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | 14 | ndk { 15 | abiFilters "armeabi-v7a" 16 | } 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | flavorDimensions "default" 26 | productFlavors { 27 | all32 { minSdkVersion 16 } 28 | all64 { minSdkVersion 21 } 29 | } 30 | 31 | repositories { 32 | flatDir { 33 | dirs 'libs' 34 | } 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation fileTree(include: ['*.jar'], dir: 'libs') 40 | implementation 'com.android.support:support-v4:28.0.0' 41 | implementation 'com.android.support:design:28.0.0' 42 | } 43 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/libs/PLACEHOLDER: -------------------------------------------------------------------------------- 1 | agora-rtc-sdk.jar 2 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/androidTest/java/io/agora/pk/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("io.agora.pk", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | 10 | import io.agora.pk.model.MyEngineEventHandler; 11 | import io.agora.pk.model.WorkerThread; 12 | import io.agora.pk.utils.PKConstants; 13 | import io.agora.rtc.RtcEngine; 14 | import io.agora.rtc.live.LiveTranscoding; 15 | 16 | public abstract class BaseActivity extends AppCompatActivity { 17 | @Override 18 | protected void onCreate(@Nullable Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | ((PKApplication) getApplication()).initWorkerThread(); 21 | } 22 | 23 | @Override 24 | protected void onPostCreate(@Nullable Bundle savedInstanceState) { 25 | super.onPostCreate(savedInstanceState); 26 | initUIandEvent(); 27 | } 28 | 29 | protected abstract void initUIandEvent(); 30 | 31 | protected abstract void deInitUIandEvent(); 32 | 33 | protected RtcEngine rtcEngine() { 34 | return ((PKApplication) getApplication()).getWorkerThread().rtcEngine(); 35 | } 36 | 37 | protected WorkerThread workThread() { 38 | return ((PKApplication) getApplication()).getWorkerThread(); 39 | } 40 | 41 | protected final MyEngineEventHandler event() { 42 | return ((PKApplication) getApplication()).getWorkerThread().eventHandler(); 43 | } 44 | 45 | // set LiveTranscoding property for each user 46 | protected LiveTranscoding updateLiveTranscoding(int localUid, int remoteUid, HashMap members) { 47 | 48 | ArrayList users = new ArrayList<>(members.size()); 49 | 50 | LiveTranscoding.TranscodingUser localUser = new LiveTranscoding.TranscodingUser(); 51 | 52 | LiveTranscoding liveTranscoding = new LiveTranscoding(); 53 | 54 | // LiveTranscoding update, the LiveTranscoding is used to set the CDN stream layout in Agora server 55 | // more details please refer to the document 56 | switch (members.size()) { 57 | case 1: 58 | // the LiveTranscoding for one person 59 | localUser.uid = localUid; 60 | 61 | localUser.x = 0; 62 | localUser.y = 0; 63 | localUser.width = PKConstants.LIVE_TRANSCODING_WIDTH; 64 | localUser.height = PKConstants.LIVE_TRANSCODING_HEIGHT; 65 | 66 | localUser.zOrder = 1; 67 | localUser.audioChannel = 0; 68 | 69 | liveTranscoding.addUser(localUser); 70 | 71 | liveTranscoding.width = PKConstants.LIVE_TRANSCODING_WIDTH; 72 | liveTranscoding.height = PKConstants.LIVE_TRANSCODING_HEIGHT; 73 | 74 | liveTranscoding.videoBitrate = PKConstants.LIVE_TRANSCODING_BITRATE; 75 | liveTranscoding.videoFramerate = PKConstants.LIVE_TRANSCODING_FPS; 76 | liveTranscoding.lowLatency = true; 77 | break; 78 | 79 | case 2: 80 | // the LiveTranscoding for two persons in PK mode 81 | localUser.uid = localUid; 82 | 83 | localUser.x = 0; 84 | localUser.y = 0; 85 | localUser.width = PKConstants.LIVE_TRANSCODING_WIDTH; 86 | localUser.height = PKConstants.LIVE_TRANSCODING_HEIGHT; 87 | 88 | localUser.zOrder = 1; 89 | localUser.audioChannel = 0; 90 | 91 | users.add(localUser); 92 | 93 | LiveTranscoding.TranscodingUser remoteUser = new LiveTranscoding.TranscodingUser(); 94 | 95 | remoteUser.uid = members.get(remoteUid); // REMOTE USER 96 | 97 | remoteUser.x = PKConstants.LIVE_TRANSCODING_WIDTH; // START FROM END OF THE FIRST USER 98 | remoteUser.y = 0; 99 | remoteUser.width = PKConstants.LIVE_TRANSCODING_WIDTH; 100 | remoteUser.height = PKConstants.LIVE_TRANSCODING_HEIGHT; 101 | 102 | remoteUser.zOrder = 1; 103 | remoteUser.audioChannel = 0; 104 | 105 | users.add(remoteUser); 106 | 107 | liveTranscoding.setUsers(users); 108 | 109 | liveTranscoding.width = PKConstants.LIVE_TRANSCODING_WIDTH * 2; 110 | liveTranscoding.height = PKConstants.LIVE_TRANSCODING_HEIGHT; 111 | 112 | liveTranscoding.videoBitrate = PKConstants.LIVE_TRANSCODING_BITRATE; 113 | liveTranscoding.videoFramerate = PKConstants.LIVE_TRANSCODING_FPS; 114 | liveTranscoding.lowLatency = true; 115 | break; 116 | 117 | default: 118 | break; 119 | } 120 | return liveTranscoding; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk; 2 | 3 | import android.Manifest; 4 | import android.annotation.TargetApi; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.os.Build; 8 | import android.support.annotation.NonNull; 9 | import android.support.annotation.Nullable; 10 | import android.os.Bundle; 11 | import android.support.v4.content.ContextCompat; 12 | import android.support.v4.content.PermissionChecker; 13 | import android.support.v7.app.AppCompatActivity; 14 | import android.text.TextUtils; 15 | import android.view.View; 16 | import android.widget.EditText; 17 | import android.widget.Toast; 18 | 19 | import io.agora.pk.utils.PKConstants; 20 | import io.agora.rtc.Constants; 21 | 22 | public class MainActivity extends AppCompatActivity { 23 | 24 | private EditText mEtChannel; 25 | 26 | @Override 27 | protected void onCreate(@Nullable Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_main); 30 | 31 | mEtChannel = findViewById(R.id.et_channel); 32 | } 33 | 34 | public void onBroadcastClicked(View v) { 35 | String channel = mEtChannel.getText().toString(); 36 | if (TextUtils.isEmpty(channel)) { 37 | Toast.makeText(this, R.string.main_channel_hint, Toast.LENGTH_LONG).show(); 38 | return; 39 | } 40 | 41 | ((PKApplication) getApplication()).getPkConfig().setBroadcasterAccount(channel); 42 | 43 | if (checkSelfPermissions()) { 44 | forwardTo(Constants.CLIENT_ROLE_BROADCASTER); 45 | } 46 | } 47 | 48 | private void forwardTo(int clientRole) { 49 | Intent intent = new Intent(MainActivity.this, PKBroadcasterActivity.class); 50 | intent.putExtra(PKConstants.USER_CLIENT_ROLE, clientRole); 51 | startActivity(intent); 52 | } 53 | 54 | private static final int PERMISSION_REQ_ID = 1024; 55 | 56 | @TargetApi(Build.VERSION_CODES.M) 57 | private void askPermission() { 58 | requestPermissions(new String[]{ 59 | Manifest.permission.CAMERA, 60 | Manifest.permission.RECORD_AUDIO, 61 | Manifest.permission.WRITE_EXTERNAL_STORAGE}, 62 | PERMISSION_REQ_ID); 63 | } 64 | 65 | private boolean checkSelfPermissions() { 66 | return checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID) && 67 | checkSelfPermission(Manifest.permission.CAMERA, PERMISSION_REQ_ID) && 68 | checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, PERMISSION_REQ_ID); 69 | } 70 | 71 | @Override 72 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 73 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 74 | if (requestCode == PERMISSION_REQ_ID) { 75 | for (int g : grantResults) { 76 | if (g != PermissionChecker.PERMISSION_GRANTED) { 77 | return; 78 | } 79 | } 80 | } 81 | } 82 | 83 | public boolean checkSelfPermission(String permission, int requestCode) { 84 | if (ContextCompat.checkSelfPermission(this, 85 | permission) 86 | != PackageManager.PERMISSION_GRANTED) { 87 | 88 | askPermission(); 89 | return false; 90 | } 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/PKApplication.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk; 2 | 3 | import android.app.Application; 4 | 5 | import java.lang.ref.WeakReference; 6 | 7 | import io.agora.pk.model.WorkerThread; 8 | 9 | public class PKApplication extends Application { 10 | private WorkerThread mWorkerThread; 11 | private PKConfig pkConfig; 12 | 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | if (pkConfig == null) { 17 | pkConfig = new PKConfig(); 18 | } 19 | } 20 | 21 | public synchronized void initWorkerThread(){ 22 | if (mWorkerThread == null) { 23 | mWorkerThread = new WorkerThread(new WeakReference<>(getApplicationContext())); 24 | mWorkerThread.start(); 25 | mWorkerThread.waitForReady(); 26 | } 27 | } 28 | 29 | public synchronized WorkerThread getWorkerThread() { 30 | return mWorkerThread; 31 | } 32 | 33 | public synchronized PKConfig getPkConfig() { 34 | return pkConfig; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/PKBroadcasterActivity.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipboardManager; 5 | import android.content.Context; 6 | import android.graphics.Color; 7 | import android.graphics.drawable.ColorDrawable; 8 | import android.os.Bundle; 9 | import android.support.v7.app.AlertDialog; 10 | import android.util.Log; 11 | import android.view.LayoutInflater; 12 | import android.view.SurfaceView; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.Button; 16 | import android.widget.EditText; 17 | import android.widget.FrameLayout; 18 | import android.widget.TextView; 19 | import android.widget.Toast; 20 | 21 | import java.util.HashMap; 22 | 23 | import io.agora.pk.model.AGEventHandler; 24 | import io.agora.pk.utils.PKConstants; 25 | import io.agora.pk.utils.StringUtils; 26 | import io.agora.rtc.Constants; 27 | import io.agora.rtc.IRtcEngineEventHandler; 28 | import io.agora.rtc.RtcEngine; 29 | import io.agora.rtc.video.VideoCanvas; 30 | 31 | public class PKBroadcasterActivity extends BaseActivity implements AGEventHandler { 32 | 33 | private static final String TAG = "PKBroadcaster"; 34 | 35 | private int mClientRole; 36 | 37 | private FrameLayout mFLSingleView; 38 | 39 | private FrameLayout mFLPKViewLeft; 40 | private FrameLayout mFLPKViewRight; 41 | 42 | private FrameLayout mFLPKMidBoard; 43 | 44 | private Button mBtnExitPk; 45 | 46 | private boolean isPKnow = false; 47 | private boolean isBroadcaster = false; 48 | 49 | private int mLocalUid = 0; 50 | private int mRemoteUid = 0; 51 | private HashMap mUserList = new HashMap<>(); 52 | 53 | private SurfaceView localView; 54 | private SurfaceView remoteView; 55 | 56 | private TextView mTvStartPk; 57 | 58 | private Button mBtnVCopyRtmpPullUrl; 59 | private TextView mTvRtmpPullUrl; 60 | 61 | @Override 62 | protected void onCreate(Bundle savedInstanceState) { 63 | super.onCreate(savedInstanceState); 64 | setContentView(R.layout.activity_pk_broadcaster); 65 | 66 | mClientRole = getIntent().getIntExtra(PKConstants.USER_CLIENT_ROLE, Constants.CLIENT_ROLE_AUDIENCE); 67 | } 68 | 69 | @Override 70 | protected void initUIandEvent() { 71 | mFLSingleView = findViewById(R.id.fl_chat_room_main_video_view); 72 | mFLPKViewLeft = findViewById(R.id.fl_chat_room_main_pk_board_left); 73 | mFLPKViewRight = findViewById(R.id.fl_chat_room_main_pk_board_right); 74 | mFLPKMidBoard = findViewById(R.id.fl_chat_room_main_pk_board); 75 | mTvStartPk = findViewById(R.id.et_chat_room_main_start_pk); 76 | mBtnExitPk = findViewById(R.id.btn_main_pk_exit_pk); 77 | 78 | mBtnVCopyRtmpPullUrl = findViewById(R.id.btn_copy_rtmp_pull_url); 79 | mBtnVCopyRtmpPullUrl.setOnClickListener(new View.OnClickListener() { 80 | 81 | @Override 82 | public void onClick(View view) { 83 | copyRtmpPullUrl(); 84 | } 85 | }); 86 | mTvRtmpPullUrl = findViewById(R.id.tv_rtmp_pull_url); 87 | 88 | initEngine(); 89 | } 90 | 91 | public void initEngine() { 92 | event().addEventHandler(this); 93 | 94 | workThread().configEngine(mClientRole); 95 | if (mClientRole == Constants.CLIENT_ROLE_BROADCASTER) { 96 | isBroadcaster = true; 97 | workThread().joinChannel(((PKApplication) getApplication()).getPkConfig().getBroadcasterAccount(), 0); 98 | } else if (mClientRole == Constants.CLIENT_ROLE_AUDIENCE) { 99 | isBroadcaster = false; 100 | } 101 | changeViewToSingle(); 102 | localView = RtcEngine.CreateRendererView(this); 103 | remoteView = RtcEngine.CreateRendererView(this); 104 | } 105 | 106 | @Override 107 | protected void deInitUIandEvent() { 108 | 109 | } 110 | 111 | public void onBackClicked(View v) { 112 | if (isBroadcaster) { 113 | removePublishUrl(); 114 | workThread().leaveChannel(); 115 | } 116 | 117 | mUserList.clear(); 118 | finish(); 119 | } 120 | 121 | @Override 122 | public void onBackPressed() { 123 | super.onBackPressed(); 124 | onBackClicked(null); 125 | } 126 | 127 | // exit pk 128 | public void onExitPKClicked(View v) { 129 | isPKnow = false; 130 | 131 | if (remoteView.getParent() != null) 132 | ((ViewGroup) (remoteView.getParent())).removeAllViews(); 133 | 134 | ((PKApplication) getApplication()).getPkConfig().setPkMediaAccount(""); 135 | 136 | mUserList.clear(); 137 | removePublishUrl(); 138 | workThread().leaveChannel(); 139 | workThread().joinChannel(((PKApplication) getApplication()).getPkConfig().getBroadcasterAccount(), 0); 140 | changeViewToSingle(); 141 | } 142 | 143 | // start pk, input a room channel to start pk 144 | public void onStartPKClicked(View v) { 145 | final AlertDialog.Builder alertDialog = new AlertDialog.Builder(this); 146 | View rootView = LayoutInflater.from(this).inflate(R.layout.pop_view_pk, null); 147 | alertDialog.setView(rootView); 148 | final AlertDialog dialog = alertDialog.create(); 149 | if (null != dialog.getWindow()) 150 | dialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); 151 | dialog.show(); 152 | 153 | Button btn = rootView.findViewById(R.id.btn_start_pk); 154 | final EditText et = rootView.findViewById(R.id.et_pk_channel); 155 | btn.setOnClickListener(new View.OnClickListener() { 156 | @Override 157 | public void onClick(View view) { 158 | if (!StringUtils.validate(et.getText().toString())) { 159 | Toast.makeText(PKBroadcasterActivity.this, "please input a channel account", Toast.LENGTH_SHORT).show(); 160 | return; 161 | } 162 | 163 | isPKnow = true; 164 | ((PKApplication) getApplication()).getPkConfig().setPkMediaAccount(et.getText().toString()); 165 | removePublishUrl(); 166 | workThread().leaveChannel(); 167 | mUserList.clear(); 168 | workThread().joinChannel(((PKApplication) getApplication()).getPkConfig().getPkMediaAccount(), 0); 169 | dialog.dismiss(); 170 | } 171 | }); 172 | } 173 | 174 | public void changeViewToSingle() { 175 | mFLPKMidBoard.setVisibility(View.INVISIBLE); 176 | mFLSingleView.setVisibility(View.VISIBLE); 177 | 178 | mFLSingleView.setBackgroundColor(Color.BLACK); 179 | if (isBroadcaster) 180 | mTvStartPk.setVisibility(View.VISIBLE); 181 | else { 182 | mTvStartPk.setVisibility(View.INVISIBLE); 183 | } 184 | } 185 | 186 | public void changeViewToPkBroadcaster() { 187 | mFLSingleView.setVisibility(View.INVISIBLE); 188 | mFLPKMidBoard.setVisibility(View.VISIBLE); 189 | mTvStartPk.setVisibility(View.VISIBLE); 190 | 191 | mFLPKViewRight.setVisibility(View.VISIBLE); 192 | mFLPKViewLeft.setVisibility(View.VISIBLE); 193 | mBtnExitPk.setVisibility(View.VISIBLE); 194 | } 195 | 196 | public void setLocalPreviewView(int uid) { 197 | workThread().preview(true, localView, uid); 198 | 199 | if (mFLSingleView.getChildCount() > 0) { 200 | mFLSingleView.removeAllViews(); 201 | } 202 | 203 | if (localView.getParent() != null) 204 | ((ViewGroup) (localView.getParent())).removeAllViews(); 205 | 206 | FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 207 | localView.setZOrderOnTop(false); 208 | localView.setZOrderMediaOverlay(false); 209 | localView.setLayoutParams(lp); 210 | mFLSingleView.addView(localView); 211 | } 212 | 213 | public void setLocalPkLeftView(int uid) { 214 | workThread().preview(true, localView, uid); 215 | 216 | if (mFLPKViewLeft.getChildCount() > 0) 217 | mFLPKViewLeft.removeAllViews(); 218 | 219 | if (localView.getParent() != null) 220 | ((ViewGroup) (localView.getParent())).removeAllViews(); 221 | 222 | mFLPKViewLeft.addView(localView); 223 | } 224 | 225 | public void setRemotePkRightView(int uid) { 226 | if (mFLPKViewRight.getChildCount() > 0) 227 | mFLPKViewRight.removeAllViews(); 228 | 229 | if (remoteView.getParent() != null) 230 | ((ViewGroup) (remoteView.getParent())).removeAllViews(); 231 | 232 | FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 233 | remoteView.setZOrderOnTop(false); 234 | remoteView.setZOrderMediaOverlay(false); 235 | remoteView.setLayoutParams(lp); 236 | 237 | rtcEngine().setupRemoteVideo(new VideoCanvas(remoteView, Constants.RENDER_MODE_HIDDEN, uid)); 238 | mFLPKViewRight.addView(remoteView); 239 | } 240 | 241 | @Override 242 | public void onJoinChannelSuccess(final String channel, final int uid, int elapsed) { 243 | runOnUiThread(new Runnable() { 244 | @Override 245 | public void run() { 246 | Log.d(TAG, "onJoinChannelSuccess channel = " + channel + " uid = " + (uid & 0XFFFFFFFFL)); 247 | 248 | mLocalUid = uid; 249 | mUserList.put(mLocalUid, mLocalUid); 250 | if (isPKnow) { 251 | changeViewToPkBroadcaster(); 252 | setLocalPkLeftView(uid); 253 | } else { 254 | changeViewToSingle(); 255 | setLocalPreviewView(uid); 256 | } 257 | 258 | // start CDN Streaming 259 | setLiveTranscoding(); 260 | publishUrl(); 261 | } 262 | }); 263 | } 264 | 265 | @Override 266 | public void onUserJoined(final int uid, int elapsed) { 267 | runOnUiThread(new Runnable() { 268 | @Override 269 | public void run() { 270 | if (mUserList.size() < PKConstants.MAX_PK_COUNT) { 271 | mRemoteUid = uid; 272 | mUserList.put(uid, uid); 273 | setLiveTranscoding(); 274 | setRemotePkRightView(uid); 275 | } 276 | } 277 | }); 278 | 279 | } 280 | 281 | @Override 282 | public void onStreamPublished(String url, int error) { 283 | } 284 | 285 | @Override 286 | public void onStreamUnpublished(String url) { 287 | } 288 | 289 | @Override 290 | public void onError(int err) { 291 | } 292 | 293 | @Override 294 | public void onUserOffline(final int uid, int reason) { 295 | runOnUiThread(new Runnable() { 296 | @Override 297 | public void run() { 298 | if (mUserList.keySet().contains(uid)) { 299 | mUserList.remove(uid); 300 | onExitPKClicked(null); 301 | setLiveTranscoding(); 302 | } 303 | } 304 | }); 305 | 306 | } 307 | 308 | @Override 309 | public void onLeaveChannel(IRtcEngineEventHandler.RtcStats stats) { 310 | } 311 | 312 | private String rtmpPullUrl() { 313 | return PKConstants.PUBLISH_PULL_URL + ((PKApplication) getApplication()).getPkConfig().getBroadcasterAccount(); 314 | } 315 | 316 | private void copyRtmpPullUrl() { 317 | ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 318 | ClipData mClipData = ClipData.newPlainText("Label", rtmpPullUrl()); 319 | cm.setPrimaryClip(mClipData); 320 | 321 | Toast.makeText(this, R.string.already_copied, Toast.LENGTH_LONG).show(); 322 | } 323 | 324 | public void publishUrl() { 325 | rtcEngine().addPublishStreamUrl(PKConstants.PUBLISH_URL + ((PKApplication) getApplication()).getPkConfig().getBroadcasterAccount(), true); 326 | 327 | mTvRtmpPullUrl.setText(rtmpPullUrl()); 328 | } 329 | 330 | public void removePublishUrl() { 331 | rtcEngine().removePublishStreamUrl(PKConstants.PUBLISH_URL + ((PKApplication) getApplication()).getPkConfig().getBroadcasterAccount()); 332 | } 333 | 334 | public void setLiveTranscoding() { 335 | rtcEngine().setLiveTranscoding(updateLiveTranscoding(mLocalUid, mRemoteUid, mUserList)); 336 | } 337 | 338 | } 339 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/PKConfig.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk; 2 | 3 | public class PKConfig { 4 | // all of the audience join the broadcaster signal channel 5 | // if the broadcaster want to pk, he should join another media channel, publish with old channel account 6 | // and keep the signal channel 7 | private String broadcasterAccount; 8 | // just for broadcaster pk to join media channel 9 | private String pkMediaAccount; 10 | 11 | public String getBroadcasterAccount() { 12 | return broadcasterAccount; 13 | } 14 | 15 | public void setBroadcasterAccount(String broadcasterAccount) { 16 | this.broadcasterAccount = broadcasterAccount; 17 | } 18 | 19 | public String getPkMediaAccount() { 20 | return pkMediaAccount; 21 | } 22 | 23 | public void setPkMediaAccount(String pkMediaAccount) { 24 | this.pkMediaAccount = pkMediaAccount; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/model/AGEventHandler.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk.model; 2 | 3 | import io.agora.rtc.IRtcEngineEventHandler; 4 | 5 | public interface AGEventHandler { 6 | void onJoinChannelSuccess(String channel, int uid, int elapsed); 7 | 8 | void onUserJoined(int uid, int elapsed); 9 | 10 | void onStreamPublished(String url, int error); 11 | 12 | void onStreamUnpublished(String url); 13 | 14 | void onError(int err); 15 | 16 | void onUserOffline(int uid, int reason); 17 | 18 | void onLeaveChannel(IRtcEngineEventHandler.RtcStats stats); 19 | } 20 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/model/MyEngineEventHandler.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk.model; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.Iterator; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | import io.agora.rtc.IRtcEngineEventHandler; 9 | 10 | public class MyEngineEventHandler { 11 | 12 | private ConcurrentHashMap handlers = new ConcurrentHashMap<>(); 13 | 14 | public void addEventHandler(AGEventHandler handler) { 15 | handlers.put(0, handler); 16 | } 17 | 18 | public void removeEventHandler(AGEventHandler handler) { 19 | handlers.remove(0); 20 | } 21 | 22 | private static final String LOG_TAG = "AG_EVT"; 23 | 24 | final IRtcEngineEventHandler mEventHandlerList = new IRtcEngineEventHandler() { 25 | @Override 26 | public void onJoinChannelSuccess(String channel, int uid, int elapsed) { 27 | Log.e(LOG_TAG, "success"); 28 | super.onJoinChannelSuccess(channel, uid, elapsed); 29 | 30 | if (handlers.isEmpty()) { 31 | return; 32 | } 33 | 34 | Iterator it = handlers.values().iterator(); 35 | while (it.hasNext()) { 36 | it.next().onJoinChannelSuccess(channel, uid, elapsed); 37 | } 38 | } 39 | 40 | @Override 41 | public void onRejoinChannelSuccess(String channel, int uid, int elapsed) { 42 | super.onRejoinChannelSuccess(channel, uid, elapsed); 43 | 44 | if (handlers.isEmpty()) { 45 | return; 46 | } 47 | 48 | Iterator it = handlers.values().iterator(); 49 | while (it.hasNext()) { 50 | it.next().onJoinChannelSuccess(channel, uid, elapsed); 51 | } 52 | } 53 | 54 | @Override 55 | public void onUserJoined(int uid, int elapsed) { 56 | Log.e(LOG_TAG, "joined"); 57 | super.onUserJoined(uid, elapsed); 58 | 59 | if (handlers.isEmpty()) { 60 | return; 61 | } 62 | 63 | Iterator it = handlers.values().iterator(); 64 | while (it.hasNext()) { 65 | it.next().onUserJoined(uid, elapsed); 66 | } 67 | } 68 | 69 | @Override 70 | public void onStreamPublished(String url, int error) { 71 | Log.e(LOG_TAG, "onStreamUnpublished: " + url); 72 | super.onStreamPublished(url, error); 73 | 74 | if (handlers.isEmpty()) { 75 | return; 76 | } 77 | 78 | Iterator it = handlers.values().iterator(); 79 | while (it.hasNext()) { 80 | it.next().onStreamPublished(url, error); 81 | } 82 | } 83 | 84 | @Override 85 | public void onStreamUnpublished(String url) { 86 | Log.e(LOG_TAG, "onStreamUnpublished: " + url); 87 | super.onStreamUnpublished(url); 88 | 89 | if (handlers.isEmpty()) { 90 | return; 91 | } 92 | 93 | Iterator it = handlers.values().iterator(); 94 | while (it.hasNext()) { 95 | it.next().onStreamUnpublished(url); 96 | } 97 | } 98 | 99 | @Override 100 | public void onError(int err) { 101 | super.onError(err); 102 | Log.e(LOG_TAG, "error: " + err); 103 | 104 | if (handlers.isEmpty()) { 105 | return; 106 | } 107 | 108 | Iterator it = handlers.values().iterator(); 109 | while (it.hasNext()) { 110 | it.next().onError(err); 111 | } 112 | } 113 | 114 | @Override 115 | public void onUserOffline(int uid, int reason) { 116 | Log.d(LOG_TAG, "joined"); 117 | super.onUserOffline(uid, reason); 118 | 119 | if (handlers.isEmpty()) { 120 | return; 121 | } 122 | 123 | Iterator it = handlers.values().iterator(); 124 | while (it.hasNext()) { 125 | it.next().onUserOffline(uid, reason); 126 | } 127 | } 128 | 129 | @Override 130 | public void onLeaveChannel(RtcStats stats) { 131 | super.onLeaveChannel(stats); 132 | 133 | if (handlers.isEmpty()) { 134 | return; 135 | } 136 | 137 | Iterator it = handlers.values().iterator(); 138 | while (it.hasNext()) { 139 | it.next().onLeaveChannel(stats); 140 | } 141 | } 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/model/WorkerThread.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk.model; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.os.Message; 7 | import android.util.Log; 8 | import android.view.SurfaceView; 9 | 10 | import java.lang.ref.WeakReference; 11 | 12 | import io.agora.pk.R; 13 | import io.agora.pk.utils.PKConstants; 14 | import io.agora.pk.utils.StringUtils; 15 | import io.agora.rtc.Constants; 16 | import io.agora.rtc.RtcEngine; 17 | import io.agora.rtc.video.VideoCanvas; 18 | 19 | public class WorkerThread extends Thread { 20 | private final static String TAG = WorkerThread.class.getName(); 21 | 22 | private final Context mContext; 23 | 24 | private static final int ACTION_WORKER_THREAD_QUIT = 0X1010; // quit this thread 25 | 26 | private static final int ACTION_WORKER_JOIN_CHANNEL = 0X2010; 27 | 28 | private static final int ACTION_WORKER_LEAVE_CHANNEL = 0X2011; 29 | 30 | private static final int ACTION_WORKER_CONFIG_ENGINE = 0X2012; 31 | 32 | private static final int ACTION_WORKER_SETUP_REMOTE_VIEW = 0X2013; 33 | 34 | private static final int ACTION_WORKER_PREVIEW = 0X2014; 35 | 36 | private RtcEngine mRtcEngine; 37 | private WorkerThreadHandler mWorkerHandler; 38 | private MyEngineEventHandler mMyEngineEventHandler; 39 | 40 | private boolean mReady = false; 41 | 42 | private static final class WorkerThreadHandler extends Handler { 43 | private WorkerThread mWorkerThread; 44 | 45 | public WorkerThreadHandler(WorkerThread thread) { 46 | this.mWorkerThread = thread; 47 | } 48 | 49 | public void release() { 50 | mWorkerThread = null; 51 | } 52 | 53 | @Override 54 | public void handleMessage(Message msg) { 55 | super.handleMessage(msg); 56 | 57 | switch (msg.what) { 58 | case ACTION_WORKER_JOIN_CHANNEL: 59 | mWorkerThread.joinChannel((String) msg.obj, msg.arg1); 60 | break; 61 | case ACTION_WORKER_PREVIEW: 62 | Object[] previewData = (Object[]) msg.obj; 63 | mWorkerThread.preview((boolean) previewData[0], (SurfaceView) previewData[1], (int) previewData[2]); 64 | break; 65 | case ACTION_WORKER_THREAD_QUIT: 66 | mWorkerThread.exit(); 67 | break; 68 | case ACTION_WORKER_LEAVE_CHANNEL: 69 | mWorkerThread.leaveChannel(); 70 | break; 71 | case ACTION_WORKER_CONFIG_ENGINE: 72 | mWorkerThread.configEngine((Integer) msg.obj); 73 | break; 74 | case ACTION_WORKER_SETUP_REMOTE_VIEW: 75 | Object[] remoteData = (Object[]) msg.obj; 76 | mWorkerThread.setupRemoteView((SurfaceView) remoteData[0], (int) remoteData[1]); 77 | break; 78 | default: 79 | throw new RuntimeException("unknown handler event"); 80 | } 81 | } 82 | } 83 | 84 | public WorkerThread(WeakReference ctx) { 85 | this.mContext = ctx.get(); 86 | this.mMyEngineEventHandler = new MyEngineEventHandler(); 87 | } 88 | 89 | public void waitForReady() { 90 | while (!mReady) { 91 | try { 92 | Thread.sleep(20); 93 | } catch (InterruptedException e) { 94 | e.printStackTrace(); 95 | } 96 | } 97 | } 98 | 99 | @Override 100 | public void run() { 101 | super.run(); 102 | Looper.prepare(); 103 | 104 | mWorkerHandler = new WorkerThreadHandler(this); 105 | 106 | ensureRtcEngineReadyLock(); 107 | 108 | mReady = true; 109 | 110 | Looper.loop(); 111 | } 112 | 113 | private void ensureRtcEngineReadyLock() { 114 | if (mRtcEngine != null) 115 | return; 116 | 117 | if (!StringUtils.validate(mContext.getString(R.string.agora_app_id))) 118 | throw new RuntimeException("You need to provide a valid Agora App Id"); 119 | 120 | try { 121 | mRtcEngine = RtcEngine.create(mContext, mContext.getString(R.string.agora_app_id), mMyEngineEventHandler.mEventHandlerList); 122 | } catch (Exception e) { 123 | e.printStackTrace(); 124 | } 125 | 126 | mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); 127 | } 128 | 129 | public RtcEngine rtcEngine() { 130 | return mRtcEngine; 131 | } 132 | 133 | public MyEngineEventHandler eventHandler() { 134 | return mMyEngineEventHandler; 135 | } 136 | 137 | /** 138 | * call this method to exit 139 | * should ONLY call this method when this thread is running 140 | */ 141 | public final void exit() { 142 | if (Thread.currentThread() != this) { 143 | Log.w(TAG, "exit() - exit app thread asynchronously"); 144 | mWorkerHandler.sendEmptyMessage(ACTION_WORKER_THREAD_QUIT); 145 | return; 146 | } 147 | 148 | mReady = false; 149 | 150 | // TODO should remove all pending(read) messages 151 | 152 | Log.d(TAG, "exit() > start"); 153 | 154 | // exit thread looper 155 | Looper.myLooper().quit(); 156 | 157 | mWorkerHandler.release(); 158 | 159 | Log.d(TAG, "exit() > end"); 160 | } 161 | 162 | public final void joinChannel(final String channel, int uid) { 163 | if (Thread.currentThread() != this) { 164 | Message envelop = new Message(); 165 | envelop.what = ACTION_WORKER_JOIN_CHANNEL; 166 | envelop.obj = channel; 167 | envelop.arg1 = uid; 168 | mWorkerHandler.sendMessage(envelop); 169 | return; 170 | } 171 | 172 | ensureRtcEngineReadyLock(); 173 | 174 | int ret = mRtcEngine.joinChannel(null, channel, "", uid); 175 | Log.d(TAG, "joinChannel: " + ret); 176 | } 177 | 178 | public final void configEngine(int channelProfile) { 179 | if (Thread.currentThread() != this) { 180 | Message msg = Message.obtain(); 181 | msg.what = ACTION_WORKER_CONFIG_ENGINE; 182 | msg.obj = channelProfile; 183 | mWorkerHandler.sendMessage(msg); 184 | return; 185 | } 186 | 187 | ensureRtcEngineReadyLock(); 188 | 189 | mRtcEngine.setClientRole(channelProfile); 190 | 191 | mRtcEngine.setVideoEncoderConfiguration(PKConstants.VIDEO_CONFIGURATION); 192 | 193 | mRtcEngine.enableVideo(); 194 | mRtcEngine.enableDualStreamMode(true); 195 | } 196 | 197 | public final void preview(boolean start, SurfaceView view, int uid) { 198 | if (Thread.currentThread() != this) { 199 | Message envelop = new Message(); 200 | envelop.what = ACTION_WORKER_PREVIEW; 201 | envelop.obj = new Object[]{start, view, uid}; 202 | mWorkerHandler.sendMessage(envelop); 203 | return; 204 | } 205 | 206 | ensureRtcEngineReadyLock(); 207 | 208 | if (start) { 209 | mRtcEngine.setupLocalVideo(new VideoCanvas(view, VideoCanvas.RENDER_MODE_HIDDEN, uid)); 210 | mRtcEngine.startPreview(); 211 | } else { 212 | mRtcEngine.stopPreview(); 213 | } 214 | } 215 | 216 | public final void setupRemoteView(SurfaceView view, int uid) { 217 | if (Thread.currentThread() != this) { 218 | Message envelop = new Message(); 219 | envelop.what = ACTION_WORKER_SETUP_REMOTE_VIEW; 220 | envelop.obj = new Object[]{view, uid}; 221 | mWorkerHandler.sendMessage(envelop); 222 | return; 223 | } 224 | 225 | ensureRtcEngineReadyLock(); 226 | 227 | mRtcEngine.setupRemoteVideo(new VideoCanvas(view, VideoCanvas.RENDER_MODE_HIDDEN, uid)); 228 | } 229 | 230 | public final void leaveChannel() { 231 | if (Thread.currentThread() != this) { 232 | Message envelop = new Message(); 233 | envelop.what = ACTION_WORKER_LEAVE_CHANNEL; 234 | mWorkerHandler.sendMessage(envelop); 235 | return; 236 | } 237 | if (mRtcEngine != null) { 238 | mRtcEngine.leaveChannel(); 239 | } 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/ui/CircleImageView.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk.ui; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.PaintFlagsDrawFilter; 8 | import android.graphics.Path; 9 | import android.graphics.Region; 10 | import android.os.Build; 11 | import android.support.v7.widget.AppCompatImageView; 12 | import android.util.AttributeSet; 13 | 14 | public class CircleImageView extends AppCompatImageView { 15 | private Path path; 16 | public PaintFlagsDrawFilter mPaintFlagsDrawFilter; 17 | private Paint paint; 18 | 19 | public CircleImageView(Context context, AttributeSet attrs, int defStyle) { 20 | super(context, attrs, defStyle); 21 | init(); 22 | } 23 | 24 | public CircleImageView(Context context, AttributeSet attrs) { 25 | super(context, attrs); 26 | init(); 27 | } 28 | 29 | public CircleImageView(Context context) { 30 | super(context); 31 | init(); 32 | } 33 | 34 | public void init() { 35 | mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, 36 | Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 37 | paint = new Paint(); 38 | paint.setAntiAlias(true); 39 | paint.setFilterBitmap(true); 40 | paint.setColor(Color.BLACK); 41 | paint.setStrokeWidth(1); 42 | } 43 | 44 | @Override 45 | protected void onDraw(Canvas cns) { 46 | float h = getMeasuredHeight() - 3.0f; 47 | float w = getMeasuredWidth() - 3.0f; 48 | if (path == null) { 49 | path = new Path(); 50 | path.addCircle( 51 | w / 2.0f 52 | , h / 2.0f 53 | , (float) Math.min(w / 2.0f, (h / 2.0)) 54 | , Path.Direction.CCW); 55 | path.close(); 56 | } 57 | cns.drawCircle(w / 2.0f, h / 2.0f, Math.min(w / 2.0f, h / 2.0f) + 1.5f, paint); 58 | cns.save(); 59 | cns.setDrawFilter(mPaintFlagsDrawFilter); 60 | 61 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 62 | cns.clipPath(path); 63 | } else { 64 | cns.clipPath(path, Region.Op.REPLACE); 65 | } 66 | 67 | cns.setDrawFilter(mPaintFlagsDrawFilter); 68 | cns.drawColor(Color.WHITE); 69 | super.onDraw(cns); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/utils/KeyboardStatusDetector.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk.utils; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Rect; 5 | import android.view.View; 6 | import android.view.ViewTreeObserver; 7 | 8 | import java.lang.ref.WeakReference; 9 | 10 | public class KeyboardStatusDetector { 11 | private static final int SOFT_KEY_BOARD_MIN_HEIGH = 100; 12 | 13 | private KeyboardListener klistener; 14 | private boolean isVisible = false; 15 | 16 | public void setCallback(KeyboardListener kl) { 17 | this.klistener = kl; 18 | } 19 | 20 | public void registerActivity(WeakReference activity) { 21 | final View v = activity.get().getWindow().getDecorView().findViewById(android.R.id.content); 22 | v.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 23 | @Override 24 | public void onGlobalLayout() { 25 | Rect r = new Rect(); 26 | v.getWindowVisibleDisplayFrame(r); 27 | int height = v.getRootView().getHeight() - (r.bottom - r.top); 28 | 29 | if (height > SOFT_KEY_BOARD_MIN_HEIGH) { 30 | if (!isVisible) { 31 | isVisible = true; 32 | if (klistener != null) 33 | klistener.onKeyBoardStatusChanged(true, height); 34 | } else { 35 | isVisible = false; 36 | if (klistener != null) 37 | klistener.onKeyBoardStatusChanged(true, 0); 38 | } 39 | } 40 | } 41 | }); 42 | } 43 | 44 | public interface KeyboardListener { 45 | void onKeyBoardStatusChanged(boolean v, int height); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/utils/MessageUtils.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk.utils; 2 | 3 | import android.text.TextUtils; 4 | 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | public class MessageUtils { 11 | 12 | private static ConcurrentHashMap maps = new ConcurrentHashMap<>(); 13 | private static ConcurrentHashMap maps2 = new ConcurrentHashMap<>(); 14 | 15 | public static String switchToChatJsonMsg(String msg) { 16 | maps.clear(); 17 | maps.put("type", "chat"); 18 | maps.put("data", msg); 19 | return new JSONObject(maps).toString(); 20 | } 21 | 22 | public static String switchToCtrlMsg(boolean msg) { 23 | maps2.clear(); 24 | maps2.put("type", "pkStatus"); 25 | maps2.put("data", msg); 26 | return new JSONObject(maps2).toString(); 27 | } 28 | 29 | public static Object getMessage(String content) { 30 | try { 31 | JSONObject obj = new JSONObject(content); 32 | String s = obj.optString("type", ""); 33 | if (TextUtils.isEmpty(s)) 34 | return null; 35 | 36 | if ("chat".equals(s)) { 37 | return obj.optString("data", ""); 38 | } else if ("pkStatus".equals(s)) { 39 | return Boolean.parseBoolean(obj.optString("data")); 40 | } 41 | 42 | } catch (JSONException e) { 43 | throw new RuntimeException("json error!"); 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/utils/PKConstants.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk.utils; 2 | 3 | import io.agora.rtc.video.VideoEncoderConfiguration; 4 | 5 | public class PKConstants { 6 | 7 | public final static String BUNDLE_ACCOUNT_NAME = "ACCOUNT_NAME"; 8 | public final static String USER_CLIENT_ROLE = "USER_CLIENT_ROLE"; 9 | public final static VideoEncoderConfiguration VIDEO_CONFIGURATION = new VideoEncoderConfiguration(VideoEncoderConfiguration.VD_640x480, 10 | VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15, VideoEncoderConfiguration.STANDARD_BITRATE, 11 | VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT); 12 | 13 | public final static int MAX_PK_COUNT = 2; 14 | 15 | public final static int LIVE_TRANSCODING_WIDTH = 360; 16 | public final static int LIVE_TRANSCODING_HEIGHT = 640; 17 | public final static int LIVE_TRANSCODING_FPS = 15; 18 | public final static int LIVE_TRANSCODING_BITRATE = 1200; 19 | 20 | public final static String PUBLISH_URL = <##YOUR PUBLISH RTMP URL##>; 21 | public final static String PUBLISH_PULL_URL = <##YOUR PLAY RTMP URL##>; 22 | } 23 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/java/io/agora/pk/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package io.agora.pk.utils; 2 | 3 | import android.text.TextUtils; 4 | 5 | public class StringUtils { 6 | private final static String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 7 | 8 | public static String random(int len) { 9 | StringBuilder sb = new StringBuilder(); 10 | for (int i = 0; i < len; i++) { 11 | sb.append(chars.charAt((int) (Math.random() * (len + 1)))); 12 | } 13 | 14 | return sb.toString(); 15 | } 16 | 17 | public static boolean validate(String s) { 18 | return null != s && !TextUtils.isEmpty(s); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/jniLibs/arm64-v8a/PLACEHOLDER: -------------------------------------------------------------------------------- 1 | libagora-rtc-sdk-jni.so 2 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/jniLibs/armeabi-v7a/PLACEHOLDER: -------------------------------------------------------------------------------- 1 | libagora-rtc-sdk-jni.so 2 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/jniLibs/x86/PLACEHOLDER: -------------------------------------------------------------------------------- 1 | libagora-rtc-sdk-jni.so 2 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/chat_room_et_style_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/chat_room_main_exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/chat_room_main_exit.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/chat_room_main_like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/chat_room_main_like.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/chat_room_main_pk_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/chat_room_main_pk_support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/chat_room_main_pk_support.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/main_btn_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/pk_exit_btn_style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_01.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_02.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_03.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_04.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_05.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_06.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_07.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Agora-Online-PK-Android/app/src/main/res/drawable/user_icon_08.png -------------------------------------------------------------------------------- /Agora-Online-PK-Android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 103 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 136 | 147 | 158 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Contants/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Agora-Online-PK 4 | // 5 | // Created by ZhangJi on 2018/6/4. 6 | // Copyright © 2018 CavanSu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Screen Width 12 | let ScreenWidth = UIScreen.main.bounds.size.width 13 | /// Screen Height 14 | let ScreenHeight = UIScreen.main.bounds.size.height 15 | 16 | /// PK view width 17 | let pkViewWidth = ScreenWidth / 2.0 18 | 19 | /// PK view height 20 | let pkViewHeight = ScreenWidth / 9.0 * 8 21 | 22 | /// RTMP Push URL 23 | let pushUrl = <##YOUR PUBLISH RTMP URL##>; 24 | 25 | /// RTMP Pull URL 26 | let pullUrl = <##YOUR PLAY RTMP URL##>; 27 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Contants/KeyCenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyCenter.swift 3 | // Agora-Online-PK 4 | // 5 | // Created by ZhangJi on 2018/6/4. 6 | // Copyright © 2018 CavanSu. All rights reserved. 7 | // 8 | 9 | struct KeyCenter { 10 | static let AppId: String = <#Your App ID#> 11 | } 12 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Controllers/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // Agora-Online-PK 4 | // 5 | // Created by ZhangJi on 2018/6/4. 6 | // Copyright © 2018 CavanSu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoginViewController: UIViewController { 12 | 13 | /**----------------------------------------------------------------------------- 14 | * This view is uesd to set the channel 15 | * 16 | * In this app we use the channel to identify 17 | * - Agora media channel name 18 | * - Agora RTMP Push URL (Constants.pushUrl + account) 19 | * ----------------------------------------------------------------------------- 20 | */ 21 | @IBOutlet weak var joinButton: UIButton! 22 | @IBOutlet weak var channelTextField: UITextField! 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | } 27 | 28 | override func didReceiveMemoryWarning() { 29 | super.didReceiveMemoryWarning() 30 | } 31 | 32 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 33 | view.endEditing(true) 34 | } 35 | 36 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 37 | guard let channelName = sender as? String else { 38 | return 39 | } 40 | 41 | let roomVC = segue.destination as! RoomViewController 42 | roomVC.mediaRoomName = channelName 43 | } 44 | 45 | @IBAction func doJoinButtonPressed(_ sender: UIButton) { 46 | guard let channelName = channelTextField.text else { 47 | return 48 | } 49 | if !check(String: channelName) { 50 | return 51 | } 52 | performSegue(withIdentifier: "toRoom", sender: channelName) 53 | } 54 | 55 | func check(String: String) -> Bool { 56 | if String.isEmpty { 57 | AlertUtil.showAlert(message: "The account is empty !") 58 | return false 59 | } 60 | 61 | return true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Controllers/RoomViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoomViewController.swift 3 | // Agora-Online-PK 4 | // 5 | // Created by ZhangJi on 2018/6/5. 6 | // Copyright © 2018 CavanSu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AgoraRtcEngineKit 11 | 12 | struct Message { 13 | // struct for message 14 | let name: String! 15 | let content: NSMutableAttributedString! 16 | } 17 | 18 | class RoomViewController: UIViewController { 19 | /**----------------------------------------------------------------------------- 20 | * This view load the mode for Live broadcast 21 | * 22 | * Live broadcast mode: 23 | * You will upload the stream to Agora and CDN you chose 24 | * You can presse PK button to start PK with another broadcast 25 | * 26 | * ----------------------------------------------------------------------------- 27 | */ 28 | @IBOutlet weak var leaveButton: UIButton! 29 | @IBOutlet weak var pkButton: UIButton! 30 | @IBOutlet weak var endPkButton: UIButton! 31 | 32 | @IBOutlet weak var hostContainView: UIView! 33 | 34 | @IBOutlet weak var urlContainerView: UIView! 35 | @IBOutlet weak var pullUrlLabel: UILabel! 36 | @IBOutlet weak var copyButton: UIButton! 37 | 38 | var agoraKit: AgoraRtcEngineKit! 39 | var myPushUrl: String? // url to push rtmp stream 40 | 41 | var mediaRoomName: String! // channel name to join Agora media room 42 | 43 | var localSession: VideoSession? 44 | var remoteSession: VideoSession? 45 | 46 | var pkRoomeName: String? // channel name for the PK room 47 | var count: Int = 0 48 | var isPk = false { 49 | // the status for PK 50 | didSet { 51 | if isPk != oldValue { 52 | updateViewWithStatus(isPk: isPk) 53 | } 54 | } 55 | } 56 | 57 | override var prefersStatusBarHidden: Bool { 58 | return true 59 | } 60 | 61 | override func viewDidLoad() { 62 | super.viewDidLoad() 63 | } 64 | 65 | override func viewDidAppear(_ animated: Bool) { 66 | super.viewDidAppear(animated) 67 | 68 | setView() 69 | 70 | loadAgoraKit(withIsPk: false) 71 | } 72 | 73 | override func didReceiveMemoryWarning() { 74 | super.didReceiveMemoryWarning() 75 | } 76 | 77 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 78 | view.endEditing(true) 79 | } 80 | 81 | func setView() { 82 | // init the view 83 | pkButton.layer.borderWidth = 1 84 | pkButton.layer.borderColor = UIColor.white.cgColor 85 | pkButton.layer.cornerRadius = 5 86 | pkButton.layer.masksToBounds = true 87 | 88 | endPkButton.layer.borderWidth = 1 89 | endPkButton.layer.borderColor = UIColor.white.cgColor 90 | endPkButton.layer.cornerRadius = 5 91 | endPkButton.layer.masksToBounds = true 92 | endPkButton.frame.size = CGSize(width: 110, height: 44) 93 | endPkButton.center = CGPoint(x: ScreenWidth / 2.0, y: ScreenHeight / 7 + pkViewHeight) 94 | 95 | copyButton.layer.cornerRadius = 4 96 | copyButton.layer.masksToBounds = true 97 | 98 | urlContainerView.layer.cornerRadius = 4 99 | urlContainerView.layer.masksToBounds = true 100 | } 101 | 102 | func updateViewWithStatus(isPk: Bool) { 103 | // update view with status 104 | self.pkButton.isHidden = isPk 105 | self.endPkButton.isHidden = !isPk 106 | } 107 | 108 | @IBAction func doLeaveButtonPressed(_ sender: UIButton) { 109 | self.isPk = false 110 | self.pkRoomeName = nil 111 | self.leaveAgoraChannel() 112 | 113 | setIdleTimerActive(true) 114 | self.dismiss(animated: true, completion: nil) 115 | } 116 | 117 | @IBAction func doPkButtonPressed(_ sender: UIButton) { 118 | let popView = PopView.newPopViewWith(buttonTitle: "PK", placeholder: "Channel name for PK") 119 | popView?.frame = CGRect(x: 0, y: ScreenHeight, width: ScreenWidth, height: ScreenHeight) 120 | popView?.delegate = self 121 | self.view.addSubview(popView!) 122 | UIView.animate(withDuration: 0.2) { 123 | popView?.frame = self.view.frame 124 | } 125 | } 126 | 127 | @IBAction func doEndPkButtonPressed(_ sender: UIButton) { 128 | self.leaveAgoraChannel() 129 | } 130 | 131 | @IBAction func doCopyPressed(_ sender: UIButton) { 132 | // Copy url 133 | UIPasteboard.general.string = pullUrl + self.mediaRoomName 134 | } 135 | 136 | func setIdleTimerActive(_ active: Bool) { 137 | UIApplication.shared.isIdleTimerDisabled = !active 138 | } 139 | } 140 | 141 | // MARK: - AgoraMedia 142 | private extension RoomViewController { 143 | func loadAgoraKit(withIsPk status: Bool) { 144 | // load agora media kit and join media channel with PK status, only the broadcaster will join agora 145 | // the agora media channel, the audience just join agora signal channel for channel chat 146 | agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: self) 147 | agoraKit.setChannelProfile(.liveBroadcasting) 148 | agoraKit.setClientRole(.broadcaster) 149 | agoraKit.enableVideo() 150 | agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: AgoraVideoDimension640x360, 151 | frameRate: .fps15, 152 | bitrate: AgoraVideoBitrateStandard, 153 | orientationMode: .adaptative)) 154 | 155 | agoraKit.startPreview() 156 | self.addLocalSession() 157 | UIView.animate(withDuration: 0.2) { 158 | self.localSession?.hostingView.frame = status ? CGRect(x: 0, y: ScreenHeight / 7, width: pkViewWidth, height: pkViewHeight) : self.hostContainView.frame 159 | } 160 | 161 | let code = agoraKit.joinChannel(byToken: nil, channelId: isPk ? self.pkRoomeName! : self.mediaRoomName, info: nil, uid: 0, joinSuccess: nil) 162 | if code == 0 { 163 | setIdleTimerActive(false) 164 | agoraKit.setEnableSpeakerphone(true) 165 | } 166 | 167 | } 168 | 169 | func addLocalSession() { 170 | if self.localSession == nil { 171 | self.localSession = VideoSession.localSession() 172 | } 173 | agoraKit.setupLocalVideo(localSession?.canvas) 174 | self.hostContainView.addSubview((localSession?.hostingView)!) 175 | } 176 | 177 | /**----------------------------------------------------------------------------- 178 | * 179 | * Key Code for Live transcode 180 | * 181 | * ----------------------------------------------------------------------------- 182 | */ 183 | func updateLiveTranscoding(withMenber menber: Int) { 184 | // LiveTranscoding update, the LiveTranscoding is used to set the CDN stream layout in Agora server 185 | // more details please refer to the document 186 | switch menber { 187 | case 1: 188 | // the LiveTranscoding for one person 189 | let localUser = AgoraLiveTranscodingUser() 190 | localUser.uid = (self.localSession?.uid)! 191 | localUser.rect = CGRect(x: 0, y: 0, width: 360, height: 640) 192 | localUser.zOrder = 1 193 | localUser.audioChannel = 0 194 | 195 | let liveTranscoding = AgoraLiveTranscoding() 196 | 197 | liveTranscoding.transcodingUsers = [localUser] 198 | liveTranscoding.size = CGSize(width: 360, height: 640) 199 | liveTranscoding.videoBitrate = 1200 200 | liveTranscoding.videoFramerate = 15 201 | liveTranscoding.backgroundColor = UIColor.clear 202 | 203 | agoraKit.setLiveTranscoding(liveTranscoding) 204 | case 2: 205 | // the LiveTranscoding for two persons in PK mode 206 | var uses = [AgoraLiveTranscodingUser]() 207 | let localUser = AgoraLiveTranscodingUser() 208 | localUser.uid = (self.localSession?.uid)! 209 | localUser.rect = CGRect(x: 0, y: 0, width: 360, height: 640) 210 | localUser.zOrder = 1 211 | localUser.audioChannel = 0 212 | uses.append(localUser) 213 | 214 | if self.remoteSession != nil { 215 | let removeUser = AgoraLiveTranscodingUser() 216 | removeUser.uid = (self.remoteSession?.uid)! 217 | removeUser.rect = CGRect(x: 360, y: 0, width: 360, height: 640) 218 | removeUser.zOrder = 1 219 | removeUser.audioChannel = 0 220 | uses.append(removeUser) 221 | } 222 | 223 | let liveTranscoding = AgoraLiveTranscoding() 224 | liveTranscoding.transcodingUsers = uses 225 | liveTranscoding.size = CGSize(width: 720, height: 640) 226 | liveTranscoding.videoBitrate = 1200 227 | liveTranscoding.videoFramerate = 15 228 | liveTranscoding.backgroundColor = UIColor.clear 229 | 230 | agoraKit.setLiveTranscoding(liveTranscoding) 231 | default: 232 | break 233 | } 234 | } 235 | 236 | func leaveAgoraChannel() { 237 | // leave agora media channel 238 | if let myPushUrl = self.myPushUrl { 239 | agoraKit.removePublishStreamUrl(myPushUrl) 240 | } 241 | agoraKit.setupLocalVideo(nil) 242 | agoraKit.leaveChannel(nil) 243 | } 244 | } 245 | 246 | // MARK: - AgoraMedia Deleagte 247 | extension RoomViewController: AgoraRtcEngineDelegate { 248 | func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { 249 | print("join media channel: \(channel)") 250 | self.localSession?.uid = uid 251 | 252 | self.updateLiveTranscoding(withMenber: self.isPk ? 2 : 1) 253 | self.myPushUrl = pushUrl + self.mediaRoomName 254 | self.pullUrlLabel.text = pullUrl + self.mediaRoomName 255 | agoraKit.addPublishStreamUrl(self.myPushUrl!, transcodingEnabled: true) 256 | } 257 | 258 | func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { 259 | if isPk && count < 1 { 260 | remoteSession = VideoSession(uid: uid) 261 | agoraKit.setupRemoteVideo((remoteSession?.canvas)!) 262 | remoteSession?.hostingView.frame = CGRect(x: pkViewWidth, y: ScreenHeight / 7, width: pkViewWidth, height: pkViewHeight) 263 | self.hostContainView.addSubview((remoteSession?.hostingView)!) 264 | count = 1 265 | self.updateLiveTranscoding(withMenber: 2) 266 | } 267 | } 268 | 269 | func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { 270 | if uid == remoteSession?.uid { 271 | remoteSession?.hostingView.removeFromSuperview() 272 | remoteSession = nil 273 | count = 0 274 | self.leaveAgoraChannel() 275 | } 276 | } 277 | 278 | func rtcEngine(_ engine: AgoraRtcEngineKit, streamPublishedWithUrl url: String, errorCode: AgoraErrorCode) { 279 | print("streamPublishedWithUrl: error \(errorCode.rawValue)") 280 | } 281 | 282 | func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { 283 | print("leave channel") 284 | guard let _ = self.pkRoomeName else { 285 | return 286 | } 287 | if isPk { 288 | // if in PK mode, the broadcaster will first leave the PK room, then go back his own room 289 | self.isPk = false 290 | self.pkRoomeName = nil 291 | self.remoteSession?.hostingView.removeFromSuperview() 292 | loadAgoraKit(withIsPk: false) 293 | } else { 294 | // if it's not in PK mode, the broadcaster will first leave his owm roonm, then join the PK room 295 | self.isPk = true 296 | loadAgoraKit(withIsPk: true) 297 | } 298 | } 299 | } 300 | 301 | extension RoomViewController: PopViewDelegate { 302 | func popViewButtonDidPressed(_ popView: PopView) { 303 | guard let pkRoomName = popView.inputTextField.text else { 304 | return 305 | } 306 | if !check(String: pkRoomName) { 307 | return 308 | } 309 | self.pkRoomeName = pkRoomName 310 | self.leaveAgoraChannel() 311 | popView.removeFromSuperview() 312 | } 313 | 314 | func check(String: String) -> Bool { 315 | if String.isEmpty { 316 | AlertUtil.showAlert(message: "The account is empty !") 317 | return false 318 | } 319 | return true 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | NSCameraUsageDescription 29 | 30 | NSMicrophoneUsageDescription 31 | 32 | UIFileSharingEnabled 33 | 34 | UILaunchStoryboardName 35 | LaunchScreen 36 | UIMainStoryboardFile 37 | Main 38 | UIRequiredDeviceCapabilities 39 | 40 | armv7 41 | 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | 46 | UISupportedInterfaceOrientations~ipad 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationPortraitUpsideDown 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Utils/AlertUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertUtil.swift 3 | // AgoraHQ 4 | // 5 | // Created by ZhangJi on 09/01/2018. 6 | // Copyright © 2018 ZhangJi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AlertUtil: NSObject { 12 | static func showAlert(message: String) { 13 | let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) 14 | alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) 15 | 16 | let rootVC = UIApplication.shared.keyWindow?.rootViewController 17 | let topVC = rootVC?.presentedViewController != nil ? rootVC?.presentedViewController : rootVC 18 | 19 | DispatchQueue.main.async { 20 | topVC?.present(alert, animated: true, completion: nil) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Utils/CommonExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonExtensions.swift 3 | // OpenVideoCall 4 | // 5 | // Created by ZhangJi on 22/08/2017. 6 | // Copyright © 2017 ZhangJi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | convenience init(hex: Int, alpha: CGFloat = 1) { 13 | func transform(_ input: Int, offset: Int = 0) -> CGFloat { 14 | let value = (input >> offset) & 0xff 15 | return CGFloat(value) / 255 16 | } 17 | 18 | self.init(red: transform(hex, offset: 16), 19 | green: transform(hex, offset: 8), 20 | blue: transform(hex), 21 | alpha: alpha) 22 | } 23 | } 24 | 25 | extension CGSize { 26 | func fixedSize(with reference: CGSize) -> CGSize { 27 | if reference.width > reference.height { 28 | return fixedLandscapeSize() 29 | } else { 30 | return fixedPortraitSize() 31 | } 32 | } 33 | 34 | func fixedLandscapeSize() -> CGSize { 35 | let width = self.width 36 | let height = self.height 37 | if width < height { 38 | return CGSize(width: height, height: width) 39 | } else { 40 | return self 41 | } 42 | } 43 | 44 | func fixedPortraitSize() -> CGSize { 45 | let width = self.width 46 | let height = self.height 47 | if width > height { 48 | return CGSize(width: height, height: width) 49 | } else { 50 | return self 51 | } 52 | } 53 | 54 | func fixedSize() -> CGSize { 55 | let width = self.width 56 | let height = self.height 57 | if width < height { 58 | return CGSize(width: height, height: width) 59 | } else { 60 | return self 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Views/PopView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopView.swift 3 | // Agora-Online-PK 4 | // 5 | // Created by ZhangJi on 2018/6/5. 6 | // Copyright © 2018 CavanSu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc protocol PopViewDelegate: NSObjectProtocol { 12 | func popViewButtonDidPressed(_ popView: PopView) 13 | 14 | @objc optional func popViewDidRemoved(_ popView: PopView) 15 | } 16 | 17 | class PopView: UIView { 18 | // this is a custom tool for pop input view 19 | @IBOutlet weak var inputTextField: UITextField! 20 | @IBOutlet weak var popViewButton: UIButton! 21 | 22 | weak var delegate: PopViewDelegate? 23 | 24 | static func newPopViewWith(buttonTitle: String, placeholder: String) -> PopView? { 25 | let nibView = Bundle.main.loadNibNamed("PopView", owner: nil, options: nil) 26 | if let view = nibView?.first as? PopView { 27 | let attDic = NSMutableDictionary() 28 | attDic[NSAttributedStringKey.foregroundColor] = UIColor.lightGray 29 | let attPlaceholder = NSAttributedString(string: placeholder, attributes: attDic as? [NSAttributedStringKey : Any]) 30 | view.inputTextField.attributedPlaceholder = attPlaceholder 31 | view.popViewButton.setTitle(buttonTitle, for: .normal) 32 | return view 33 | } 34 | return nil 35 | } 36 | 37 | override init(frame: CGRect) { 38 | super.init(frame: frame) 39 | } 40 | 41 | required init?(coder aDecoder: NSCoder) { 42 | super.init(coder: aDecoder) 43 | } 44 | 45 | @IBAction func doCancelButton(_ sender: UIButton) { 46 | if delegate != nil { 47 | delegate?.popViewDidRemoved?(self) 48 | } 49 | self.removeFromSuperview() 50 | } 51 | 52 | @IBAction func doPopViewButtonPressed(_ sender: UIButton) { 53 | if delegate != nil { 54 | delegate?.popViewButtonDidPressed(self) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Views/PopView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 39 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/Agora-Online-PK/Views/VideoSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoSession.swift 3 | // Agora-Online-PK 4 | // 5 | // Created by ZhangJi on 2018/6/4. 6 | // Copyright © 2018 CavanSu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AgoraRtcEngineKit 11 | 12 | class VideoSession: NSObject { 13 | // this is a custom object for agora video view 14 | var uid: UInt = 0 15 | var hostingView: UIView! 16 | var canvas: AgoraRtcVideoCanvas! 17 | 18 | init(uid: UInt) { 19 | self.uid = uid 20 | 21 | hostingView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 22 | hostingView.translatesAutoresizingMaskIntoConstraints = false 23 | 24 | canvas = AgoraRtcVideoCanvas() 25 | canvas.uid = UInt(uid) 26 | canvas.view = hostingView 27 | canvas.renderMode = .hidden 28 | } 29 | } 30 | 31 | extension VideoSession { 32 | static func localSession() -> VideoSession { 33 | return VideoSession(uid: 0) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Agora-Online-PK-iOS/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Agora.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Image/API_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Image/API_list.png -------------------------------------------------------------------------------- /Image/API_list_EN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Image/API_list_EN.png -------------------------------------------------------------------------------- /Image/ArchitectureDesign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Image/ArchitectureDesign.png -------------------------------------------------------------------------------- /Image/ArchitectureDesign_EN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Usecase/Online-PK/f290ed7dc79649455953afb790d4c78699cc3ebf/Image/ArchitectureDesign_EN.png -------------------------------------------------------------------------------- /Image/descritpion.md: -------------------------------------------------------------------------------- 1 | This folder contains all the images required in the readme 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Online PK 2 | 3 | *Other Languages: [中文](README.zh.md)* 4 | 5 | ## **Agora PK Hosting** 6 | 7 | The Agora PK Hosting solution is designed for CDN Live push-and-pull stream scenarios that involve switching between the following scenarios: 8 | 9 | - Co-hosting in Standard mode 10 | - Co-hosting in PK mode 11 | 12 | ## Co-hosting in Standard Mode 13 | 14 | The hosts can use third-party applications such as ijkplayer to push streams to CDN Live. The push stream address for the hosts are independent and the audience can only see the corresponding host. 15 | 16 | ## Co-hosting in PK Mode 17 | 18 | The hosts need to quit the CDN Live push stream process, join the same Agora channel, and set the co-hosting composite mode on the Agora server using the _setLiveTranscoding_ API method; then push the composite stream to the original CDN address using the push stream _addPublishStreamUrl_ API method. 19 | 20 | The CDN Live audience can then participate in the PK between the hosts. The CDN Live audience does not need to change the CDN Live URL address as the hosts will still use the previous CDN Live push stream URL address. When either one of the hosts quit the Agora channel, the other host will switch to the Standard mode. 21 | 22 | ## **Architectural Design** 23 | ![ArchitectureDesign.png](Image/ArchitectureDesign_EN.png) 24 | 25 | You can find the Agora [implementation code](https://github.com/AgoraIO/ARD-Agora-Online-PK/tree/master/Agora-Online-PK-Android) for Android on Github. You can also download the [APK file](https://github.com/AgoraIO-Usecase/Online-PK/releases/download/v1.0/Agora-PK-Online.apk). 26 | 27 | ## **API methods** 28 | 29 | ![PK 连麦架构设计](Image/API_list_EN.png) 30 | 31 | The API methods related to the Agora Online PK: 32 | 33 | iOS|Android 34 | ---|--- 35 | [sharedEngineWithAppId:delegate:](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/sharedEngineWithAppId:delegate:)|[create](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a35466f690d0a9332f24ea8280021d5ed) 36 | [setChannelProfile](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/setChannelProfile:)|[setChannelProfile](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a1bfb76eb4365b8b97648c3d1b69f2bd6) 37 | [setClientRole](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/setClientRole:)|[setClientRole](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#aa2affa28a23d44d18b6889fba03f47ec) 38 | [enableVideo](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/enableVideo)|[enableVideo](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a99ae52334d3fa255dfcb384b78b91c52) 39 | [joinChannel](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/joinChannelByToken:channelId:info:uid:joinSuccess:)|[joinChannel](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a8b308c9102c08cb8dafb4672af1a3b4c) 40 | [setLiveTranscoding](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_ios?platform=iOS#livetranscoding-ios)|[setLiveTranscoding](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_android?platform=Android#setlivetranscoding) 41 | [addPublishStreamUrl](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_ios?platform=iOS#addpublishstreamurl-transcodingenabled)|[addPublishStreamUrl](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_android?platform=Android#addpublishstreamurl) 42 | [removePublishStreamUrl](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_ios?platform=iOS#removepublishstreamurl)|[removePublishStreamUrl](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_android?platform=Android#removepublishstreamurl) 43 | [leaveChannel](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/leaveChannel:)|[leaveChannel](https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a2929e4a46d5342b68d0deb552c29d597) 44 | 45 | ## **Implementation** 46 | 47 | - The Agora PK Hosting solution uses the Agora Video SDK in the communication mode. 48 | 49 | - When switching to the PK mode from the Standard mode, each host needs to quit the original CDN Live stream and join the same Agora channel through the application logic. 50 | 51 | - Under the PK mode: 52 | 1. Each host needs to set the composite configuration using the _setLiveTranscoding API method a_nd add the CDN Live push stream URL address, using the _addPublishStreamUrl API method,_ in the Agora channel. 53 | 2. The hosts need to ensure that the CDN Live push stream URL address will not change after switching from the Standard mode. 54 | 3. When either one of the hosts quit the Agora channel, the other host will quit the channel and switch to the Standard mode through the application. 55 | 56 | - Before switching to the Standard mode from the PK mode: 57 | 1. Each host needs to remove the previous CDN Live push stream URL address using the _removePublishStreamUrl API_ method. 58 | 2. Each host needs to push the stream to the original CDN URL address. 59 | 60 | 61 | ## **Integration Guide** 62 | 63 | ### Integration SDK 64 | 65 | - For Android, see [Configuring the DEV runtime](https://docs.agora.io/en/Interactive%20Broadcast/android_video?platform=Android). 66 | - For IOS, see [Configuring the DEV runtime.](https://docs.agora.io/en/Interactive%20Broadcast/ios_video?platform=iOS) 67 | 68 | ### Switching Between the Co-hosting Standard Mode and Co-hosting PK Mode 69 | 70 | _Android_: 71 | 72 | 1. [Video broadcasting realization](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/Quickstart%20Guide/broadcast_video_android?platform=Android) 73 | 2. [Stream pushing to CDN Live](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/Quickstart%20Guide/push_stream_android2.0?platform=Android) 74 | 3. Call the [_removePublishStreamUrl_](https://docs.agora.io/en/2.4/product/Interactive%20Broadcast/API%20Reference/live_video_android?platform=Android) API method to remove the stream URL address. 75 | 76 | _IOS_: 77 | 78 | 1. [Video broadcasting realization](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/Quickstart%20Guide/broadcast_video_ios?platform=iOS) 79 | 80 | 1. [Stream pushing to CDN Live](https://docs.agora.io/en/2.3.1/product/Interactive%20Broadcast/Quickstart%20Guide/push_stream_ios2.0?platform=iOS) 81 | 2. Call the [_removePublishStreamUrl_](https://docs.agora.io/en/2.4/product/Interactive%20Broadcast/API%20Reference/live_video_ios?platform=iOS) API method to remove the stream URL address. 82 | 83 | Ijkplayer Realization (Optional) 84 | 85 | Android: See ['Integration of ijkplayer framework for Android development'](https://github.com/Bilibili/ijkplayer). 86 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # PK 连麦 2 | 3 | *Other Languages: [English](README.md)* 4 | 5 | 声网 PK 连麦方案场景针对 CDN 推流拉流场景设计,包含以下内容: 6 | 7 | * 场景描述 8 | * 架构设计 9 | * 集成步骤 10 | * 集成注意事项 11 | 12 | ## 场景描述 13 | 14 | 声网 PK 连麦场景针对 CDN 推流场景设计,主要涉及两种模式的切换: 15 | 16 | * 单主播模式 17 | * 双主播 PK 模式 18 | * 观众模式(可选) 19 | 20 | ### 单主播模式 21 | 22 | 主播可以采用Agora SDK 或者第三方推流工具(比如 ijkplayer)推流到 CDN。观众通过 CDN 播放器拉取主播视频流观看直播。 23 | 24 | ### 双主播 PK 模式 25 | 26 | 两个主播退出 CDN 推流并同时加入同一个声网频道,各自在声网服务端设置双主播的合图(setLiveTranscoding),并通过声网提供的推流接口(addPublishStreamUrl)将合图后的流推送到原先的 CDN 地址。各自的 CDN 观众看到两个主播开始 PK。由于两个 PK 主播各自的 CDN 推流地址未发生改变,CDN 观众端不需要切换 CDN 拉流地址。只要任意一个主播离开声网 PK 频道,另一主播也退出 PK 模式返回普通模式。 27 | 28 | ### 观众模式(可选) 29 | 30 | 观众一般使用第三方 CDN 播放器(比如 ijkplayer)拉取视频流观看直播,本示例程序中并不包含观众模式。 31 | 32 | ## 架构设计 33 | 34 | 下图为一起 PK 连麦场景的声网实现架构图: 35 | 36 | ![PK 连麦架构设计](Image/ArchitectureDesign.png) 37 | 38 | 声网已在 GitHub 提供了 Android 平台的 [实现代码](https://github.com/AgoraIO/ARD-Agora-Online-PK/tree/master/Agora-Online-PK-Android)。你也可以下载实现的 [apk 文件](https://github.com/AgoraIO-Usecase/Online-PK/releases/download/v1.0/Agora-PK-Online.apk)。 39 | 40 | ## API 列表 41 | 示例 App 的 API 流程如下图所示。 42 | ![PK 连麦架构设计](Image/API_list.png) 43 | 44 | Agora SDK 关键 API 列表: 45 | 46 | iOS|Android 47 | ---|--- 48 | [sharedEngineWithAppId:delegate:](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/sharedEngineWithAppId:delegate:)|[create](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a35466f690d0a9332f24ea8280021d5ed) 49 | [setChannelProfile](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/setChannelProfile:)|[setChannelProfile](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a1bfb76eb4365b8b97648c3d1b69f2bd6) 50 | [setClientRole](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/setClientRole:)|[setClientRole](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#aa2affa28a23d44d18b6889fba03f47ec) 51 | [enableVideo](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/enableVideo)|[enableVideo](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a99ae52334d3fa255dfcb384b78b91c52) 52 | [joinChannel](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/joinChannelByToken:channelId:info:uid:joinSuccess:)|[joinChannel](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a8b308c9102c08cb8dafb4672af1a3b4c) 53 | [setLiveTranscoding](https://docs.agora.io/cn/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_ios?platform=iOS#livetranscoding-ios)|[setLiveTranscoding](https://docs.agora.io/cn/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_android?platform=Android#setlivetranscoding) 54 | [addPublishStreamUrl](https://docs.agora.io/cn/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_ios?platform=iOS#addpublishstreamurl-transcodingenabled)|[addPublishStreamUrl](https://docs.agora.io/cn/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_android?platform=Android#addpublishstreamurl) 55 | [removePublishStreamUrl](https://docs.agora.io/cn/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_ios?platform=iOS#removepublishstreamurl)|[removePublishStreamUrl](https://docs.agora.io/cn/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_android?platform=Android#removepublishstreamurl) 56 | [leaveChannel](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/leaveChannel:)|[leaveChannel](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a2929e4a46d5342b68d0deb552c29d597) 57 | 58 | ## 集成步骤 59 | 60 | ### PK 连麦方案场景实现 61 | 62 | **PK 场景需要实现以下功能:** 63 | 64 | 1. 集成声网 SDK 实现视频直播 65 | * Android, 详见 [集成客户端](https://docs.agora.io/cn/Interactive%20Broadcast/start_live_android?platform=Android) 66 | * iOS, 详见 [集成客户端](https://docs.agora.io/cn/Interactive%20Broadcast/start_live_ios?platform=iOS) 67 | 68 | 2. 实现 CDN 推流,和服务器合图 69 | * Android, 详见 [推流到 CDN ](https://docs.agora.io/cn/Interactive%20Broadcast/cdn_streaming_android?platform=Android) 70 | * iOS, 详见 [推流到 CDN ](https://docs.agora.io/cn/Interactive%20Broadcast/cdn_streaming_apple?platform=iOS) 71 | 72 | 3. 实现第三方推流,拉流(可选) 73 | * 如单主播模式需要使用第三方推流工具,需要自行集成第三方推流。 74 | * 如 APP 需要实现观众模式,需要自行集成第三方拉流播放器。 75 | 76 | ### 实现细节 77 | 78 | * 声网 PK 连麦方案采用直播模式的 Agora Video SDK。 79 | * 从单主播模式进入 PK 模式时,每个主播都需要退出原来的旁路推流。 80 | * 从单主播模式进入 PK 模式时,各位主播需要同时加入同一声网频道,可由 APP 控制实现。 81 | * PK 模式下,每个主播都需要设置合图(setLiveTranscoding,[Android](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a3cb9804ae71819038022d7575834b88c), [iOS](https://docs.agora.io/cn/2.3.1/product/Interactive%20Broadcast/API%20Reference/live_video_ios?platform=iOS#livetranscoding-ios)), 并重新添加 CDN 推流地址(addPublishStreamUrl,[Android](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a4445b4ca9509cc4e2966b6d308a8f08f), [iOS](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/addPublishStreamUrl:transcodingEnabled:)) 82 | * PK 模式下,设置合图(setLiveTranscoding)和添加 CDN 推流地址(addPublishStreamUrl)需要在声网频道内进行。 83 | * PK 模式下,双方主播的 CDN 推流地址应与普通模式时选用的 URL 地址一致确保 CDN 观众无需切换 CDN 地址。 84 | * PK 模式下,只要有一位主播退出声网频道,其余主播也同时退出声网频道进入单主播模式,可由 APP 控制实现。 85 | * 从 PK 模式进入单主播模式前,每个主播都需要移除原先的 CDN 推流地址(removePublishStreamUrl,[Android](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#a87b3f2f17bce8f4cc42b3ee6312d30d4), [iOS](https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/removePublishStreamUrl:))。 86 | * 从 PK 模式进入普通模式时,每个主播需要重新向原来的 CDN 地址推流确保 CDN 观众无需切换地址。 87 | 88 | ### 运行示例程序 89 | 90 | 首先在 [Agora.io 注册](https://dashboard.agora.io/cn/signup/) 注册账号,并创建自己的测试项目,获取到 AppID。 91 | 然后在 [Agora.io SDK](https://docs.agora.io/cn/Interactive%20Broadcast/downloads) 下载 视频通话/视频直播 SDK 92 | 93 | **iOS** 94 | 95 | 1. 将 AppID 填写进 KeyCenter.m 96 | 97 | ``` 98 | static let AppId: String = "Your App ID" 99 | ``` 100 | 101 | 2. 解压视频通话/视频直播 SDK 包,将其中的 libs/AgoraRtcEngineKit.framework 复制到项目文件夹下。 102 | 3. 最后使用 XCode 打开 Agora-Online-PK.xcodeproj,连接 iPhone/iPad 测试设备,设置有效的开发者签名后即可运行。 103 | 104 | ``` 105 | * XCode 9.0 + 106 | * iOS 9.0 真机设备 107 | * 不支持模拟器 108 | ``` 109 | 110 | **Android** 111 | 112 | 1. 将 AppID 填写进 PKConstants 的 MEDIA_APP_ID以及SIGNALING_APP_ID 中 113 | 2. 解压视频通话/视频直播 SDK 包,将其中的jar和so复制到项目对应文件夹下。 114 | 3. 最后使用 AndroidStudio 打开项目,连接 Android 测试设备,编译并运行。 115 | 116 | ``` 117 | * Android Studio 2.0 + 118 | * minSdkVersion 16 119 | * 部分模拟器会存在功能缺失或者性能问题,所以推荐使用真机设备 120 | ``` 121 | 122 | ### 示例程序功能 123 | - 开始直播:在首页输入直播频道名,点击“开始直播”按钮,进入直播房间,开始直播和 CDN 推流; 124 | - 发起PK:在房间内点击“PK”按钮,并输入“PK房间名”进入PK(需要两个主播同时输入相同的“PK房间名”以进入同一房间); 125 | - 退出PK:点击“退出PK”按钮,退出PK模式,返回单主播模式; 126 | - 退出房间:点击右上角“离开”按钮,离开直播房间; 127 | - 拷贝拉流地址:在直播中可点击“拷贝”按钮,拷贝拉流地址,使用 CDN 播放器(如 VLC) 128 | 129 | ## 集成注意事项 130 | - 单主播模式与 PK 模式切换时一定要先停止原先的推流,再重新开始推流,否则会推流失败 131 | - 观众一般需要感知主播模式的改变来更新UI,一般是通过信令通知观众,由于信令和 CDN 视频流存在时间差(CDN 推流一般存在数秒的延迟),为了更好的用户体验,可以在切换模式时做一个延时动画,让用户忽略这个时间差。也可以通过 CDN 播放器的特有回调(如视频尺寸改变)来感知主播状态的变化。 132 | 133 | ## 联系我们 134 | 135 | - 如果发现了示例程序的 bug,欢迎提交 [issue](https://github.com/AgoraIO/ARD-Agora-Online-PK/issues) 136 | - 声网 SDK 完整 API 文档见 [文档中心](https://docs.agora.io/cn/) 137 | - 如果在集成中遇到问题,你可以到 [开发者社区](https://dev.agora.io/cn/) 提问 138 | - 如果有售前咨询问题,可以拨打 400 632 6626,或加入官方Q群 12742516 提问 139 | - 如果需要售后技术支持,你可以在 [Agora Dashboard](https://dashboard.agora.io) 提交工单 140 | 141 | ## 代码许可 142 | 143 | The MIT License (MIT). 144 | --------------------------------------------------------------------------------